How rails reloads your source code in development mode?

Yuva's avatar

Yuva

We all know Rails has this feature, which reloads source code in development mode everytime a request hits server. Starting from version 3.2.0, it introduced faster dev mode where it reloads application code when the code changes, and not on every request.

This blog will talk about important parts of Rails source code which helps in achieving faster dev mode.

  • ActiveSupport::FileUpdateChecker: Checks whether any of the application code files are changed.
  • Activesupport::Dependencies: Actual module responsible for reloading classes.
  • ActionDispatch::Reloader: The middleware which helps in reloading classes.
  • reload_classes_only_on_change: As name states, this configuration option tell Rails to enable/disable code for re-loading classes only if the code changes.

Lets go through them one by one.

ActiveSupport::FileUpdateChecker

This class helps in checking whether application code is updated or not. It exposes 2 methods: updated? and execute. The former tells whether files are updated or not, while latter executes a block given by updating timestamp of latest changed file. The code can be found here. How file checker checks whether a file is updated or not depends on the modified time of the file. There is a small function max_time which returns timestamp for recently modified path (most recent one)

class ActiveSupport::FileUpdateChecker
  # NOTE: Removed some code to reflect the logic
  def updated?
    current_updated_at = updated_at(current_watched)
    if @last_update_at < current_updated_at
      @updated_at = current_updated_at
      true
    else
      false
    end
  end
 
  # Executes the given block and updates the timestamp.
  def execute
    @last_update_at = updated_at(@last_watched)
    @block.call
  end
 
  def updated_at(paths)
    @updated_at || max_mtime(paths) || Time.at(0)
  end
 
  # This method returns the maximum mtime of the files in +paths+
  def max_mtime(paths)
    paths.map {|path| File.mtime(path)}.max
  end
end

ActiveSupport::Dependencies

This module consists of the core mechanism to load classes, by following Rails naming conventions. It uses const_missing to catch missing classes, and then searches in autoload_paths to load those missing classes. The code can be found here.

module Dependencies
  def const_missing(const_name)
    from_mod = anonymous? ? ::Object : self
    Dependencies.load_missing_constant(from_mod, const_name)
  end
 
  def load_missing_constant(from_mod, const_name)
    qualified_name = qualified_name_for(from_mod, const_name)
    path_suffix = qualified_name.underscore
 
    file_path = search_for_file(path_suffix)
 
    if file_path
      expanded = File.expand_path(file_path)
      require_or_load(expanded)
    end
 
    raise NameError, "uninitialized constant #{qualified_name}"
  end
end

ActionDispatch::Reloader

This is a middleware which provides hooks that can be run while code reloading. It has 2 callback hooks, :prepare and :cleanup. Rails code will make use of these hooks to install code which determine whether to reload code or not. :prepare callbacks will run before request is processed, and :cleanup callbacks will run after request is processed. You can see call(env) of reloader here

class ActionDispatch::Reloader
  def call(env)
    @validated = @condition.call
    prepare!
 
    response = @app.call(env)
    response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
 
    response
  rescue Exception
    cleanup!
    raise
  end
end

reload_classes_only_on_change

This configuration option is defined in railties. By default, it is set to true, so Rails reloads classes only if code changes. Set it to false, and Rails will reload on each request. Digging into the place where this boolean is defined, we find that there is an initializer set_clear_dependencies_hook. This initializer is defined here.

initializer :set_clear_dependencies_hook, group: :all do
  callback = lambda do
    ActiveSupport::DescendantsTracker.clear
    ActiveSupport::Dependencies.clear
  end
 if config.reload_classes_only_on_change
    reloader = config.file_watcher.new(*watchable_args, &callback)
    self.reloaders << reloader
   ActionDispatch::Reloader.to_prepare(prepend: true) do
      reloader.execute
    end
  else
    ActionDispatch::Reloader.to_cleanup(&callback)
  end
end

The above code installs a file watcher if config var is true. watchable_args consists of autoload_paths along with other files like schema.rb. So, file_watcher is configured to watch these paths. If config var is false, it just installs callback as :cleanup hook, which means all the code will be unloaded after each request is processed.

How do these components fall in place?

By joining all the dots, the sequence is:

  • Request hits the server. The middleware ActionDispatch::Reloader kicks in, and executes callbacks inserted
  • One of the callback inserted is to check whether any application code files have been changed and execute the lambda (shown above which clears dependencies)
  • The callback (lambda) will clear all the dependencies, i.e it unloads all the class constants (by using builtin remove_const).
  • The middleware passes the request down the middleware stack for proper handling. Most probably routes will process the request.
  • Once the request handling starts, since all the constants are unloaded, the main module ActiveSupport::Dependencies kicks in, uses const_missing and loads all classes once again, thus loading the modified code.
Bonus

If you want to know how routes reloading happens, check this file

Hope you have enjoyed this article, and follow us on twitter

@codemancershq

&bsp;for all the awesome blogs, or you can use rss feeds.