puma 4.3.3-java → 5.0.0.beta2-java

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +79 -8
  3. data/LICENSE +23 -20
  4. data/README.md +18 -12
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +9 -3
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/jungle/README.md +13 -0
  9. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma +0 -0
  11. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  12. data/{tools → docs}/jungle/upstart/README.md +0 -0
  13. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  14. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  15. data/docs/signals.md +5 -4
  16. data/docs/systemd.md +1 -63
  17. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  18. data/ext/puma_http11/extconf.rb +4 -3
  19. data/ext/puma_http11/http11_parser.c +3 -1
  20. data/ext/puma_http11/http11_parser.rl +3 -1
  21. data/ext/puma_http11/mini_ssl.c +12 -2
  22. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  23. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +37 -6
  24. data/ext/puma_http11/puma_http11.c +3 -38
  25. data/lib/puma.rb +5 -0
  26. data/lib/puma/app/status.rb +18 -3
  27. data/lib/puma/binder.rb +66 -63
  28. data/lib/puma/cli.rb +7 -15
  29. data/lib/puma/client.rb +64 -14
  30. data/lib/puma/cluster.rb +183 -74
  31. data/lib/puma/commonlogger.rb +2 -2
  32. data/lib/puma/configuration.rb +30 -42
  33. data/lib/puma/const.rb +2 -3
  34. data/lib/puma/control_cli.rb +27 -17
  35. data/lib/puma/detect.rb +8 -0
  36. data/lib/puma/dsl.rb +72 -36
  37. data/lib/puma/error_logger.rb +96 -0
  38. data/lib/puma/events.rb +33 -31
  39. data/lib/puma/io_buffer.rb +9 -2
  40. data/lib/puma/jruby_restart.rb +0 -58
  41. data/lib/puma/launcher.rb +46 -31
  42. data/lib/puma/minissl.rb +47 -10
  43. data/lib/puma/null_io.rb +1 -1
  44. data/lib/puma/plugin.rb +1 -10
  45. data/lib/puma/puma_http11.jar +0 -0
  46. data/lib/puma/rack/builder.rb +0 -4
  47. data/lib/puma/reactor.rb +8 -3
  48. data/lib/puma/runner.rb +6 -35
  49. data/lib/puma/server.rb +138 -182
  50. data/lib/puma/single.rb +7 -64
  51. data/lib/puma/state_file.rb +6 -3
  52. data/lib/puma/thread_pool.rb +90 -49
  53. data/lib/rack/handler/puma.rb +1 -3
  54. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  55. metadata +18 -21
  56. data/docs/tcp_mode.md +0 -96
  57. data/ext/puma_http11/io_buffer.c +0 -155
  58. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  59. data/lib/puma/tcp_logger.rb +0 -41
  60. data/tools/jungle/README.md +0 -19
  61. data/tools/jungle/init.d/README.md +0 -61
  62. data/tools/jungle/init.d/puma +0 -421
  63. data/tools/jungle/init.d/run-puma +0 -18
@@ -80,7 +80,7 @@ module Puma
80
80
  @launcher.run
81
81
  end
82
82
 
83
- private
83
+ private
84
84
  def unsupported(str)
85
85
  @events.error(str)
86
86
  raise UnsupportedOption
@@ -112,21 +112,11 @@ module Puma
112
112
  configure_control_url(arg)
113
113
  end
114
114
 
115
- # alias --control-url for backwards-compatibility
116
- o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
- configure_control_url(arg)
118
- end
119
-
120
115
  o.on "--control-token TOKEN",
121
116
  "The token to use as authentication for the control server" do |arg|
122
117
  @control_options[:auth_token] = arg
123
118
  end
124
119
 
125
- o.on "-d", "--daemon", "Daemonize the server into the background" do
126
- user_config.daemonize
127
- user_config.quiet
128
- end
129
-
130
120
  o.on "--debug", "Log lowlevel debugging information" do
131
121
  user_config.debug
132
122
  end
@@ -140,6 +130,12 @@ module Puma
140
130
  user_config.environment arg
141
131
  end
142
132
 
