puma 3.11.4 → 6.0.1

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1717 -432
  3. data/LICENSE +23 -20
  4. data/README.md +190 -64
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +59 -21
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +69 -58
  9. data/docs/fork_worker.md +31 -0
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +2 -2
  19. data/docs/plugins.md +22 -12
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +95 -120
  25. data/docs/testing_benchmarks_local_files.md +150 -0
  26. data/docs/testing_test_rackup_ci_files.md +36 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +61 -3
  30. data/ext/puma_http11/http11_parser.c +106 -118
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +22 -38
  33. data/ext/puma_http11/http11_parser.rl +6 -4
  34. data/ext/puma_http11/http11_parser_common.rl +6 -6
  35. data/ext/puma_http11/mini_ssl.c +376 -93
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +250 -88
  40. data/ext/puma_http11/puma_http11.c +49 -57
  41. data/lib/puma/app/status.rb +71 -49
  42. data/lib/puma/binder.rb +243 -148
  43. data/lib/puma/cli.rb +50 -36
  44. data/lib/puma/client.rb +373 -233
  45. data/lib/puma/cluster/worker.rb +175 -0
  46. data/lib/puma/cluster/worker_handle.rb +97 -0
  47. data/lib/puma/cluster.rb +268 -235
  48. data/lib/puma/commonlogger.rb +4 -2
  49. data/lib/puma/configuration.rb +116 -88
  50. data/lib/puma/const.rb +49 -30
  51. data/lib/puma/control_cli.rb +123 -76
  52. data/lib/puma/detect.rb +33 -2
  53. data/lib/puma/dsl.rb +685 -135
  54. data/lib/puma/error_logger.rb +112 -0
  55. data/lib/puma/events.rb +17 -111
  56. data/lib/puma/io_buffer.rb +44 -5
  57. data/lib/puma/jruby_restart.rb +4 -59
  58. data/lib/puma/json_serialization.rb +96 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  60. data/lib/puma/launcher.rb +196 -130
  61. data/lib/puma/log_writer.rb +137 -0
  62. data/lib/puma/minissl/context_builder.rb +92 -0
  63. data/lib/puma/minissl.rb +249 -69
  64. data/lib/puma/null_io.rb +20 -1
  65. data/lib/puma/plugin/tmp_restart.rb +3 -1
  66. data/lib/puma/plugin.rb +9 -13
  67. data/lib/puma/rack/builder.rb +8 -9
  68. data/lib/puma/rack/urlmap.rb +2 -0
  69. data/lib/puma/rack_default.rb +3 -1
  70. data/lib/puma/reactor.rb +90 -187
  71. data/lib/puma/request.rb +644 -0
  72. data/lib/puma/runner.rb +94 -71
  73. data/lib/puma/server.rb +337 -715
  74. data/lib/puma/single.rb +27 -72
  75. data/lib/puma/state_file.rb +46 -7
  76. data/lib/puma/systemd.rb +47 -0
  77. data/lib/puma/thread_pool.rb +184 -93
  78. data/lib/puma/util.rb +23 -10
  79. data/lib/puma.rb +60 -3
  80. data/lib/rack/handler/puma.rb +17 -15
  81. data/tools/Dockerfile +16 -0
  82. data/tools/trickletest.rb +0 -1
  83. metadata +53 -33
  84. data/ext/puma_http11/io_buffer.c +0 -155
  85. data/lib/puma/accept_nonblock.rb +0 -23
  86. data/lib/puma/compat.rb +0 -14
  87. data/lib/puma/convenient.rb +0 -23
  88. data/lib/puma/daemon_ext.rb +0 -31
  89. data/lib/puma/delegation.rb +0 -11
  90. data/lib/puma/java_io_buffer.rb +0 -45
  91. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  92. data/lib/puma/tcp_logger.rb +0 -39
  93. data/tools/jungle/README.md +0 -19
  94. data/tools/jungle/init.d/README.md +0 -61
  95. data/tools/jungle/init.d/puma +0 -421
  96. data/tools/jungle/init.d/run-puma +0 -18
  97. data/tools/jungle/upstart/README.md +0 -61
  98. data/tools/jungle/upstart/puma-manager.conf +0 -31
  99. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/cluster.rb CHANGED
@@ -1,36 +1,53 @@
1
- require 'puma/runner'
2
- require 'puma/util'
3
- require 'puma/plugin'
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'
4
8
 
5
9
  require 'time'
6
10
 
7
11
  module Puma
