spring 4.3.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e92617adaa3e6c31e4d466d76894c94ec17a46fd7206864501218795ac8998d
4
- data.tar.gz: aee2d23a222df8747942652dda1d6a725f59bb68b395fbd37a9084c5ed75fe7b
3
+ metadata.gz: a37d78748e084596d151cde2918d30799c8dfe9e10e029920ece9e9eee8118ad
4
+ data.tar.gz: c47e0897d3938b7350245854672812436505379cc498638c3eca9e2d52537fe6
5
5
  SHA512:
6
- metadata.gz: 16e03232e4453b528812c925c4186360e1157b10aab0d663d1dc3d4a80a7d4bdf1da8f06d31c6ace1456fa4f6f5e20b2c62637ce0a23da6cae705b56baa6882b
7
- data.tar.gz: 1a4c70a381783cc3f96372e28efc6b702b3b75a28c31a1a88eacace55c3a3cbd2aee3d3ff81236526366149a612754c22d9ec0861bc0a3dfb8669108f9bb0aba
6
+ metadata.gz: cda176e1dd4d22cabb680cb85c4fd562c256f8fb3dc04f9064fc61f910cfb384f044f63bee74a424501505b62f80e0746f6c05bddf327c6be0150f12cc678a3f
7
+ data.tar.gz: e8b1675241b3fc42c52eabb9e2f501d4abae16c35ae3e3d19080de332f3d28b258b7245c127264326871f09c35c333a3b71fcfb8fbbfecdc87648a72e1328c14
data/README.md CHANGED
@@ -16,8 +16,8 @@ boot it every time you run a test, rake task or migration.
16
16
 
17
17
  ## Compatibility
18
18
 
19
- * Ruby versions: MRI 2.7, MRI 3.0, MRI 3.1, MRI 3.2
20
- * Rails versions: 6.0, 6.1, 7.0
19
+ * Ruby versions: MRI 3.1+
20
+ * Rails versions: 7.1+
21
21
  * Bundler v2.1+
22
22
 
23
23
  Spring makes extensive use of `Process.fork`, so won't be able to
@@ -79,6 +79,31 @@ Spring manages. That setting is typically configured in
79
79
  Note: in versions of Rails before 7.1, the setting is called `cache_classes`,
80
80
  and it needs to be `false` for Spring to work.
81
81
 
82
+ #### Running Spring with reloading disabled (advanced, risky)
83
+
84
+ If you know what you are doing, and if every reloadable resource is configured
85
+ to materialize *lazily in the forked child*, not in the Spring server process,
86
+ then you can decide to run Spring without Rail's in-process reloaders.
87
+
88
+ To opt into this, set in `config/spring.rb`:
89
+
90
+ ```ruby
91
+ Spring.dangerously_allow_disabling_reloading = true
92
+ ```
93
+
94
+ If you set this, you should make sure that you are NOT pre-loading reloadable
95
+ resource in the Spring server. One approach is to add boot-time assertions at
96
+ the end of `Spring.after_environment_load`, e.g.:
97
+
98
+ ```ruby
99
+ Spring.after_environment_load do
100
+ raise "routes were drawn at boot" if Rails.application.routes_reloader.loaded
101
+ raise "i18n locales where loaded at boot" unless I18n.backend.instance_variable_get(:@translations).nil?
102
+ end
103
+ ```
104
+
105
+ When unsure, leave this disabled.
106
+
82
107
  ### Usage
83
108
 
84
109
  For this walkthrough I've generated a new Rails application, and run
@@ -334,7 +359,8 @@ server process is started, it can be used to add new top-level commands.
334
359
  Spring must know how to find your Rails application. If you have a
335
360
  normal app everything works out of the box. If you are working on a
336
361
  project with a special setup (an engine for example), you must tell
337
- Spring where your app is located:
362
+ Spring where your app is located by specifying the below code in a
363
+ `config/spring.rb` file relative to your engines root directory :
338
364
 
