puma 3.12.6 → 5.3.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1400 -451
  3. data/LICENSE +23 -20
  4. data/README.md +131 -60
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +24 -19
  7. data/docs/compile_options.md +19 -0
  8. data/docs/deployment.md +38 -13
  9. data/docs/fork_worker.md +33 -0
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +20 -10
  20. data/docs/rails_dev_mode.md +29 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +7 -6
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +48 -70
  25. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  26. data/ext/puma_http11/ext_help.h +1 -1
  27. data/ext/puma_http11/extconf.rb +27 -0
  28. data/ext/puma_http11/http11_parser.c +81 -108
  29. data/ext/puma_http11/http11_parser.h +1 -1
  30. data/ext/puma_http11/http11_parser.java.rl +22 -38
  31. data/ext/puma_http11/http11_parser.rl +1 -1
  32. data/ext/puma_http11/http11_parser_common.rl +3 -3
  33. data/ext/puma_http11/mini_ssl.c +254 -91
  34. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  35. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  36. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +89 -106
  37. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +92 -22
  38. data/ext/puma_http11/puma_http11.c +34 -50
  39. data/lib/puma.rb +54 -0
  40. data/lib/puma/app/status.rb +68 -49
  41. data/lib/puma/binder.rb +191 -139
  42. data/lib/puma/cli.rb +15 -15
  43. data/lib/puma/client.rb +247 -226
  44. data/lib/puma/cluster.rb +221 -212
  45. data/lib/puma/cluster/worker.rb +183 -0
  46. data/lib/puma/cluster/worker_handle.rb +90 -0
  47. data/lib/puma/commonlogger.rb +2 -2
  48. data/lib/puma/configuration.rb +58 -51
  49. data/lib/puma/const.rb +32 -20
  50. data/lib/puma/control_cli.rb +109 -67
  51. data/lib/puma/detect.rb +24 -3
  52. data/lib/puma/dsl.rb +519 -121
  53. data/lib/puma/error_logger.rb +104 -0
  54. data/lib/puma/events.rb +55 -31
  55. data/lib/puma/io_buffer.rb +7 -5
  56. data/lib/puma/jruby_restart.rb +0 -58
  57. data/lib/puma/json.rb +96 -0
  58. data/lib/puma/launcher.rb +178 -68
  59. data/lib/puma/minissl.rb +147 -48
  60. data/lib/puma/minissl/context_builder.rb +79 -0
  61. data/lib/puma/null_io.rb +13 -1
  62. data/lib/puma/plugin.rb +6 -12
  63. data/lib/puma/plugin/tmp_restart.rb +2 -0
  64. data/lib/puma/queue_close.rb +26 -0
  65. data/lib/puma/rack/builder.rb +2 -4
  66. data/lib/puma/rack/urlmap.rb +2 -0
  67. data/lib/puma/rack_default.rb +2 -0
  68. data/lib/puma/reactor.rb +85 -316
  69. data/lib/puma/request.rb +467 -0
  70. data/lib/puma/runner.rb +31 -52
  71. data/lib/puma/server.rb +275 -726
  72. data/lib/puma/single.rb +11 -67
  73. data/lib/puma/state_file.rb +8 -3
  74. data/lib/puma/systemd.rb +46 -0
  75. data/lib/puma/thread_pool.rb +129 -81
  76. data/lib/puma/util.rb +13 -6
  77. data/lib/rack/handler/puma.rb +5 -6
  78. data/tools/Dockerfile +16 -0
  79. data/tools/trickletest.rb +0 -1
  80. metadata +45 -28
  81. data/ext/puma_http11/io_buffer.c +0 -155
  82. data/lib/puma/accept_nonblock.rb +0 -23
  83. data/lib/puma/compat.rb +0 -14
  84. data/lib/puma/convenient.rb +0 -25
  85. data/lib/puma/daemon_ext.rb +0 -33
  86. data/lib/puma/delegation.rb +0 -13
  87. data/lib/puma/java_io_buffer.rb +0 -47
  88. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  89. data/lib/puma/tcp_logger.rb +0 -41
  90. data/tools/jungle/README.md +0 -19
  91. data/tools/jungle/init.d/README.md +0 -61
  92. data/tools/jungle/init.d/puma +0 -421
  93. data/tools/jungle/init.d/run-puma +0 -18
  94. data/tools/jungle/upstart/README.md +0 -61
  95. data/tools/jungle/upstart/puma-manager.conf +0 -31
  96. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/cluster.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'puma/runner'
