puma 6.6.0 → 7.2.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +248 -5
  3. data/README.md +40 -40
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +5 -5
  6. data/docs/jungle/README.md +1 -1
  7. data/docs/kubernetes.md +11 -16
  8. data/docs/plugins.md +2 -2
  9. data/docs/restart.md +2 -2
  10. data/docs/signals.md +21 -21
  11. data/docs/stats.md +4 -3
  12. data/docs/systemd.md +4 -4
  13. data/ext/puma_http11/extconf.rb +2 -17
  14. data/ext/puma_http11/mini_ssl.c +18 -8
  15. data/ext/puma_http11/org/jruby/puma/Http11.java +10 -2
  16. data/ext/puma_http11/puma_http11.c +122 -118
  17. data/lib/puma/app/status.rb +10 -2
  18. data/lib/puma/binder.rb +10 -8
  19. data/lib/puma/cli.rb +3 -5
  20. data/lib/puma/client.rb +122 -72
  21. data/lib/puma/cluster/worker.rb +17 -17
  22. data/lib/puma/cluster/worker_handle.rb +38 -7
  23. data/lib/puma/cluster.rb +43 -29
  24. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  25. data/lib/puma/commonlogger.rb +3 -3
  26. data/lib/puma/configuration.rb +104 -51
  27. data/lib/puma/const.rb +11 -11
  28. data/lib/puma/control_cli.rb +6 -2
  29. data/lib/puma/detect.rb +2 -0
  30. data/lib/puma/dsl.rb +151 -100
  31. data/lib/puma/error_logger.rb +3 -1
  32. data/lib/puma/events.rb +25 -10
  33. data/lib/puma/io_buffer.rb +8 -4
  34. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  35. data/lib/puma/launcher.rb +54 -49
  36. data/lib/puma/minissl.rb +0 -1
  37. data/lib/puma/plugin/systemd.rb +3 -3
  38. data/lib/puma/rack/urlmap.rb +1 -1
  39. data/lib/puma/reactor.rb +19 -13
  40. data/lib/puma/request.rb +54 -39
  41. data/lib/puma/runner.rb +9 -18
  42. data/lib/puma/server.rb +114 -64
  43. data/lib/puma/single.rb +7 -4
  44. data/lib/puma/state_file.rb +3 -2
  45. data/lib/puma/thread_pool.rb +47 -82
  46. data/lib/puma/util.rb +0 -7
  47. data/lib/puma.rb +10 -0
  48. data/lib/rack/handler/puma.rb +2 -2
  49. data/tools/Dockerfile +15 -5
  50. metadata +7 -6
  51. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/client.rb CHANGED
@@ -1,13 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class IO
4
- # We need to use this for a jruby work around on both 1.8 and 1.9.
5
- # So this either creates the constant (on 1.8), or harmlessly
6
- # reopens it (on 1.9).
7
- module WaitReadable
8
- end
9
- end
10
-
11
3
  require_relative 'detect'
12
4
  require_relative 'io_buffer'
13
5
  require 'tempfile'
@@ -64,6 +56,11 @@ module Puma
64
56
 
65
57
  TE_ERR_MSG = 'Invalid Transfer-Encoding'
66
58
 
59
+ # See:
60
+ # https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
61
+ # https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
62
+ STRIP_OWS = /\A[ \t]+|[ \t]+\z/
63
+
67
64
  # The object used for a request with no body. All requests with
68
65
  # no body share this one object since it has no state.
69
66
  EmptyBody = NullIO.new
@@ -111,7 +108,8 @@ module Puma
111
108
  end
112
109
 
113
110
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
114
- :tempfile, :io_buffer, :http_content_length_limit_exceeded
111
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
112
+ :requests_served
115
113
 
116
114
  attr_writer :peerip, :http_content_length_limit
117
115
 
@@ -133,9 +131,9 @@ module Puma
133
131
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
134
132
  end
135
133
 
136
- # For the hijack protocol (allows us to just put the Client object
137
- # into the env)
138
- def call
134
+ # For the full hijack protocol, `env['rack.hijack']` is set to
135
+ # `client.method :full_hijack`
136
+ def full_hijack
139
137
  @hijacked = true
