puma 3.0.0.rc1 → 5.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/{History.txt → History.md} +703 -70
  3. data/LICENSE +23 -20
  4. data/README.md +173 -163
  5. data/docs/architecture.md +37 -0
  6. data/{DEPLOYMENT.md → docs/deployment.md} +28 -6
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  9. data/docs/images/puma-connection-flow.png +0 -0
  10. data/docs/images/puma-general-arch.png +0 -0
  11. data/docs/jungle/README.md +13 -0
  12. data/docs/jungle/rc.d/README.md +74 -0
  13. data/docs/jungle/rc.d/puma +61 -0
  14. data/docs/jungle/rc.d/puma.conf +10 -0
  15. data/{tools → docs}/jungle/upstart/README.md +0 -0
  16. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  17. data/{tools → docs}/jungle/upstart/puma.conf +1 -1
  18. data/docs/nginx.md +2 -2
  19. data/docs/plugins.md +38 -0
  20. data/docs/restart.md +41 -0
  21. data/docs/signals.md +57 -3
  22. data/docs/systemd.md +228 -0
  23. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  24. data/ext/puma_http11/extconf.rb +16 -0
  25. data/ext/puma_http11/http11_parser.c +287 -468
  26. data/ext/puma_http11/http11_parser.h +1 -0
  27. data/ext/puma_http11/http11_parser.java.rl +21 -37
  28. data/ext/puma_http11/http11_parser.rl +10 -9
  29. data/ext/puma_http11/http11_parser_common.rl +4 -4
  30. data/ext/puma_http11/mini_ssl.c +159 -10
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +99 -132
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +30 -6
  34. data/ext/puma_http11/puma_http11.c +6 -38
  35. data/lib/puma.rb +25 -5
  36. data/lib/puma/accept_nonblock.rb +7 -1
  37. data/lib/puma/app/status.rb +53 -26
  38. data/lib/puma/binder.rb +150 -119
  39. data/lib/puma/cli.rb +56 -38
  40. data/lib/puma/client.rb +277 -80
  41. data/lib/puma/cluster.rb +326 -130
  42. data/lib/puma/commonlogger.rb +21 -20
  43. data/lib/puma/configuration.rb +160 -161
  44. data/lib/puma/const.rb +50 -47
  45. data/lib/puma/control_cli.rb +104 -63
  46. data/lib/puma/detect.rb +13 -1
  47. data/lib/puma/dsl.rb +463 -114
  48. data/lib/puma/events.rb +22 -13
  49. data/lib/puma/io_buffer.rb +9 -5
  50. data/lib/puma/jruby_restart.rb +2 -59
  51. data/lib/puma/launcher.rb +195 -105
  52. data/lib/puma/minissl.rb +110 -4
  53. data/lib/puma/minissl/context_builder.rb +76 -0
  54. data/lib/puma/null_io.rb +9 -14
  55. data/lib/puma/plugin.rb +32 -12
  56. data/lib/puma/plugin/tmp_restart.rb +19 -6
  57. data/lib/puma/rack/builder.rb +7 -5
  58. data/lib/puma/rack/urlmap.rb +11 -8
  59. data/lib/puma/rack_default.rb +2 -0
  60. data/lib/puma/reactor.rb +242 -32
  61. data/lib/puma/runner.rb +41 -30
  62. data/lib/puma/server.rb +265 -183
  63. data/lib/puma/single.rb +22 -63
  64. data/lib/puma/state_file.rb +9 -2
  65. data/lib/puma/thread_pool.rb +179 -68
  66. data/lib/puma/util.rb +3 -11
  67. data/lib/rack/handler/puma.rb +60 -11
  68. data/tools/Dockerfile +16 -0
  69. data/tools/trickletest.rb +1 -2
  70. metadata +35 -99
  71. data/COPYING +0 -55
  72. data/Gemfile +0 -13
  73. data/Manifest.txt +0 -79
  74. data/Rakefile +0 -158
  75. data/docs/config.md +0 -0
  76. data/ext/puma_http11/io_buffer.c +0 -155
  77. data/lib/puma/capistrano.rb +0 -94
  78. data/lib/puma/compat.rb +0 -18
  79. data/lib/puma/convenient.rb +0 -23
  80. data/lib/puma/daemon_ext.rb +0 -31
  81. data/lib/puma/delegation.rb +0 -11
  82. data/lib/puma/java_io_buffer.rb +0 -45
  83. data/lib/puma/rack/backports/uri/common_18.rb +0 -56
  84. data/lib/puma/rack/backports/uri/common_192.rb +0 -52
  85. data/lib/puma/rack/backports/uri/common_193.rb +0 -29
  86. data/lib/puma/tcp_logger.rb +0 -32
  87. data/puma.gemspec +0 -52
  88. data/tools/jungle/README.md +0 -9
  89. data/tools/jungle/init.d/README.md +0 -54
  90. data/tools/jungle/init.d/puma +0 -394
  91. data/tools/jungle/init.d/run-puma +0 -3
