puma 5.6.8 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +322 -16
  3. data/README.md +79 -29
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +1 -3
  7. data/docs/kubernetes.md +12 -0
  8. data/docs/nginx.md +1 -1
  9. data/docs/restart.md +1 -0
  10. data/docs/systemd.md +3 -6
  11. data/docs/testing_benchmarks_local_files.md +150 -0
  12. data/docs/testing_test_rackup_ci_files.md +36 -0
  13. data/ext/puma_http11/extconf.rb +16 -9
  14. data/ext/puma_http11/http11_parser.c +1 -1
  15. data/ext/puma_http11/http11_parser.h +1 -1
  16. data/ext/puma_http11/http11_parser.java.rl +2 -2
  17. data/ext/puma_http11/http11_parser.rl +2 -2
  18. data/ext/puma_http11/http11_parser_common.rl +2 -2
  19. data/ext/puma_http11/mini_ssl.c +127 -19
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  21. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  23. data/ext/puma_http11/puma_http11.c +17 -9
  24. data/lib/puma/app/status.rb +4 -4
  25. data/lib/puma/binder.rb +50 -53
  26. data/lib/puma/cli.rb +16 -18
  27. data/lib/puma/client.rb +59 -19
  28. data/lib/puma/cluster/worker.rb +18 -11
  29. data/lib/puma/cluster/worker_handle.rb +4 -1
  30. data/lib/puma/cluster.rb +102 -40
  31. data/lib/puma/commonlogger.rb +21 -14
  32. data/lib/puma/configuration.rb +77 -59
  33. data/lib/puma/const.rb +129 -92
  34. data/lib/puma/control_cli.rb +15 -11
  35. data/lib/puma/detect.rb +7 -4
  36. data/lib/puma/dsl.rb +250 -56
  37. data/lib/puma/error_logger.rb +18 -9
  38. data/lib/puma/events.rb +6 -126
  39. data/lib/puma/io_buffer.rb +39 -4
  40. data/lib/puma/jruby_restart.rb +2 -1
  41. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  42. data/lib/puma/launcher.rb +102 -175
  43. data/lib/puma/log_writer.rb +147 -0
  44. data/lib/puma/minissl/context_builder.rb +26 -12
  45. data/lib/puma/minissl.rb +104 -11
  46. data/lib/puma/null_io.rb +16 -2
  47. data/lib/puma/plugin/systemd.rb +90 -0
  48. data/lib/puma/plugin/tmp_restart.rb +1 -1
  49. data/lib/puma/rack/builder.rb +6 -6
  50. data/lib/puma/rack/urlmap.rb +1 -1
  51. data/lib/puma/rack_default.rb +19 -4
  52. data/lib/puma/reactor.rb +19 -10
  53. data/lib/puma/request.rb +365 -170
  54. data/lib/puma/runner.rb +56 -20
  55. data/lib/puma/sd_notify.rb +149 -0
  56. data/lib/puma/server.rb +137 -89
  57. data/lib/puma/single.rb +13 -11
  58. data/lib/puma/state_file.rb +3 -6
  59. data/lib/puma/thread_pool.rb +57 -19
  60. data/lib/puma/util.rb +0 -11
  61. data/lib/puma.rb +9 -10
  62. data/lib/rack/handler/puma.rb +113 -86
  63. data/tools/Dockerfile +2 -2
  64. metadata +9 -5
  65. data/lib/puma/queue_close.rb +0 -26
  66. data/lib/puma/systemd.rb +0 -46
  67. data/lib/rack/version_restriction.rb +0 -15
data/lib/puma/client.rb CHANGED
@@ -8,9 +8,9 @@ class IO
8
8
  end
9
9
  end
10
10
 
11
- require 'puma/detect'
11
+ require_relative 'detect'
12
+ require_relative 'io_buffer'
12
13
  require 'tempfile'
13
- require 'forwardable'
14
14
 
15
15
  if Puma::IS_JRUBY
16
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -25,6 +25,9 @@ module Puma
25
25
 
26
26
  class HttpParserError501 < IOError; end
27
27
 
28
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
29
+
30
+
28
31
  # An instance of this class represents a unique request from a client.
29
32
  # For example, this could be a web request from a browser or from CURL.
30
33
  #
@@ -38,7 +41,7 @@ module Puma
38
41
  # the header and body are fully buffered via the `try_to_finish` method.
39
42
  # They can be used to "time out" a response via the `timeout_at` reader.
40
43
  #
41
- class Client
44
+ class Client # :nodoc:
42
45
 
