puma 4.3.12 → 5.6.6

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1511 -524
  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/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 +15 -15
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +46 -23
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +85 -128
  25. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  26. data/ext/puma_http11/ext_help.h +1 -1
  27. data/ext/puma_http11/extconf.rb +44 -10
  28. data/ext/puma_http11/http11_parser.c +45 -47
  29. data/ext/puma_http11/http11_parser.h +1 -1
  30. data/ext/puma_http11/http11_parser.java.rl +1 -1
  31. data/ext/puma_http11/http11_parser.rl +1 -1
  32. data/ext/puma_http11/http11_parser_common.rl +0 -0
  33. data/ext/puma_http11/mini_ssl.c +225 -89
  34. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  35. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  36. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +3 -5
  37. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +109 -67
  38. data/ext/puma_http11/puma_http11.c +32 -51
  39. data/lib/puma/app/status.rb +50 -36
  40. data/lib/puma/binder.rb +225 -106
  41. data/lib/puma/cli.rb +24 -18
  42. data/lib/puma/client.rb +104 -76
  43. data/lib/puma/cluster/worker.rb +173 -0
  44. data/lib/puma/cluster/worker_handle.rb +94 -0
  45. data/lib/puma/cluster.rb +212 -220
  46. data/lib/puma/commonlogger.rb +2 -2
  47. data/lib/puma/configuration.rb +58 -49
  48. data/lib/puma/const.rb +13 -6
  49. data/lib/puma/control_cli.rb +99 -76
  50. data/lib/puma/detect.rb +29 -2
  51. data/lib/puma/dsl.rb +368 -96
  52. data/lib/puma/error_logger.rb +104 -0
  53. data/lib/puma/events.rb +55 -34
  54. data/lib/puma/io_buffer.rb +9 -2
  55. data/lib/puma/jruby_restart.rb +0 -58
  56. data/lib/puma/json_serialization.rb +96 -0
  57. data/lib/puma/launcher.rb +128 -46
  58. data/lib/puma/minissl/context_builder.rb +14 -9
  59. data/lib/puma/minissl.rb +137 -50
  60. data/lib/puma/null_io.rb +18 -1
  61. data/lib/puma/plugin/tmp_restart.rb +0 -0
  62. data/lib/puma/plugin.rb +3 -12
  63. data/lib/puma/queue_close.rb +26 -0
  64. data/lib/puma/rack/builder.rb +1 -5
  65. data/lib/puma/rack/urlmap.rb +0 -0
  66. data/lib/puma/rack_default.rb +0 -0
  67. data/lib/puma/reactor.rb +85 -369
  68. data/lib/puma/request.rb +476 -0
  69. data/lib/puma/runner.rb +46 -61
  70. data/lib/puma/server.rb +292 -763
  71. data/lib/puma/single.rb +9 -65
  72. data/lib/puma/state_file.rb +48 -8
  73. data/lib/puma/systemd.rb +46 -0
  74. data/lib/puma/thread_pool.rb +125 -57
  75. data/lib/puma/util.rb +32 -4
  76. data/lib/puma.rb +48 -0
  77. data/lib/rack/handler/puma.rb +2 -3
  78. data/lib/rack/version_restriction.rb +15 -0
  79. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  80. data/tools/trickletest.rb +0 -0
  81. metadata +28 -23
  82. data/docs/tcp_mode.md +0 -96
  83. data/ext/puma_http11/io_buffer.c +0 -155
  84. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  85. data/lib/puma/accept_nonblock.rb +0 -29
  86. data/lib/puma/tcp_logger.rb +0 -41
  87. data/tools/jungle/README.md +0 -19
  88. data/tools/jungle/init.d/README.md +0 -61
  89. data/tools/jungle/init.d/puma +0 -421
  90. data/tools/jungle/init.d/run-puma +0 -18
  91. data/tools/jungle/upstart/README.md +0 -61
  92. data/tools/jungle/upstart/puma-manager.conf +0 -31
  93. 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,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
  #