@@ -1,15 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/runner'
4
+ require 'puma/util'
5
+ require 'puma/plugin'
6
+
7
+ require 'time'
8
+ require 'json'
2
9
 
3
10
  module Puma
11
+ # This class is instantiated by the `Puma::Launcher` and used
12
+ # to boot and serve a Ruby application when puma "workers" are needed
13
+ # i.e. when using multi-processes. For example `$ puma -w 5`
14
+ #
15
+ # At the core of this class is running an instance of `Puma::Server` which
16
+ # gets created via the `start_server` method from the `Puma::Runner` class
17
+ # that this inherits from.
18
+ #
19
+ # An instance of this class will spawn the number of processes passed in
20
+ # via the `spawn_workers` method call. Each worker will have it's own
21
+ # instance of a `Puma::Server`.
4
22
  class Cluster < Runner
5
23
  def initialize(cli, events)
6
24
  super cli, events
7
25
 
8
26
  @phase = 0
9
27
  @workers = []
10
- @next_check = nil
28
+ @next_check = Time.now
11
29
 
12
- @phased_state = :idle
13
30
  @phased_restart = false
14
31
  end
15
32
 
@@ -18,7 +35,11 @@ module Puma
18
35
  @workers.each { |x| x.term }
19
36
 
20
37
  begin
21
- Process.waitall
38
+ loop do
39
+ wait_workers
40
+ break if @workers.reject {|w| w.pid.nil?}.empty?
41
+ sleep 0.2
42
+ end
22
43
  rescue Interrupt
23
44
  log "! Cancelled waiting for workers"
24
45
  end
@@ -30,10 +51,9 @@ module Puma
30
51
 
31
52
  # Be sure to change the directory again before loading
32
53
  # the app. This way we can pick up new code.
33
- if dir = @options[:worker_directory]
34
- log "+ Changing to #{dir}"
35
- Dir.chdir dir
36
- end
54
+ dir = @launcher.restart_dir
55
+ log "+ Changing to #{dir}"
56
+ Dir.chdir dir
37
57
  end
38
58
 
39
59
  def redirect_io
@@ -51,11 +71,14 @@ module Puma
51
71
  @signal = "TERM"
52
72
  @options = options
53
73
  @first_term_sent = nil
74
+ @started_at = Time.now
54
75
  @last_checkin = Time.now
55
- @dead = false
76
+ @last_status = {}
77
+ @term = false
56
78
  end
57
79
 
58
- attr_reader :index, :pid, :phase, :signal, :last_checkin
80
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
81
+ attr_writer :pid, :phase
59
82
 
60
83
  def booted?
61
84
  @stage == :booted
@@ -66,20 +89,21 @@ module Puma
66
89
  @stage = :booted
67
90
  end
68
91
 
69
- def dead?
70
- @dead
71
- end
72
-
73
- def dead!
74
- @dead = true
92
+ def term?
93
+ @term
75
94
  end
76
95
 
77
- def ping!
96
+ def ping!(status)
78
97
  @last_checkin = Time.now
98
+ @last_status = JSON.parse(status, symbolize_names: true)
79
99
  end
80
100
 
81
- def ping_timeout?(which)
82
- Time.now - @last_checkin > which
101
+ def ping_timeout
102
+ @last_checkin +
103
+ (booted? ?
104
+ @options[:worker_timeout] :
105
+ @options[:worker_boot_timeout]
106
+ )
83
107
  end
84
108
 
85
109
  def term
@@ -87,17 +111,17 @@ module Puma
87
111
  if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
88
112
  @signal = "KILL"
89
113
  else
114
+ @term ||= true
90
115
  @first_term_sent ||= Time.now
91
116
  end
92
-
93
- Process.kill @signal, @pid
117
+ Process.kill @signal, @pid if @pid
94
118
  rescue Errno::ESRCH
95
119
  end
96
120
  end
97
121
 
98
122
  def kill
