puma 3.12.1 → 5.6.7

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1608 -447
  3. data/LICENSE +23 -20
  4. data/README.md +175 -63
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +59 -21
  7. data/docs/compile_options.md +21 -0
  8. data/docs/deployment.md +69 -58
  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 +22 -12
  16. data/docs/rails_dev_mode.md +28 -0
  17. data/docs/restart.md +47 -22
  18. data/docs/signals.md +13 -11
  19. data/docs/stats.md +142 -0
  20. data/docs/systemd.md +95 -120
  21. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  22. data/ext/puma_http11/ext_help.h +1 -1
  23. data/ext/puma_http11/extconf.rb +57 -2
  24. data/ext/puma_http11/http11_parser.c +105 -117
  25. data/ext/puma_http11/http11_parser.h +1 -1
  26. data/ext/puma_http11/http11_parser.java.rl +22 -38
  27. data/ext/puma_http11/http11_parser.rl +4 -2
  28. data/ext/puma_http11/http11_parser_common.rl +4 -4
  29. data/ext/puma_http11/mini_ssl.c +339 -98
  30. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +124 -71
  34. data/ext/puma_http11/puma_http11.c +35 -51
  35. data/lib/puma/app/status.rb +71 -49
  36. data/lib/puma/binder.rb +234 -137
  37. data/lib/puma/cli.rb +28 -18
  38. data/lib/puma/client.rb +350 -230
  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 +247 -232
  42. data/lib/puma/commonlogger.rb +2 -2
  43. data/lib/puma/configuration.rb +61 -51
  44. data/lib/puma/const.rb +42 -21
  45. data/lib/puma/control_cli.rb +115 -67
  46. data/lib/puma/detect.rb +29 -2
  47. data/lib/puma/dsl.rb +619 -123
  48. data/lib/puma/error_logger.rb +104 -0
  49. data/lib/puma/events.rb +55 -31
  50. data/lib/puma/io_buffer.rb +7 -5
  51. data/lib/puma/jruby_restart.rb +0 -58
  52. data/lib/puma/json_serialization.rb +96 -0
  53. data/lib/puma/launcher.rb +193 -69
  54. data/lib/puma/minissl/context_builder.rb +81 -0
  55. data/lib/puma/minissl.rb +170 -65
  56. data/lib/puma/null_io.rb +18 -1
  57. data/lib/puma/plugin/tmp_restart.rb +2 -0
  58. data/lib/puma/plugin.rb +7 -13
  59. data/lib/puma/queue_close.rb +26 -0
  60. data/lib/puma/rack/builder.rb +3 -5
  61. data/lib/puma/rack/urlmap.rb +2 -0
  62. data/lib/puma/rack_default.rb +2 -0
  63. data/lib/puma/reactor.rb +85 -316
  64. data/lib/puma/request.rb +476 -0
  65. data/lib/puma/runner.rb +48 -55
  66. data/lib/puma/server.rb +305 -695
  67. data/lib/puma/single.rb +11 -67
  68. data/lib/puma/state_file.rb +48 -8
  69. data/lib/puma/systemd.rb +46 -0
  70. data/lib/puma/thread_pool.rb +132 -82
  71. data/lib/puma/util.rb +33 -10
  72. data/lib/puma.rb +56 -0
  73. data/lib/rack/handler/puma.rb +5 -6
  74. data/lib/rack/version_restriction.rb +15 -0
  75. data/tools/Dockerfile +16 -0
  76. data/tools/trickletest.rb +0 -1
  77. metadata +46 -29
  78. data/ext/puma_http11/io_buffer.c +0 -155
  79. data/lib/puma/accept_nonblock.rb +0 -23
  80. data/lib/puma/compat.rb +0 -14
  81. data/lib/puma/convenient.rb +0 -25
  82. data/lib/puma/daemon_ext.rb +0 -33
  83. data/lib/puma/delegation.rb +0 -13
  84. data/lib/puma/java_io_buffer.rb +0 -47
  85. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  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
  94. /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,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,155 +60,103 @@ 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
148
85
 
149
- @launcher.config.run_hooks :after_worker_fork, idx
86
+ if @options[:fork_worker] &&
87
+ @workers.all? {|x| x.phase == @phase}
88
+
89
+ @fork_writer << "0\n"
150
90
  end
91
+ end
151
92
 
152
- if diff > 0
153
- @phased_state = :idle
93
+ # @version 5.0.0
94
+ def spawn_worker(idx, master)
95
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
96
+
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
158
109
  diff = @workers.size - @options[:workers]
159
110
  return if diff < 1