140
138
  env[HIJACK_IO] ||= @io
141
139
  end
@@ -150,15 +148,16 @@ module Puma
150
148
  end
151
149
 
152
150
  # Number of seconds until the timeout elapses.
151
+ # @!attribute [r] timeout
153
152
  def timeout
154
153
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
155
154
  end
156
155
 
157
- def reset(fast_check=true)
156
+ def reset
158
157
  @parser.reset
159
158
  @io_buffer.reset
160
159
  @read_header = true
161
- @read_proxy = !!@expect_proxy_proto
160
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
162
161
  @env = @proto_env.dup
163
162
  @parsed_bytes = 0
164
163
  @ready = false
@@ -166,11 +165,14 @@ module Puma
166
165
  @peerip = nil if @remote_addr_header
167
166
  @in_last_chunk = false
168
167
  @http_content_length_limit_exceeded = false
168
+ end
169
169
 
170
+ # only used with back-to-back requests contained in the buffer
171
+ def process_back_to_back_requests
170
172
  if @buffer
171
173
  return false unless try_to_parse_proxy_protocol
172
174
 
173
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
175
+ @parsed_bytes = parser_execute
174
176
 
175
177
  if @parser.finished?
176
178
  return setup_body
@@ -178,25 +180,20 @@ module Puma
178
180
  raise HttpParserError,
179
181
  "HEADER is longer than allowed, aborting client early."
180
182
  end
181
-
182
- return false
183
- else
184
- begin
185
- if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
186
- return try_to_finish
187
- end
188
- rescue IOError
189
- # swallow it
190
- end
191
183
  end
192
184
  end
193
185
 
186
+ # if a client sends back-to-back requests, the buffer may contain one or more
187
+ # of them.
188
+ def has_back_to_back_requests?
189
+ !(@buffer.nil? || @buffer.empty?)
190
+ end
191
+
194
192
  def close
195
193
  tempfile_close
196
194
  begin
197
195
  @io.close
198
196
  rescue IOError, Errno::EBADF
199
- Puma::Util.purge_interrupt_queue
200
197
  end
201
198
  end
202
199
 
@@ -214,20 +211,36 @@ module Puma
214
211
  def try_to_parse_proxy_protocol
215
212
  if @read_proxy
216
213
  if @expect_proxy_proto == :v1
217
- if @buffer.include? "\r\n"
218
- if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
219
- if md[1]
220
- @peerip = md[1].split(" ")[0]
214
+ crlf_index = @buffer.index "\r\n"
215
+
216
+ unless crlf_index
217
+ if "PROXY ".start_with? @buffer
218
+ return false
219
+ elsif @buffer.start_with? "PROXY "
220
+ if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
221
+ raise ConnectionError, "PROXY protocol v1 line is too long"
221
222
  end
222
- @buffer = md.post_match
223
+ return false
223
224
  end
224
- # if the buffer has a \r\n but doesn't have a PROXY protocol
225
- # request, this is just HTTP from a non-PROXY client; move on
225
+
226
226
  @read_proxy = false
227
- return @buffer.size > 0
228
- else
229
- return false
227
+ return true
230
228
  end
229
+
230
+ if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
231
+ raise ConnectionError, "PROXY protocol v1 line is too long"
232
+ end
233
+
234
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
235
+ if md[1]
236
+ @peerip = md[1].split(" ")[0]
237
+ end
238
+ @buffer = md.post_match
239
+ end
240
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
241
+ # request, this is just HTTP from a non-PROXY client; move on
242
+ @read_proxy = false
243
+ return @buffer.size > 0
231
244
  end
232
245
  end
233
246
  true
@@ -273,26 +286,28 @@ module Puma
273
286
 
274
287
  return false unless try_to_parse_proxy_protocol
275
288
 
276
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
289
+ @parsed_bytes = parser_execute
277
290
 
278
291
  if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
279
292
  @http_content_length_limit_exceeded = true
280
293
  end
