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