133
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
134
+ "Fork new workers from existing worker. Cluster mode only",
135
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
136
+ user_config.fork_worker(*args.compact)
137
+ end
138
+
143
139
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
140
  $LOAD_PATH.unshift(*arg.split(':'))
145
141
  end
@@ -192,10 +188,6 @@ module Puma
192
188
  end
193
189
  end
194
190
 
195
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
196
- user_config.tcp_mode!
197
- end
198
-
199
191
  o.on "--early-hints", "Enable early hints support" do
200
192
  user_config.early_hints
201
193
  end
@@ -153,7 +153,7 @@ module Puma
153
153
 
154
154
  begin
155
155
  data = @io.read_nonblock(CHUNK_SIZE)
156
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
157
157
  return false
158
158
  rescue SystemCallError, IOError, EOFError
159
159
  raise ConnectionError, "Connection error detected during read"
@@ -238,12 +238,23 @@ module Puma
238
238
  return false unless IO.select([@to_io], nil, nil, 0)
239
239
  try_to_finish
240
240
  end
241
+
242
+ # For documentation, see https://github.com/puma/puma/issues/1754
243
+ send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
241
244
  end # IS_JRUBY
242
245
 
243
- def finish
246
+ def finish(timeout)
244
247
  return true if @ready
245
248
  until try_to_finish
246
- IO.select([@to_io], nil, nil)
249
+ can_read = begin
250
+ IO.select([@to_io], nil, nil, timeout)
251
+ rescue ThreadPool::ForceShutdown
252
+ nil
253
+ end
254
+ unless can_read
255
+ write_error(408) if in_data_phase
256
+ raise ConnectionError
257
+ end
247
258
  end
248
259
  true
249
260
  end
@@ -259,7 +270,7 @@ module Puma
259
270
  return @peerip if @peerip
260
271
 
261
272
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
273
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
263
274
  @peerip = hdr
264
275
  return hdr
265
276
  end
@@ -267,6 +278,18 @@ module Puma
267
278
  @peerip ||= @io.peeraddr.last
268
279
  end
269
280
 
281
+ # Returns true if the persistent connection can be closed immediately
282
+ # without waiting for the configured idle/shutdown timeout.
283
+ def can_close?
284
+ # Allow connection to close if it's received at least one full request
285
+ # and hasn't received any data for a future request.
286
+ #
287
+ # From RFC 2616 section 8.1.4:
288
+ # Servers SHOULD always respond to at least one request per connection,
289
+ # if at all possible.
290
+ @requests_served > 0 && @parsed_bytes == 0
291
+ end
292
+
270
293
  private
271
294
 
272
295
  def setup_body
@@ -285,8 +308,16 @@ module Puma
285
308
 
286
309
  te = @env[TRANSFER_ENCODING2]
287
310
 
288
- if te && CHUNKED.casecmp(te) == 0
289
- return setup_chunked_body(body)
311
+ if te
312
+ if te.include?(",")
313
+ te.split(",").each do |part|
314
+ if CHUNKED.casecmp(part.strip) == 0
315
+ return setup_chunked_body(body)
316
+ end
317
+ end
318
+ elsif CHUNKED.casecmp(te) == 0
319
+ return setup_chunked_body(body)
320
+ end
290
321
  end
291
322
 
292
323
  @chunked_body = false
@@ -343,7 +374,7 @@ module Puma
343
374
 
344
375
  begin
345
376
  chunk = @io.read_nonblock(want)
346
- rescue Errno::EAGAIN
377
+ rescue IO::WaitReadable
347
378
  return false
348
379
  rescue SystemCallError, IOError
349
380
  raise ConnectionError, "Connection error detected during read"
@@ -389,7 +420,10 @@ module Puma
389
420
  raise EOFError
390
421
  end
391
422
 
392
- return true if decode_chunk(chunk)
423
+ if decode_chunk(chunk)
424
+ @env[CONTENT_LENGTH] = @chunked_content_length
425
+ return true
426
+ end
393
427
  end
394
428
  end
395
429
 
@@ -401,20 +435,36 @@ module Puma
401
435
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
402
436
  @body.binmode
403
437
  @tempfile = @body