339
365
  ```ruby
340
366
  Spring.application_root = './test/dummy'
@@ -101,16 +101,28 @@ module Spring
101
101
  end
102
102
 
103
103
  Rails::Application.initializer :ensure_reloading_is_enabled, group: :all do
104
+ next if Spring.dangerously_allow_disabling_reloading
105
+
104
106
  if Rails.application.config.cache_classes
107
+ config_name, set_to = if Rails.application.config.respond_to?(:enable_reloading=)
108
+ ["enable_reloading", "true"]
109
+ else
110
+ ["cache_classes", "false"]
111
+ end
105
112
  raise <<-MSG.strip_heredoc
106
113
  Spring reloads, and therefore needs the application to have reloading enabled.
107
- Please, set config.cache_classes to false in config/environments/#{Rails.env}.rb.
114
+ Please, set config.#{config_name} to #{set_to} in config/environments/#{Rails.env}.rb.
115
+ (If you understand the trade-offs and want to disable Rails' reloader anyway,
116
+ set `Spring.dangerously_allow_disabling_reloading = true` in config/spring.rb.)
108
117
  MSG
109
118
  end
110
119
  end
111
120
 
112
121
  require Spring.application_root_path.join("config", "environment")
113
122
 
123
+ invoke_after_environment_load_callbacks
124
+ preload_framework_base_classes
125
+
114
126
  disconnect_database
115
127
 
116
128
  @preloaded = :success
@@ -124,8 +136,9 @@ module Spring
124
136
 
125
137
  if defined?(Rails) && Rails.application
126
138
  watcher.add Rails.application.paths["config/initializers"]
127
- Rails::Engine.descendants.each do |engine|
128
- if engine.root.to_s.start_with?(Rails.root.to_s)
139
+ rails_root = Rails.root.to_s
140
+ Rails::Engine.subclasses.each do |engine|
141
+ if engine.root.to_s.start_with?(rails_root)
129
142
  watcher.add engine.paths["config/initializers"].expanded
130
143
  end
131
144
  end
@@ -136,6 +149,19 @@ module Spring
136
149
  end
137
150
  end
138
151
 
152
+ # Eagerly autoload framework base classes
153
+ FRAMEWORK_BASE_CLASSES = %w[
154
+ ActionMailer::Base
155
+ ActionController::Base
156
+ ActionController::API
157
+ ].freeze
158
+
159
+ def preload_framework_base_classes
160
+ FRAMEWORK_BASE_CLASSES.each do |const|
161
+ Object.const_get(const) if Object.const_defined?(const)
162
+ end
163
+ end
164
+
139
165
  def eager_preload
140
166
  with_pty do
141
167
  # we can't see stderr and there could be issues when it's overflown
@@ -177,7 +203,7 @@ module Spring
177
203
  client.puts(0) # preload success
178
204
  rescue Exception
179
205
  log "preload failed"
180
- client.puts(1) # preload failure
206
+ ignore_client_disconnect { client.puts(1) } # preload failure
181
207
  raise
182
208
  end
183
209
  end
@@ -238,15 +264,19 @@ module Spring
238
264
 
239
265
  wait pid, streams, client
240
266
  rescue Exception => e
241
- log "exception: #{e}"
267
+ if e.is_a?(Errno::EPIPE)
268
+ log "client disconnected (#{e.message}), ignoring command"
269
+ else
270
+ log "exception: #{e}"
271
+ end
242
272
  manager.puts unless pid
243
273
 
244
274
  if streams && !e.is_a?(SystemExit)
245
- print_exception(stderr, e)
246
- streams.each(&:close)
275
+ ignore_client_disconnect { print_exception(stderr, e) }
276
+ streams.each { |stream| ignore_client_disconnect { stream.close } }
247
277
  end
248
278
 
249
- client.puts(1) if pid
279
+ ignore_client_disconnect { client.puts(1) if pid }
250
280
  client.close
251
281
  ensure
252
282
  # Redirect STDOUT and STDERR to prevent from keeping the original FDs
@@ -294,6 +324,12 @@ module Spring
294
324
  end
295
325
  end
296
326
 
327
+ def invoke_after_environment_load_callbacks
328
+ Spring.after_environment_load_callbacks.each do |callback|
329
+ callback.call
330
+ end
331
+ end
332
+
297
333
  def loaded_application_features
298
334
  root = Spring.application_root_path.to_s
299
335
  $LOADED_FEATURES.select { |f| f.start_with?(root) }
@@ -320,6 +356,7 @@ module Spring
320
356
  if $!
321
357
  lib = File.expand_path("..", __FILE__)
322
358
  $!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
359
+ $!.backtrace_locations.reject! { |line| line.path&.start_with?(lib) } unless $!.backtrace_locations.frozen?
323
360
  end
324
361
  end
325
362
  end
@@ -331,6 +368,7 @@ module Spring
331
368
  if $!
332
369
  lib = File.expand_path("..", __FILE__)
333
370
  $!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
371
+ $!.backtrace_locations.reject! { |line| line.path&.start_with?(lib) } unless $!.backtrace_locations.frozen?
334
372
  end
335
373
  end
336
374
  end
@@ -370,10 +408,10 @@ module Spring
370
408
  Spring.failsafe_thread {
371
409
  begin
372
410
  _, status = Process.wait2 pid
373
- log "#{pid} exited with #{status.exitstatus}"
411
+ log "#{pid} exited with #{status.exitstatus || status.inspect}"
374
412
 
375
413
  streams.each(&:close)
376
- client.puts(status.exitstatus)
414
+ client.puts(status.exitstatus || status.to_i)
377
415
  client.close
378
416
  ensure
379
417
  @mutex.synchronize { @waiting.delete pid }
@@ -395,6 +433,15 @@ module Spring
395
433
 
396
434
  private
397
435
 
436
+ # Tolerate Errno::EPIPE on writes to the client socket. Once the client
437
+ # disconnects, every subsequent write to it raises — that's expected
438
+ # during `serve`'s status reporting; we'd be talking to a process
439
+ # that's gone.
440
+ def ignore_client_disconnect
441
+ yield
442
+ rescue Errno::EPIPE
443
+ end
444
+
398
445
  def active_record_configured?
399
446
  defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
400
447
  end
@@ -42,7 +42,7 @@ module Spring
42
42
  if alive?
43
43
  begin
44
44
  yield
45
- rescue Errno::ECONNRESET, Errno::EPIPE
45
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
46
46
  # The child has died but has not been collected by the wait thread yet,
47
47
  # so start a new child and try again.
48
48
  log "child dead; starting"
@@ -102,7 +102,7 @@ module Spring
102
102
  "RACK_ENV" => app_env,
103
103
  "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
104
104
  "SPRING_PRELOAD" => preload ? "1" : "0",
105
- "SPRING_SPAWN_ENV" => JSON.dump(spawn_env),
105
+ "SPRING_SPAWN_ENV" => JSON.dump(spawn_env.compact),
106
106
  **spawn_env,
107
107
  },
108
108
  "ruby",
@@ -26,7 +26,7 @@ module Spring
26
26
  if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
27
27
  require "bundler"
28
28
 
29
- Bundler.locked_gems.specs.find { |spec| spec.name == "spring" }&.tap do |spring|
29
+ Bundler.definition.requested_specs.find { |spec| spec.name == "spring" }&.tap do |spring|
30
30
  Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
31
31
  gem "spring", spring.version
32
32
  require "spring/binstub"
@@ -6,6 +6,7 @@ module Spring
6
6
  module Client
7
7
  class Run < Command
8
8
  FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
9
+ ServerReadTimeout = Class.new(StandardError)
9
10
 
10
11
  attr_reader :server
11
12
 
@@ -120,13 +121,27 @@ module Spring
120
121
  end
121
122
 
122
123
  def verify_server_version
123
- unless IO.select([server], [], [], Spring.connect_timeout)
124
- raise "Error connecting to Spring server"
124
+ begin
125
+ line = read_server_line
126
+ rescue ServerReadTimeout
127
+ if waiting_for_server_boot?
128
+ begin
129
+ # Try again, but with same timeout as booting, as server might still be booting
130
+ # from another client starting it.
131
+ line = read_server_line(Spring.boot_timeout)
132
+ rescue ServerReadTimeout
133
+ reboot_or_raise_connection_error
134
+ return
135
+ end
136
+ else
137
+ reboot_or_raise_connection_error
138
+ return
139
+ end
125
140
  end
126
141
 
127
- line = server.gets
128
- unless line
129
- raise "Error connecting to Spring server"
142
+ if line.nil?
143
+ reboot_or_raise_connection_error
144
+ return
130
145
  end
131
146
 
132
147
  server_version = line.chomp
@@ -145,6 +160,25 @@ module Spring
145
160
  end
146
161
  end
147
162
 
163
+ def read_server_line(timeout = Spring.connect_timeout)
164
+ raise ServerReadTimeout if IO.select([server], [], [], timeout).nil?
165
+
166
+ server.gets
167
+ end
168
+
169
+ def waiting_for_server_boot?
170
+ !server_booted? && env.server_running?
171
+ end
172
+
173
+ def reboot_or_raise_connection_error
174
+ if server_booted?
175
+ raise "Error connecting to Spring server"
176
+ else
177
+ stop_server
178
+ cold_run
179
+ end
180
+ end
181
+
148
182
  def connect_to_application(client)
149
183
  server.send_io client
150
184
  send_json server, "args" => args, "default_rails_env" => default_rails_env, "spawn_env" => spawn_env, "reset_env" => reset_env
@@ -184,11 +218,16 @@ module Spring
184
218
  suspend_resume_on_tstp_cont(pid)
185
219
 
186
220
  forward_signals(application)
187
- status = application.read.to_i
221
+ status = application.read
222
+ log "got exit status #{status.inspect}"
188
223
 
189
- log "got exit status #{status}"
224
+ # Status should always be an integer. If it is empty, something unexpected must have happened to the server.
225
+ if status.to_s.strip.empty?
226
+ log "unexpected empty exit status, app crashed?"
227
+ exit 1
228
+ end
190
229
 
191
- exit status
230
+ exit status.to_i
192
231
  else
193
232
  log "got no pid"
194
233
  exit 1
@@ -249,7 +288,9 @@ module Spring
249
288
  end
250
289
 
251
290
  def spawn_env
252
- ENV.slice(*Spring.spawn_on_env)
291
+ Spring.spawn_on_env.to_h do |key|
292
+ [key, ENV[key]]
293
+ end
253
294
  end
254
295
  end
255
296
  end
@@ -3,11 +3,17 @@ require "spring/errors"
3
3
  module Spring
4
4
  @connect_timeout = 5
5
5
  @boot_timeout = 20
6
+ @dangerously_allow_disabling_reloading = false
6
7
 
7
8
  class << self
8
9
  attr_accessor :application_root, :connect_timeout, :boot_timeout
9
10
  attr_writer :quiet
10
11
 
12
+ # Opt-in: skip the `:ensure_reloading_is_enabled` initializer so Spring
13
+ # boots with `config.enable_reloading = false`. Default `false`. See
14
+ # README "Running Spring with reloading disabled" for what this gives up.
15
+ attr_accessor :dangerously_allow_disabling_reloading
16
+
11
17
  def gemfile
12
18
  require "bundler"
13
19
 
@@ -35,6 +41,14 @@ module Spring
35
41
  after_fork_callbacks << block
36
42
  end
37
43
 
44
+ def after_environment_load_callbacks
45
+ @after_environment_load_callbacks ||= []
46
+ end
47
+
48
+ def after_environment_load(&block)
49
+ after_environment_load_callbacks << block
50
+ end
51
+
38
52
  def spawn_on_env
39
53
  @spawn_on_env ||= []
40
54
  end
data/lib/spring/json.rb CHANGED
@@ -13,6 +13,7 @@
13
13
  module Spring
14
14
  module JSON
15
15
  def self.load(string)
16
+ string = string.dup.force_encoding("utf-8") unless string.encoding == Encoding::UTF_8
16
17
  OkJson.decode(string)
17
18
  end
18
19
 
data/lib/spring/server.rb CHANGED
@@ -74,6 +74,8 @@ module Spring
74
74
  end
75
75
  rescue SocketError => e
76
76
  raise e unless client.eof?
77
+ rescue Errno::EPIPE => e
78
+ log "client disconnected with error #{e.message}, ignoring command"
77
79
  ensure
78
80
  redirect_output
79
81
  end
@@ -1,3 +1,3 @@
1
1
  module Spring
2
- VERSION = "4.3.0"
2
+ VERSION = "4.7.0"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spring
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Leighton
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Preloads your application so things like console, rake and tests run
13
13
  faster
@@ -56,6 +56,7 @@ licenses:
56
56
  - MIT
57
57
  metadata:
58
58
  rubygems_mfa_required: 'true'
59
+ changelog_uri: https://github.com/rails/spring/blob/main/CHANGELOG.md
59
60
  rdoc_options: []
60
61
  require_paths:
61
62
  - lib
@@ -63,14 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
64
  requirements:
64
65
  - - ">="
65
66
  - !ruby/object:Gem::Version
66
- version: 2.7.0
67
+ version: 3.1.0
67
68
  required_rubygems_version: !ruby/object:Gem::Requirement
68
69
  requirements:
69
70
  - - ">="
70
71
  - !ruby/object:Gem::Version
71
72
  version: '0'
72
73
  requirements: []
73
- rubygems_version: 3.6.2
74
+ rubygems_version: 3.6.7
74
75
  specification_version: 4
75
76
  summary: Rails application preloader
76
77
  test_files: []