99
- Process.kill "KILL", @pid
100
- rescue Errno::ESRCH
123
+ @signal = 'KILL'
124
+ term
101
125
  end
102
126
 
103
127
  def hup
@@ -108,22 +132,60 @@ module Puma
108
132
 
109
133
  def spawn_workers
110
134
  diff = @options[:workers] - @workers.size
135
+ return if diff < 1
111
136
 
112
137
  master = Process.pid
138
+ if @options[:fork_worker]
139
+ @fork_writer << "-1\n"
140
+ end
113
141
 
114
142
  diff.times do
115
143
  idx = next_worker_index
116
- @launcher.config.run_hooks :before_worker_fork, idx
117
144
 
118
- pid = fork { worker(idx, master) }
145
+ if @options[:fork_worker] && idx != 0
146
+ @fork_writer << "#{idx}\n"
147
+ pid = nil
148
+ else
149
+ pid = spawn_worker(idx, master)
150
+ end
151
+
119
152
  debug "Spawned worker: #{pid}"
120
153
  @workers << Worker.new(idx, pid, @phase, @options)
154
+ end
155
+
156
+ if @options[:fork_worker] &&
157
+ @workers.all? {|x| x.phase == @phase}
158
+
159
+ @fork_writer << "0\n"
160
+ end
161
+ end
162
+
163
+ def spawn_worker(idx, master)
164
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
121
165
 
122
- @launcher.config.run_hooks :after_worker_fork, idx
166
+ pid = fork { worker(idx, master) }
167
+ if !pid
168
+ log "! Complete inability to spawn new workers detected"
169
+ log "! Seppuku is the only choice."
170
+ exit! 1
123
171
  end
124
172
 
125
- if diff > 0
126
- @phased_state = :idle
173
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
174
+ pid
175
+ end
176
+
177
+ def cull_workers
178
+ diff = @workers.size - @options[:workers]
179
+ return if diff < 1
180
+
181
+ debug "Culling #{diff.inspect} workers"
182
+
183
+ workers_to_cull = @workers[-diff,diff]
184
+ debug "Workers to cull: #{workers_to_cull.inspect}"
185
+
186
+ workers_to_cull.each do |worker|
187
+ log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
188
+ worker.term
127
189
  end
128
190
  end
129
191
 
@@ -138,35 +200,14 @@ module Puma
138
200
  @workers.count { |w| !w.booted? } == 0
139
201
  end
140
202
 
141
- def check_workers(force=false)
142
- return if !force && @next_check && @next_check >= Time.now
143
-
144
- @next_check = Time.now + 5
145
-
146
- any = false
147
-
148
- @workers.each do |w|
149
- next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
150
- if w.ping_timeout?(@options[:worker_timeout])
151
- log "! Terminating timed out worker: #{w.pid}"
152
- w.kill
153
- any = true
154
- end
155
- end
156
-
157
- # If we killed any timed out workers, try to catch them
158
- # during this loop by giving the kernel time to kill them.
159
- sleep 1 if any
160
-
161
- while @workers.any?
162
- pid = Process.waitpid(-1, Process::WNOHANG)
163
- break unless pid
164
-
165
- @workers.delete_if { |w| w.pid == pid }
166
- end
203
+ def check_workers
204
+ return if @next_check >= Time.now
167
205
 
168
- @workers.delete_if(&:dead?)
206
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
169
207
 
208
+ timeout_workers
209
+ wait_workers
210
+ cull_workers
170
211
  spawn_workers
171
212
 
172
213
  if all_workers_booted?
@@ -177,15 +218,18 @@ module Puma
177
218
  w = @workers.find { |x| x.phase != @phase }
178
219
 
179
220
  if w
180
- if @phased_state == :idle
181
- @phased_state = :waiting
182
- log "- Stopping #{w.pid} for phased upgrade..."
221
+ log "- Stopping #{w.pid} for phased upgrade..."
222
+ unless w.term?
223
+ w.term
224
+ log "- #{w.signal} sent to #{w.pid}..."
183
225
  end
184
-
185
- w.term
186
- log "- #{w.signal} sent to #{w.pid}..."
187
226
  end
188
227
  end
228
+
229
+ @next_check = [
230
+ @workers.reject(&:term?).map(&:ping_timeout).min,
231
+ @next_check
232
+ ].compact.min
189
233
  end
190
234
 
191
235
  def wakeup!
@@ -194,21 +238,28 @@ module Puma
194
238
  begin