4
4
  require 'puma/util'
5
5
  require 'puma/plugin'
6
+ require 'puma/cluster/worker_handle'
7
+ require 'puma/cluster/worker'
6
8
 
7
9
  require 'time'
8
10
 
@@ -11,24 +13,17 @@ module Puma
11
13
  # to boot and serve a Ruby application when puma "workers" are needed
12
14
  # i.e. when using multi-processes. For example `$ puma -w 5`
13
15
  #
14
- # At the core of this class is running an instance of `Puma::Server` which
15
- # gets created via the `start_server` method from the `Puma::Runner` class
16
- # that this inherits from.
17
- #
18
16
  # An instance of this class will spawn the number of processes passed in
19
17
  # via the `spawn_workers` method call. Each worker will have it's own
20
18
  # instance of a `Puma::Server`.
21
19
  class Cluster < Runner
22
- WORKER_CHECK_INTERVAL = 5
23
-
24
20
  def initialize(cli, events)
25
21
  super cli, events
26
22
 
27
23
  @phase = 0
28
24
  @workers = []
29
- @next_check = nil
25
+ @next_check = Time.now
30
26
 
31
- @phased_state = :idle
32
27
  @phased_restart = false
33
28
  end
34
29
 
@@ -37,13 +32,18 @@ module Puma
37
32
  @workers.each { |x| x.term }
38
33
 
39
34
  begin
40
- @workers.each { |w| Process.waitpid(w.pid) }
35
+ loop do
36
+ wait_workers
37
+ break if @workers.reject {|w| w.pid.nil?}.empty?
38
+ sleep 0.2
39
+ end
41
40
  rescue Interrupt
42
41
  log "! Cancelled waiting for workers"
43
42
  end
44
43
  end
45
44
 
46
45
  def start_phased_restart
46
+ @events.fire_on_restart!
47
47
  @phase += 1
48
48
  log "- Starting phased worker restart, phase: #{@phase}"
49
49
 
@@ -60,98 +60,49 @@ module Puma
60
60
  @workers.each { |x| x.hup }
61
61
  end
62
62
 
63
- class Worker
64
- def initialize(idx, pid, phase, options)
65
- @index = idx
66
- @pid = pid
67
- @phase = phase
68
- @stage = :started
69
- @signal = "TERM"
70
- @options = options
71
- @first_term_sent = nil
72
- @last_checkin = Time.now
73
- @last_status = '{}'
74
- @dead = false
75
- end
76
-
77
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
78
-
79
- def booted?
80
- @stage == :booted
81
- end
82
-
83
- def boot!
84
- @last_checkin = Time.now
85
- @stage = :booted
86
- end
87
-
88
- def dead?
89
- @dead
90
- end
91
-
92
- def dead!
93
- @dead = true
94
- end
95
-
96
- def ping!(status)
97
- @last_checkin = Time.now
98
- @last_status = status
99
- end
100
-
101
- def ping_timeout?(which)
102
- Time.now - @last_checkin > which
103
- end
104
-
105
- def term
106
- begin
107
- if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
108
- @signal = "KILL"
109
- else
110
- @first_term_sent ||= Time.now
111
- end
112
-
113
- Process.kill @signal, @pid
114
- rescue Errno::ESRCH
115
- end
116
- end
117
-
118
- def kill
119
- Process.kill "KILL", @pid
120
- rescue Errno::ESRCH
121
- end
122
-
123
- def hup
124
- Process.kill "HUP", @pid
125
- rescue Errno::ESRCH
126
- end
127
- end
128
-
129
63
  def spawn_workers
130
64
  diff = @options[:workers] - @workers.size
131
65
  return if diff < 1
132
66
 
133
67
  master = Process.pid