43
46
  # this tests all values but the last, which must be chunked
44
47
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
@@ -66,17 +69,13 @@ module Puma
66
69
  EmptyBody = NullIO.new
67
70
 
68
71
  include Puma::Const
69
- extend Forwardable
70
72
 
71
73
  def initialize(io, env=nil)
72
74
  @io = io
73
75
  @to_io = io.to_io
76
+ @io_buffer = IOBuffer.new
74
77
  @proto_env = env
75
- if !env
76
- @env = nil
77
- else
78
- @env = env.dup
79
- end
78
+ @env = env&.dup
80
79
 
81
80
  @parser = HttpParser.new
82
81
  @parsed_bytes = 0
@@ -94,7 +93,11 @@ module Puma
94
93
  @requests_served = 0
95
94
  @hijacked = false
96
95
 
96
+ @http_content_length_limit = nil
97
+ @http_content_length_limit_exceeded = false
98
+
97
99
  @peerip = nil
100
+ @peer_family = nil
98
101
  @listener = nil
99
102
  @remote_addr_header = nil
100
103
  @expect_proxy_proto = false
@@ -102,16 +105,22 @@ module Puma
102
105
  @body_remain = 0
103
106
 
104
107
  @in_last_chunk = false
108
+
109
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
110
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
105
111
  end
106
112
 
107
113
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
108
- :tempfile
114
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded
109
115
 
110
- attr_writer :peerip
116
+ attr_writer :peerip, :http_content_length_limit
111
117
 
112
118
  attr_accessor :remote_addr_header, :listener
113
119
 
114
- def_delegators :@io, :closed?
120
+ # Remove in Puma 7?
121
+ def closed?
122
+ @to_io.closed?
123
+ end
115
124
 
116
125
  # Test to see if io meets a bare minimum of functioning, @to_io needs to be
117
126
  # used for MiniSSL::Socket
@@ -147,6 +156,7 @@ module Puma
147
156
 
148
157
  def reset(fast_check=true)
149
158
  @parser.reset
159
+ @io_buffer.reset
150
160
  @read_header = true
151
161
  @read_proxy = !!@expect_proxy_proto
152
162
  @env = @proto_env.dup
@@ -157,6 +167,7 @@ module Puma
157
167
  @body_remain = 0
158
168
  @peerip = nil if @remote_addr_header
159
169
  @in_last_chunk = false
170
+ @http_content_length_limit_exceeded = false
160
171
 
161
172
  if @buffer
162
173
  return false unless try_to_parse_proxy_protocol
@@ -216,6 +227,17 @@ module Puma
216
227
  end
217
228
 
218
229
  def try_to_finish
230
+ if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
231
+ @http_content_length_limit_exceeded = true
232
+ end
233
+
234
+ if @http_content_length_limit_exceeded
235
+ @buffer = nil
236
+ @body = EmptyBody
237
+ set_ready
238
+ return true
239
+ end
240
+
219
241
  return read_body if in_data_phase
220
242
 
221
243
  begin
@@ -245,6 +267,10 @@ module Puma
245
267
 
246
268
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
247
269
 
270
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
271
+ @http_content_length_limit_exceeded = true
272
+ end
273
+
248
274
  if @parser.finished?
249
275
  return setup_body
250
276
  elsif @parsed_bytes >= MAX_HEADER
@@ -282,7 +308,7 @@ module Puma
282
308
  return @peerip if @peerip
283
309
 
284
310
  if @remote_addr_header
285
- hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
311
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
286
312
  @peerip = hdr
287
313
  return hdr
288
314
  end
@@ -290,6 +316,16 @@ module Puma
290
316
  @peerip ||= @io.peeraddr.last
291
317
  end
292
318
 
319
+ def peer_family
320
+ return @peer_family if @peer_family
321
+
322
+ @peer_family ||= begin
323
+ @io.local_address.afamily
324
+ rescue
325
+ Socket::AF_INET
326
+ end
327
+ end
328
+
293
329
  # Returns true if the persistent connection can be closed immediately
294
330
  # without waiting for the configured idle/shutdown timeout.
295
331
  # @version 5.0.0
@@ -313,7 +349,7 @@ module Puma
313
349
  private
314
350
 
315
351
  def setup_body
316
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
352
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
317
353
 
318
354
  if @env[HTTP_EXPECT] == CONTINUE
319
355
  # TODO allow a hook here to check the headers before
@@ -357,7 +393,7 @@ module Puma
357
393
 