281
294
 
282
295
  if @parser.finished?
283
- return setup_body
296
+ setup_body
284
297
  elsif @parsed_bytes >= MAX_HEADER
285
298
  raise HttpParserError,
286
299
  "HEADER is longer than allowed, aborting client early."
300
+ else
301
+ false
287
302
  end
288
-
289
- false
290
303
  end
291
304
 
292
305
  def eagerly_finish
293
306
  return true if @ready
294
- return false unless @to_io.wait_readable(0)
295
- try_to_finish
307
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
308
+ return true if try_to_finish
309
+ end
310
+ false
296
311
  end
297
312
 
298
313
  def finish(timeout)
@@ -300,6 +315,44 @@ module Puma
300
315
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
301
316
  end
302
317
 
318
+ # Wraps `@parser.execute` and adds meaningful error messages
319
+ # @return [Integer] bytes of buffer read by parser
320
+ #
321
+ def parser_execute
322
+ @parser.execute(@env, @buffer, @parsed_bytes)
323
+ rescue => e
324
+ @env[HTTP_CONNECTION] = 'close'
325
+ raise e unless HttpParserError === e && e.message.include?('non-SSL')
326
+
327
+ req, _ = @buffer.split "\r\n\r\n"
328
+ request_line, headers = req.split "\r\n", 2
329
+
330
+ # below checks for request issues and changes error message accordingly
331
+ if !@env.key? REQUEST_METHOD
332
+ if request_line.count(' ') != 2
333
+ # maybe this is an SSL connection ?
334
+ raise e
335
+ else
336
+ method = request_line[/\A[^ ]+/]
337
+ raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
338
+ end
339
+ elsif !@env.key? REQUEST_PATH
340
+ path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
341
+ raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
342
+ elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
343
+ query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
344
+ raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
345
+ elsif !@env.key? SERVER_PROTOCOL
346
+ # protocol is bad
347
+ text = request_line[/[^ ]*\z/]
348
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
349
+ elsif !headers.empty?
350
+ # headers are bad
351
+ hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
352
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
353
+ end
354
+ end
355
+
303
356
  def timeout!
304
357
  write_error(408) if in_data_phase
305
358
  raise ConnectionError
@@ -374,17 +427,18 @@ module Puma
374
427
  if te
375
428
  te_lwr = te.downcase
376
429
  if te.include? ','
377
- te_ary = te_lwr.split ','
430
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
378
431
  te_count = te_ary.count CHUNKED
379
432
  te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
380
- if te_ary.last == CHUNKED && te_count == 1 && te_valid
381
- @env.delete TRANSFER_ENCODING2
382
- return setup_chunked_body body
383
- elsif te_count >= 1
433
+ if te_count > 1
384
434
  raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
435
+ elsif te_ary.last != CHUNKED
436
+ raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
385
437
  elsif !te_valid
386
438
  raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
387
439
  end
440
+ @env.delete TRANSFER_ENCODING2
441
+ return setup_chunked_body body
388
442
  elsif te_lwr == CHUNKED
389
443
  @env.delete TRANSFER_ENCODING2
390
444
  return setup_chunked_body body
@@ -462,40 +516,36 @@ module Puma
462
516
  # after this
463
517
  remain = @body_remain
464
518
 
465
- if remain > CHUNK_SIZE
466
- want = CHUNK_SIZE
467
- else
468
- want = remain
469
- end
519
+ # don't bother with reading zero bytes
520
+ unless remain.zero?
521
+ begin
522
+ chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
523
+ rescue IO::WaitReadable
524
+ return false
525
+ rescue SystemCallError, IOError
526
+ raise ConnectionError, "Connection error detected during read"
527
+ end
470
528
 
471
- begin
472
- chunk = @io.read_nonblock(want, @read_buffer)
473
- rescue IO::WaitReadable
474
- return false
475
- rescue SystemCallError, IOError
476
- raise ConnectionError, "Connection error detected during read"
477
- end
529
+ # No chunk means a closed socket
530
+ unless chunk
531
+ @body.close
532
+ @buffer = nil
533
+ set_ready
534
+ raise EOFError
535
+ end
478
536
 
