From 84d5c98868210c42fc97163d854866a1354011c3 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Mon, 25 May 2026 14:48:08 -0400 Subject: [PATCH] Add Spring.allow_reloading_disabled opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default Spring refuses to boot when `config.cache_classes = true` (a.k.a. `config.enable_reloading = false`) via the `:ensure_reloading_is_enabled` initializer, with the message "Spring reloads, and therefore needs the application to have reloading enabled." That guard predates the recognition that Spring's reload mechanism is mold-restart-on-file-change via its own watcher (loaded application features + initializers + config paths) — which works regardless of whether Rails' in-process reloader is enabled. For projects that want to disable Rails' reloader (e.g. to shed the per-serve `reloaders.any?(&:updated?)` stat() pass over thousands of watched files in large monorepos), the guard is unnecessarily strict. This commit adds `Spring.allow_reloading_disabled` as an explicit opt-in. Default remains `false` (the existing strict check). When set to `true` the initializer is not registered and Spring boots normally with reloading disabled. The docstring on `Spring.allow_reloading_disabled` spells out what the user gives up: 1. Rails' `app.reloader.reload!` becomes a no-op. Anything that depends on it won't fire between Spring serves. 2. With Zeitwerk + reloading disabled, autoloaded constants are non-reloadable. Tests that rely on the reloader resetting class state between runs may break. 3. The user is responsible for asserting that lazy resources (routes, I18n, view paths) stay unmaterialized in the Spring mold so that each fork loads them fresh. Acceptance test added covering the new opt-in. Co-authored-by: AI (Pi/Claude Opus 4.7) --- CHANGELOG.md | 7 +++++++ README.md | 25 +++++++++++++++++++++++++ lib/spring/application.rb | 4 ++++ lib/spring/configuration.rb | 6 ++++++ test/support/acceptance_test.rb | 14 ++++++++++++++ 5 files changed, 56 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e5d39e..c92cc5ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +* Add `Spring.dangerously_allow_disabling_reloading` opt-in to skip the `:ensure_reloading_is_enabled` initializer check, so projects that want to run with `config.cache_classes = true` / `config.enable_reloading = false` can. +The default behavior (refuse to boot) is unchanged, as using this option requires a Rails application that uses +lazy-loader for everything (most importantly, routes & i18n translations). + + ## 4.5.0 * Skip spring without error if spring is not in installed bundler groups. diff --git a/README.md b/README.md index 6084c18c..11ac0ad2 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,31 @@ Spring manages. That setting is typically configured in Note: in versions of Rails before 7.1, the setting is called `cache_classes`, and it needs to be `false` for Spring to work. +#### Running Spring with reloading disabled (advanced, risky) + +If you know what you are doing, and if every reloadable resource is configured +to materialize *lazily in the forked child*, not in the Spring server process, +then you can decide to run Spring without Rail's in-process reloaders. + +To opt into this, set in `config/spring.rb`: + +```ruby +Spring.dangerously_allow_disabling_reloading = true +``` + +If you set this, you should make sure that you are NOT pre-loading reloadable +resource in the Spring server. One approach is to add boot-time assertions at +the end of `Spring.after_environment_load`, e.g.: + +```ruby +Spring.after_environment_load do + raise "routes were drawn at boot" if Rails.application.routes_reloader.loaded + raise "i18n locales where loaded at boot" unless I18n.backend.instance_variable_get(:@translations).nil? +end +``` + +When unsure, leave this disabled. + ### Usage For this walkthrough I've generated a new Rails application, and run diff --git a/lib/spring/application.rb b/lib/spring/application.rb index 2e0c7f46..338a7fea 100644 --- a/lib/spring/application.rb +++ b/lib/spring/application.rb @@ -101,6 +101,8 @@ def preload end Rails::Application.initializer :ensure_reloading_is_enabled, group: :all do + next if Spring.dangerously_allow_disabling_reloading + if Rails.application.config.cache_classes config_name, set_to = if Rails.application.config.respond_to?(:enable_reloading=) ["enable_reloading", "true"] @@ -110,6 +112,8 @@ def preload raise <<-MSG.strip_heredoc Spring reloads, and therefore needs the application to have reloading enabled. Please, set config.#{config_name} to #{set_to} in config/environments/#{Rails.env}.rb. + (If you understand the trade-offs and want to disable Rails' reloader anyway, + set `Spring.dangerously_allow_disabling_reloading = true` in config/spring.rb.) MSG end end diff --git a/lib/spring/configuration.rb b/lib/spring/configuration.rb index 8476aaeb..6f2179fa 100644 --- a/lib/spring/configuration.rb +++ b/lib/spring/configuration.rb @@ -3,11 +3,17 @@ module Spring @connect_timeout = 5 @boot_timeout = 20 + @dangerously_allow_disabling_reloading = false class << self attr_accessor :application_root, :connect_timeout, :boot_timeout attr_writer :quiet + # Opt-in: skip the `:ensure_reloading_is_enabled` initializer so Spring + # boots with `config.enable_reloading = false`. Default `false`. See + # README "Running Spring with reloading disabled" for what this gives up. + attr_accessor :dangerously_allow_disabling_reloading + def gemfile require "bundler" diff --git a/test/support/acceptance_test.rb b/test/support/acceptance_test.rb index 4c263fdf..31d8a4f6 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -158,6 +158,20 @@ def without_gem(name) assert_failure "bin/rails runner 1", stderr: expected_error end + test "Spring.dangerously_allow_disabling_reloading bypasses the reloading-enabled check" do + config_path = app.path("config/environments/development.rb") + config = File.read(config_path) + if config.include?("config.cache_classes") + config.sub!(/config\.cache_classes\s*=\s*false/, "config.cache_classes = true") + else + config.sub!(/config.enable_reloading = true/, "config.enable_reloading = true\nconfig.cache_classes = true") + end + File.write(config_path, config) + File.write(app.path("config/spring.rb"), "Spring.dangerously_allow_disabling_reloading = true\n") + + assert_success "bin/rails runner 1" + end + test "test changes are picked up" do assert_speedup do assert_success app.spring_test_command, stdout: "0 failures"