12
+ # This class is instantiated by the `Puma::Launcher` and used
13
+ # to boot and serve a Ruby application when puma "workers" are needed
14
+ # i.e. when using multi-processes. For example `$ puma -w 5`
15
+ #
16
+ # An instance of this class will spawn the number of processes passed in
17
+ # via the `spawn_workers` method call. Each worker will have it's own
18
+ # instance of a `Puma::Server`.
8
19
  class Cluster < Runner
9
- WORKER_CHECK_INTERVAL = 5
10
-
11
- def initialize(cli, events)
12
- super cli, events
20
+ def initialize(launcher)
21
+ super(launcher)
13
22
 
14
23
  @phase = 0
15
24
  @workers = []
16
- @next_check = nil
25
+ @next_check = Time.now
17
26
 
18
- @phased_state = :idle
19
27
  @phased_restart = false
20
28
  end
21
29
 
30
+ # Returns the list of cluster worker handles.
31
+ # @return [Array<Puma::Cluster::WorkerHandle>]
32
+ attr_reader :workers
33
+
22
34
  def stop_workers
23
35
  log "- Gracefully shutting down workers..."
24
36
  @workers.each { |x| x.term }
25
37
 
26
38
  begin
27
- @workers.each { |w| Process.waitpid(w.pid) }
39
+ loop do
40
+ wait_workers
41
+ break if @workers.reject {|w| w.pid.nil?}.empty?
42
+ sleep 0.2
43
+ end
28
44
  rescue Interrupt
29
45
  log "! Cancelled waiting for workers"
30
46
  end
31
47
  end
32
48
 
33
49
  def start_phased_restart
50
+ @events.fire_on_restart!
34
51
  @phase += 1
35
52
  log "- Starting phased worker restart, phase: #{@phase}"
36
53
 
@@ -47,155 +64,103 @@ module Puma
47
64
  @workers.each { |x| x.hup }
48
65
  end
49
66
 
50
- class Worker
51
- def initialize(idx, pid, phase, options)
52
- @index = idx
53
- @pid = pid
54
- @phase = phase
55
- @stage = :started
56
- @signal = "TERM"
57
- @options = options
58
- @first_term_sent = nil
59
- @last_checkin = Time.now
60
- @last_status = '{}'
61
- @dead = false
62
- end
63
-
64
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
65
-
66
- def booted?
67
- @stage == :booted
68
- end
69
-
70
- def boot!
71
- @last_checkin = Time.now
72
- @stage = :booted
73
- end
74
-
75
- def dead?
76
- @dead
77
- end
78
-
79
- def dead!
80
- @dead = true
81
- end
82
-
83
- def ping!(status)
84
- @last_checkin = Time.now
85
- @last_status = status
86
- end
87
-
88
- def ping_timeout?(which)
89
- Time.now - @last_checkin > which
90
- end
91
-
92
- def term
93
- begin
94
- if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
95
- @signal = "KILL"
96
- else
97
- @first_term_sent ||= Time.now
98
- end
99
-
100
- Process.kill @signal, @pid
101
- rescue Errno::ESRCH
102
- end
103
- end
104
-
105
- def kill
106
- Process.kill "KILL", @pid
107
- rescue Errno::ESRCH
108
- end
109
-
110
- def hup
111
- Process.kill "HUP", @pid
112
- rescue Errno::ESRCH
113
- end
114
- end
115
-
116
67
  def spawn_workers
117
68
  diff = @options[:workers] - @workers.size
118
69
  return if diff < 1
119
70
 
120
71
  master = Process.pid
72
+ if @options[:fork_worker]
73
+ @fork_writer << "-1\n"
74
+ end
121
75
 
122
76
  diff.times do
123
77
  idx = next_worker_index
124
- @launcher.config.run_hooks :before_worker_fork, idx
125
78
 
126
- pid = fork { worker(idx, master) }
127
- if !pid
128
- log "! Complete inability to spawn new workers detected"
129
- log "! Seppuku is the only choice."
130
- exit! 1
79
+ if @options[:fork_worker] && idx != 0
80
+ @fork_writer << "#{idx}\n"
81
+ pid = nil
82
+ else
83
+ pid = spawn_worker(idx, master)
131
84
  end
132
85
 
133
86
  debug "Spawned worker: #{pid}"
134
- @workers << Worker.new(idx, pid, @phase, @options)
87
+ @workers << WorkerHandle.new(idx, pid, @phase, @options)
88
+ end
89
+
90
+ if @options[:fork_worker] &&
91
+ @workers.all? {|x| x.phase == @phase}
135
92
 