358
394
  if cl
359
395
  # cannot contain characters that are not \d, or be empty
360
- if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
396
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
361
397
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
362
398
  end
363
399
  else
@@ -410,7 +446,7 @@ module Puma
410
446
  end
411
447
 
412
448
  begin
413
- chunk = @io.read_nonblock(want)
449
+ chunk = @io.read_nonblock(want, @read_buffer)
414
450
  rescue IO::WaitReadable
415
451
  return false
416
452
  rescue SystemCallError, IOError
@@ -442,7 +478,7 @@ module Puma
442
478
  def read_chunked_body
443
479
  while true
444
480
  begin
445
- chunk = @io.read_nonblock(4096)
481
+ chunk = @io.read_nonblock(4096, @read_buffer)
446
482
  rescue IO::WaitReadable
447
483
  return false
448
484
  rescue SystemCallError, IOError
@@ -523,7 +559,7 @@ module Puma
523
559
  # Puma doesn't process chunk extensions, but should parse if they're
524
560
  # present, which is the reason for the semicolon regex
525
561
  chunk_hex = line.strip[/\A[^;]+/]
526
- if chunk_hex =~ CHUNK_SIZE_INVALID
562
+ if CHUNK_SIZE_INVALID.match? chunk_hex
527
563
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
528
564
  end
529
565
  len = chunk_hex.to_i(16)
@@ -610,10 +646,14 @@ module Puma
610
646
 
611
647
  def set_ready
612
648
  if @body_read_start
613
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
649
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
614
650
  end
615
651
  @requests_served += 1
616
652
  @ready = true
617
653
  end
654
+
655
+ def above_http_content_limit(value)
656
+ @http_content_length_limit&.< value
657
+ end
618
658
  end
619
659
  end
@@ -2,27 +2,29 @@
2
2
 
3
3
  module Puma
4
4
  class Cluster < Puma::Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
5
8
  # This class is instantiated by the `Puma::Cluster` and represents a single
6
9
  # worker process.
7
10
  #
8
11
  # At the core of this class is running an instance of `Puma::Server` which
9
12
  # gets created via the `start_server` method from the `Puma::Runner` class
10
13
  # that this inherits from.
11
- class Worker < Puma::Runner
14
+ class Worker < Puma::Runner # :nodoc:
12
15
  attr_reader :index, :master
13
16
 
14
17
  def initialize(index:, master:, launcher:, pipes:, server: nil)
15
- super launcher, launcher.events
18
+ super(launcher)
16
19
 
17
20
  @index = index
18
21
  @master = master
19
- @launcher = launcher
20
- @options = launcher.options
21
22
  @check_pipe = pipes[:check_pipe]
22
23
  @worker_write = pipes[:worker_write]
23
24
  @fork_pipe = pipes[:fork_pipe]
24
25
  @wakeup = pipes[:wakeup]
25
26
  @server = server
27
+ @hook_data = {}
26
28
  end
27
29
 
28
30
  def run
@@ -52,13 +54,14 @@ module Puma
52
54
 
53
55
  # Invoke any worker boot hooks so they can get
54
56
  # things in shape before booting the app.
55
- @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
57
+ @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
56
58
 
57
59
  begin
58
60
  server = @server ||= start_server
59
61
  rescue Exception => e
60
62
  log "! Unable to start worker"
61
- log e.backtrace[0]
63
+ log e
64
+ log e.backtrace.join("\n ")
62
65
  exit 1
63
66
  end
64
67
 
@@ -83,8 +86,7 @@ module Puma
83
86
  if restart_server.length > 0
84
87
  restart_server.clear
85
88
  server.begin_restart(true)
86
- @launcher.config.run_hooks :before_refork, nil, @launcher.events
87
- Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
89
+ @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
88
90
  end
89
91
  elsif idx == 0 # restart server
90
92
  restart_server << true << false
@@ -113,6 +115,11 @@ module Puma
113
115
 
114
116
  while restart_server.pop
115
117
  server_thread = server.run
118
+
119
+ if @log_writer.debug? && index == 0
120
+ debug_loaded_extensions "Loaded Extensions - worker 0:"
121
+ end
122
+
116
123
  stat_thread ||= Thread.new(@worker_write) do |io|
117
124
  Puma.set_thread_name "stat pld"
118
125
  base_payload = "p#{Process.pid}"
@@ -138,7 +145,7 @@ module Puma
138
145
 
139
146
  # Invoke any worker shutdown hooks so they can prevent the worker
140
147
  # exiting until any background operations are completed