479
- # No chunk means a closed socket
480
- unless chunk
481
- @body.close
482
- @buffer = nil
483
- set_ready
484
- raise EOFError
537
+ remain -= @body.write(chunk)
485
538
  end
486
539
 
487
- remain -= @body.write(chunk)
488
-
489
540
  if remain <= 0
490
541
  @body.rewind
491
542
  @buffer = nil
492
543
  set_ready
493
- return true
544
+ true
545
+ else
546
+ @body_remain = remain
547
+ false
494
548
  end
495
-
496
- @body_remain = remain
497
-
498
- false
499
549
  end
500
550
 
501
551
  def read_chunked_body
@@ -14,7 +14,7 @@ module Puma
14
14
  class Worker < Puma::Runner # :nodoc:
15
15
  attr_reader :index, :master
16
16
 
17
- def initialize(index:, master:, launcher:, pipes:, server: nil)
17
+ def initialize(index:, master:, launcher:, pipes:, app: nil)
18
18
  super(launcher)
19
19
 
20
20
  @index = index
@@ -23,7 +23,8 @@ module Puma
23
23
  @worker_write = pipes[:worker_write]
24
24
  @fork_pipe = pipes[:fork_pipe]
25
25
  @wakeup = pipes[:wakeup]
26
- @server = server
26
+ @app = app
27
+ @server = nil
27
28
  @hook_data = {}
28
29
  end
29
30
 
@@ -57,7 +58,7 @@ module Puma
57
58
  @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
58
59
 
59
60
  begin
60
- server = @server ||= start_server
61
+ @server = start_server
61
62
  rescue Exception => e
62
63
  log "! Unable to start worker"
63
64
  log e
@@ -85,7 +86,7 @@ module Puma
85
86
  if idx == -1 # stop server
86
87
  if restart_server.length > 0
87
88
  restart_server.clear
88
- server.begin_restart(true)
89
+ @server.begin_restart(true)
89
90
  @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
90
91
  end
91
92
  elsif idx == -2 # refork cycle is done
@@ -103,20 +104,19 @@ module Puma
103
104
  Signal.trap "SIGTERM" do
104
105
  @worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
105
106
  restart_server.clear
106
- server.stop
107
+ @server.stop
107
108
  restart_server << false
108
109
  end
109
110
 
110
111
  begin
111
112
  @worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
112
113
  rescue SystemCallError, IOError
113
- Puma::Util.purge_interrupt_queue
114
114
  STDERR.puts "Master seems to have exited, exiting."
115
115
  return
116
116
  end
117
117
 
118
118
  while restart_server.pop
119
- server_thread = server.run
119
+ server_thread = @server.run
120
120
 
121
121
  if @log_writer.debug? && index == 0
122
122
  debug_loaded_extensions "Loaded Extensions - worker 0:"
@@ -128,16 +128,16 @@ module Puma
128
128
 
129
129
  while true
130
130
  begin
131
- b = server.backlog || 0
132
- r = server.running || 0
133
- t = server.pool_capacity || 0
134
- m = server.max_threads || 0
135
- rc = server.requests_count || 0
136
- bt = server.busy_threads || 0
137
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads":#{m}, "requests_count":#{rc}, "busy_threads":#{bt} }\n!
138
- io << payload
131
+ payload = base_payload.dup
132
+
133
+ hsh = @server.stats
134
+ hsh.each do |k, v|
135
+ payload << %Q! "#{k}":#{v || 0},!
136
+ end
137
+ # sub call properly adds 'closing' string
138
+ io << payload.sub(/,\z/, " }\n")
139
+ @server.reset_max
139
140
  rescue IOError
140
- Puma::Util.purge_interrupt_queue
141
141
  break
142
142
  end
143
143
  sleep @options[:worker_check_interval]
@@ -165,7 +165,7 @@ module Puma
165
165
  launcher: @launcher,
166
166
  pipes: { check_pipe: @check_pipe,
167
167
  worker_write: @worker_write },