136
- @launcher.config.run_hooks :after_worker_fork, idx
93
+ @fork_writer << "0\n"
137
94
  end
95
+ end
96
+
97
+ # @version 5.0.0
98
+ def spawn_worker(idx, master)
99
+ @config.run_hooks(:before_worker_fork, idx, @log_writer)
138
100
 
139
- if diff > 0
140
- @phased_state = :idle
101
+ pid = fork { worker(idx, master) }
102
+ if !pid
103
+ log "! Complete inability to spawn new workers detected"
104
+ log "! Seppuku is the only choice."
105
+ exit! 1
141
106
  end
107
+
108
+ @config.run_hooks(:after_worker_fork, idx, @log_writer)
109
+ pid
142
110
  end
143
111
 
144
112
  def cull_workers
145
113
  diff = @workers.size - @options[:workers]
146
114
  return if diff < 1
115
+ debug "Culling #{diff} workers"
147
116
 
148
- debug "Culling #{diff.inspect} workers"
117
+ workers = workers_to_cull(diff)
118
+ debug "Workers to cull: #{workers.inspect}"
149
119
 
150
- workers_to_cull = @workers[-diff,diff]
151
- debug "Workers to cull: #{workers_to_cull.inspect}"
152
-
153
- workers_to_cull.each do |worker|
154
- log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
120
+ workers.each do |worker|
121
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
155
122
  worker.term
156
123
  end
157
124
  end
158
125
 
159
- def next_worker_index
160
- all_positions = 0...@options[:workers]
161
- occupied_positions = @workers.map { |w| w.index }
162
- available_positions = all_positions.to_a - occupied_positions
163
- available_positions.first
164
- end
165
-
166
- def all_workers_booted?
167
- @workers.count { |w| !w.booted? } == 0
168
- end
169
-
170
- def check_workers(force=false)
171
- return if !force && @next_check && @next_check >= Time.now
126
+ def workers_to_cull(diff)
127
+ workers = @workers.sort_by(&:started_at)
172
128
 
173
- @next_check = Time.now + WORKER_CHECK_INTERVAL
129
+ # In fork_worker mode, worker 0 acts as our master process.
130
+ # We should avoid culling it to preserve copy-on-write memory gains.
131
+ workers.reject! { |w| w.index == 0 } if @options[:fork_worker]
174
132
 
175
- any = false
133
+ workers[cull_start_index(diff), diff]
134
+ end
176
135
 
177
- @workers.each do |w|
178
- next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
179
- if w.ping_timeout?(@options[:worker_timeout])
180
- log "! Terminating timed out worker: #{w.pid}"
181
- w.kill
182
- any = true
183
- end
136
+ def cull_start_index(diff)
137
+ case @options[:worker_culling_strategy]
138
+ when :oldest
139
+ 0
140
+ else # :youngest
141
+ -diff
184
142
  end
143
+ end
185
144
 
186
- # If we killed any timed out workers, try to catch them
187
- # during this loop by giving the kernel time to kill them.
188
- sleep 1 if any
145
+ # @!attribute [r] next_worker_index
146
+ def next_worker_index
147
+ occupied_positions = @workers.map(&:index)
148
+ idx = 0
149
+ idx += 1 until !occupied_positions.include?(idx)
150
+ idx
151
+ end
189
152
 
190
- while @workers.any?
191
- pid = Process.waitpid(-1, Process::WNOHANG)
192
- break unless pid
153
+ def all_workers_booted?
154
+ @workers.count { |w| !w.booted? } == 0
155
+ end
193
156
 
194
- @workers.delete_if { |w| w.pid == pid }
195
- end
157
+ def check_workers
158
+ return if @next_check >= Time.now
196
159
 
197
- @workers.delete_if(&:dead?)
160
+ @next_check = Time.now + @options[:worker_check_interval]
198
161
 
162
+ timeout_workers
163
+ wait_workers
199
164
  cull_workers
200
165
  spawn_workers
201
166
 
@@ -207,97 +172,40 @@ module Puma
207
172
  w = @workers.find { |x| x.phase != @phase }
208
173
 
209
174
  if w
210
- if @phased_state == :idle
211
- @phased_state = :waiting
212
- log "- Stopping #{w.pid} for phased upgrade..."
175
+ log "- Stopping #{w.pid} for phased upgrade..."
176
+ unless w.term?
177
+ w.term
178
+ log "- #{w.signal} sent to #{w.pid}..."
213
179
  end