438
+ @chunked_content_length = 0
404
439
 
405
- return decode_chunk(body)
440
+ if decode_chunk(body)
441
+ @env[CONTENT_LENGTH] = @chunked_content_length
442
+ return true
443
+ end
444
+ end
445
+
446
+ def write_chunk(str)
447
+ @chunked_content_length += @body.write(str)
406
448
  end
407
449
 
408
450
  def decode_chunk(chunk)
409
451
  if @partial_part_left > 0
410
452
  if @partial_part_left <= chunk.size
411
453
  if @partial_part_left > 2
412
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
454
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
413
455
  end
414
456
  chunk = chunk[@partial_part_left..-1]
415
457
  @partial_part_left = 0
416
458
  else
417
- @body << chunk if @partial_part_left > 2 # don't include the last \r\n
459
+ if @partial_part_left > 2
460
+ if @partial_part_left == chunk.size + 1
461
+ # Don't include the last \r
462
+ write_chunk(chunk[0..(@partial_part_left-3)])
463
+ else
464
+ # don't include the last \r\n
465
+ write_chunk(chunk)
466
+ end
467
+ end
418
468
  @partial_part_left -= chunk.size
419
469
  return false
420
470
  end
@@ -461,12 +511,12 @@ module Puma
461
511
 
462
512
  case
463
513
  when got == len
464
- @body << part[0..-3] # to skip the ending \r\n
514
+ write_chunk(part[0..-3]) # to skip the ending \r\n
465
515
  when got <= len - 2
466
- @body << part
516
+ write_chunk(part)
467
517
  @partial_part_left = len - part.size
468
518
  when got == len - 1 # edge where we get just \r but not \n
469
- @body << part[0..-2]
519
+ write_chunk(part[0..-2])
470
520
  @partial_part_left = len - part.size
471
521
  end
472
522
  else
@@ -24,9 +24,8 @@ module Puma
24
24
 
25
25
  @phase = 0
26
26
  @workers = []
27
- @next_check = nil
27
+ @next_check = Time.now
28
28
 
29
- @phased_state = :idle
30
29
  @phased_restart = false
31
30
  end
32
31
 
@@ -37,7 +36,7 @@ module Puma
37
36
  begin
38
37
  loop do
39
38
  wait_workers
40
- break if @workers.empty?
39
+ break if @workers.reject {|w| w.pid.nil?}.empty?
41
40
  sleep 0.2
42
41
  end
43
42
  rescue Interrupt
@@ -73,11 +72,12 @@ module Puma
73
72
  @first_term_sent = nil
74
73
  @started_at = Time.now
75
74
  @last_checkin = Time.now
76
- @last_status = '{}'
75
+ @last_status = {}
77
76
  @term = false
78
77
  end
79
78
 
80
79
  attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
80
+ attr_writer :pid, :phase
81
81
 
82
82
  def booted?
83
83
  @stage == :booted
@@ -94,11 +94,16 @@ module Puma
94
94
 
95
95
  def ping!(status)
96
96
  @last_checkin = Time.now
97
- @last_status = status
97
+ require 'json'
98
+ @last_status = JSON.parse(status, symbolize_names: true)
98
99
  end
99
100
 
100
- def ping_timeout?(which)
101
- Time.now - @last_checkin > which
101
+ def ping_timeout
102
+ @last_checkin +
103
+ (booted? ?
104
+ @options[:worker_timeout] :
105
+ @options[:worker_boot_timeout]
106
+ )
102
107
  end
103
108
 
104
109
  def term
@@ -109,14 +114,14 @@ module Puma
109
114
  @term ||= true
110
115
  @first_term_sent ||= Time.now
111
116
  end
112
- Process.kill @signal, @pid
117
+ Process.kill @signal, @pid if @pid
113
118
  rescue Errno::ESRCH
114
119
  end
115
120
  end
116
121
 
117
122
  def kill
118
- Process.kill "KILL", @pid
119
- rescue Errno::ESRCH
123
+ @signal = 'KILL'
124
+ term
120
125
  end
121
126
 
122
127
  def hup