68
+ if @options[:fork_worker]
69
+ @fork_writer << "-1\n"
70
+ end
134
71
 
135
72
  diff.times do
136
73
  idx = next_worker_index
137
- @launcher.config.run_hooks :before_worker_fork, idx
138
74
 
139
- pid = fork { worker(idx, master) }
140
- if !pid
141
- log "! Complete inability to spawn new workers detected"
142
- log "! Seppuku is the only choice."
143
- exit! 1
75
+ if @options[:fork_worker] && idx != 0
76
+ @fork_writer << "#{idx}\n"
77
+ pid = nil
78
+ else
79
+ pid = spawn_worker(idx, master)
144
80
  end
145
81
 
146
82
  debug "Spawned worker: #{pid}"
147
- @workers << Worker.new(idx, pid, @phase, @options)
83
+ @workers << WorkerHandle.new(idx, pid, @phase, @options)
84
+ end
85
+
86
+ if @options[:fork_worker] &&
87
+ @workers.all? {|x| x.phase == @phase}
148
88
 
149
- @launcher.config.run_hooks :after_worker_fork, idx
89
+ @fork_writer << "0\n"
150
90
  end
91
+ end
92
+
93
+ # @version 5.0.0
94
+ def spawn_worker(idx, master)
95
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
151
96
 
152
- if diff > 0
153
- @phased_state = :idle
97
+ pid = fork { worker(idx, master) }
98
+ if !pid
99
+ log "! Complete inability to spawn new workers detected"
100
+ log "! Seppuku is the only choice."
101
+ exit! 1
154
102
  end
103
+
104
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
105
+ pid
155
106
  end
156
107
 
157
108
  def cull_workers
@@ -164,11 +115,12 @@ module Puma
164
115
  debug "Workers to cull: #{workers_to_cull.inspect}"
165
116
 
166
117
  workers_to_cull.each do |worker|
167
- log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
118
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
168
119
  worker.term
169
120
  end
170
121
  end
171
122
 
123
+ # @!attribute [r] next_worker_index
172
124
  def next_worker_index
173
125
  all_positions = 0...@options[:workers]
174
126
  occupied_positions = @workers.map { |w| w.index }
@@ -180,35 +132,13 @@ module Puma
180
132
  @workers.count { |w| !w.booted? } == 0
181
133
  end
182
134
 
183
- def check_workers(force=false)
184
- return if !force && @next_check && @next_check >= Time.now
185
-
186
- @next_check = Time.now + WORKER_CHECK_INTERVAL
187
-
188
- any = false
189
-
190
- @workers.each do |w|
191
- next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
192
- if w.ping_timeout?(@options[:worker_timeout])
193
- log "! Terminating timed out worker: #{w.pid}"
194
- w.kill
195
- any = true
196
- end
197
- end
198
-
199
- # If we killed any timed out workers, try to catch them
200
- # during this loop by giving the kernel time to kill them.
201
- sleep 1 if any
202
-
203
- while @workers.any?
204
- pid = Process.waitpid(-1, Process::WNOHANG)
205
- break unless pid
206
-
207
- @workers.delete_if { |w| w.pid == pid }
208
- end
135
+ def check_workers
136
+ return if @next_check >= Time.now
209
137
 
210
- @workers.delete_if(&:dead?)
138
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
211
139
 
140
+ timeout_workers
141
+ wait_workers
212
142
  cull_workers
213
143
  spawn_workers
214
144
 
@@ -220,15 +150,18 @@ module Puma
220
150
  w = @workers.find { |x| x.phase != @phase }
221
151
 
222
152
  if w
223
- if @phased_state == :idle
224
- @phased_state = :waiting
225
- log "- Stopping #{w.pid} for phased upgrade..."
153
+ log "- Stopping #{w.pid} for phased upgrade..."
154
+ unless w.term?
155
+ w.term
156
+ log "- #{w.signal} sent to #{w.pid}..."
226
157
  end
227
-
228
- w.term
229
- log "- #{w.signal} sent to #{w.pid}..."
230
158
  end
231
159
  end
