ed-precompiled_puma 7.0.4

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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3172 -0
  3. data/LICENSE +29 -0
  4. data/README.md +477 -0
  5. data/bin/puma +10 -0
  6. data/bin/puma-wild +25 -0
  7. data/bin/pumactl +12 -0
  8. data/docs/architecture.md +74 -0
  9. data/docs/compile_options.md +55 -0
  10. data/docs/deployment.md +102 -0
  11. data/docs/fork_worker.md +41 -0
  12. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  13. data/docs/images/puma-connection-flow.png +0 -0
  14. data/docs/images/puma-general-arch.png +0 -0
  15. data/docs/java_options.md +54 -0
  16. data/docs/jungle/README.md +9 -0
  17. data/docs/jungle/rc.d/README.md +74 -0
  18. data/docs/jungle/rc.d/puma +61 -0
  19. data/docs/jungle/rc.d/puma.conf +10 -0
  20. data/docs/kubernetes.md +80 -0
  21. data/docs/nginx.md +80 -0
  22. data/docs/plugins.md +42 -0
  23. data/docs/rails_dev_mode.md +28 -0
  24. data/docs/restart.md +65 -0
  25. data/docs/signals.md +98 -0
  26. data/docs/stats.md +148 -0
  27. data/docs/systemd.md +253 -0
  28. data/docs/testing_benchmarks_local_files.md +150 -0
  29. data/docs/testing_test_rackup_ci_files.md +36 -0
  30. data/ext/puma_http11/PumaHttp11Service.java +17 -0
  31. data/ext/puma_http11/ext_help.h +15 -0
  32. data/ext/puma_http11/extconf.rb +65 -0
  33. data/ext/puma_http11/http11_parser.c +1057 -0
  34. data/ext/puma_http11/http11_parser.h +65 -0
  35. data/ext/puma_http11/http11_parser.java.rl +145 -0
  36. data/ext/puma_http11/http11_parser.rl +149 -0
  37. data/ext/puma_http11/http11_parser_common.rl +54 -0
  38. data/ext/puma_http11/mini_ssl.c +852 -0
  39. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  40. data/ext/puma_http11/org/jruby/puma/Http11.java +257 -0
  41. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +455 -0
  42. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +509 -0
  43. data/ext/puma_http11/puma_http11.c +507 -0
  44. data/lib/puma/app/status.rb +96 -0
  45. data/lib/puma/binder.rb +511 -0
  46. data/lib/puma/cli.rb +245 -0
  47. data/lib/puma/client.rb +720 -0
  48. data/lib/puma/cluster/worker.rb +182 -0
  49. data/lib/puma/cluster/worker_handle.rb +127 -0
  50. data/lib/puma/cluster.rb +635 -0
  51. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  52. data/lib/puma/commonlogger.rb +115 -0
  53. data/lib/puma/configuration.rb +452 -0
  54. data/lib/puma/const.rb +307 -0
  55. data/lib/puma/control_cli.rb +320 -0
  56. data/lib/puma/detect.rb +47 -0
  57. data/lib/puma/dsl.rb +1480 -0
  58. data/lib/puma/error_logger.rb +115 -0
  59. data/lib/puma/events.rb +72 -0
  60. data/lib/puma/io_buffer.rb +50 -0
  61. data/lib/puma/jruby_restart.rb +11 -0
  62. data/lib/puma/json_serialization.rb +96 -0
  63. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  64. data/lib/puma/launcher.rb +496 -0
  65. data/lib/puma/log_writer.rb +147 -0
  66. data/lib/puma/minissl/context_builder.rb +96 -0
  67. data/lib/puma/minissl.rb +463 -0
  68. data/lib/puma/null_io.rb +101 -0
  69. data/lib/puma/plugin/systemd.rb +90 -0
  70. data/lib/puma/plugin/tmp_restart.rb +36 -0
  71. data/lib/puma/plugin.rb +111 -0
  72. data/lib/puma/rack/builder.rb +297 -0
  73. data/lib/puma/rack/urlmap.rb +93 -0
  74. data/lib/puma/rack_default.rb +24 -0
  75. data/lib/puma/reactor.rb +140 -0
  76. data/lib/puma/request.rb +701 -0
  77. data/lib/puma/runner.rb +211 -0
  78. data/lib/puma/sd_notify.rb +146 -0
  79. data/lib/puma/server.rb +734 -0
  80. data/lib/puma/single.rb +72 -0
  81. data/lib/puma/state_file.rb +69 -0
  82. data/lib/puma/thread_pool.rb +402 -0
  83. data/lib/puma/util.rb +134 -0
  84. data/lib/puma.rb +93 -0
  85. data/lib/rack/handler/puma.rb +144 -0
  86. data/tools/Dockerfile +18 -0
  87. data/tools/trickletest.rb +44 -0
  88. metadata +152 -0
