Living in sin (with Spring)
Share this post on
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
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"
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 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 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.
Unfortunately, it’s almost impossible to avoid such caveats when using Spring.
Nowadays, I prefer to disable it entirely (and rely on
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
#!/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