@@ -130,27 +135,43 @@ module Puma
130
135
  return if diff < 1
131
136
 
132
137
  master = Process.pid
138
+ if @options[:fork_worker]
139
+ @fork_writer << "-1\n"
140
+ end
133
141
 
134
142
  diff.times do
135
143
  idx = next_worker_index
136
- @launcher.config.run_hooks :before_worker_fork, idx
137
144
 
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
145
+ if @options[:fork_worker] && idx != 0
146
+ @fork_writer << "#{idx}\n"
147
+ pid = nil
148
+ else
149
+ pid = spawn_worker(idx, master)
143
150
  end
144
151
 
145
152
  debug "Spawned worker: #{pid}"
146
153
  @workers << Worker.new(idx, pid, @phase, @options)
154
+ end
147
155
 
148
- @launcher.config.run_hooks :after_worker_fork, idx
156
+ if @options[:fork_worker] &&
157
+ @workers.all? {|x| x.phase == @phase}
158
+
159
+ @fork_writer << "0\n"
149
160
  end
161
+ end
150
162
 
151
- if diff > 0
152
- @phased_state = :idle
163
+ def spawn_worker(idx, master)
164
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
165
+
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
153
171
  end
172
+
173
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
174
+ pid
154
175
  end
155
176
 
156
177
  def cull_workers
@@ -179,26 +200,12 @@ module Puma
179
200
  @workers.count { |w| !w.booted? } == 0
180
201
  end
181
202
 
182
- def check_workers(force=false)
183
- return if !force && @next_check && @next_check >= Time.now
203
+ def check_workers
204
+ return if @next_check >= Time.now
184
205
 
185
206
  @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
186
207
 
187
- any = false
188
-
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
201
-
208
+ timeout_workers
202
209
  wait_workers
203
210
  cull_workers
204
211
  spawn_workers
@@ -211,17 +218,18 @@ module Puma
211
218
  w = @workers.find { |x| x.phase != @phase }
212
219
 
213
220
  if w
214
- if @phased_state == :idle
215
- @phased_state = :waiting
216
- log "- Stopping #{w.pid} for phased upgrade..."
217
- end
218
-
221
+ log "- Stopping #{w.pid} for phased upgrade..."
219
222
  unless w.term?
220
223
  w.term
221
224
  log "- #{w.signal} sent to #{w.pid}..."
222
225
  end
223
226
  end
224
227
  end
228
+
229
+ @next_check = [
230
+ @workers.reject(&:term?).map(&:ping_timeout).min,
231
+ @next_check
232
+ ].compact.min
225
233
  end
226
234
 
227
235
  def wakeup!
@@ -240,10 +248,16 @@ module Puma
240
248
  $0 = title
241
249
 
242
250
  Signal.trap "SIGINT", "IGNORE"
251
+ Signal.trap "SIGCHLD", "DEFAULT"
252
+
253
+ fork_worker = @options[:fork_worker] && index == 0
243
254
 
244
255
  @workers = []
245
- @master_read.close
246
- @suicide_pipe.close
256
+ if !@options[:fork_worker] || fork_worker
257
+ @master_read.close
258
+ @suicide_pipe.close
259
+ @fork_writer.close
260
+ end
247
261
 
248
262
  Thread.new do
249
263
  Puma.set_thread_name "worker check pipe"
@@ -264,17 +278,49 @@ module Puma
264
278
 
265
279
  # Invoke any worker boot hooks so they can get
266
280
  # things in shape before booting the app.
267
- @launcher.config.run_hooks :before_worker_boot, index
281
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
282
+
283
+ server = @server ||= start_server
284
+ restart_server = Queue.new << true << false
268
285
 