168
- server: @server
168
+ app: @app
169
169
  new_worker.run
170
170
  end
171
171
 
@@ -4,13 +4,15 @@ module Puma
4
4
  class Cluster < Runner
5
5
  #—————————————————————— DO NOT USE — this class is for internal use only ———
6
6
 
7
-
8
7
  # This class represents a worker process from the perspective of the puma
9
8
  # master process. It contains information about the process and its health
10
9
  # and it exposes methods to control the process via IPC. It does not
11
10
  # include the actual logic executed by the worker process itself. For that,
12
11
  # see Puma::Cluster::Worker.
13
12
  class WorkerHandle # :nodoc:
13
+ # array of stat 'max' keys
14
+ WORKER_MAX_KEYS = [:backlog_max, :reactor_max]
15
+
14
16
  def initialize(idx, pid, phase, options)
15
17
  @index = idx
16
18
  @pid = pid
@@ -23,12 +25,13 @@ module Puma
23
25
  @last_checkin = Time.now
24
26
  @last_status = {}
25
27
  @term = false
28
+ @worker_max = Array.new WORKER_MAX_KEYS.length, 0
26
29
  end
27
30
 
28
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
31
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at, :process_status
29
32
 
30
33
  # @version 5.0.0
31
- attr_writer :pid, :phase
34
+ attr_writer :pid, :phase, :process_status
32
35
 
33
36
  def booted?
34
37
  @stage == :booted
@@ -51,12 +54,40 @@ module Puma
51
54
  @term
52
55
  end
53
56
 
54
- STATUS_PATTERN = /{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads":(?<max_threads>\d*), "requests_count":(?<requests_count>\d*), "busy_threads":(?<busy_threads>\d*) }/
55
- private_constant :STATUS_PATTERN
56
-
57
57
  def ping!(status)
58
+ hsh = {}
59
+ k, v = nil, nil
60
+ status.tr('}{"', '').strip.split(", ") do |kv|
61
+ cntr = 0
62
+ kv.split(':') do |t|
63
+ if cntr == 0
64
+ k = t
65
+ cntr = 1
66
+ else
67
+ v = t
68
+ end
69
+ end
70
+ hsh[k.to_sym] = v.to_i
71
+ end
72
+
73
+ # check stat max values, we can't signal workers to reset the max values,
74
+ # so we do so here
75
+ WORKER_MAX_KEYS.each_with_index do |key, idx|
76
+ next unless hsh[key]
77
+
78
+ if hsh[key] < @worker_max[idx]
79
+ hsh[key] = @worker_max[idx]
80
+ else
81
+ @worker_max[idx] = hsh[key]
82
+ end
83
+ end
58
84
  @last_checkin = Time.now
59
- @last_status = status.match(STATUS_PATTERN).named_captures.map { |c_name, c| [c_name.to_sym, c.to_i] }.to_h
85
+ @last_status = hsh
86
+ end
87
+
88
+ # Resets max values to zero. Called whenever `Cluster#stats` is called
89
+ def reset_max
90
+ WORKER_MAX_KEYS.length.times { |idx| @worker_max[idx] = 0 }
60
91
  end
61
92
 
62
93
  # @see Puma::Cluster#check_workers
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]
@@ -181,10 +186,15 @@ module Puma
181
186
  # we need to phase any workers out (which will restart
182
187
  # in the right phase).
183
188
  #
184
- w = @workers.find { |x| x.phase != @phase }
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}..."
@@ -211,12 +221,11 @@ module Puma
211
221
  pipes[:wakeup] = @wakeup
212
222
  end
213
223
 
214
- server = start_server if preload?
215
224
  new_worker = Worker.new index: index,
216
225
  master: master,
217
226
  launcher: @launcher,
218
227
  pipes: pipes,
219
- server: server
228
+ app: (app if preload?)
220
229
  new_worker.run
221
230
  end
222
231
 
@@ -228,7 +237,7 @@ module Puma
228
237
  def phased_restart(refork = false)
229
238
  return false if @options[:preload_app] && !refork