195
239
  @wakeup.write "!" unless @wakeup.closed?
196
240
  rescue SystemCallError, IOError
241
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
197
242
  end
198
243
  end
199
244
 
200
245
  def worker(index, master)
201
- title = "puma: cluster worker #{index}: #{master}"
202
- title << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
246
+ title = "puma: cluster worker #{index}: #{master}"
247
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
203
248
  $0 = title
204
249
 
205
250
  Signal.trap "SIGINT", "IGNORE"
206
251
 
252
+ fork_worker = @options[:fork_worker] && index == 0
253
+
207
254
  @workers = []
208
- @master_read.close
209
- @suicide_pipe.close
255
+ if !@options[:fork_worker] || fork_worker
256
+ @master_read.close
257
+ @suicide_pipe.close
258
+ @fork_writer.close
259
+ end
210
260
 
211
261
  Thread.new do
262
+ Puma.set_thread_name "worker check pipe"
212
263
  IO.select [@check_pipe]
213
264
  log "! Detected parent died, dying"
214
265
  exit! 1
@@ -216,45 +267,82 @@ module Puma
216
267
 
217
268
  # If we're not running under a Bundler context, then
218
269
  # report the info about the context we will be using
219
- if !ENV['BUNDLE_GEMFILE'] and File.exist?("Gemfile")
220
- log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
270
+ if !ENV['BUNDLE_GEMFILE']
271
+ if File.exist?("Gemfile")
272
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
273
+ elsif File.exist?("gems.rb")
274
+ log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
275
+ end
221
276
  end
222
277
 
223
278
  # Invoke any worker boot hooks so they can get
224
279
  # things in shape before booting the app.
225
- @launcher.config.run_hooks :before_worker_boot, index
280
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
281
+
282
+ server = @server ||= start_server
283
+ restart_server = Queue.new << true << false
284
+
285
+ if fork_worker
286
+ restart_server.clear
287
+ Signal.trap "SIGCHLD" do
288
+ Process.wait(-1, Process::WNOHANG) rescue nil
289
+ wakeup!
290
+ end
226
291
 
227
- server = start_server
292
+ Thread.new do
293
+ Puma.set_thread_name "worker fork pipe"
294
+ while (idx = @fork_pipe.gets)
295
+ idx = idx.to_i
296
+ if idx == -1 # stop server
297
+ if restart_server.length > 0
298
+ restart_server.clear
299
+ server.begin_restart(true)
300
+ @launcher.config.run_hooks :before_refork, nil, @launcher.events
301
+ nakayoshi_gc
302
+ end
303
+ elsif idx == 0 # restart server
304
+ restart_server << true << false
305
+ else # fork worker
306
+ pid = spawn_worker(idx, master)
307
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
308
+ end
309
+ end
310
+ end
311
+ end
228
312
 
229
313
  Signal.trap "SIGTERM" do
314
+ @worker_write << "e#{Process.pid}\n" rescue nil
230
315
  server.stop
316
+ restart_server << false
231
317
  end
232
318
 
233
319
  begin
234
- @worker_write << "b#{Process.pid}\n"
320
+ @worker_write << "b#{Process.pid}:#{index}\n"
235
321
  rescue SystemCallError, IOError
322
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
236
323
  STDERR.puts "Master seems to have exited, exiting."
237
324
  return
238
325
  end
239
326
 
240
327
  Thread.new(@worker_write) do |io|
241
- payload = "p#{Process.pid}\n"
328
+ Puma.set_thread_name "stat payload"
242
329
 
243
330
  while true
244
- sleep 5
331
+ sleep Const::WORKER_CHECK_INTERVAL
245
332
  begin
246
- io << payload
333
+ io << "p#{Process.pid}#{server.stats.to_json}\n"
247
334
  rescue IOError
335
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
248
336
  break
249
337
  end
250
338
  end
251
339
  end
252
340
 
253
- server.run.join
341
+ server.run.join while restart_server.pop
254
342
 
255
343
  # Invoke any worker shutdown hooks so they can prevent the worker
256
344
  # exiting until any background operations are completed
257
- @launcher.config.run_hooks :before_worker_shutdown, index
345
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
258
346
  ensure
259
347
  @worker_write << "t#{Process.pid}\n" rescue nil
260
348
  @worker_write.close
@@ -292,22 +380,101 @@ module Puma
292
380
  end
293
381
 
294
382
  def reload_worker_directory
