AutoReloader is a lightweight code reloader intended to be used specially in development mode of server applications.
It will override require
and require_relative
when activated.
Installation
Add this line to your application's Gemfile:
gem 'auto_reloader'
And then execute:
$ bundle
Or install it yourself as:
$ gem install auto_reloader
Usage
AutoReloader will try to add auto-reloading code support transparently by unloading all files belonging to the list of reloadable paths and the constants defined by them. This is not always possible to handle transparently, so please read the Known Caveats to properly understand what you should do to avoid them.
Here's how it would be used in a Rack application.
app.rb:
App = ->(env){['200', {'Content-Type' => 'text/plain'}, ['Sample output']]}
config.ru:
if ENV['RACK_ENV'] != 'development'
require_relative 'app'
run App
else
require 'auto_reloader'
# won't reload before 1s elapsed since last reload by default. It can be overridden
# in the reload! call below
AutoReloader.activate reloadable_paths: [__dir__], delay: 1
run ->(env) {
AutoReloader.reload! do |unloaded|
# by default, AutoReloader only unloads constants when a watched file changes;
# when it unloads code before calling this block, the value for unloaded will be true.
ActiveSupport::Dependencies.clear if unloaded && defined?(ActiveSupport::Dependencies)
require_relative 'app'
App.call env
end
}
end
Just change "Sample output" to something else and reload the page.
By default reloading will only happen if one of the reloadable file was changed since it was
required. This can be overriden by providing the onchange: false
option to either activate
or reload!
. When the listen
gem is available it's used by default to detect changes in
the reloadable_paths
using specific OS watch mechanisms which allows AutoReloader to speed
up reload!
when no changes happened although it won't probably do much difference unless
your application has tons of reloadable files loaded upon each request. If you don't want to
use listen
when available, set watch_paths: false
when calling activate
.
Currently AutoReloader does not watch files other than those being required and I'm not sure
if it would be a good idea to provide this kind of feature through some option. However if
you want to force unloading the reloadable files when some configuration file (YAML, JSON, etc)
changes, it should be quite simple with the listen
gem. Here's an example:
app_config = File.expand_path('config/app.json', __dir__)
Listen.to(File.expand_path('config', __dir__)) do |added, modified, removed|
AutoReloader.force_next_reload if (added + modified + removed).include?(app_config)
end.start
Thread-safety
In order for the automatic constants and required files detection to work correctly it should process a single require at a time. If your code has multiple threads requiring code, then it might cause a race condition that could cause unexpected bugs to happen in AutoReloader. This is the default behavior because it's not common to call require from multiple threads in the development environment but adding a monitor around require could create a dead-lock which is a more serious issue.
For example, if requiring a file would start a web server and block, if the web server is started in a separate thread (which could be joined so that the require doesn't return), then it wouldn't be able to require new files because the lock was acquired by another thread and won't be released while the web server is running.
If you are sure that no require should block in your application (which is also common), you're
encouraged to call AutoReloader.sync_require!
. Or pass sync_require: true
to
AutoReloader.activate
. You may even control this behavior dynamically so that you call
AutoReloader.async_require!
before the blocking require and then reenable the sync behavior.
The sync behavior will ensure no race conditions that would break the automatic detection
mechanism would ever happen.
Also, it may be dangerous to unload classes while some requests are being processed. So, since
version 0.4, the default is to await for all blocks executed by reload!
to finish running before
unloading. In case you prefer the old behavior because some requests may never return, which
could happen with some implementations of websocket connections handled by the same process
for example, just set the await_before_unload
to false
on activate
or reload!
calls.
Known Caveats
In order to work transparently AutoReloader will override require
and require_relative
when
activated and track changes to the top-level constants after each require. Top-level constants
defined by reloadable files are removed upon reload!
or unload!
. So, if your application
does something crazy like this:
json-extension.rb:
class JSON
class MyExtension
# crazy stuff: don't do that
end
end
If you require 'json-extension' before requiring 'json', supposing it's reloadable, unload!
and reload!
would remove the JSON constant because AutoReloader will think it was defined
by 'json-extension'. If you require 'json' before this file, then JSON won't be removed but
neither will JSON::MyExtension.
As a general rule, any reloadable file should not change the behavior of code in non reloadable files.
Implementation description
AutoReloader doesn't try to reload only the changed files. If any of the reloadable files change
then all reloadable files are unloaded and the constants they defined are removed. Reloadable
files are those living in one of the reloadable_paths
entries. The more paths it has to search
the bigger will be the overhead to require
and require_relative
.
Currently this implementation does not use an evented watcher to detect changes to files but it may be considered in future versions. Currently it traverses each loaded reloadable file and check whether it was changed.
AutoReloadable does not support automatic autoload
AutoReloadable does not provide automatic autoload features like ActiveSupport::Dependencies by design and won't support it ever, although such feature could be implemented as an extension or as a separate gem. Personally I don't find it a good practice and I think all dependencies should be declared explicitly by all files depending on them even if it's not necessary because it was already required by another file.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow
you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a
new version, update the version number in version.rb
, and then run bundle exec rake release
,
which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub.
License
The gem is available as open source under the terms of the MIT License.