141
- @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
148
+ @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
142
149
  ensure
143
150
  @worker_write << "t#{Process.pid}\n" rescue nil
144
151
  @worker_write.close
@@ -147,7 +154,7 @@ module Puma
147
154
  private
148
155
 
149
156
  def spawn_worker(idx)
150
- @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
157
+ @config.run_hooks(:before_worker_fork, idx, @log_writer, @hook_data)
151
158
 
152
159
  pid = fork do
153
160
  new_worker = Worker.new index: idx,
@@ -165,7 +172,7 @@ module Puma
165
172
  exit! 1
166
173
  end
167
174
 
168
- @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
175
+ @config.run_hooks(:after_worker_fork, idx, @log_writer, @hook_data)
169
176
  pid
170
177
  end
171
178
  end
@@ -2,12 +2,15 @@
2
2
 
3
3
  module Puma
4
4
  class Cluster < Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
5
8
  # This class represents a worker process from the perspective of the puma
6
9
  # master process. It contains information about the process and its health
7
10
  # and it exposes methods to control the process via IPC. It does not
8
11
  # include the actual logic executed by the worker process itself. For that,
9
12
  # see Puma::Cluster::Worker.
10
- class WorkerHandle
13
+ class WorkerHandle # :nodoc:
11
14
  def initialize(idx, pid, phase, options)
12
15
  @index = idx
13
16
  @pid = pid
data/lib/puma/cluster.rb CHANGED
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/runner'
4
- require 'puma/util'
5
- require 'puma/plugin'
6
- require 'puma/cluster/worker_handle'
7
- require 'puma/cluster/worker'
8
-
9
- require 'time'
3
+ require_relative 'runner'
4
+ require_relative 'util'
5
+ require_relative 'plugin'
6
+ require_relative 'cluster/worker_handle'
7
+ require_relative 'cluster/worker'
10
8
 
11
9
  module Puma
12
10
  # This class is instantiated by the `Puma::Launcher` and used
@@ -17,8 +15,8 @@ module Puma
17
15
  # via the `spawn_workers` method call. Each worker will have it's own
18
16
  # instance of a `Puma::Server`.
19
17
  class Cluster < Runner
20
- def initialize(cli, events)
21
- super cli, events
18
+ def initialize(launcher)
19
+ super(launcher)
22
20
 
23
21
  @phase = 0
24
22
  @workers = []
@@ -27,6 +25,10 @@ module Puma
27
25
  @phased_restart = false
28
26
  end
29
27
 
28
+ # Returns the list of cluster worker handles.
29
+ # @return [Array<Puma::Cluster::WorkerHandle>]
30
+ attr_reader :workers
31
+
30
32
  def stop_workers
31
33
  log "- Gracefully shutting down workers..."
32
34
  @workers.each { |x| x.term }
@@ -83,16 +85,14 @@ module Puma
83
85
  @workers << WorkerHandle.new(idx, pid, @phase, @options)
84
86
  end
85
87
 
86
- if @options[:fork_worker] &&
87
- @workers.all? {|x| x.phase == @phase}
88
-
88
+ if @options[:fork_worker] && all_workers_in_phase?
89
89
  @fork_writer << "0\n"
90
90
  end
91
91
  end
92
92
 
93
93
  # @version 5.0.0
94
94
  def spawn_worker(idx, master)
95
- @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
95
+ @config.run_hooks(:before_worker_fork, idx, @log_writer)
96
96
 
97
97
  pid = fork { worker(idx, master) }
98
98
  if !pid
@@ -101,7 +101,7 @@ module Puma
101
101
  exit! 1
102
102
  end
103
103
 
104
- @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
104
+ @config.run_hooks(:after_worker_fork, idx, @log_writer)
105
105
  pid
106
106
  end
107
107
 
@@ -146,10 +146,22 @@ module Puma
146
146
  idx
147
147
  end
148
148
 
149
+ def worker_at(idx)
150
+ @workers.find { |w| w.index == idx }
151
+ end
152
+
149
153
  def all_workers_booted?
150
154
  @workers.count { |w| !w.booted? } == 0
151
155
  end
152
156
 
