Spring is a very popular tool in Rails application development (especially since it has been added to the Rails default Gemfile).
Your codebase and the number of dependencies grow the application boot time increases as well. It could take a dozen seconds before rails c
becomes responsive.
Spring tries to solve this problem by loading application code once and reloading it when necessary. Why “tries”? Well, because sometimes it fails to reflect the changes.
For example, Spring couldn’t detect anything except the code changes: environmental variables, randomization seeds, etc.
Let me share some tricks on working with Spring.
Spring vs. external dependencies
Suppose that you store some configuration in a file called config/my_config.txt
.
Spring won’t reload the application if you change this file unless you explicitly tell it to do that:
# config/spring.rb
Spring.watch "config/my_config.txt"
Easy.
What if your configuration depends on something unwatchable? Consider an example:
# development.rb
Rails.application.configure do
# We want to dump the structure.sql only if there are any
# uncommitted changes migrations (i.e. do not re-generate the dump when
# you're running migrations written by someone else)
res = `git status db/migrate/ --porcelain`
ActiveRecord::Base.dump_schema_after_migration = res.nil? || res.present?
end
The value of ActiveRecord::Base.dump_schema_after_migration
is populated once on the initial load. What if you add a migration later? The dump won’t be generated.
To handle this situation, we must use Spring.after_fork
callback:
Spring.after_fork do
res = `git status db/migrate/ --porcelain`
ActiveRecord::Base.dump_schema_after_migration = res.nil? || res.present?
end
Cool! That works!
But what if someone in your team not using Spring? We should take care of it:
if defined?(::Spring::Application)
Spring.after_fork do
res = `git status db/migrate/ --porcelain`
ActiveRecord::Base.dump_schema_after_migration = res.nil? || res.present?
end
else
res = `git status db/migrate/ --porcelain`
ActiveRecord::Base.dump_schema_after_migration = res.nil? || res.present?
end
NOTE: you’ve probably noticed that we’re checking for the presence of ::Spring::Application
constant and not just ::Spring
. It turned out that some gems also initiate the ::Spring
constant even without spring
itself.
Spring vs. RSpec
RSpec has a feature of running tests in random order.
How does it work?
It generates a random seed on load and uses it later to order the tests.
When running tests with Spring, this seed value is not reloading—your tests order is always the same (note: while Spring server is running)!
Hopefully, we already know how to fix this:
if defined?(::Spring::Application)
Spring.after_fork do
RSpec.configure do |config|
# Make sure that seed value is randomized for each test run
config.seed = rand(0xFFFF)
end
end
end
That snippet should be probably included into the default rails_helper.rb
by default.
.offspring
Unfortunately, it’s almost impossible to avoid such caveats when using Spring.
Nowadays, I prefer to disable it entirely (and rely on bootsnap
instead).
What if other developers on your team are still in love with Spring? We need a way to disable it locally.
Spring provides a way to do that out-of-the-box: set DISABLE_SPRING=1
environment variable and you’re done.
This approach has some disadvantages:
- you cannot disable Spring per-project
- it’s not clear whether is’s disabled or not; you have to run a command to see the env var value
- it doesn’t play well with containers.
I found a much better (IMO) way to do that: toggle the Spring state depending on the presence of .offspring
file in your project root.
All you need is to add a single line to your bin/spring
:
#!/usr/bin/env ruby
ENV['DISABLE_SPRING'] = '1' if File.exist?(File.join(__dir__, '../.offspring'))
# ...
Now you can enable/disable Spring with the following commands:
$ touch .offspring #=> disables Spring
$ rm .offspring #=> enables Spring
That’s it!