111
+ debug "Culling #{diff} workers"
160
112
 
161
- debug "Culling #{diff.inspect} workers"
162
-
163
- workers_to_cull = @workers[-diff,diff]
164
- debug "Workers to cull: #{workers_to_cull.inspect}"
113
+ workers = workers_to_cull(diff)
114
+ debug "Workers to cull: #{workers.inspect}"
165
115
 
166
- workers_to_cull.each do |worker|
167
- log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
116
+ workers.each do |worker|
117
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
168
118
  worker.term
169
119
  end
170
120
  end
171
121
 
172
- def next_worker_index
173
- all_positions = 0...@options[:workers]
174
- occupied_positions = @workers.map { |w| w.index }
175
- available_positions = all_positions.to_a - occupied_positions
176
- available_positions.first
177
- end
178
-
179
- def all_workers_booted?
180
- @workers.count { |w| !w.booted? } == 0
181
- end
182
-
183
- def check_workers(force=false)
184
- return if !force && @next_check && @next_check >= Time.now
122
+ def workers_to_cull(diff)
123
+ workers = @workers.sort_by(&:started_at)
185
124
 
186
- @next_check = Time.now + WORKER_CHECK_INTERVAL
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]
187
128
 
188
- any = false
129
+ workers[cull_start_index(diff), diff]
130
+ end
189
131
 
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
132
+ def cull_start_index(diff)
133
+ case @options[:worker_culling_strategy]
134
+ when :oldest
135
+ 0
136
+ else # :youngest
137
+ -diff
197
138
  end
139
+ end
198
140
 
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
141
+ # @!attribute [r] next_worker_index
142
+ def next_worker_index
143
+ occupied_positions = @workers.map(&:index)
144
+ idx = 0
145
+ idx += 1 until !occupied_positions.include?(idx)
146
+ idx
147
+ end
202
148
 
203
- while @workers.any?
204
- pid = Process.waitpid(-1, Process::WNOHANG)
205
- break unless pid
149
+ def all_workers_booted?
150
+ @workers.count { |w| !w.booted? } == 0
151
+ end
206
152
 
207
- @workers.delete_if { |w| w.pid == pid }
208
- end
153
+ def check_workers
154
+ return if @next_check >= Time.now
209
155
 
210
- @workers.delete_if(&:dead?)
156
+ @next_check = Time.now + @options[:worker_check_interval]
211
157
 
158
+ timeout_workers
159
+ wait_workers
212
160
  cull_workers
213
161
  spawn_workers
214
162
 
@@ -220,99 +168,40 @@ module Puma
220
168
  w = @workers.find { |x| x.phase != @phase }
221
169
 
222
170
  if w
223
- if @phased_state == :idle
224
- @phased_state = :waiting
225
- log "- Stopping #{w.pid} for phased upgrade..."
171
+ log "- Stopping #{w.pid} for phased upgrade..."
172
+ unless w.term?
173
+ w.term
174
+ log "- #{w.signal} sent to #{w.pid}..."
226
175
  end
227
-
228
- w.term
229
- log "- #{w.signal} sent to #{w.pid}..."
230
176
  end
231
177
  end
232
- end
233
178
 
234
- def wakeup!
235
- return unless @wakeup
236
-
237
- begin
238
- @wakeup.write "!" unless @wakeup.closed?
239
- rescue SystemCallError, IOError
240
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
241
- end
179
+ @next_check = [
180
+ @workers.reject(&:term?).map(&:ping_timeout).min,
181
+ @next_check
182
+ ].compact.min
242
183
  end
243
184
 
244
185
  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
186
  @workers = []
187
+
252
188
  @master_read.close
253
189
  @suicide_pipe.close
190
+ @fork_writer.close
254
191
 
255
- Thread.new do
256
- IO.select [@check_pipe]
257
- log "! Detected parent died, dying"
258
- exit! 1
259
- end
260
-
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
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
306
196
  end
307
197
 
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
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
316
205
  end
317
206
 
318
207
  def restart
@@ -352,20 +241,63 @@ module Puma
352
241
  Dir.chdir dir
353
242
  end
354
243
 
244
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
245
+ # the master process.
246
+ # @!attribute [r] stats
355
247
  def stats
356
248
  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} }!
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
+ }
360
269
  end
361
270
 
362
271
  def preload?
363
272
  @options[:preload_app]
364
273
  end
365
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
+
366
283
  # We do this in a separate method to keep the lambda scope
367
284
  # of the signals handlers as small as possible.
368
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
+
369
301
  Signal.trap "SIGCHLD" do
370
302
  wakeup!
