tiny-fast-gem 0.0.1

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