Rodrigo Rosenfeld Rosas

AutoReloader: a transparent automatic code reloader for Ruby

Mon, 18 Jul 2016 14:00:00 +0000 (last updated at Mon, 18 Jul 2016 14:35:00 +0000)

I've been writing some Roda apps recently. Roda doesn't come with any automatic code reloader, like Rails does. Its README lists quite a few code reloaders that could be used with Roda but while converting a JRuby on Rails small application to Roda I noticed I didn't really like any of the options. I've written a review about the available options if you're curious.

I could simply use ActiveSupport::Dependencies since I knew it was easy to set up and worked mostly fine but one of the reasons I'm thinking about leaving Rails is the autoloading behavior of ActiveSupport::Dependencies and the monkey patches to Ruby core classes added by ActiveSupport as a whole. So, I decided to create auto_reloader which provides the following features:

  • just like Rack::Reloader it works transparently. Just use "require" and "require_relative". To automatically track constants definitions one has to override them anyway and I can't think of any reliable way to track top-level constants automatically without overriding those methods. However those methods are only overridden when AutoReloader is activated, which doesn't happen in the production environment as it does with ActiveSupport::Dependencies. Those are the only monkey patches happening in development mode;
  • differently from Rack::Reloader, it will detect new top-level constants defined after a request and unload them upon reloading, preventing several issues caused by not doing that;
  • no monkey patches to core Ruby classes in production mode;
  • it can use the 'listen' gem as a file watcher to speed up the request when no reloadable files have been changed, in which case the application would respond almost as fast as in production environments, which is important when we are working on performance optimizations. It will use 'listen' by default when available but it can be opted out and it won't make much difference unless, maybe, if some request would load many reloadable files;
  • it's also possible to force reloading even if no loaded files have been changed. This could be useful if such files would load some non-Ruby configuration files and they have changed but the README provides another alternative to better handle those cases by using Listen to watch them and call AutoReloader.force_next_reload;
  • it doesn't provide autoloading like ActiveSupport::Dependencies does;
  • it's possible to configure a minimal delay time between two code reloading procedures;
  • it unloads all reloadable files rather than only the changed files as I believe this is a safer approach and the one also used by ActiveSupport::Dependencies;
  • reloadable files are those found in one of the reloadable_paths option provided to AutoReloader;
  • not specific to Rack application, but could be used with any Ruby application.

What AutoReloader does not implement:

  • autoloading of files on missing constants. Use Ruby's "autoload" for that if you want;
  • it doesn't provide a hook system to notify when some file is loaded like ActiveSupport::Dependencies does;
  • it doesn't provide an option to specify load-once files. An option would be to place them in different directories and do not include them in the reloadable_paths option;
  • it doesn't reload on changes to files other than the loaded ones, like JSON or YAML configuration files, but it's easy to set up them as explained in the project's README.

Usage with a Rack application

1# app.rb
2App = -> { [ '200', { 'Content-Type' => 'text/plain' }, [ 'Sample output' ] ] }
5if ENV['RACK_ENV'] != 'development'
6 require_relative 'app'
7 run App
9 require 'auto_reloader'
10 # won't reload before 1s elapsed since last reload by default. It can be overridden
11 # in the reload! call below
12 AutoReloader.activate reloadable_paths: [ '.' ]
13 run -> (env) {
14 AutoReloader.reload! do
15 require_relative 'app'
16 env
17 end
18 }

If you also want it to reload if the "app.json" configuration file has changed:

1# app.rb
2require 'json'
3config = JSON.parse 'config/app.json'
4App = -> { [ '200', { 'Content-Type' => 'text/plain' }, [ config['output'] ] ] }
6# append this to
7require 'listen' # add the 'listen' gem to your Gemfile
8app_config = File.expand_path 'config/app.json' 'config') do |added, modified, removed|
10 AutoReloader.force_next_reload if (added + modified + removed).include?(app_config)

If you decided to give it a try and found any bugs please let me know.

Powered by Disqus