puma 6.6.0 → 7.1.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +170 -5
  3. data/README.md +24 -32
  4. data/docs/fork_worker.md +5 -5
  5. data/docs/kubernetes.md +8 -6
  6. data/docs/restart.md +2 -2
  7. data/docs/signals.md +11 -11
  8. data/docs/stats.md +3 -2
  9. data/docs/systemd.md +1 -1
  10. data/ext/puma_http11/extconf.rb +2 -17
  11. data/ext/puma_http11/mini_ssl.c +18 -8
  12. data/ext/puma_http11/org/jruby/puma/Http11.java +10 -2
  13. data/ext/puma_http11/puma_http11.c +23 -11
  14. data/lib/puma/binder.rb +10 -8
  15. data/lib/puma/cli.rb +3 -5
  16. data/lib/puma/client.rb +95 -61
  17. data/lib/puma/cluster/worker.rb +9 -10
  18. data/lib/puma/cluster/worker_handle.rb +38 -7
  19. data/lib/puma/cluster.rb +41 -26
  20. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  21. data/lib/puma/commonlogger.rb +3 -3
  22. data/lib/puma/configuration.rb +89 -43
  23. data/lib/puma/const.rb +9 -10
  24. data/lib/puma/control_cli.rb +6 -2
  25. data/lib/puma/detect.rb +2 -0
  26. data/lib/puma/dsl.rb +135 -94
  27. data/lib/puma/error_logger.rb +3 -1
  28. data/lib/puma/events.rb +25 -10
  29. data/lib/puma/io_buffer.rb +8 -4
  30. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  31. data/lib/puma/launcher.rb +52 -48
  32. data/lib/puma/minissl.rb +0 -1
  33. data/lib/puma/plugin/systemd.rb +3 -3
  34. data/lib/puma/rack/urlmap.rb +1 -1
  35. data/lib/puma/reactor.rb +19 -4
  36. data/lib/puma/request.rb +45 -32
  37. data/lib/puma/runner.rb +8 -17
  38. data/lib/puma/server.rb +111 -61
  39. data/lib/puma/single.rb +5 -2
  40. data/lib/puma/state_file.rb +3 -2
  41. data/lib/puma/thread_pool.rb +47 -82
  42. data/lib/puma/util.rb +0 -7
  43. data/lib/puma.rb +10 -0
  44. data/lib/rack/handler/puma.rb +2 -2
  45. data/tools/Dockerfile +3 -1
  46. metadata +6 -4
data/lib/puma/cluster.rb CHANGED
@@ -22,7 +22,8 @@ module Puma
22
22
  @workers = []
23
23
  @next_check = Time.now
24
24
 
25
- @phased_restart = false
25
+ @worker_max = [] # keeps track of 'max' stat values
26
+ @pending_phased_restart = false
26
27
  end
27
28
 
28
29
  # Returns the list of cluster worker handles.
@@ -44,10 +45,14 @@ module Puma
44
45
  end
45
46
  end
46
47
 
47
- def start_phased_restart
48
- @events.fire_on_restart!
48
+ def start_phased_restart(refork = false)
49
+ @events.fire_before_restart!
49
50
  @phase += 1
50
- log "- Starting phased worker restart, phase: #{@phase}"
51
+ if refork
52
+ log "- Starting worker refork, phase: #{@phase}"
53
+ else
54
+ log "- Starting phased worker restart, phase: #{@phase}"
55
+ end
51
56
 
52
57
  # Be sure to change the directory again before loading
53
58
  # the app. This way we can pick up new code.