@@ -0,0 +1,635 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runner'
4
+ require_relative 'util'
5
+ require_relative 'plugin'
6
+ require_relative 'cluster/worker_handle'
7
+ require_relative 'cluster/worker'
8
+
9
+ module Puma
10
+ # This class is instantiated by the `Puma::Launcher` and used
11
+ # to boot and serve a Ruby application when puma "workers" are needed
12
+ # i.e. when using multi-processes. For example `$ puma -w 5`
13
+ #
14
+ # An instance of this class will spawn the number of processes passed in
15
+ # via the `spawn_workers` method call. Each worker will have it's own
16
+ # instance of a `Puma::Server`.
17
+ class Cluster < Runner
18
+ def initialize(launcher)
19
+ super(launcher)
20
+
21
+ @phase = 0
22
+ @workers = []
23
+ @next_check = Time.now
24
+
25
+ @worker_max = [] # keeps track of 'max' stat values
26
+ @phased_restart = false
27
+ end
28
+
29
+ # Returns the list of cluster worker handles.
30
+ # @return [Array<Puma::Cluster::WorkerHandle>]
31
+ attr_reader :workers
32
+
33
+ def stop_workers
34
+ log "- Gracefully shutting down workers..."
35
+ @workers.each { |x| x.term }
36
+
37
+ begin
38
+ loop do
39
+ wait_workers
40
+ break if @workers.reject {|w| w.pid.nil?}.empty?
41
+ sleep 0.2
42
+ end
43
+ rescue Interrupt
44
+ log "! Cancelled waiting for workers"
45
+ end
46
+ end
47
+
48
+ def start_phased_restart(refork = false)
49
+ @events.fire_before_restart!
50
+ @phase += 1
51
+ if refork
52
+ log "- Starting worker refork, phase: #{@phase}"
53
+ else
54
+ log "- Starting phased worker restart, phase: #{@phase}"
55
+ end
56
+
57
+ # Be sure to change the directory again before loading
58
+ # the app. This way we can pick up new code.
59
+ dir = @launcher.restart_dir
60
+ log "+ Changing to #{dir}"
61
+ Dir.chdir dir
62
+ end
63
+
64
+ def redirect_io
65
+ super
66
+
67
+ @workers.each { |x| x.hup }
68
+ end
69
+
70
+ def spawn_workers
71
+ diff = @options[:workers] - @workers.size
72
+ return if diff < 1
73
+
74
+ master = Process.pid
75
+ if @options[:fork_worker]
76
+ @fork_writer << "-1\n"
77
+ end
78
+
79
+ diff.times do
80
+ idx = next_worker_index
81
+
82
+ if @options[:fork_worker] && idx != 0
83
+ @fork_writer << "#{idx}\n"
84
+ pid = nil
85
+ else
86
+ pid = spawn_worker(idx, master)
87
+ end
88
+
89
+ debug "Spawned worker: #{pid}"
90
+ @workers << WorkerHandle.new(idx, pid, @phase, @options)
91
+ end
92
+
93
+ if @options[:fork_worker] && all_workers_in_phase?
94
+ @fork_writer << "0\n"
95
+
96
+ if worker_at(0).phase > 0
97
+ @fork_writer << "-2\n"
98
+ end
99
+ end
100
+ end
101
+
102
+ # @version 5.0.0
103
+ def spawn_worker(idx, master)
104
+ @config.run_hooks(:before_worker_fork, idx, @log_writer)
105
+
106
+ pid = fork { worker(idx, master) }
107
+ if !pid
108
+ log "! Complete inability to spawn new workers detected"
109
+ log "! Seppuku is the only choice."
110
+ exit! 1
111
+ end
112
+
113
+ @config.run_hooks(:after_worker_fork, idx, @log_writer)
114
+ pid
115
+ end
116
+
117
+ def cull_workers
118
+ diff = @workers.size - @options[:workers]
119
+ return if diff < 1
120
+ debug "Culling #{diff} workers"
121
+
122
+ workers = workers_to_cull(diff)
123
+ debug "Workers to cull: #{workers.inspect}"
124
+
125
+ workers.each do |worker|
126
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
127
+ worker.term
128
+ end
129
+ end
130
+
131
+ def workers_to_cull(diff)
132
+ workers = @workers.sort_by(&:started_at)
133
+
134
+ # In fork_worker mode, worker 0 acts as our master process.
135
+ # We should avoid culling it to preserve copy-on-write memory gains.
136
+ workers.reject! { |w| w.index == 0 } if @options[:fork_worker]
137
+
138
+ workers[cull_start_index(diff), diff]
139
+ end
140
+
141
+ def cull_start_index(diff)
142
+ case @options[:worker_culling_strategy]
143
+ when :oldest
144
+ 0
145
+ else # :youngest
146
+ -diff
147
+ end
148
+ end
149
+
150
+ # @!attribute [r] next_worker_index
151
+ def next_worker_index
152
+ occupied_positions = @workers.map(&:index)
153
+ idx = 0
154
+ idx += 1 until !occupied_positions.include?(idx)
155
+ idx
156
+ end
157
+
158
+ def worker_at(idx)
159
+ @workers.find { |w| w.index == idx }
160
+ end
161
+
162
+ def all_workers_booted?
163
+ @workers.count { |w| !w.booted? } == 0
164
+ end
165
+
166
+ def all_workers_in_phase?
167
+ @workers.all? { |w| w.phase == @phase }
168
+ end
169
+
170
+ def all_workers_idle_timed_out?
171
+ (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
172
+ end
173
+
174
+ def check_workers(refork = false)
175
+ return if @next_check >= Time.now
176
+
177
+ @next_check = Time.now + @options[:worker_check_interval]
178
+
179
+ timeout_workers
180
+ wait_workers
181
+ cull_workers
182
+ spawn_workers
183
+
184
+ if all_workers_booted?
185
+ # If we're running at proper capacity, check to see if
186
+ # we need to phase any workers out (which will restart
187
+ # in the right phase).
188
+ #
189
+ w = @workers.find { |x| x.phase != @phase }
190
+
191
+ if w
192
+ if refork
193
+ log "- Stopping #{w.pid} for refork..."
194
+ else
195
+ log "- Stopping #{w.pid} for phased upgrade..."
196
+ end
197
+
198
+ unless w.term?
199
+ w.term
200
+ log "- #{w.signal} sent to #{w.pid}..."
201
+ end
202
+ end
203
+ end
204
+
205
+ t = @workers.reject(&:term?)
206
+ t.map!(&:ping_timeout)
207
+
208
+ @next_check = [t.min, @next_check].compact.min
209
+ end
210
+
211
+ def worker(index, master)
212
+ @workers = []
213
+
214
+ @master_read.close
215
+ @suicide_pipe.close
216
+ @fork_writer.close
217
+
218
+ pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
219
+ if @options[:fork_worker]
220
+ pipes[:fork_pipe] = @fork_pipe
221
+ pipes[:wakeup] = @wakeup
222
+ end
223
+
224
+ server = start_server if preload?
225
+ new_worker = Worker.new index: index,
226
+ master: master,
227
+ launcher: @launcher,
228
+ pipes: pipes,
229
+ server: server
230
+ new_worker.run
231
+ end
232
+
233
+ def restart
234
+ @restart = true
235
+ stop
236
+ end
237
+
238
+ def phased_restart(refork = false)
239
+ return false if @options[:preload_app] && !refork
240
+
241
+ @phased_restart = refork ? :refork : true
242
+ wakeup!
243
+
244
+ true
245
+ end
246
+
247
+ def stop
248
+ @status = :stop
249
+ wakeup!
250
+ end
251
+
252
+ def stop_blocked
253
+ @status = :stop if @status == :run
254
+ wakeup!
255
+ @control&.stop true
256
+ Process.waitall
257
+ end
258
+
259
+ def halt
260
+ @status = :halt
261
+ wakeup!
262
+ end
263
+
264
+ def reload_worker_directory
265
+ dir = @launcher.restart_dir
266
+ log "+ Changing to #{dir}"
267
+ Dir.chdir dir
268
+ end
269
+
270
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
271
+ # the master process. Calling this also resets stat 'max' values to zero.
272
+ # @!attribute [r] stats
273
+ # @return [Hash]
274
+
275
+ def stats
276
+ old_worker_count = @workers.count { |w| w.phase != @phase }
277
+ worker_status = @workers.map do |w|
278
+ w.reset_max
279
+ {
280
+ started_at: utc_iso8601(w.started_at),
281
+ pid: w.pid,
282
+ index: w.index,
283
+ phase: w.phase,
284
+ booted: w.booted?,
285
+ last_checkin: utc_iso8601(w.last_checkin),
286
+ last_status: w.last_status,
287
+ }
288
+ end
289
+ {
290
+ started_at: utc_iso8601(@started_at),
291
+ workers: @workers.size,
292
+ phase: @phase,
293
+ booted_workers: worker_status.count { |w| w[:booted] },
294
+ old_workers: old_worker_count,
295
+ worker_status: worker_status,
296
+ }.merge(super)
297
+ end
298
+
299
+ def preload?
300
+ @options[:preload_app]
301
+ end
302
+
303
+ # @version 5.0.0
304
+ def fork_worker!
305
+ if (worker = worker_at 0)
306
+ worker.phase += 1
307
+ end
308
+ phased_restart(true)
309
+ end
310
+
311
+ # We do this in a separate method to keep the lambda scope
312
+ # of the signals handlers as small as possible.
313
+ def setup_signals
314
+ if @options[:fork_worker]
315
+ Signal.trap "SIGURG" do
316
+ fork_worker!
317
+ end
318
+
319
+ # Auto-fork after the specified number of requests.
320
+ if (fork_requests = @options[:fork_worker].to_i) > 0
321
+ @events.register(:ping!) do |w|
322
+ fork_worker! if w.index == 0 &&
323
+ w.phase == 0 &&
324
+ w.last_status[:requests_count] >= fork_requests
325
+ end
326
+ end
327
+ end
328
+
329
+ Signal.trap "SIGCHLD" do
330
+ wakeup!
331
+ end
332
+
333
+ Signal.trap "TTIN" do
334
+ @options[:workers] += 1
335
+ wakeup!
336
+ end
337
+
338
+ Signal.trap "TTOU" do
339
+ @options[:workers] -= 1 if @options[:workers] >= 2
340
+ wakeup!
341
+ end
342
+
343
+ master_pid = Process.pid
344
+
345
+ Signal.trap "SIGTERM" do
346
+ # The worker installs their own SIGTERM when booted.
347
+ # Until then, this is run by the worker and the worker
348
+ # should just exit if they get it.
349
+ if Process.pid != master_pid
350
+ log "Early termination of worker"
351
+ exit! 0
352
+ else
353
+ @launcher.close_binder_listeners
354
+
355
+ stop_workers
356
+ stop
357
+ @events.fire_after_stopped!
358
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
359
+ exit 0 # Clean exit, workers were stopped
360
+ end
361
+ end
362
+ end
363
+
364
+ def run
365
+ @status = :run
366
+
367
+ output_header "cluster"
368
+
369
+ # This is aligned with the output from Runner, see Runner#output_header
370
+ log "* Workers: #{@options[:workers]}"
371
+
372
+ if preload?
373
+ # Threads explicitly marked as fork safe will be ignored. Used in Rails,
374
+ # but may be used by anyone.
375
+ fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
376
+
377
+ before = Thread.list.reject(&fork_safe)
378
+
379
+ log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
380
+ log "* Preloading application"
381
+ load_and_bind
382
+
383
+ after = Thread.list.reject(&fork_safe)
384
+
385
+ if after.size > before.size
386
+ threads = (after - before)
387
+ if threads.first.respond_to? :backtrace
388
+ log "! WARNING: Detected #{after.size-before.size} Thread(s) started in app boot:"
389
+ threads.each do |t|
390
+ log "! #{t.inspect} - #{t.backtrace ? t.backtrace.first : ''}"
391
+ end
392
+ else
393
+ log "! WARNING: Detected #{after.size-before.size} Thread(s) started in app boot"
394
+ end
395
+ end
396
+ else
397
+ log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
398
+
399
+ unless @config.app_configured?
400
+ error "No application configured, nothing to run"
401
+ exit 1
402
+ end
403
+
404
+ @launcher.binder.parse @options[:binds]
405
+ end
406
+
407
+ read, @wakeup = Puma::Util.pipe
408
+
409
+ setup_signals
410
+
411
+ # Used by the workers to detect if the master process dies.
412
+ # If select says that @check_pipe is ready, it's because the
413
+ # master has exited and @suicide_pipe has been automatically
414
+ # closed.
415
+ #
416
+ @check_pipe, @suicide_pipe = Puma::Util.pipe
417
+
418
+ # Separate pipe used by worker 0 to receive commands to
419
+ # fork new worker processes.
420
+ @fork_pipe, @fork_writer = Puma::Util.pipe
421
+
422
+ log "Use Ctrl-C to stop"
423
+
424
+ warn_ruby_mn_threads
425
+ single_worker_warning
426
+
427
+ redirect_io
428
+
429
+ Plugins.fire_background
430
+
431
+ @launcher.write_state
432
+
433
+ start_control
434
+
435
+ @master_read, @worker_write = read, @wakeup
436
+
437
+ @options[:worker_write] = @worker_write
438
+
439
+ @config.run_hooks(:before_fork, nil, @log_writer)
440
+
441
+ spawn_workers
442
+
443
+ Signal.trap "SIGINT" do
444
+ stop
445
+ end
446
+
447
+ begin
448
+ booted = false
449
+ in_phased_restart = false
450
+ workers_not_booted = @options[:workers]
451
+
452
+ while @status == :run
453
+ begin
454
+ if @options[:idle_timeout] && all_workers_idle_timed_out?
455
+ log "- All workers reached idle timeout"
456
+ break
457
+ end
458
+
459
+ if @phased_restart
460
+ start_phased_restart(@phased_restart == :refork)
461
+
462
+ in_phased_restart = @phased_restart
463
+ @phased_restart = false
464
+
465
+ workers_not_booted = @options[:workers]
466
+ # worker 0 is not restarted on refork
467
+ workers_not_booted -= 1 if in_phased_restart == :refork
468
+ end
469
+
470
+ check_workers(in_phased_restart == :refork)
471
+
472
+ if read.wait_readable([0, @next_check - Time.now].max)
473
+ req = read.read_nonblock(1)
474
+ next unless req
475
+
476
+ if req == PIPE_WAKEUP
477
+ @next_check = Time.now
478
+ next
479
+ end
480
+
481
+ result = read.gets
482
+ pid = result.to_i
483
+
484
+ if req == PIPE_BOOT || req == PIPE_FORK
485
+ pid, idx = result.split(':').map(&:to_i)
486
+ w = worker_at idx
487
+ w.pid = pid if w.pid.nil?
488
+ end
489
+
490
+ if w = @workers.find { |x| x.pid == pid }
491
+ case req
492
+ when PIPE_BOOT
493
+ w.boot!
494
+ log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
495
+ @next_check = Time.now
496
+ workers_not_booted -= 1
497
+ when PIPE_EXTERNAL_TERM
498
+ # external term, see worker method, Signal.trap "SIGTERM"
499
+ w.term!
500
+ when PIPE_TERM
501
+ w.term unless w.term?
502
+ when PIPE_PING
503
+ status = result.sub(/^\d+/,'').chomp
504
+ w.ping!(status)
505
+ @events.fire(:ping!, w)
506
+
507
+ if in_phased_restart && @options[:fork_worker] && workers_not_booted.positive? && w0 = worker_at(0)
508
+ w0.ping!(status)
509
+ @events.fire(:ping!, w0)
510
+ end
511
+
512
+ if !booted && @workers.none? {|worker| worker.last_status.empty?}
513
+ @events.fire_after_booted!
514
+ debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
515
+ booted = true
516
+ end
517
+ when PIPE_IDLE
518
+ if idle_workers[pid]
519
+ idle_workers.delete pid
520
+ else
521
+ idle_workers[pid] = true
522
+ end
523
+ end
524
+ else
525
+ log "! Out-of-sync worker list, no #{pid} worker"
526
+ end
527
+ end
528
+
529
+ if in_phased_restart && workers_not_booted.zero?
530
+ @events.fire_after_booted!
531
+ debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
532
+ in_phased_restart = false
533
+ end
534
+ rescue Interrupt
535
+ @status = :stop
536
+ end
537
+ end
538
+
539
+ stop_workers unless @status == :halt
540
+ ensure
541
+ @check_pipe.close
542
+ @suicide_pipe.close
543
+ read.close
544
+ @wakeup.close
545
+ end
546
+ end
547
+
548
+ private
549
+
550
+ def single_worker_warning
551
+ return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
552
+
553
+ log "! WARNING: Detected running cluster mode with 1 worker."
554
+ log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
555
+ log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
556
+ log "! Set the `silence_single_worker_warning` option to silence this warning message."
557
+ end
558
+
559
+ # loops thru @workers, removing workers that exited, and calling
560
+ # `#term` if needed
561
+ def wait_workers
562
+ # Reap all children, known workers or otherwise.
563
+ # If puma has PID 1, as it's common in containerized environments,
564
+ # then it's responsible for reaping orphaned processes, so we must reap
565
+ # all our dead children, regardless of whether they are workers we spawned
566
+ # or some reattached processes.
567
+ reaped_children = {}
568
+ loop do
569
+ begin
570
+ pid, status = Process.wait2(-1, Process::WNOHANG)
571
+ break unless pid
572
+ reaped_children[pid] = status
573
+ rescue Errno::ECHILD
574
+ break
575
+ end
576
+ end
577
+
578
+ @workers.reject! do |w|
579
+ next false if w.pid.nil?
580
+ begin
581
+ # We may need to check the PID individually because:
582
+ # 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
583
+ # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
584
+ # 2. When `fork_worker` is enabled, some worker may not be direct children,
585
+ # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
586
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
587
+ w.process_status = status
588
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
589
+ true
590
+ else
591
+ w.term if w.term?
592
+ nil
593
+ end
594
+ rescue Errno::ECHILD
595
+ begin
596
+ Process.kill(0, w.pid)
597
+ # child still alive but has another parent (e.g., using fork_worker)
598
+ w.term if w.term?
599
+ false
600
+ rescue Errno::ESRCH, Errno::EPERM
601
+ true # child is already terminated
602
+ end
603
+ end
604
+ end
605
+
606
+ # Log unknown children
607
+ reaped_children.each do |pid, status|
608
+ log "! reaped unknown child process pid=#{pid} status=#{status}"
609
+ end
610
+ end
611
+
612
+ # @version 5.0.0
613
+ def timeout_workers
614
+ @workers.each do |w|
615
+ if !w.term? && w.ping_timeout <= Time.now
616
+ details = if w.booted?
617
+ "(Worker #{w.index} failed to check in within #{@options[:worker_timeout]} seconds)"
618
+ else
619
+ "(Worker #{w.index} failed to boot within #{@options[:worker_boot_timeout]} seconds)"
620
+ end
621
+ log "! Terminating timed out worker #{details}: #{w.pid}"
622
+ w.kill
623
+ end
624
+ end
625
+ end
626
+
627
+ def idle_timed_out_worker_pids
628
+ idle_workers.keys
629
+ end
630
+
631
+ def idle_workers
632
+ @idle_workers ||= {}
633
+ end
634
+ end
635
+ end