230
239
 
231
- @phased_restart = true
240
+ @pending_phased_restart = refork ? :refork : true
232
241
  wakeup!
233
242
 
234
243
  true
@@ -258,11 +267,14 @@ module Puma
258
267
  end
259
268
 
260
269
  # Inside of a child process, this will return all zeroes, as @workers is only populated in
261
- # the master process.
270
+ # the master process. Calling this also resets stat 'max' values to zero.
262
271
  # @!attribute [r] stats
272
+ # @return [Hash]
273
+
263
274
  def stats
264
275
  old_worker_count = @workers.count { |w| w.phase != @phase }
265
276
  worker_status = @workers.map do |w|
277
+ w.reset_max
266
278
  {
267
279
  started_at: utc_iso8601(w.started_at),
268
280
  pid: w.pid,
@@ -273,7 +285,6 @@ module Puma
273
285
  last_status: w.last_status,
274
286
  }
275
287
  end
276
-
277
288
  {
278
289
  started_at: utc_iso8601(@started_at),
279
290
  workers: @workers.size,
@@ -342,7 +353,7 @@ module Puma
342
353
 
343
354
  stop_workers
344
355
  stop
345
- @events.fire_on_stopped!
356
+ @events.fire_after_stopped!
346
357
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
347
358
  exit 0 # Clean exit, workers were stopped
348
359
  end
@@ -359,16 +370,12 @@ module Puma
359
370
 
360
371
  if preload?
361
372
  # 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) }
373
+ # but may be used by anyone.
374
+ fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
368
375
 
369
376
  before = Thread.list.reject(&fork_safe)
370
377
 
371
- log "* Restarts: (\u2714) hot (\u2716) phased"
378
+ log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
372
379
  log "* Preloading application"
373
380
  load_and_bind
374
381
 
@@ -386,7 +393,7 @@ module Puma
386
393
  end
387
394
  end
388
395
  else
389
- log "* Restarts: (\u2714) hot (\u2714) phased"
396
+ log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
390
397
 
391
398
  unless @config.app_configured?
392
399
  error "No application configured, nothing to run"
@@ -413,6 +420,7 @@ module Puma
413
420
 
414
421
  log "Use Ctrl-C to stop"
415
422
 
423
+ warn_ruby_mn_threads
416
424
  single_worker_warning
417
425
 
418
426
  redirect_io
@@ -447,14 +455,18 @@ module Puma
447
455
  break
448
456
  end
449
457
 
450
- if @phased_restart
451
- start_phased_restart
452
- @phased_restart = false
453
- in_phased_restart = true
458
+ if @pending_phased_restart
459
+ start_phased_restart(@pending_phased_restart == :refork)
460
+
461
+ in_phased_restart = @pending_phased_restart
462
+ @pending_phased_restart = false
463
+
454
464
  workers_not_booted = @options[:workers]
465
+ # worker 0 is not restarted on refork
466
+ workers_not_booted -= 1 if in_phased_restart == :refork
455
467
  end
456
468
 
457
- check_workers
469
+ check_workers(in_phased_restart == :refork)
458
470
 
459
471
  if read.wait_readable([0, @next_check - Time.now].max)
460
472
  req = read.read_nonblock(1)
@@ -497,7 +509,7 @@ module Puma
497
509
  end
498
510
 
499
511
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
500
- @events.fire_on_booted!
512
+ @events.fire_after_booted!
501
513
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
502
514
  booted = true
503
515
  end
@@ -514,7 +526,7 @@ module Puma
514
526
  end
515
527
 
516
528
  if in_phased_restart && workers_not_booted.zero?
517
- @events.fire_on_booted!
529
+ @events.fire_after_booted!
518
530
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
519
531
  in_phased_restart = false
520
532
  end
@@ -570,7 +582,9 @@ module Puma
570
582
  # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
571
583
  # 2. When `fork_worker` is enabled, some worker may not be direct children,
572
584
  # 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)
585
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
586
+ w.process_status = status
587
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
574
588
  true
575
589
  else
576
590
  w.term if w.term?