214
-
215
- w.term
216
- log "- #{w.signal} sent to #{w.pid}..."
217
180
  end
218
181
  end
219
- end
220
182
 
221
- def wakeup!
222
- return unless @wakeup
183
+ t = @workers.reject(&:term?)
184
+ t.map!(&:ping_timeout)
223
185
 
224
- begin
225
- @wakeup.write "!" unless @wakeup.closed?
226
- rescue SystemCallError, IOError
227
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
228
- end
186
+ @next_check = [t.min, @next_check].compact.min
229
187
  end
230
188
 
231
189
  def worker(index, master)
232
- title = "puma: cluster worker #{index}: #{master}"
233
- title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
234
- $0 = title
235
-
236
- Signal.trap "SIGINT", "IGNORE"
237
-
238
190
  @workers = []
191
+
239
192
  @master_read.close
240
193
  @suicide_pipe.close
194
+ @fork_writer.close
241
195
 
242
- Thread.new do
243
- IO.select [@check_pipe]
244
- log "! Detected parent died, dying"
245
- exit! 1
246
- end
247
-
248
- # If we're not running under a Bundler context, then
249
- # report the info about the context we will be using
250
- if !ENV['BUNDLE_GEMFILE']
251
- if File.exist?("Gemfile")
252
- log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
253
- elsif File.exist?("gems.rb")
254
- log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
255
- end
256
- end
257
-
258
- # Invoke any worker boot hooks so they can get
259
- # things in shape before booting the app.
260
- @launcher.config.run_hooks :before_worker_boot, index
261
-
262
- server = start_server
263
-
264
- Signal.trap "SIGTERM" do
265
- server.stop
266
- end
267
-
268
- begin
269
- @worker_write << "b#{Process.pid}\n"
270
- rescue SystemCallError, IOError
271
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
272
- STDERR.puts "Master seems to have exited, exiting."
273
- return
274
- end
275
-
276
- Thread.new(@worker_write) do |io|
277
- base_payload = "p#{Process.pid}"
278
-
279
- while true
280
- sleep WORKER_CHECK_INTERVAL
281
- begin
282
- b = server.backlog || 0
283
- r = server.running || 0
284
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
285
- io << payload
286
- rescue IOError
287
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
288
- break
289
- end
290
- end
196
+ pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
197
+ if @options[:fork_worker]
198
+ pipes[:fork_pipe] = @fork_pipe
199
+ pipes[:wakeup] = @wakeup
291
200
  end
292
201
 
293
- server.run.join
294
-
295
- # Invoke any worker shutdown hooks so they can prevent the worker
296
- # exiting until any background operations are completed
297
- @launcher.config.run_hooks :before_worker_shutdown, index
298
- ensure
299
- @worker_write << "t#{Process.pid}\n" rescue nil
300
- @worker_write.close
202
+ server = start_server if preload?
203
+ new_worker = Worker.new index: index,
204
+ master: master,
205
+ launcher: @launcher,
206
+ pipes: pipes,
207
+ server: server
208
+ new_worker.run
301
209
  end
302
210
 
303
211
  def restart
@@ -305,8 +213,8 @@ module Puma
305
213
  stop
306
214
  end
307
215
 
308
- def phased_restart
309
- return false if @options[:preload_app]
216
+ def phased_restart(refork = false)
217
+ return false if @options[:preload_app] && !refork
310
218
 
311
219
  @phased_restart = true
312
220
  wakeup!
@@ -322,7 +230,7 @@ module Puma
322
230
  def stop_blocked
323
231
  @status = :stop if @status == :run
324
232
  wakeup!
325
- @control.stop(true) if @control
233
+ @control&.stop true
326
234
  Process.waitall
327
235
  end
328
236
 
@@ -337,20 +245,63 @@ module Puma
337
245
  Dir.chdir dir
338
246
  end
339
247
 
248
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
249
+ # the master process.
250
+ # @!attribute [r] stats
340
251
  def stats
341
252
  old_worker_count = @workers.count { |w| w.phase != @phase }