160
+
161
+ @next_check = [
162
+ @workers.reject(&:term?).map(&:ping_timeout).min,
163
+ @next_check
164
+ ].compact.min
232
165
  end
233
166
 
234
167
  def wakeup!
@@ -242,77 +175,25 @@ module Puma
242
175
  end
243
176
 
244
177
  def worker(index, master)
245
- title = "puma: cluster worker #{index}: #{master}"
246
- title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
247
- $0 = title
248
-
249
- Signal.trap "SIGINT", "IGNORE"
250
-
251
178
  @workers = []
179
+
252
180
  @master_read.close
253
181
  @suicide_pipe.close
182
+ @fork_writer.close
254
183
 
255
- Thread.new do
256
- IO.select [@check_pipe]
257
- log "! Detected parent died, dying"
258
- exit! 1
184
+ pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
185
+ if @options[:fork_worker]
186
+ pipes[:fork_pipe] = @fork_pipe
187
+ pipes[:wakeup] = @wakeup
259
188
  end
260
189
 
261
- # If we're not running under a Bundler context, then
262
- # report the info about the context we will be using
263
- if !ENV['BUNDLE_GEMFILE']
264
- if File.exist?("Gemfile")
265
- log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
266
- elsif File.exist?("gems.rb")
267
- log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
268
- end
269
- end
270
-
271
- # Invoke any worker boot hooks so they can get
272
- # things in shape before booting the app.
273
- @launcher.config.run_hooks :before_worker_boot, index
274
-
275
- server = start_server
276
-
277
- Signal.trap "SIGTERM" do
278
- server.stop
279
- end
280
-
281
- begin
282
- @worker_write << "b#{Process.pid}\n"
283
- rescue SystemCallError, IOError
284
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
285
- STDERR.puts "Master seems to have exited, exiting."
286
- return
287
- end
288
-
289
- Thread.new(@worker_write) do |io|
290
- base_payload = "p#{Process.pid}"
291
-
292
- while true
293
- sleep WORKER_CHECK_INTERVAL
294
- begin
295
- b = server.backlog || 0
296
- r = server.running || 0
297
- t = server.pool_capacity || 0
298
- m = server.max_threads || 0
299
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
300
- io << payload
301
- rescue IOError
302
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
303
- break
304
- end
305
- end
306
- end
307
-
308
- server.run.join
309
-
310
- # Invoke any worker shutdown hooks so they can prevent the worker
311
- # exiting until any background operations are completed
312
- @launcher.config.run_hooks :before_worker_shutdown, index
313
- ensure
314
- @worker_write << "t#{Process.pid}\n" rescue nil
315
- @worker_write.close
190
+ server = start_server if preload?
191
+ new_worker = Worker.new index: index,
192
+ master: master,
193
+ launcher: @launcher,
194
+ pipes: pipes,
195
+ server: server
196
+ new_worker.run
316
197
  end
317
198
 
318
199
  def restart
@@ -352,20 +233,63 @@ module Puma
352
233
  Dir.chdir dir
353
234
  end
354
235
 
236
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
237
+ # the master process.
238
+ # @!attribute [r] stats
355
239
  def stats
356
240
  old_worker_count = @workers.count { |w| w.phase != @phase }