@@ -166,7 +171,7 @@ module Puma
166
171
  (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
167
172
  end
168
173
 
169
- def check_workers
174
+ def check_workers(refork = false)
170
175
  return if @next_check >= Time.now
171
176
 
172
177
  @next_check = Time.now + @options[:worker_check_interval]
@@ -184,7 +189,12 @@ module Puma
184
189
  w = @workers.find { |x| x.phase != @phase }
185
190
 
186
191
  if w
187
- log "- Stopping #{w.pid} for phased upgrade..."
192
+ if refork
193
+ log "- Stopping #{w.pid} for refork..."
194
+ else
195
+ log "- Stopping #{w.pid} for phased upgrade..."
196
+ end
197
+
188
198
  unless w.term?
189
199
  w.term
190
200
  log "- #{w.signal} sent to #{w.pid}..."
@@ -228,7 +238,7 @@ module Puma
228
238
  def phased_restart(refork = false)
229
239
  return false if @options[:preload_app] && !refork
230
240
 
231
- @phased_restart = true
241
+ @pending_phased_restart = refork ? :refork : true
232
242
  wakeup!
233
243
 
234
244
  true
@@ -258,11 +268,14 @@ module Puma
258
268
  end
259
269
 
260
270
  # Inside of a child process, this will return all zeroes, as @workers is only populated in
261
- # the master process.
271
+ # the master process. Calling this also resets stat 'max' values to zero.
262
272
  # @!attribute [r] stats
273
+ # @return [Hash]
274
+
263
275
  def stats
264
276
  old_worker_count = @workers.count { |w| w.phase != @phase }
265
277
  worker_status = @workers.map do |w|
278
+ w.reset_max
266
279
  {
267
280
  started_at: utc_iso8601(w.started_at),
268
281
  pid: w.pid,
@@ -273,7 +286,6 @@ module Puma
273
286
  last_status: w.last_status,
274
287
  }
275
288
  end
276
-
277
289
  {
278
290
  started_at: utc_iso8601(@started_at),
279
291
  workers: @workers.size,
@@ -342,7 +354,7 @@ module Puma
342
354
 
343
355
  stop_workers
344
356
  stop
345
- @events.fire_on_stopped!
357
+ @events.fire_after_stopped!
346
358
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
347
359
  exit 0 # Clean exit, workers were stopped
348
360
  end
@@ -359,16 +371,12 @@ module Puma
359
371
 
360
372
  if preload?
361
373
  # Threads explicitly marked as fork safe will be ignored. Used in Rails,
362
- # but may be used by anyone. Note that we need to explicit
363
- # Process::Waiter check here because there's a bug in Ruby 2.6 and below
364
- # where calling thread_variable_get on a Process::Waiter will segfault.
365
- # We can drop that clause once those versions of Ruby are no longer
366
- # supported.
367
- fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
374
+ # but may be used by anyone.
375
+ fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
368
376
 
369
377
  before = Thread.list.reject(&fork_safe)
370
378
 
371
- log "* Restarts: (\u2714) hot (\u2716) phased"
379
+ log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
372
380
  log "* Preloading application"
373
381
  load_and_bind
374
382
 
@@ -386,7 +394,7 @@ module Puma
386
394
  end
387
395
  end
388
396
  else
389
- log "* Restarts: (\u2714) hot (\u2714) phased"
397
+ log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
390
398
 
391
399
  unless @config.app_configured?
392
400
  error "No application configured, nothing to run"
@@ -413,6 +421,7 @@ module Puma
413
421
 
414
422
  log "Use Ctrl-C to stop"
415
423
 
424
+ warn_ruby_mn_threads
416
425
  single_worker_warning
417
426
 
418
427
  redirect_io
@@ -447,14 +456,18 @@ module Puma
447
456
  break
448
457
  end
449
458
 
450
- if @phased_restart
451
- start_phased_restart
452
- @phased_restart = false
453
- in_phased_restart = true
459
+ if @pending_phased_restart
460
+ start_phased_restart(@pending_phased_restart == :refork)
461
+
462
+ in_phased_restart = @pending_phased_restart
463
+ @pending_phased_restart = false
464
+
454
465
  workers_not_booted = @options[:workers]
466
+ # worker 0 is not restarted on refork
467
+ workers_not_booted -= 1 if in_phased_restart == :refork
455
468
  end
456
469
 
457
- check_workers
470
+ check_workers(in_phased_restart == :refork)
458
471
 
459
472
  if read.wait_readable([0, @next_check - Time.now].max)
460
473
  req = read.read_nonblock(1)
@@ -497,7 +510,7 @@ module Puma
497
510
  end
498
511
 
499
512
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
500
- @events.fire_on_booted!
513
+ @events.fire_after_booted!
501
514
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
502
515
  booted = true
503
516
  end
@@ -514,7 +527,7 @@ module Puma
514
527
  end
515
528
 
516
529
  if in_phased_restart && workers_not_booted.zero?
517
- @events.fire_on_booted!
530
+ @events.fire_after_booted!
518
531
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
519
532
  in_phased_restart = false
520
533
  end
@@ -570,7 +583,9 @@ module Puma
570
583
  # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
571
584
  # 2. When `fork_worker` is enabled, some worker may not be direct children,
572
585
  # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
573
- if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
586
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
587
+ w.process_status = status
588
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
574
589
  true
575
590
  else
576
591
  w.term if w.term?
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ # Calculate a delay value for sleeping when running in clustered mode
5
+ #
6
+ # The main reason this is a class is so it can be unit tested independently.
7
+ # This makes modification easier in the future if we can encode properties of the
8
+ # delay into a test instead of relying on end-to-end testing only.
9
+ #
10
+ # This is an imprecise mechanism to address specific goals:
11
+ #
12
+ # - Evenly distribute requests across all workers at start
13
+ # - Evenly distribute CPU resources across all workers
14
+ #
15
+ # ## Goal: Distribute requests across workers at start
16
+ #
17
+ # There was a perf bug in Puma where one worker would wake up slightly before the rest and accept
18
+ # all the requests on the socket even though it didn't have enough resources to process all of them.
19
+ # This was originally fixed by never calling accept when a worker had more requests than threads
20
+ # already https://github.com/puma/puma/pull/3678/files/2736ebddb3fc8528e5150b5913fba251c37a8bf7#diff-a95f46e7ce116caddc9b9a9aa81004246d5210d5da5f4df90a818c780630166bL251-L291
21
+ #
22
+ # With the introduction of true keepalive support, there are two ways a request can come in:
23
+ # - A new request from a new client comes into the socket and it must be "accept"-ed
24
+ # - A keepalive request is served and the connection is retained. Another request is then accepted
25
+ #
26
+ # Ideally the server handles requests in the order they come in, and ideally it doesn't accept more requests than it can handle.
27
+ # These goals are contradictory, because when the server is at maximum capacity due to keepalive connections, it could mean we
28
+ # block all new requests, even if those came in before the new request on the older keepalive connection.
29
+ #
30
+ # ## Goal: Distribute CPU resources across all workers
31
+ #
32
+ # - This issue was opened https://github.com/puma/puma/issues/2078
33
+ #
34
+ # There are several entangled issues and it's not exactly clear what the root cause is, but the observable outcome
35
+ # was that performance was better with a small sleep, and that eventually became the default.
36
+ #
37
+ # An attempt to describe why this works is here: https://github.com/puma/puma/issues/2078#issuecomment-3287032470.
38
+ #
39
+ # Summarizing: The delay is for tuning the rate at which "accept" is called on the socket.
40
+ # Puma works by calling "accept" nonblock on the socket in a loop. When there are multiple workers
41
+ # (processes), they will "race" to accept a request at roughly the same rate. However, if one
42
+ # worker has all threads busy processing requests, then accepting a new request might "steal" it from
43
+ # a less busy worker. If a worker has no work to do, it should loop as fast as possible.
44
+ #
45
+ # ## Solution: Distribute requests across workers at start
46
+ #
47
+ # For now, both goals are framed as "load balancing" across workers (processes) and achieved through
48
+ # the same mechanism of sleeping longer to delay busier workers. Rather than the prior Puma 6.x
49
+ # and earlier behavior of using a binary on/off sleep value, we increase it an amount proportional
50
+ # to the load the server is under, capping the maximum delay to the scenario where all threads are busy
51
+ # and the todo list has reached a multiplier of the maximum number of threads.
52
+ #
53
+ # Private: API may change unexpectedly
54
+ class ClusterAcceptLoopDelay
55
+ attr_reader :max_delay
56
+
57
+ # Initialize happens once, `call` happens often. Perform global calculations here.
58
+ def initialize(
59
+ # Number of workers in the cluster
60
+ workers: ,
61
+ # Maximum delay in seconds i.e. 0.005 is 5 milliseconds
62
+ max_delay:
63
+ )
64
+ @on = max_delay > 0 && workers >= 2
65
+ @max_delay = max_delay.to_f
66
+
67
+ # Reach maximum delay when `max_threads * overload_multiplier` is reached in the system
68
+ @overload_multiplier = 25.0
69
+ end
70
+
71
+ def on?
72
+ @on
73
+ end
74
+
75
+ # We want the extreme values of this delay to be known (minimum and maximum) as well as
76
+ # a predictable curve between the two. i.e. no step functions or hard cliffs.
77
+ #
78
+ # Return value is always numeric. Returns 0 if there should be no delay.
79
+ def calculate(
80
+ # Number of threads working right now, plus number of requests in the todo list
81
+ busy_threads_plus_todo:,
82
+ # Maximum number of threads in the pool, note that the busy threads (alone) may go over this value at times
83
+ # if the pool needs to be reaped. The busy thread plus todo count may go over this value by a large amount.
84
+ max_threads:
85
+ )
86
+ max_value = @overload_multiplier * max_threads
87
+ # Approaches max delay when `busy_threads_plus_todo` approaches `max_value`
88
+ return max_delay * busy_threads_plus_todo.clamp(0, max_value) / max_value
89
+ end
90
+ end
91
+ end
@@ -29,13 +29,13 @@ module Puma
29
29
 
30
30
  CONTENT_LENGTH = 'Content-Length' # should be lower case from app,
31
31
  # Util::HeaderHash allows mixed
32
- HTTP_VERSION = Const::HTTP_VERSION
33
32
  HTTP_X_FORWARDED_FOR = Const::HTTP_X_FORWARDED_FOR
34
33
  PATH_INFO = Const::PATH_INFO
35
34
  QUERY_STRING = Const::QUERY_STRING
36
35
  REMOTE_ADDR = Const::REMOTE_ADDR
37
36
  REMOTE_USER = 'REMOTE_USER'
38
37
  REQUEST_METHOD = Const::REQUEST_METHOD
38
+ SERVER_PROTOCOL = Const::SERVER_PROTOCOL
39
39
 
40
40
  def initialize(app, logger=nil)
41
41
  @app = app
@@ -70,7 +70,7 @@ module Puma
70
70
  env[REQUEST_METHOD],
71
71
  env[PATH_INFO],
72
72
  env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
73
- env[HTTP_VERSION],
73
+ env[SERVER_PROTOCOL],
74
74
  now - began_at ]
75
75
 
76
76
  write(msg)
@@ -87,7 +87,7 @@ module Puma
87
87
  env[REQUEST_METHOD],
88
88
  env[PATH_INFO],
89
89
  env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
90
- env[HTTP_VERSION],
90
+ env[SERVER_PROTOCOL],
91
91
  status.to_s[0..3],
92
92
  length,
93
93
  now - began_at ]
