Search code examples
ruby-on-railsrails-springwisper

Rails spring wisper listener method caching


It turns out that Spring caches my wisper listener method (I'm writing quite simple Engine).

Example:

app/models/myengine/my_class.rb

class Myengine::MyClass
  include Wisper::Publisher

  def something
    # some logic
    publish(:after_something, self)
  end
end

config/initializers/wisper.rb

Wisper.subscribe(Myengine::MyObserver.new)

app/observers/myengine/my_observer.rb

class Myengine::MyObserver
  def after_something my_class_instance
    # any changes here requires Spring manual restart in order to be reflected in tests
    another_method
  end

  def another_method
    # all changes here or in any other class methods works ok with Spring and are instantly visible in tests
    return true
  end
end

By Spring restart I mean manual execution of spring stop command which is really annoying.

What is more mysterious I may change another_method return value to false and then tests are failing which is OK, but when I change after_something method body to let say return false it doesn't have any effect on tests (like the body of the after_something is somehow cached).

It is not high priority problem because this strange behaviour is only visible inside listener method body and easy to overcome by moving all logic to another method in the class. Anyway it might be confusing (especially at the beginning when I didn't know the exact problem).


Solution

  • The problem is properly caused because when you subscribe a listener globally, even if its class is reloaded, the object remains in memory pointing to the class it was originally constructed from, even if the class has been reloaded in the meantime.

    Try this in config/initializers/wisper.rb:

    Rails.application.config.to_prepare do
      Wisper.clear if Rails.env.development?
      Wisper.subscribe(Myengine::MyObserver.new)
    end
    

    to_prepare will run the block before every request for development environment, but once, as normal for production environment. Therefore provided your listener does not maintain any state it should work as expected.

    The Wisper.clear is needed to remove the existing listeners subscribed before we re-subscribe a new instance from the reloaded class. Be aware that #clear will clear all subscribers, so if you have similar code as the above in more than one engine only the last engine to be loaded will have its listeners subscribed.