371
303
  end
@@ -390,10 +322,13 @@ module Puma
390
322
  log "Early termination of worker"
391
323
  exit! 0
392
324
  else
325
+ @launcher.close_binder_listeners
326
+
393
327
  stop_workers
394
328
  stop
395
-
396
- raise SignalException, "SIGTERM"
329
+ @events.fire_on_stopped!
330
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
331
+ exit 0 # Clean exit, workers were stopped
397
332
  end
398
333
  end
399
334
  end
@@ -403,15 +338,25 @@ module Puma
403
338
 
404
339
  output_header "cluster"
405
340
 
406
- log "* Process workers: #{@options[:workers]}"
407
-
408
- before = Thread.list
341
+ # This is aligned with the output from Runner, see Runner#output_header
342
+ log "* Workers: #{@options[:workers]}"
409
343
 
410
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"
411
356
  log "* Preloading application"
412
357
  load_and_bind
413
358
 
414
- after = Thread.list
359
+ after = Thread.list.reject(&fork_safe)
415
360
 
416
361
  if after.size > before.size
417
362
  threads = (after - before)
@@ -425,7 +370,7 @@ module Puma
425
370
  end
426
371
  end
427
372
  else
428
- log "* Phased restart available"
373
+ log "* Restarts: (\u2714) hot (\u2714) phased"
429
374
 
430
375
  unless @launcher.config.app_configured?
431
376
  error "No application configured, nothing to run"
@@ -446,12 +391,13 @@ module Puma
446
391
  #
447
392
  @check_pipe, @suicide_pipe = Puma::Util.pipe
448
393
 
449
- if daemon?
450
- log "* Daemonizing..."
451
- Process.daemon(true)
452
- else
453
- log "Use Ctrl-C to stop"
454
- 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
455
401
 
456
402
  redirect_io
457
403
 
@@ -463,7 +409,8 @@ module Puma
463
409
 
464
410
  @master_read, @worker_write = read, @wakeup
465
411
 
466
- @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]
467
414
 
468
415
  spawn_workers
469
416
 
@@ -471,48 +418,65 @@ module Puma
471
418
  stop
472
419
  end
473
420
 
474
- @launcher.events.fire_on_booted!
475
-
476
421
  begin
477
- force_check = false
422
+ booted = false
423
+ in_phased_restart = false
424
+ workers_not_booted = @options[:workers]
478
425
 
479
426
  while @status == :run
480
427
  begin
481
428
  if @phased_restart
482
429
  start_phased_restart
483
430
  @phased_restart = false
431
+ in_phased_restart = true
432
+ workers_not_booted = @options[:workers]
484
433
  end
485
434
 
486
- check_workers force_check
435
+ check_workers
487
436
 
488
- force_check = false
489
-
490
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
491
-
492
- if res
437
+ if read.wait_readable([0, @next_check - Time.now].max)
493
438
  req = read.read_nonblock(1)
494
439
 
440
+ @next_check = Time.now if req == "!"
495
441
  next if !req || req == "!"
496
442
 
497
443
  result = read.gets
498
444
  pid = result.to_i
499
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
+
500
452
  if w = @workers.find { |x| x.pid == pid }
501
453
  case req
502
454
  when "b"
503
455
  w.boot!
504
- log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
505
- 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
459
+ when "e"
460
+ # external term, see worker method, Signal.trap "SIGTERM"
461
+ w.term!
506
462
  when "t"
507
- w.dead!
508
- force_check = true
463
+ w.term unless w.term?
509
464
  when "p"
510
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
511
471
  end
512
472
  else
513
473
  log "! Out-of-sync worker list, no #{pid} worker"
514
474
  end
515
475
  end
476
+ if in_phased_restart && workers_not_booted.zero?
477
+ @events.fire_on_booted!
478
+ in_phased_restart = false
479
+ end
516
480
 
517
481
  rescue Interrupt
518
482
  @status = :stop
@@ -527,5 +491,56 @@ module Puma
527
491
  @wakeup.close
528
492
  end
529
493
  end
494
+
495
+ private
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
+
506
+ # loops thru @workers, removing workers that exited, and calling
507
+ # `#term` if needed
508
+ def wait_workers
509
+ @workers.reject! do |w|
510
+ next false if w.pid.nil?
511
+ begin
512
+ if Process.wait(w.pid, Process::WNOHANG)
513
+ true
514
+ else
515
+ w.term if w.term?
516
+ nil
517
+ end
518
+ rescue Errno::ECHILD
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
542
+ end
543
+ end
544
+ end
530
545
  end
531
546
  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
  #