157
+ def all_workers_in_phase?
158
+ @workers.all? { |w| w.phase == @phase }
159
+ end
160
+
161
+ def all_workers_idle_timed_out?
162
+ (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
163
+ end
164
+
153
165
  def check_workers
154
166
  return if @next_check >= Time.now
155
167
 
@@ -176,10 +188,10 @@ module Puma
176
188
  end
177
189
  end
178
190
 
179
- @next_check = [
180
- @workers.reject(&:term?).map(&:ping_timeout).min,
181
- @next_check
182
- ].compact.min
191
+ t = @workers.reject(&:term?)
192
+ t.map!(&:ping_timeout)
193
+
194
+ @next_check = [t.min, @next_check].compact.min
183
195
  end
184
196
 
185
197
  def worker(index, master)
@@ -209,8 +221,8 @@ module Puma
209
221
  stop
210
222
  end
211
223
 
212
- def phased_restart
213
- return false if @options[:preload_app]
224
+ def phased_restart(refork = false)
225
+ return false if @options[:preload_app] && !refork
214
226
 
215
227
  @phased_restart = true
216
228
  wakeup!
@@ -226,7 +238,7 @@ module Puma
226
238
  def stop_blocked
227
239
  @status = :stop if @status == :run
228
240
  wakeup!
229
- @control.stop(true) if @control
241
+ @control&.stop true
230
242
  Process.waitall
231
243
  end
232
244
 
@@ -248,24 +260,24 @@ module Puma
248
260
  old_worker_count = @workers.count { |w| w.phase != @phase }
249
261
  worker_status = @workers.map do |w|
250
262
  {
251
- started_at: w.started_at.utc.iso8601,
263
+ started_at: utc_iso8601(w.started_at),
252
264
  pid: w.pid,
253
265
  index: w.index,
254
266
  phase: w.phase,
255
267
  booted: w.booted?,
256
- last_checkin: w.last_checkin.utc.iso8601,
268
+ last_checkin: utc_iso8601(w.last_checkin),
257
269
  last_status: w.last_status,
258
270
  }
259
271
  end
260
272
 
261
273
  {
262
- started_at: @started_at.utc.iso8601,
274
+ started_at: utc_iso8601(@started_at),
263
275
  workers: @workers.size,
264
276
  phase: @phase,
265
277
  booted_workers: worker_status.count { |w| w[:booted] },
266
278
  old_workers: old_worker_count,
267
279
  worker_status: worker_status,
268
- }
280
+ }.merge(super)
269
281
  end
270
282
 
271
283
  def preload?
@@ -274,10 +286,10 @@ module Puma
274
286
 
275
287
  # @version 5.0.0
276
288
  def fork_worker!
277
- if (worker = @workers.find { |w| w.index == 0 })
289
+ if (worker = worker_at 0)
278
290
  worker.phase += 1
279
291
  end
280
- phased_restart
292
+ phased_restart(true)
281
293
  end
282
294
 
283
295
  # We do this in a separate method to keep the lambda scope
@@ -290,7 +302,7 @@ module Puma
290
302
 
291
303
  # Auto-fork after the specified number of requests.
292
304
  if (fork_requests = @options[:fork_worker].to_i) > 0
293
- @launcher.events.register(:ping!) do |w|
305
+ @events.register(:ping!) do |w|
294
306
  fork_worker! if w.index == 0 &&
295
307
  w.phase == 0 &&
296
308
  w.last_status[:requests_count] >= fork_requests
@@ -336,6 +348,8 @@ module Puma
336
348
  def run
337
349
  @status = :run
338
350
 
351
+ @idle_workers = {}
352
+
339
353
  output_header "cluster"
340
354
 
341
355
  # This is aligned with the output from Runner, see Runner#output_header
@@ -372,12 +386,12 @@ module Puma
372
386
  else
373
387
  log "* Restarts: (\u2714) hot (\u2714) phased"
374
388
 
375
- unless @launcher.config.app_configured?
389
+ unless @config.app_configured?
376
390
  error "No application configured, nothing to run"
377
391
  exit 1
378
392
  end
379
393
 
380
- @launcher.binder.parse @options[:binds], self
394
+ @launcher.binder.parse @options[:binds]
381
395
  end
382
396
 
383
397
  read, @wakeup = Puma::Util.pipe
@@ -409,8 +423,9 @@ module Puma
409
423
 
410
424
  @master_read, @worker_write = read, @wakeup
411
425
 
412
- @launcher.config.run_hooks :before_fork, nil, @launcher.events
413
- Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
426
+ @options[:worker_write] = @worker_write
427
+
428
+ @config.run_hooks(:before_fork, nil, @log_writer)
414
429
 
415
430
  spawn_workers
416
431
 
@@ -425,6 +440,11 @@ module Puma
425
440
 
426
441
  while @status == :run
427
442
  begin