269
- server = start_server
286
+ if fork_worker
287
+ restart_server.clear
288
+ worker_pids = []
289
+ Signal.trap "SIGCHLD" do
290
+ wakeup! if worker_pids.reject! do |p|
291
+ Process.wait(p, Process::WNOHANG) rescue true
292
+ end
293
+ end
294
+
295
+ Thread.new do
296
+ Puma.set_thread_name "worker fork pipe"
297
+ while (idx = @fork_pipe.gets)
298
+ idx = idx.to_i
299
+ if idx == -1 # stop server
300
+ if restart_server.length > 0
301
+ restart_server.clear
302
+ server.begin_restart(true)
303
+ @launcher.config.run_hooks :before_refork, nil, @launcher.events
304
+ nakayoshi_gc
305
+ end
306
+ elsif idx == 0 # restart server
307
+ restart_server << true << false
308
+ else # fork worker
309
+ worker_pids << pid = spawn_worker(idx, master)
310
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
311
+ end
312
+ end
313
+ end
314
+ end
270
315
 
271
316
  Signal.trap "SIGTERM" do
272
317
  @worker_write << "e#{Process.pid}\n" rescue nil
273
318
  server.stop
319
+ restart_server << false
274
320
  end
275
321
 
276
322
  begin
277
- @worker_write << "b#{Process.pid}\n"
323
+ @worker_write << "b#{Process.pid}:#{index}\n"
278
324
  rescue SystemCallError, IOError
279
325
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
280
326
  STDERR.puts "Master seems to have exited, exiting."
@@ -283,17 +329,12 @@ module Puma
283
329
 
284
330
  Thread.new(@worker_write) do |io|
285
331
  Puma.set_thread_name "stat payload"
286
- base_payload = "p#{Process.pid}"
287
332
 
288
333
  while true
289
334
  sleep Const::WORKER_CHECK_INTERVAL
290
335
  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
336
+ require 'json'
337
+ io << "p#{Process.pid}#{server.stats.to_json}\n"
297
338
  rescue IOError
298
339
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
299
340
  break
@@ -301,11 +342,11 @@ module Puma
301
342
  end
302
343
  end
303
344
 
304
- server.run.join
345
+ server.run.join while restart_server.pop
305
346
 
306
347
  # Invoke any worker shutdown hooks so they can prevent the worker
307
348
  # exiting until any background operations are completed
308
- @launcher.config.run_hooks :before_worker_shutdown, index
349
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
309
350
  ensure
310
351
  @worker_write << "t#{Process.pid}\n" rescue nil
311
352
  @worker_write.close
@@ -352,18 +393,57 @@ module Puma
352
393
  # the master process.
353
394
  def stats
354
395
  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} }!
396
+ worker_status = @workers.map do |w|
397
+ {
398
+ started_at: w.started_at.utc.iso8601,
399
+ pid: w.pid,
400
+ index: w.index,
401
+ phase: w.phase,
402
+ booted: w.booted?,
403
+ last_checkin: w.last_checkin.utc.iso8601,
404
+ last_status: w.last_status,
405
+ }
406
+ end
407
+
408
+ {
409
+ started_at: @started_at.utc.iso8601,
410
+ workers: @workers.size,
411
+ phase: @phase,
412
+ booted_workers: worker_status.count { |w| w[:booted] },
413
+ old_workers: old_worker_count,
414
+ worker_status: worker_status,
415
+ }
358
416
  end
359
417
 
360
418
  def preload?
361
419
  @options[:preload_app]
362
420
  end
363
421
 
422
+ def fork_worker!
423
+ if (worker = @workers.find { |w| w.index == 0 })
424
+ worker.phase += 1
425
+ end
426
+ phased_restart
427
+ end
428
+
364
429
  # We do this in a separate method to keep the lambda scope
365
430
  # of the signals handlers as small as possible.
366
431
  def setup_signals
432
+ if @options[:fork_worker]
433
+ Signal.trap "SIGURG" do
434
+ fork_worker!
435
+ end
436
+
437
+ # Auto-fork after the specified number of requests.
438
+ if (fork_requests = @options[:fork_worker].to_i) > 0
439
+ @launcher.events.register(:ping!) do |w|
440
+ fork_worker! if w.index == 0 &&
441
+ w.phase == 0 &&
442
+ w.last_status[:requests_count] >= fork_requests
443
+ end
444
+ end
445
+ end
446
+
367
447
  Signal.trap "SIGCHLD" do
368
448
  wakeup!
369
449
  end
@@ -447,12 +527,11 @@ module Puma
447
527
  #
