Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #9795 (closed defect: fixed)

Opened 1 year ago

Last modified 10 months ago

[PATCH] Experimental Plugin::Locator/Plugin::Loader implementation to make future changes to plugin loading system easier

Reported by: lazyatom Assigned to: technoweenie
Priority: normal Milestone: 2.x
Component: ActiveRecord Version: edge
Severity: normal Keywords: verified plugin loading patch dependencies plugins
Cc: frederick.cheung@gmail.com, david.heinemeier@gmail.com

Description

I just recently created Ticket #9793, which includes a simple patch to add plugin lib directories to the load path before plugins are loaded "properly".

In building the patch on #9793, I couldn't help but feel that I was fighting against the existing implementation of Rails::Plugin::Loader and Rails::Plugin::Locator. The load_plugins loop in Rails::Initializer is difficult to refactor, and difficult to test, and the relationship between a "plugin" and a "plugin loader" and a "plugin locator" didn't seem clear to me.

So, while I imagine that the patch on my previous ticket (#9793) will be applied in favour of the patch attached here, I would really like some feedback about this approach (anyone who doesn't care can happily stop reading now :)) as I think there's definite value in it.

In the patch, I've cleared up the responsibilities of the Loader and Locator, and added a new class (Rails::Plugin), so now it's hopefully easier to develop in the future. In particular, I felt it was important that Rails::Plugin::Loader was broken into two classes (most of the functionality in Rails::Plugin comes from Loader), because it didn't feel right that the object which represented a plugin could order itself based on a configuration setting obtained in a convoluted fashion from the Initialiser instance.

With this patch, Plugin instances are returned by the Locator, which are then passed to the Loader. The Loader determines which plugins should be loaded, and takes care of loading them in the right order. The Plugin instance itself contains most the logic for "loading" a plugin (i.e. evaluating init.rb).

The upshot is that I think this implementation is easier to extend, but also much easier to test - much of the bulk of this patch is testing. The rest is just a fairly aggressive refactoring of the classes that were already there, to try and make sense of the way that each type of object interacts.

Again, I'm eager to get some feedback about this, so I'm looking forward to your comments.

Attachments

refactor_plugin_locator_and_loader_into_more_testable_classes.patch (45.4 kB) - added by lazyatom on 10/05/07 15:47:02.
Refactor plugin locator and loader into more testable classes

Change History

10/05/07 15:47:02 changed by lazyatom

  • attachment refactor_plugin_locator_and_loader_into_more_testable_classes.patch added.

Refactor plugin locator and loader into more testable classes

10/05/07 17:14:02 changed by lifofifo

This feels excellent. One question however ( as I said in IRC ) :

Why does a plugin's init.rb need to access configuration object ? Can you please point to some good use case of that ?

+1

10/05/07 18:36:56 changed by lazyatom

As far as I can tell, there's no use-case - in fact, the config variable was present in Jamis' original plugin checkin. That said, I think it's probably useful for a plugin to have access to the configuration. Once example might be detecting which frameworks are being loaded - if action_mailer isn't in the config.frameworks list, the plugin might opt not to enable it's mailing functionality.

10/09/07 08:12:01 changed by fcheung

+1 Works well for me with one small change: I think the call to add_plugin_load_paths in initializer.rb needs to be lower down.

As it is I can't autoload things from plugins as set_autoload_paths clobbers Dependencies.load_paths, killing whatever is set by add_plugin_load_paths. I'm not sure where the best place is, but anywhere after set_autoload_paths seems to work for me.

Other than that does exactly what it says on the tin.

11/07/07 10:20:39 changed by h-lame

+1

This seems like a good refactoring to me. Clearly #9793 also fixes the problem, however, this is a much cleaner attempt at making the affected area easier to understand.

11/07/07 10:25:53 changed by lazyatom

  • keywords changed from plugin loading patch dependencies plugins to verified plugin loading patch dependencies plugins.

Three +1s - adding the verified keyword.

11/07/07 18:16:55 changed by technoweenie

  • owner changed from core to technoweenie.
  • status changed from new to assigned.

I don't have a wifi connection to commit, but I ended up making the change that fcheung mentioned. I'll commit later today.

11/08/07 05:29:47 changed by rick

  • status changed from assigned to closed.
  • resolution set to fixed.

(In [8115]) Refactor Plugin Loader. Add plugin lib paths early, and add lots of tests. Closes #9795 [lazyatom]

11/08/07 21:16:58 changed by cch1

  • status changed from closed to reopened.
  • resolution deleted.

I've found that this changeset has introduced bugs in my testing environment.

Specifically, I have a plugin that introduces some subclasses of Exception. The file containing these exceptions (exceptions.rb) looks something like this:

module MyModule
  class MyError < StandardError
  end
end

This file is require'd within the plugin's init.rb.

Then, within a functional test I do something like:

def test_error_found
  assert_raises MyModule::MyError do
    get :show
  end
end

After r8115, I get an exception "uninitialized constant MyModule::MyError"

(follow-up: ↓ 10 ) 11/08/07 22:20:21 changed by technoweenie

Looks like you have an unconventional setup. Normally I'd put such an exception in my_module.rb or my_module/my_error.rb. What's the problem though? Is init.rb not being run? What plugin is it?

(in reply to: ↑ 9 ) 11/09/07 04:50:43 changed by cch1

Replying to technoweenie:

Looks like you have an unconventional setup. Normally I'd put such an exception in my_module.rb or my_module/my_error.rb. What's the problem though? Is init.rb not being run? What plugin is it?

I was baffled by the fact that init.rb IS being run yet the exceptions are not available in tests. After a bit of logging, I determined that exceptions.rb IS NOT being loaded despite a very clear "require 'exceptions'" in init.rb. Furthermore, I don't get any "MissingSourceFile exceptions either! After a bit more research...

Rails is loading exceptions.rb, but from another plugin

Yup -after some logging I determined that Rails is loading a same-named file but from a different plugin. In fact, it's loading the other exceptions.rb twice!

I hope this explanation is clear and helps pinpoint the source of the problem.

-Chris

11/09/07 08:42:10 changed by lazyatom

It sounds like this would be because all plugins are available on the $LOAD_PATH now, and the other plugin was loaded before the one in question. So, this could be a namespacing issue?

11/09/07 13:06:02 changed by cch1

No, it's not a namespacing issue -both plugins with their respective lib/exceptions.rb file are in difference namespaces (named after the plugin and with a simple single-level namespace. The exception classes in the exceptions.rb files are in subordinate namespaces within those differing namespaces. Keep in mind that I never had a problem with either of these plugins until r8115.

FYI, the two names spaces from the two exceptions.rb files are as follows:

module Authorize
  class AuthorizationError < StandardError; end
  class TrusteeDoesntImplementRoles < AuthorizationError; end  
  ...
end
module Authenticate
  class AuthenticationError < StandardError; end
  class InvalidTokenExpiry < AuthenticationError; end
  ...
end

11/09/07 14:12:26 changed by technoweenie

Yes, but the files are called 'exceptions.rb'. You should name them differently by following my previous suggestion to prevent clashes.

11/09/07 17:28:24 changed by lazyatom

cch1: Are both the files called exceptions.rb, and in the root lib dir of each plugin?, i.e.

plugin_a/lib/exceptions.rb:
  module PluginA
    class ExceptionA
    end
  end

plugin_b/lib/exceptions.rb:
  module PluginB
    class ExceptionB
    end
  end

... if this is the case, there's no way for ruby to 'know' which file you mean when you call require 'exceptions' other than precidence (ordering) in the $LOAD_PATH. So, chances are that the file from plugin_a will get loaded in preference to the one in plugin_b.

To get around this, your exceptions.rb files will need to be in paths that are distinct, such as technoweenie suggests:

plugin_a/lib/plugin_a/exception_a.rb
plugin_b/lib/plugin_b/exception_b.rb

(actually it's the plugin_a/plugin_b part of the path that's important, the files could still be called exception.rb)

Does this help? If not, feel free to email me (james.adam@gmail.com) with more details about the plugin layout, and I'll see what I can do.

11/10/07 00:34:51 changed by cch1

  • status changed from reopened to closed.
  • resolution set to fixed.

My previous good fortune with everything being loaded as needed was apparently happenstance. I've now taken the advice of lazyatom and technoweenie and change the directory structure of my plugins to ensure that the load namespace is not ambiguous. Effectively, I no longer require 'exception' but rather 'my_plugin/exceptions'.

I sincerely hope I am the only one who needed to learn this lesson and that, in the name of greater flexibility, this changeset quietly becomes "Rails."

-Chris