443
+ if all_workers_idle_timed_out?
444
+ log "- All workers reached idle timeout"
445
+ break
446
+ end
447
+
428
448
  if @phased_restart
429
449
  start_phased_restart
430
450
  @phased_restart = false
@@ -445,7 +465,7 @@ module Puma
445
465
 
446
466
  if req == "b" || req == "f"
447
467
  pid, idx = result.split(':').map(&:to_i)
448
- w = @workers.find {|x| x.index == idx}
468
+ w = worker_at idx
449
469
  w.pid = pid if w.pid.nil?
450
470
  end
451
471
 
@@ -462,22 +482,37 @@ module Puma
462
482
  when "t"
463
483
  w.term unless w.term?
464
484
  when "p"
465
- w.ping!(result.sub(/^\d+/,'').chomp)
466
- @launcher.events.fire(:ping!, w)
485
+ status = result.sub(/^\d+/,'').chomp
486
+ w.ping!(status)
487
+ @events.fire(:ping!, w)
488
+
489
+ if in_phased_restart && workers_not_booted.positive? && w0 = worker_at(0)
490
+ w0.ping!(status)
491
+ @events.fire(:ping!, w0)
492
+ end
493
+
467
494
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
468
- @launcher.events.fire_on_booted!
495
+ @events.fire_on_booted!
496
+ debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
469
497
  booted = true
470
498
  end
499
+ when "i"
500
+ if @idle_workers[pid]
501
+ @idle_workers.delete pid
502
+ else
503
+ @idle_workers[pid] = true
504
+ end
471
505
  end
472
506
  else
473
507
  log "! Out-of-sync worker list, no #{pid} worker"
474
508
  end
475
509
  end
510
+
476
511
  if in_phased_restart && workers_not_booted.zero?
477
512
  @events.fire_on_booted!
513
+ debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
478
514
  in_phased_restart = false
479
515
  end
480
-
481
516
  rescue Interrupt
482
517
  @status = :stop
483
518
  end
@@ -506,10 +541,28 @@ module Puma
506
541
  # loops thru @workers, removing workers that exited, and calling
507
542
  # `#term` if needed
508
543
  def wait_workers
544
+ # Reap all children, known workers or otherwise.
545
+ # If puma has PID 1, as it's common in containerized environments,
546
+ # then it's responsible for reaping orphaned processes, so we must reap
547
+ # all our dead children, regardless of whether they are workers we spawned
548
+ # or some reattached processes.
549
+ reaped_children = {}
550
+ loop do
551
+ begin
552
+ pid, status = Process.wait2(-1, Process::WNOHANG)
553
+ break unless pid
554
+ reaped_children[pid] = status
555
+ rescue Errno::ECHILD
556
+ break
557
+ end
558
+ end
559
+
509
560
  @workers.reject! do |w|
510
561
  next false if w.pid.nil?
511
562
  begin
512
- if Process.wait(w.pid, Process::WNOHANG)
563
+ # When `fork_worker` is enabled, some worker may not be direct children, but grand children.
564
+ # Because of this they won't be reaped by `Process.wait2(-1)`, so we need to check them individually)
565
+ if reaped_children.delete(w.pid) || (@options[:fork_worker] && Process.wait(w.pid, Process::WNOHANG))
513
566
  true
514
567
  else
515
568
  w.term if w.term?
@@ -526,6 +579,11 @@ module Puma
526
579
  end
527
580
  end
528
581
  end
582
+
583
+ # Log unknown children
584
+ reaped_children.each do |pid, status|
585
+ log "! reaped unknown child process pid=#{pid} status=#{status}"
586
+ end
529
587
  end
530
588
 
531
589
  # @version 5.0.0
@@ -533,14 +591,18 @@ module Puma
533
591
  @workers.each do |w|
534
592
  if !w.term? && w.ping_timeout <= Time.now
535
593
  details = if w.booted?
536
- "(worker failed to check in within #{@options[:worker_timeout]} seconds)"
594
+ "(Worker #{w.index} failed to check in within #{@options[:worker_timeout]} seconds)"
537
595
  else
538
- "(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
596
+ "(Worker #{w.index} failed to boot within #{@options[:worker_boot_timeout]} seconds)"
539
597
  end
540
598
  log "! Terminating timed out worker #{details}: #{w.pid}"
541
599
  w.kill
542
600
  end
543
601
  end
544
602
  end
603
+
604
+ def idle_timed_out_worker_pids
605
+ @idle_workers.keys
606
+ end
545
607
  end
546
608
  end