448
528
  @check_pipe, @suicide_pipe = Puma::Util.pipe
449
529
 
450
- if daemon?
451
- log "* Daemonizing..."
452
- Process.daemon(true)
453
- else
454
- log "Use Ctrl-C to stop"
455
- end
530
+ # Separate pipe used by worker 0 to receive commands to
531
+ # fork new worker processes.
532
+ @fork_pipe, @fork_writer = Puma::Util.pipe
533
+
534
+ log "Use Ctrl-C to stop"
456
535
 
457
536
  redirect_io
458
537
 
@@ -464,7 +543,8 @@ module Puma
464
543
 
465
544
  @master_read, @worker_write = read, @wakeup
466
545
 
467
- @launcher.config.run_hooks :before_fork, nil
546
+ @launcher.config.run_hooks :before_fork, nil, @launcher.events
547
+ nakayoshi_gc
468
548
 
469
549
  spawn_workers
470
550
 
@@ -475,8 +555,6 @@ module Puma
475
555
  @launcher.events.fire_on_booted!
476
556
 
477
557
  begin
478
- force_check = false
479
-
480
558
  while @status == :run
481
559
  begin
482
560
  if @phased_restart
@@ -484,34 +562,39 @@ module Puma
484
562
  @phased_restart = false
485
563
  end
486
564
 
487
- check_workers force_check
565
+ check_workers
488
566
 
489
- force_check = false
490
-
491
- res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
567
+ res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
492
568
 
493
569
  if res
494
570
  req = read.read_nonblock(1)
495
571
 
572
+ @next_check = Time.now if req == "!"
496
573
  next if !req || req == "!"
497
574
 
498
575
  result = read.gets
499
576
  pid = result.to_i
500
577
 
578
+ if req == "b" || req == "f"
579
+ pid, idx = result.split(':').map(&:to_i)
580
+ w = @workers.find {|x| x.index == idx}
581
+ w.pid = pid if w.pid.nil?
582
+ end
583
+
501
584
  if w = @workers.find { |x| x.pid == pid }
502
585
  case req
503
586
  when "b"
504
587
  w.boot!
505
588
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
506
- force_check = true
589
+ @next_check = Time.now
507
590
  when "e"
508
591
  # external term, see worker method, Signal.trap "SIGTERM"
509
592
  w.instance_variable_set :@term, true
510
593
  when "t"
511
594
  w.term unless w.term?
512
- force_check = true
513
595
  when "p"
514
596
  w.ping!(result.sub(/^\d+/,'').chomp)
597
+ @launcher.events.fire(:ping!, w)
515
598
  end
516
599
  else
517
600
  log "! Out-of-sync worker list, no #{pid} worker"
@@ -538,6 +621,7 @@ module Puma
538
621
  # `#term` if needed
539
622
  def wait_workers
540
623
  @workers.reject! do |w|
624
+ next false if w.pid.nil?
541
625
  begin
542
626
  if Process.wait(w.pid, Process::WNOHANG)
543
627
  true
@@ -546,9 +630,34 @@ module Puma
546
630
  nil
547
631
  end
548
632
  rescue Errno::ECHILD
549
- true # child is already terminated
633
+ begin
634
+ Process.kill(0, w.pid)
635
+ false # child still alive, but has another parent
636
+ rescue Errno::ESRCH, Errno::EPERM
637
+ true # child is already terminated
638
+ end
550
639
  end
551
640
  end
552
641
  end
642
+
643
+ def timeout_workers
644
+ @workers.each do |w|
645
+ if !w.term? && w.ping_timeout <= Time.now
646
+ log "! Terminating timed out worker: #{w.pid}"
647
+ w.kill
648
+ end
649
+ end
650
+ end
651
+
652
+ def nakayoshi_gc
653
+ return unless @options[:nakayoshi_fork]
654
+ log "! Promoting existing objects to old generation..."
655
+ 4.times { GC.start(full_mark: false) }
656
+ if GC.respond_to?(:compact)
657
+ log "! Compacting..."
658
+ GC.compact
659
+ end
660
+ log "! Friendly fork preparation complete."
661
+ end
553
662
  end
554
663
  end