295
- if dir = @options[:worker_directory]
296
- log "+ Changing to #{dir}"
297
- Dir.chdir dir
298
- end
383
+ dir = @launcher.restart_dir
384
+ log "+ Changing to #{dir}"
385
+ Dir.chdir dir
299
386
  end
300
387
 
388
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
389
+ # the master process.
301
390
  def stats
302
391
  old_worker_count = @workers.count { |w| w.phase != @phase }
303
- booted_worker_count = @workers.count { |w| w.booted? }
304
- %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count} }!
392
+ worker_status = @workers.map do |w|
393
+ {
394
+ started_at: w.started_at.utc.iso8601,
395
+ pid: w.pid,
396
+ index: w.index,
397
+ phase: w.phase,
398
+ booted: w.booted?,
399
+ last_checkin: w.last_checkin.utc.iso8601,
400
+ last_status: w.last_status,
401
+ }
402
+ end
403
+
404
+ {
405
+ started_at: @started_at.utc.iso8601,
406
+ workers: @workers.size,
407
+ phase: @phase,
408
+ booted_workers: worker_status.count { |w| w[:booted] },
409
+ old_workers: old_worker_count,
410
+ worker_status: worker_status,
411
+ }
305
412
  end
306
413
 
307
414
  def preload?
308
415
  @options[:preload_app]
309
416
  end
310
417
 
418
+ def fork_worker!
419
+ if (worker = @workers.find { |w| w.index == 0 })
420
+ worker.phase += 1
421
+ end
422
+ phased_restart
423
+ end
424
+
425
+ # We do this in a separate method to keep the lambda scope
426
+ # of the signals handlers as small as possible.
427
+ def setup_signals
428
+ if @options[:fork_worker]
429
+ Signal.trap "SIGURG" do
430
+ fork_worker!
431
+ end
432
+
433
+ # Auto-fork after the specified number of requests.
434
+ if (fork_requests = @options[:fork_worker].to_i) > 0
435
+ @launcher.events.register(:ping!) do |w|
436
+ fork_worker! if w.index == 0 &&
437
+ w.phase == 0 &&
438
+ w.last_status[:requests_count] >= fork_requests
439
+ end
440
+ end
441
+ end
442
+
443
+ Signal.trap "SIGCHLD" do
444
+ wakeup!
445
+ end
446
+
447
+ Signal.trap "TTIN" do
448
+ @options[:workers] += 1
449
+ wakeup!
450
+ end
451
+
452
+ Signal.trap "TTOU" do
453
+ @options[:workers] -= 1 if @options[:workers] >= 2
454
+ wakeup!
455
+ end
456
+
457
+ master_pid = Process.pid
458
+
459
+ Signal.trap "SIGTERM" do
460
+ # The worker installs their own SIGTERM when booted.
461
+ # Until then, this is run by the worker and the worker
462
+ # should just exit if they get it.
463
+ if Process.pid != master_pid
464
+ log "Early termination of worker"
465
+ exit! 0
466
+ else
467
+ @launcher.close_binder_listeners
468
+
469
+ stop_workers
470
+ stop
471
+
472
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
473
+ exit 0 # Clean exit, workers were stopped
474
+ end
475
+ end
476
+ end
477
+
311
478
  def run
312
479
  @status = :run
313
480
 
@@ -347,34 +514,7 @@ module Puma
347
514
 
348
515
  read, @wakeup = Puma::Util.pipe
349
516
 
350
- Signal.trap "SIGCHLD" do
351
- wakeup!
352
- end
353
-
354
- Signal.trap "TTIN" do
355
- @options[:workers] += 1
356
- wakeup!
357
- end
358
-
359
- Signal.trap "TTOU" do
360
- @options[:workers] -= 1 if @options[:workers] >= 2
361
- @workers.last.term
362
- wakeup!
363
- end
364
-
365
- master_pid = Process.pid
366
-
367
- Signal.trap "SIGTERM" do
368
- # The worker installs their own SIGTERM when booted.
369
- # Until then, this is run by the worker and the worker
370
- # should just exit if they get it.
371
- if Process.pid != master_pid
372
- log "Early termination of worker"
373
- exit! 0
374
- else
375
- stop
376
- end
377
- end
517
+ setup_signals
378
518
 
379
519
  # Used by the workers to detect if the master process dies.
380
520
  # If select says that @check_pipe is ready, it's because the
@@ -383,22 +523,24 @@ module Puma
383
523
  #
384
524
  @check_pipe, @suicide_pipe = Puma::Util.pipe