357
- booted_worker_count = @workers.count { |w| w.booted? }
358
- worker_status = '[' + @workers.map { |w| %Q!{ "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
359
- %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
241
+ worker_status = @workers.map do |w|
242
+ {
243
+ started_at: w.started_at.utc.iso8601,
244
+ pid: w.pid,
245
+ index: w.index,
246
+ phase: w.phase,
247
+ booted: w.booted?,
248
+ last_checkin: w.last_checkin.utc.iso8601,
249
+ last_status: w.last_status,
250
+ }
251
+ end
252
+
253
+ {
254
+ started_at: @started_at.utc.iso8601,
255
+ workers: @workers.size,
256
+ phase: @phase,
257
+ booted_workers: worker_status.count { |w| w[:booted] },
258
+ old_workers: old_worker_count,
259
+ worker_status: worker_status,
260
+ }
360
261
  end
361
262
 
362
263
  def preload?
363
264
  @options[:preload_app]
364
265
  end
365
266
 
267
+ # @version 5.0.0
268
+ def fork_worker!
269
+ if (worker = @workers.find { |w| w.index == 0 })
270
+ worker.phase += 1
271
+ end
272
+ phased_restart
273
+ end
274
+
366
275
  # We do this in a separate method to keep the lambda scope
367
276
  # of the signals handlers as small as possible.
368
277
  def setup_signals
278
+ if @options[:fork_worker]
279
+ Signal.trap "SIGURG" do
280
+ fork_worker!
281
+ end
282
+
283
+ # Auto-fork after the specified number of requests.
284
+ if (fork_requests = @options[:fork_worker].to_i) > 0
285
+ @launcher.events.register(:ping!) do |w|
286
+ fork_worker! if w.index == 0 &&
287
+ w.phase == 0 &&
288
+ w.last_status[:requests_count] >= fork_requests
289
+ end
290
+ end
291
+ end
292
+
369
293
  Signal.trap "SIGCHLD" do
370
294
  wakeup!
371
295
  end
@@ -390,10 +314,13 @@ module Puma
390
314
  log "Early termination of worker"
391
315
  exit! 0
392
316
  else
317
+ @launcher.close_binder_listeners
318
+
393
319
  stop_workers
394
320
  stop
395
-
396
- raise SignalException, "SIGTERM"
321
+ @events.fire_on_stopped!
322
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
323
+ exit 0 # Clean exit, workers were stopped
397
324
  end
398
325
  end
399
326
  end
@@ -403,15 +330,25 @@ module Puma
403
330
 
404
331
  output_header "cluster"
405
332
 
406
- log "* Process workers: #{@options[:workers]}"
407
-
408
- before = Thread.list
333
+ # This is aligned with the output from Runner, see Runner#output_header
334
+ log "* Workers: #{@options[:workers]}"
409
335
 
410
336
  if preload?
337
+ # Threads explicitly marked as fork safe will be ignored. Used in Rails,
338
+ # but may be used by anyone. Note that we need to explicit
339
+ # Process::Waiter check here because there's a bug in Ruby 2.6 and below
340
+ # where calling thread_variable_get on a Process::Waiter will segfault.
341
+ # We can drop that clause once those versions of Ruby are no longer
342
+ # supported.
343
+ fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
344
+
345
+ before = Thread.list.reject(&fork_safe)
346
+
347
+ log "* Restarts: (\u2714) hot (\u2716) phased"
411
348
  log "* Preloading application"
412
349
  load_and_bind
413
350
 
414
- after = Thread.list
351
+ after = Thread.list.reject(&fork_safe)
415
352
 
416
353
  if after.size > before.size
417
354
  threads = (after - before)
@@ -425,7 +362,7 @@ module Puma
425
362
  end
426
363
  end
427
364
  else
428
- log "* Phased restart available"
365
+ log "* Restarts: (\u2714) hot (\u2714) phased"
429
366
 
430
367
  unless @launcher.config.app_configured?
431
368
  error "No application configured, nothing to run"
@@ -446,12 +383,13 @@ module Puma
446
383
  #
447
384
  @check_pipe, @suicide_pipe = Puma::Util.pipe
448
385
 
449
- if daemon?
450
- log "* Daemonizing..."
451
- Process.daemon(true)
452
- else
453
- log "Use Ctrl-C to stop"
454
- end
386
+ # Separate pipe used by worker 0 to receive commands to
387
+ # fork new worker processes.
388
+ @fork_pipe, @fork_writer = Puma::Util.pipe
389
+
390
+ log "Use Ctrl-C to stop"
391
+
392
+ single_worker_warning
455
393
 
456
394
  redirect_io
457
395
 
@@ -463,7 +401,8 @@ module Puma
463
401
 
464
402
  @master_read, @worker_write = read, @wakeup
465
403
 
466
- @launcher.config.run_hooks :before_fork, nil
404
+ @launcher.config.run_hooks :before_fork, nil, @launcher.events
405
+ Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
467
406
 
468
407
  spawn_workers
469
408
 
@@ -471,48 +410,67 @@ module Puma
471
410
  stop
472
411
  end
473
412
 
474
- @launcher.events.fire_on_booted!
475
-
476
413
  begin
477
- force_check = false
414
+ booted = false
415
+ in_phased_restart = false
416
+ workers_not_booted = @options[:workers]
478
417
 
479
418
  while @status == :run
480
419
  begin
481
420
  if @phased_restart
482
421
  start_phased_restart
483
422
  @phased_restart = false
423
+ in_phased_restart = true
424
+ workers_not_booted = @options[:workers]
484
425
  end
485
426
 
486
- check_workers force_check
427
+ check_workers
487
428
 
488
- force_check = false
489
-
490
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
429
+ res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
491
430
 
492
431
  if res
493
432
  req = read.read_nonblock(1)
494
433
 
434
+ @next_check = Time.now if req == "!"
495
435
  next if !req || req == "!"
496
436
 
497
437
  result = read.gets
498
438
  pid = result.to_i
499
439
 
440
+ if req == "b" || req == "f"
441
+ pid, idx = result.split(':').map(&:to_i)
442
+ w = @workers.find {|x| x.index == idx}
443
+ w.pid = pid if w.pid.nil?
444
+ end
445
+
500
446
  if w = @workers.find { |x| x.pid == pid }
501
447
  case req
502
448
  when "b"
503
449
  w.boot!
504
- log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
505
- force_check = true
450
+ log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
451
+ @next_check = Time.now
452
+ workers_not_booted -= 1
453
+ when "e"
454
+ # external term, see worker method, Signal.trap "SIGTERM"
455
+ w.instance_variable_set :@term, true
506
456
  when "t"
507
- w.dead!
508
- force_check = true
457
+ w.term unless w.term?
509
458
  when "p"
510
459
  w.ping!(result.sub(/^\d+/,'').chomp)
460
+ @launcher.events.fire(:ping!, w)
461
+ if !booted && @workers.none? {|worker| worker.last_status.empty?}
462
+ @launcher.events.fire_on_booted!
463
+ booted = true
464
+ end
511
465
  end
512
466
  else
513
467
  log "! Out-of-sync worker list, no #{pid} worker"
514
468
  end
515
469
  end
470
+ if in_phased_restart && workers_not_booted.zero?
471
+ @events.fire_on_booted!
472
+ in_phased_restart = false
473
+ end
516
474
 
517
475
  rescue Interrupt
518
476
  @status = :stop
@@ -527,5 +485,56 @@ module Puma
527
485
  @wakeup.close
528
486
  end
529
487
  end
488
+
489
+ private
490
+
491
+ def single_worker_warning
492
+ return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
493
+
494
+ log "! WARNING: Detected running cluster mode with 1 worker."
495
+ log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
496
+ log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
497
+ log "! Set the `silence_single_worker_warning` option to silence this warning message."
498
+ end
499
+
500
+ # loops thru @workers, removing workers that exited, and calling
501
+ # `#term` if needed
502
+ def wait_workers
503
+ @workers.reject! do |w|
504
+ next false if w.pid.nil?
505
+ begin
506
+ if Process.wait(w.pid, Process::WNOHANG)
507
+ true
508
+ else
509
+ w.term if w.term?
510
+ nil
511
+ end
512
+ rescue Errno::ECHILD
513
+ begin
514
+ Process.kill(0, w.pid)
515
+ # child still alive but has another parent (e.g., using fork_worker)
516
+ w.term if w.term?
517
+ false
518
+ rescue Errno::ESRCH, Errno::EPERM
519
+ true # child is already terminated
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ # @version 5.0.0
526
+ def timeout_workers
527
+ @workers.each do |w|
528
+ if !w.term? && w.ping_timeout <= Time.now
529
+ details = if w.booted?
530
+ "(worker failed to check in within #{@options[:worker_timeout]} seconds)"
531
+ else
532
+ "(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
533
+ end
534
+ log "! Terminating timed out worker #{details}: #{w.pid}"
535
+ w.kill
536
+ end
537
+ end
538
+ end
530
539
  end
531
540
  end