Living in sin (with Spring)

Topics

Share this post on


Translations

If you’re interested in translating or adapting this post, please contact us first.

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.

Run Test Run at RubyConfBy 2017

“Run Test Run” at RubyConfBy 2017

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!

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.

How can we help you?

Martians at a glance
17
years in business

We transform growth-stage startups into unicorns, build developer tools, and create open source products.

If you prefer email, write to us at surrender@evilmartians.com