@@ -3,6 +3,7 @@
3
3
  require_relative 'plugin'
4
4
  require_relative 'const'
5
5
  require_relative 'dsl'
6
+ require_relative 'events'
6
7
 
7
8
  module Puma
8
9
  # A class used for storing "leveled" configuration options.
@@ -112,7 +113,7 @@ module Puma
112
113
  # config = Configuration.new({}) do |user_config, file_config, default_config|
113
114
  # user_config.port 3003
114
115
  # end
115
- # config.load
116
+ # config.clamp
116
117
  # puts config.options[:port]
117
118
  # # => 3003
118
119
  #
@@ -125,10 +126,13 @@ module Puma
125
126
  # is done because an environment variable may have been modified while loading
126
127
  # configuration files.
127
128
  class Configuration
129
+ class NotLoadedError < StandardError; end
130
+ class NotClampedError < StandardError; end
131
+
128
132
  DEFAULTS = {
129
133
  auto_trim_time: 30,
130
134
  binds: ['tcp://0.0.0.0:9292'.freeze],
131
- clean_thread_locals: false,
135
+ fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
132
136
  debug: false,
133
137
  enable_keep_alives: true,
134
138
  early_hints: nil,
@@ -140,19 +144,18 @@ module Puma
140
144
  io_selector_backend: :auto,
141
145
  log_requests: false,
142
146
  logger: STDOUT,
143
- # How many requests to attempt inline before sending a client back to
144
- # the reactor to be subject to normal ordering. The idea here is that
145
- # we amortize the cost of going back to the reactor for a well behaved
146
- # but very "greedy" client across 10 requests. This prevents a not
147
- # well behaved client from monopolizing the thread forever.
148
- max_fast_inline: 10,
147
+ # Limits how many requests a keep alive connection can make.
148
+ # The connection will be closed after it reaches `max_keep_alive`
149
+ # requests.
150
+ max_keep_alive: 999,
149
151
  max_threads: Puma.mri? ? 5 : 16,
150
152
  min_threads: 0,
151
153
  mode: :http,
152
154
  mutate_stdout_and_stderr_to_sync_on_write: true,
153
155
  out_of_band: [],
154
156
  # Number of seconds for another request within a persistent session.
155
- persistent_timeout: 20,
157
+ persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
158
+ prune_bundler: false,
156
159
  queue_requests: true,
157
160
  rackup: 'config.ru'.freeze,
158
161
  raise_exception_on_sigterm: true,
@@ -176,24 +179,31 @@ module Puma
176
179
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
177
180
  default_options = self.puma_default_options(env).merge(default_options)
178
181
 
179
- @options = UserFileDefaultOptions.new(user_options, default_options)
182
+ @_options = UserFileDefaultOptions.new(user_options, default_options)
180
183
  @plugins = PluginLoader.new
181
- @user_dsl = DSL.new(@options.user_options, self)
182
- @file_dsl = DSL.new(@options.file_options, self)
183
- @default_dsl = DSL.new(@options.default_options, self)
184
-
185
- if !@options[:prune_bundler]
186
- default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
187
- end
184
+ @events = @_options[:events] || Events.new
185
+ @hooks = {}
186
+ @user_dsl = DSL.new(@_options.user_options, self)
187
+ @file_dsl = DSL.new(@_options.file_options, self)
188
+ @default_dsl = DSL.new(@_options.default_options, self)
188
189
 
189
190
  @puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
190
191
 
191
192
  if block
192
193
  configure(&block)
193
194
  end
195
+
196
+ @loaded = false
197
+ @clamped = false
194
198
  end
195
199
 
196
- attr_reader :options, :plugins
200
+ attr_reader :plugins, :events, :hooks
201
+
202
+ def options
203
+ raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
204
+
205
+ @_options
206
+ end
197
207
 
198
208
  def configure
199
209
  yield @user_dsl, @file_dsl, @default_dsl
@@ -206,7 +216,7 @@ module Puma
206
216
  def initialize_copy(other)
207
217
  @conf = nil
208
218
  @cli_options = nil
209
- @options = @options.dup
219
+ @_options = @_options.dup
210
220
  end
211
221
 
212
222
  def flatten
@@ -214,7 +224,7 @@ module Puma
214
224
  end
215
225
 
216
226
  def flatten!
217
- @options = @options.flatten
227
+ @_options = @_options.flatten
218
228
  self
219
229
  end
220
230
 
@@ -227,6 +237,7 @@ module Puma
227
237
  def puma_options_from_env(env = ENV)
228
238
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
229
239
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
240
+ persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
230
241
  workers = if env['WEB_CONCURRENCY'] == 'auto'
231
242
  require_processor_counter
232
243
  ::Concurrent.available_processor_count
@@ -237,24 +248,27 @@ module Puma
237
248
  {
238
249
  min_threads: min && min != "" && Integer(min),
239
250
  max_threads: max && max != "" && Integer(max),
251
+ persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
240
252
  workers: workers && workers != "" && Integer(workers),
241
253
  environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
242
254
  }
243
255
  end
244
256
 
245
257
  def load
258
+ @loaded = true
246
259
  config_files.each { |config_file| @file_dsl._load_from(config_file) }
247
-
248
- @options
260
+ @_options
249
261
  end
250
262
 
251
263
  def config_files
252
- files = @options.all_of(:config_files)
264
+ raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded
265
+
266
+ files = @_options.all_of(:config_files)
253
267
 
254
268
  return [] if files == ['-']
255
269
  return files if files.any?
256
270
 
257
- first_default_file = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find do |f|
271
+ first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
258
272
  File.exist?(f)
259
273
  end
260
274
 
@@ -262,9 +276,16 @@ module Puma
262
276
  end
263
277
 
264
278
  # Call once all configuration (included from rackup files)
265
- # is loaded to flesh out any defaults
279
+ # is loaded to finalize defaults and lock in the configuration.
280
+ #
281
+ # This also calls load if it hasn't been called yet.
266
282
  def clamp
267
- @options.finalize_values
283
+ load unless @loaded
284
+ set_conditional_default_options
285
+ @_options.finalize_values
286
+ @clamped = true
287
+ warn_hooks
288
+ options
268
289
  end
269
290
 
270
291
  # Injects the Configuration object into the env
@@ -283,11 +304,11 @@ module Puma
283
304
  # Indicate if there is a properly configured app
284
305
  #
285
306
  def app_configured?
286
- @options[:app] || File.exist?(rackup)
307
+ options[:app] || File.exist?(rackup)
287
308
  end
288
309
 
289
310
  def rackup
290
- @options[:rackup]
311
+ options[:rackup]
291
312
  end
292
313
 
293
314
  # Load the specified rackup file, pull options from
@@ -296,9 +317,9 @@ module Puma
296
317
  def app
297
318
  found = options[:app] || load_rackup
298
319
 
299
- if @options[:log_requests]
320
+ if options[:log_requests]
300
321
  require_relative 'commonlogger'
301
- logger = @options[:logger]
322
+ logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
302
323
  found = CommonLogger.new(found, logger)
303
324
  end
304
325
 
@@ -307,7 +328,7 @@ module Puma
307
328
 
308
329
  # Return which environment we're running in
309
330
  def environment
310
- @options[:environment]
331
+ options[:environment]
311
332
  end
312
333
 
313
334
  def load_plugin(name)
@@ -315,18 +336,19 @@ module Puma
315
336
  end
316
337
 
317
338
  # @param key [:Symbol] hook to run
318
- # @param arg [Launcher, Int] `:on_restart` passes Launcher
339
+ # @param arg [Launcher, Int] `:before_restart` passes Launcher
319
340
  #
320
341
  def run_hooks(key, arg, log_writer, hook_data = nil)
321
342
  log_writer.debug "Running #{key} hooks"
322
343
 
323
- @options.all_of(key).each do |b|
344
+ options.all_of(key).each do |hook_options|
324
345
  begin
325
- if Array === b
326
- hook_data[b[1]] ||= Hash.new
327
- b[0].call arg, hook_data[b[1]]
346
+ block = hook_options[:block]
347
+ if id = hook_options[:id]
348
+ hook_data[id] ||= Hash.new
349
+ block.call arg, hook_data[id]
328
350
  else
329
- b.call arg
351
+ block.call arg
330
352
  end
331
353
  rescue => e
332
354
  log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
@@ -336,7 +358,7 @@ module Puma
336
358
  end
337
359
 
338
360
  def final_options
339
- @options.final_options
361
+ options.final_options
340
362
  end
341
363
 
342
364
  def self.temp_path
@@ -346,6 +368,12 @@ module Puma
346
368
  "#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
347
369
  end
348
370
 
371
+ def self.random_token
372
+ require 'securerandom' unless defined?(SecureRandom)
373
+
374
+ SecureRandom.hex(16)
375
+ end
376
+
349
377
  private
350
378
 
351
379
  def require_processor_counter
@@ -386,22 +414,40 @@ module Puma
386
414
  rack_app, rack_options = rack_builder.parse_file(rackup)
387
415
  rack_options = rack_options || {}
388
416
 
389
- @options.file_options.merge!(rack_options)
417
+ options.file_options.merge!(rack_options)
390
418
 
391
419
  config_ru_binds = []
392
420
  rack_options.each do |k, v|
393
421
  config_ru_binds << v if k.to_s.start_with?("bind")
394
422
  end
395
423
 
396
- @options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
424
+ options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
397
425
 
398
426
  rack_app
399
427
  end
400
428
 
401
- def self.random_token
402
- require 'securerandom' unless defined?(SecureRandom)
429
+ def set_conditional_default_options
430
+ @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
431
+ (@_options[:workers] > 1) && Puma.forkable?
432
+ end
403
433
 
404
- SecureRandom.hex(16)
434
+ def warn_hooks
435
+ return if options[:workers] > 0
436
+ return if options[:silence_fork_callback_warning]
437
+
438
+ log_writer = LogWriter.stdio
439
+ @hooks.each_key do |hook|
440
+ options.all_of(hook).each do |hook_options|
441
+ next unless hook_options[:cluster_only]
442
+
443
+ log_writer.log(<<~MSG.tr("\n", " "))
444
+ Warning: The code in the `#{hook}` block will not execute
445
+ in the current Puma configuration. The `#{hook}` block only
446
+ executes in Puma's cluster mode. To fix this, either remove the
447
+ `#{hook}` call or increase Puma's worker count above zero.
448
+ MSG
449
+ end
450
+ end
405
451
  end
406
452
  end
407
453
  end
data/lib/puma/const.rb CHANGED
@@ -100,13 +100,11 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "6.6.0"
104
- CODE_NAME = "Return to Forever"
103
+ PUMA_VERSION = VERSION = "7.1.0"
104
+ CODE_NAME = "Neon Witch"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
108
- FAST_TRACK_KA_TIMEOUT = 0.2
109
-
110
108
  # How long to wait when getting some write blocking on the socket when
111
109
  # sending data back
112
110
  WRITE_TIMEOUT = 10
@@ -125,9 +123,9 @@ module Puma
125
123
  # Indicate that we couldn't parse the request
126
124
  400 => "HTTP/1.1 400 Bad Request\r\n\r\n",
127
125
  # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
128
- 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n",
126
+ 404 => "HTTP/1.1 404 Not Found\r\nconnection: close\r\n\r\n",
129
127
  # The standard empty 408 response for requests that timed out.
130
- 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n",
128
+ 408 => "HTTP/1.1 408 Request Timeout\r\nconnection: close\r\n\r\n",
131
129
  # Indicate that there was an internal error, obviously.
132
130
  500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n",
133
131
  # Incorrect or invalid header value
@@ -230,6 +228,7 @@ module Puma
230
228
  RACK_INPUT = "rack.input"
231
229
  RACK_URL_SCHEME = "rack.url_scheme"
232
230
  RACK_AFTER_REPLY = "rack.after_reply"
231
+ RACK_RESPONSE_FINISHED = "rack.response_finished"
233
232
  PUMA_SOCKET = "puma.socket"
234
233
  PUMA_CONFIG = "puma.config"
235
234
  PUMA_PEERCERT = "puma.peercert"
@@ -252,14 +251,14 @@ module Puma
252
251
  KEEP_ALIVE = "keep-alive"
253
252
 
254
253
  CONTENT_LENGTH2 = "content-length"
255
- CONTENT_LENGTH_S = "Content-Length: "
254
+ CONTENT_LENGTH_S = "content-length: "
256
255
  TRANSFER_ENCODING = "transfer-encoding"
257
256
  TRANSFER_ENCODING2 = "HTTP_TRANSFER_ENCODING"
258
257
 
259
- CONNECTION_CLOSE = "Connection: close\r\n"
260
- CONNECTION_KEEP_ALIVE = "Connection: Keep-Alive\r\n"
258
+ CONNECTION_CLOSE = "connection: close\r\n"
259
+ CONNECTION_KEEP_ALIVE = "connection: keep-alive\r\n"
261
260
 
262
- TRANSFER_ENCODING_CHUNKED = "Transfer-Encoding: chunked\r\n"
261
+ TRANSFER_ENCODING_CHUNKED = "transfer-encoding: chunked\r\n"
263
262
  CLOSE_CHUNKED = "0\r\n\r\n"
264
263
 
265
264
  CHUNKED = "chunked"
@@ -124,11 +124,15 @@ module Puma
124
124
  end
125
125
 
126
126
  if @config_file
127
+ # needed because neither `Puma::CLI` or `Puma::Server` are loaded
128
+ require_relative '../puma'
129
+
127
130
  require_relative 'configuration'
128
131
  require_relative 'log_writer'
129
132
 
130
133
  config = Puma::Configuration.new({ config_files: [@config_file] }, {} , env)
131
- config.load
134
+ config.clamp
135
+
132
136
  @state ||= config.options[:state]
133
137
  @control_url ||= config.options[:control_url]
134
138
  @control_auth_token ||= config.options[:control_auth_token]
@@ -248,7 +252,7 @@ module Puma
248
252
  @stdout.flush unless @stdout.sync
249
253
  return
250
254
  elsif sig.start_with? 'SIG'
251
- if Signal.list.key? sig.sub(/\ASIG/, '')
255
+ if Signal.list.key? sig.delete_prefix('SIG')
252
256
  Process.kill sig, @pid
253
257
  else
254
258
  raise "Signal '#{sig}' not available'"