puma 4.3.8 → 5.6.9

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