385
525
 
386
- if daemon?
387
- log "* Daemonizing..."
388
- Process.daemon(true)
389
- else
390
- log "Use Ctrl-C to stop"
391
- end
526
+ # Separate pipe used by worker 0 to receive commands to
527
+ # fork new worker processes.
528
+ @fork_pipe, @fork_writer = Puma::Util.pipe
529
+
530
+ log "Use Ctrl-C to stop"
392
531
 
393
532
  redirect_io
394
533
 
395
- start_control
534
+ Plugins.fire_background
396
535
 
397
536
  @launcher.write_state
398
537
 
538
+ start_control
539
+
399
540
  @master_read, @worker_write = read, @wakeup
400
541
 
401
- @launcher.config.run_hooks :before_fork, nil
542
+ @launcher.config.run_hooks :before_fork, nil, @launcher.events
543
+ nakayoshi_gc
402
544
 
403
545
  spawn_workers
404
546
 
@@ -411,41 +553,50 @@ module Puma
411
553
  begin
412
554
  while @status == :run
413
555
  begin
414
- res = IO.select([read], nil, nil, 5)
556
+ if @phased_restart
557
+ start_phased_restart
558
+ @phased_restart = false
559
+ end
415
560
 
416
- force_check = false
561
+ check_workers
562
+
563
+ res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
417
564
 
418
565
  if res
419
566
  req = read.read_nonblock(1)
420
567
 
568
+ @next_check = Time.now if req == "!"
421
569
  next if !req || req == "!"
422
570
 
423
- pid = read.gets.to_i
571
+ result = read.gets
572
+ pid = result.to_i
573
+
574
+ if req == "b" || req == "f"
575
+ pid, idx = result.split(':').map(&:to_i)
576
+ w = @workers.find {|x| x.index == idx}
577
+ w.pid = pid if w.pid.nil?
578
+ end
424
579
 
425
580
  if w = @workers.find { |x| x.pid == pid }
426
581
  case req
427
582
  when "b"
428
583
  w.boot!
429
584
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
430
- force_check = true
585
+ @next_check = Time.now
586
+ when "e"
587
+ # external term, see worker method, Signal.trap "SIGTERM"
588
+ w.instance_variable_set :@term, true
431
589
  when "t"
432
- w.dead!
433
- force_check = true
590
+ w.term unless w.term?
434
591
  when "p"
435
- w.ping!
592
+ w.ping!(result.sub(/^\d+/,'').chomp)
593
+ @launcher.events.fire(:ping!, w)
436
594
  end
437
595
  else
438
596
  log "! Out-of-sync worker list, no #{pid} worker"
439
597
  end
440
598
  end
441
599
 
442
- if @phased_restart
443
- start_phased_restart
444
- @phased_restart = false
445
- end
446
-
447
- check_workers force_check
448
-
449
600
  rescue Interrupt
450
601
  @status = :stop
451
602
  end
@@ -459,5 +610,50 @@ module Puma
459
610
  @wakeup.close
460
611
  end
461
612
  end
613
+
614
+ private
615
+
616
+ # loops thru @workers, removing workers that exited, and calling
617
+ # `#term` if needed
618
+ def wait_workers
619
+ @workers.reject! do |w|
620
+ next false if w.pid.nil?
621
+ begin
622
+ if Process.wait(w.pid, Process::WNOHANG)
623
+ true
624
+ else
625
+ w.term if w.term?
626
+ nil
627
+ end
628
+ rescue Errno::ECHILD
629
+ begin
630
+ Process.kill(0, w.pid)
631
+ false # child still alive, but has another parent
632
+ rescue Errno::ESRCH, Errno::EPERM
633
+ true # child is already terminated
634
+ end
635
+ end
636
+ end
637
+ end
638
+
639
+ def timeout_workers
640
+ @workers.each do |w|
641
+ if !w.term? && w.ping_timeout <= Time.now
642
+ log "! Terminating timed out worker: #{w.pid}"
643
+ w.kill
644
+ end
645
+ end
646
+ end
647
+
648
+ def nakayoshi_gc
649
+ return unless @options[:nakayoshi_fork]
650
+ log "! Promoting existing objects to old generation..."
651
+ 4.times { GC.start(full_mark: false) }
652
+ if GC.respond_to?(:compact)
653
+ log "! Compacting..."
654
+ GC.compact
655
+ end
656
+ log "! Friendly fork preparation complete."
657
+ end
462
658
  end
463
659
  end