342
- booted_worker_count = @workers.count { |w| w.booted? }
343
- worker_status = '[' + @workers.map { |w| %Q!{ "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(",") + ']'
344
- %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
253
+ worker_status = @workers.map do |w|
254
+ {
255
+ started_at: w.started_at.utc.iso8601,
256
+ pid: w.pid,
257
+ index: w.index,
258
+ phase: w.phase,
259
+ booted: w.booted?,
260
+ last_checkin: w.last_checkin.utc.iso8601,
261
+ last_status: w.last_status,
262
+ }
263
+ end
264
+
265
+ {
266
+ started_at: @started_at.utc.iso8601,
267
+ workers: @workers.size,
268
+ phase: @phase,
269
+ booted_workers: worker_status.count { |w| w[:booted] },
270
+ old_workers: old_worker_count,
271
+ worker_status: worker_status,
272
+ }.merge(super)
345
273
  end
346
274
 
347
275
  def preload?
348
276
  @options[:preload_app]
349
277
  end
350
278
 
279
+ # @version 5.0.0
280
+ def fork_worker!
281
+ if (worker = @workers.find { |w| w.index == 0 })
282
+ worker.phase += 1
283
+ end
284
+ phased_restart(true)
285
+ end
286
+
351
287
  # We do this in a separate method to keep the lambda scope
352
288
  # of the signals handlers as small as possible.
353
289
  def setup_signals
290
+ if @options[:fork_worker]
291
+ Signal.trap "SIGURG" do
292
+ fork_worker!
293
+ end
294
+
295
+ # Auto-fork after the specified number of requests.
296
+ if (fork_requests = @options[:fork_worker].to_i) > 0
297
+ @events.register(:ping!) do |w|
298
+ fork_worker! if w.index == 0 &&
299
+ w.phase == 0 &&
300
+ w.last_status[:requests_count] >= fork_requests
301
+ end
302
+ end
303
+ end
304
+
354
305
  Signal.trap "SIGCHLD" do
355
306
  wakeup!
356
307
  end
@@ -375,10 +326,13 @@ module Puma
375
326
  log "Early termination of worker"
376
327
  exit! 0
377
328
  else
329
+ @launcher.close_binder_listeners
330
+
378
331
  stop_workers
379
332
  stop
380
-
381
- raise SignalException, "SIGTERM"
333
+ @events.fire_on_stopped!
334
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
335
+ exit 0 # Clean exit, workers were stopped
382
336
  end
383
337
  end
384
338
  end
@@ -388,15 +342,25 @@ module Puma
388
342
 
389
343
  output_header "cluster"
390
344
 
391
- log "* Process workers: #{@options[:workers]}"
392
-
393
- before = Thread.list
345
+ # This is aligned with the output from Runner, see Runner#output_header
346
+ log "* Workers: #{@options[:workers]}"
394
347
 
395
348
  if preload?
349
+ # Threads explicitly marked as fork safe will be ignored. Used in Rails,
350
+ # but may be used by anyone. Note that we need to explicit
351
+ # Process::Waiter check here because there's a bug in Ruby 2.6 and below
352
+ # where calling thread_variable_get on a Process::Waiter will segfault.
353
+ # We can drop that clause once those versions of Ruby are no longer
354
+ # supported.
355
+ fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
356
+
357
+ before = Thread.list.reject(&fork_safe)
358
+
359
+ log "* Restarts: (\u2714) hot (\u2716) phased"
396
360
  log "* Preloading application"
397
361
  load_and_bind
398
362
 
399
- after = Thread.list
363
+ after = Thread.list.reject(&fork_safe)
400
364
 
401
365
  if after.size > before.size
402
366
  threads = (after - before)
@@ -410,14 +374,14 @@ module Puma
410
374
  end
411
375
  end
412
376
  else
413
- log "* Phased restart available"
377
+ log "* Restarts: (\u2714) hot (\u2714) phased"
414
378
 
415
- unless @launcher.config.app_configured?
379
+ unless @config.app_configured?
416
380
  error "No application configured, nothing to run"
417
381
  exit 1
418
382
  end
419
383
 
420
- @launcher.binder.parse @options[:binds], self
384
+ @launcher.binder.parse @options[:binds]
421
385
  end
422
386
 
423
387
  read, @wakeup = Puma::Util.pipe
@@ -431,12 +395,13 @@ module Puma
431
395
  #
432
396
  @check_pipe, @suicide_pipe = Puma::Util.pipe
433
397
 
434
- if daemon?
435
- log "* Daemonizing..."
436
- Process.daemon(true)
437
- else
438
- log "Use Ctrl-C to stop"
439
- end
398
+ # Separate pipe used by worker 0 to receive commands to
399
+ # fork new worker processes.
400
+ @fork_pipe, @fork_writer = Puma::Util.pipe
401
+
402
+ log "Use Ctrl-C to stop"
403
+
404
+ single_worker_warning
440
405
 
441
406
  redirect_io
442
407
 
@@ -448,7 +413,7 @@ module Puma
448
413
 
449
414
  @master_read, @worker_write = read, @wakeup
450
415
 
451
- @launcher.config.run_hooks :before_fork, nil
416
+ @config.run_hooks(:before_fork, nil, @log_writer)
452
417
 
453
418
  spawn_workers
454
419
 
@@ -456,48 +421,65 @@ module Puma
456
421
  stop
457
422
  end
458
423
 
459
- @launcher.events.fire_on_booted!
460
-
461
424
  begin
462
- force_check = false
425
+ booted = false
426
+ in_phased_restart = false
427
+ workers_not_booted = @options[:workers]
463
428
 
464
429
  while @status == :run
465
430
  begin
466
431
  if @phased_restart
467
432
  start_phased_restart
468
433
  @phased_restart = false
434
+ in_phased_restart = true
435
+ workers_not_booted = @options[:workers]
469
436
  end
470
437
 
471
- check_workers force_check
438
+ check_workers
472
439
 
473
- force_check = false
474
-
475
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
476
-
477
- if res
440
+ if read.wait_readable([0, @next_check - Time.now].max)
478
441
  req = read.read_nonblock(1)
479
442
 
443
+ @next_check = Time.now if req == "!"
480
444
  next if !req || req == "!"
481
445
 
482
446
  result = read.gets
483
447
  pid = result.to_i
484
448
 
449
+ if req == "b" || req == "f"
450
+ pid, idx = result.split(':').map(&:to_i)
451
+ w = @workers.find {|x| x.index == idx}
452
+ w.pid = pid if w.pid.nil?
453
+ end
454
+
485
455
  if w = @workers.find { |x| x.pid == pid }
486
456
  case req
487
457
  when "b"
488
458
  w.boot!
489
- log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
490
- force_check = true
459
+ log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
460
+ @next_check = Time.now
461
+ workers_not_booted -= 1
462
+ when "e"
463
+ # external term, see worker method, Signal.trap "SIGTERM"
464
+ w.term!
491
465
  when "t"
492
- w.dead!
493
- force_check = true
466
+ w.term unless w.term?
494
467
  when "p"
495
468
  w.ping!(result.sub(/^\d+/,'').chomp)
469
+ @events.fire(:ping!, w)
470
+ if !booted && @workers.none? {|worker| worker.last_status.empty?}
471
+ @events.fire_on_booted!
472
+ booted = true
473
+ end
496
474
  end
497
475
  else
498
476
  log "! Out-of-sync worker list, no #{pid} worker"
499
477
  end
500
478
  end
479
+ if in_phased_restart && workers_not_booted.zero?
480
+ @events.fire_on_booted!
481
+ in_phased_restart = false
482
+ end
501
483
 
502
484
  rescue Interrupt
503
485
  @status = :stop
@@ -512,5 +494,56 @@ module Puma
512
494
  @wakeup.close
513
495
  end
514
496
  end
497
+
498
+ private
499
+
500
+ def single_worker_warning
501
+ return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
502
+
503
+ log "! WARNING: Detected running cluster mode with 1 worker."
504
+ log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
505
+ log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
506
+ log "! Set the `silence_single_worker_warning` option to silence this warning message."
507
+ end
508
+
509
+ # loops thru @workers, removing workers that exited, and calling
510
+ # `#term` if needed
511
+ def wait_workers
512
+ @workers.reject! do |w|
513
+ next false if w.pid.nil?
514
+ begin
515
+ if Process.wait(w.pid, Process::WNOHANG)
516
+ true
517
+ else
518
+ w.term if w.term?
519
+ nil
520
+ end
521
+ rescue Errno::ECHILD
522
+ begin
523
+ Process.kill(0, w.pid)
524
+ # child still alive but has another parent (e.g., using fork_worker)
525
+ w.term if w.term?
526
+ false
527
+ rescue Errno::ESRCH, Errno::EPERM
528
+ true # child is already terminated
529
+ end
530
+ end
531
+ end
532
+ end
533
+
534
+ # @version 5.0.0
535
+ def timeout_workers
536
+ @workers.each do |w|
537
+ if !w.term? && w.ping_timeout <= Time.now
538
+ details = if w.booted?
539
+ "(worker failed to check in within #{@options[:worker_timeout]} seconds)"
540
+ else
541
+ "(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
542
+ end
543
+ log "! Terminating timed out worker #{details}: #{w.pid}"
544
+ w.kill
545
+ end
546
+ end
547
+ end
515
548
  end
516
549
  end