puma 4.3.5 → 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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1639 -519
  3. data/LICENSE +23 -20
  4. data/README.md +130 -42
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +63 -26
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +60 -69
  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 +15 -15
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +46 -23
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +85 -128
  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 -4
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +56 -11
  30. data/ext/puma_http11/http11_parser.c +69 -58
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +3 -3
  33. data/ext/puma_http11/http11_parser.rl +3 -3
  34. data/ext/puma_http11/http11_parser_common.rl +3 -3
  35. data/ext/puma_http11/mini_ssl.c +322 -130
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +52 -52
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  40. data/ext/puma_http11/puma_http11.c +47 -57
  41. data/lib/puma/app/status.rb +53 -37
  42. data/lib/puma/binder.rb +232 -119
  43. data/lib/puma/cli.rb +33 -33
  44. data/lib/puma/client.rb +197 -101
  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 +224 -229
  48. data/lib/puma/commonlogger.rb +2 -2
  49. data/lib/puma/configuration.rb +112 -87
  50. data/lib/puma/const.rb +30 -25
  51. data/lib/puma/control_cli.rb +99 -79
  52. data/lib/puma/detect.rb +31 -2
  53. data/lib/puma/dsl.rb +426 -110
  54. data/lib/puma/error_logger.rb +112 -0
  55. data/lib/puma/events.rb +16 -115
  56. data/lib/puma/io_buffer.rb +44 -2
  57. data/lib/puma/jruby_restart.rb +2 -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 +170 -148
  61. data/lib/puma/log_writer.rb +137 -0
  62. data/lib/puma/minissl/context_builder.rb +35 -19
  63. data/lib/puma/minissl.rb +213 -55
  64. data/lib/puma/null_io.rb +18 -1
  65. data/lib/puma/plugin/tmp_restart.rb +1 -1
  66. data/lib/puma/plugin.rb +3 -12
  67. data/lib/puma/rack/builder.rb +5 -9
  68. data/lib/puma/rack/urlmap.rb +0 -0
  69. data/lib/puma/rack_default.rb +1 -1
  70. data/lib/puma/reactor.rb +85 -369
  71. data/lib/puma/request.rb +644 -0
  72. data/lib/puma/runner.rb +83 -77
  73. data/lib/puma/server.rb +303 -773
  74. data/lib/puma/single.rb +18 -74
  75. data/lib/puma/state_file.rb +45 -8
  76. data/lib/puma/systemd.rb +47 -0
  77. data/lib/puma/thread_pool.rb +136 -68
  78. data/lib/puma/util.rb +21 -4
  79. data/lib/puma.rb +54 -5
  80. data/lib/rack/handler/puma.rb +11 -12
  81. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  82. data/tools/trickletest.rb +0 -0
  83. metadata +36 -28
  84. data/docs/tcp_mode.md +0 -96
  85. data/ext/puma_http11/io_buffer.c +0 -155
  86. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  87. data/lib/puma/accept_nonblock.rb +0 -29
  88. data/lib/puma/tcp_logger.rb +0 -41
  89. data/tools/jungle/README.md +0 -19
  90. data/tools/jungle/init.d/README.md +0 -61
  91. data/tools/jungle/init.d/puma +0 -421
  92. data/tools/jungle/init.d/run-puma +0 -18
  93. data/tools/jungle/upstart/README.md +0 -61
  94. data/tools/jungle/upstart/puma-manager.conf +0 -31
  95. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/cluster.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/runner'
4
- require 'puma/util'
5
- require 'puma/plugin'
3
+ require_relative 'runner'
4
+ require_relative 'util'
5
+ require_relative 'plugin'
6
+ require_relative 'cluster/worker_handle'
7
+ require_relative 'cluster/worker'
6
8
 
7
9
  require 'time'
8
10
 
@@ -11,25 +13,24 @@ module Puma
11
13
  # to boot and serve a Ruby application when puma "workers" are needed
12
14
  # i.e. when using multi-processes. For example `$ puma -w 5`
13
15
  #
14
- # At the core of this class is running an instance of `Puma::Server` which
15
- # gets created via the `start_server` method from the `Puma::Runner` class
16
- # that this inherits from.
17
- #
18
16
  # An instance of this class will spawn the number of processes passed in
19
17
  # via the `spawn_workers` method call. Each worker will have it's own
20
18
  # instance of a `Puma::Server`.
21
19
  class Cluster < Runner
22
- def initialize(cli, events)
23
- super cli, events
20
+ def initialize(launcher)
21
+ super(launcher)
24
22
 
25
23
  @phase = 0
26
24
  @workers = []
27
- @next_check = nil
25
+ @next_check = Time.now
28
26
 
29
- @phased_state = :idle
30
27
  @phased_restart = false
31
28
  end
32
29
 
30
+ # Returns the list of cluster worker handles.
31
+ # @return [Array<Puma::Cluster::WorkerHandle>]
32
+ attr_reader :workers
33
+
33
34
  def stop_workers
34
35
  log "- Gracefully shutting down workers..."
35
36
  @workers.each { |x| x.term }
@@ -37,7 +38,7 @@ module Puma
37
38
  begin
38
39
  loop do
39
40
  wait_workers
40
- break if @workers.empty?
41
+ break if @workers.reject {|w| w.pid.nil?}.empty?
41
42
  sleep 0.2
42
43
  end
43
44
  rescue Interrupt
@@ -46,6 +47,7 @@ module Puma
46
47
  end
47
48
 
48
49
  def start_phased_restart
50
+ @events.fire_on_restart!
49
51
  @phase += 1
50
52
  log "- Starting phased worker restart, phase: #{@phase}"
51
53
 
@@ -62,143 +64,102 @@ module Puma
62
64
  @workers.each { |x| x.hup }
63
65
  end
64
66
 
65
- class Worker
66
- def initialize(idx, pid, phase, options)
67
- @index = idx
68
- @pid = pid
69
- @phase = phase
70
- @stage = :started
71
- @signal = "TERM"
72
- @options = options
73
- @first_term_sent = nil
74
- @started_at = Time.now
75
- @last_checkin = Time.now
76
- @last_status = '{}'
77
- @term = false
78
- end
79
-
80
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
81
-
82
- def booted?
83
- @stage == :booted
84
- end
85
-
86
- def boot!
87
- @last_checkin = Time.now
88
- @stage = :booted
89
- end
90
-
91
- def term?
92
- @term
93
- end
94
-
95
- def ping!(status)
96
- @last_checkin = Time.now
97
- @last_status = status
98
- end
99
-
100
- def ping_timeout?(which)
101
- Time.now - @last_checkin > which
102
- end
103
-
104
- def term
105
- begin
106
- if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
107
- @signal = "KILL"
108
- else
109
- @term ||= true
110
- @first_term_sent ||= Time.now
111
- end
112
- Process.kill @signal, @pid
113
- rescue Errno::ESRCH
114
- end
115
- end
116
-
117
- def kill
118
- Process.kill "KILL", @pid
119
- rescue Errno::ESRCH
120
- end
121
-
122
- def hup
123
- Process.kill "HUP", @pid
124
- rescue Errno::ESRCH
125
- end
126
- end
127
-
128
67
  def spawn_workers
129
68
  diff = @options[:workers] - @workers.size
130
69
  return if diff < 1
131
70
 
132
71
  master = Process.pid
72
+ if @options[:fork_worker]
73
+ @fork_writer << "-1\n"
74
+ end
133
75
 
134
76
  diff.times do
135
77
  idx = next_worker_index
136
- @launcher.config.run_hooks :before_worker_fork, idx
137
78
 
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
79
+ if @options[:fork_worker] && idx != 0
80
+ @fork_writer << "#{idx}\n"
81
+ pid = nil
82
+ else
83
+ pid = spawn_worker(idx, master)
143
84
  end
144
85
 
145
86
  debug "Spawned worker: #{pid}"
146
- @workers << Worker.new(idx, pid, @phase, @options)
87
+ @workers << WorkerHandle.new(idx, pid, @phase, @options)
88
+ end
147
89
 
148
- @launcher.config.run_hooks :after_worker_fork, idx
90
+ if @options[:fork_worker] &&
91
+ @workers.all? {|x| x.phase == @phase}
92
+
93
+ @fork_writer << "0\n"
149
94
  end
95
+ end
150
96
 
151
- if diff > 0
152
- @phased_state = :idle
97
+ # @version 5.0.0
98
+ def spawn_worker(idx, master)
99
+ @config.run_hooks(:before_worker_fork, idx, @log_writer)
100
+
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
153
106
  end
107
+
108
+ @config.run_hooks(:after_worker_fork, idx, @log_writer)
109
+ pid
154
110
  end
155
111
 
156
112
  def cull_workers
157
113
  diff = @workers.size - @options[:workers]
158
114
  return if diff < 1
115
+ debug "Culling #{diff} workers"
159
116
 
160
- debug "Culling #{diff.inspect} workers"
161
-
162
- workers_to_cull = @workers[-diff,diff]
163
- debug "Workers to cull: #{workers_to_cull.inspect}"
117
+ workers = workers_to_cull(diff)
118
+ debug "Workers to cull: #{workers.inspect}"
164
119
 
165
- workers_to_cull.each do |worker|
166
- log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
120
+ workers.each do |worker|
121
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
167
122
  worker.term
168
123
  end
169
124
  end
170
125
 
126
+ def workers_to_cull(diff)
127
+ workers = @workers.sort_by(&:started_at)
128
+
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]
132
+
133
+ workers[cull_start_index(diff), diff]
134
+ end
135
+
136
+ def cull_start_index(diff)
137
+ case @options[:worker_culling_strategy]
138
+ when :oldest
139
+ 0
140
+ else # :youngest
141
+ -diff
142
+ end
143
+ end
144
+
145
+ # @!attribute [r] next_worker_index
171
146
  def next_worker_index
172
- all_positions = 0...@options[:workers]
173
- occupied_positions = @workers.map { |w| w.index }
174
- available_positions = all_positions.to_a - occupied_positions
175
- available_positions.first
147
+ occupied_positions = @workers.map(&:index)
148
+ idx = 0
149
+ idx += 1 until !occupied_positions.include?(idx)
150
+ idx
176
151
  end
177
152
 
178
153
  def all_workers_booted?
179
154
  @workers.count { |w| !w.booted? } == 0
180
155
  end
181
156
 
182
- def check_workers(force=false)
183
- return if !force && @next_check && @next_check >= Time.now
184
-
185
- @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
157
+ def check_workers
158
+ return if @next_check >= Time.now
186
159
 
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
160
+ @next_check = Time.now + @options[:worker_check_interval]
201
161
 
162
+ timeout_workers
202
163
  wait_workers
203
164
  cull_workers
204
165
  spawn_workers
@@ -211,104 +172,40 @@ module Puma
211
172
  w = @workers.find { |x| x.phase != @phase }
212
173
 
213
174
  if w
214
- if @phased_state == :idle
215
- @phased_state = :waiting
216
- log "- Stopping #{w.pid} for phased upgrade..."
217
- end
218
-
175
+ log "- Stopping #{w.pid} for phased upgrade..."
219
176
  unless w.term?
220
177
  w.term
221
178
  log "- #{w.signal} sent to #{w.pid}..."
222
179
  end
223
180
  end
224
181
  end
225
- end
226
182
 
227
- def wakeup!
228
- return unless @wakeup
183
+ t = @workers.reject(&:term?)
184
+ t.map!(&:ping_timeout)
229
185
 
230
- begin
231
- @wakeup.write "!" unless @wakeup.closed?
232
- rescue SystemCallError, IOError
233
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
234
- end
186
+ @next_check = [t.min, @next_check].compact.min
235
187
  end
236
188
 
237
189
  def worker(index, master)
238
- title = "puma: cluster worker #{index}: #{master}"
239
- title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
240
- $0 = title
241
-
242
- Signal.trap "SIGINT", "IGNORE"
243
-
244
190
  @workers = []
191
+
245
192
  @master_read.close
246
193
  @suicide_pipe.close
194
+ @fork_writer.close
247
195
 
248
- Thread.new do
249
- Puma.set_thread_name "worker check pipe"
250
- IO.select [@check_pipe]
251
- log "! Detected parent died, dying"
252
- exit! 1
253
- end
254
-
255
- # If we're not running under a Bundler context, then
256
- # report the info about the context we will be using
257
- if !ENV['BUNDLE_GEMFILE']
258
- if File.exist?("Gemfile")
259
- log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
260
- elsif File.exist?("gems.rb")
261
- log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
262
- end
263
- end
264
-
265
- # Invoke any worker boot hooks so they can get
266
- # things in shape before booting the app.
267
- @launcher.config.run_hooks :before_worker_boot, index
268
-
269
- server = start_server
270
-
271
- Signal.trap "SIGTERM" do
272
- @worker_write << "e#{Process.pid}\n" rescue nil
273
- server.stop
274
- end
275
-
276
- begin
277
- @worker_write << "b#{Process.pid}\n"
278
- rescue SystemCallError, IOError
279
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
280
- STDERR.puts "Master seems to have exited, exiting."
281
- return
282
- end
283
-
284
- Thread.new(@worker_write) do |io|
285
- Puma.set_thread_name "stat payload"
286
- base_payload = "p#{Process.pid}"
287
-
288
- while true
289
- sleep Const::WORKER_CHECK_INTERVAL
290
- 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
297
- rescue IOError
298
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
299
- break
300
- end
301
- 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
302
200
  end
303
201
 
304
- server.run.join
305
-
306
- # Invoke any worker shutdown hooks so they can prevent the worker
307
- # exiting until any background operations are completed
308
- @launcher.config.run_hooks :before_worker_shutdown, index
309
- ensure
310
- @worker_write << "t#{Process.pid}\n" rescue nil
311
- @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
312
209
  end
313
210
 
314
211
  def restart
@@ -316,8 +213,8 @@ module Puma
316
213
  stop
317
214
  end
318
215
 
319
- def phased_restart
320
- return false if @options[:preload_app]
216
+ def phased_restart(refork = false)
217
+ return false if @options[:preload_app] && !refork
321
218
 
322
219
  @phased_restart = true
323
220
  wakeup!
@@ -333,7 +230,7 @@ module Puma
333
230
  def stop_blocked
334
231
  @status = :stop if @status == :run
335
232
  wakeup!
336
- @control.stop(true) if @control
233
+ @control&.stop true
337
234
  Process.waitall
338
235
  end
339
236
 
@@ -350,20 +247,61 @@ module Puma
350
247
 
351
248
  # Inside of a child process, this will return all zeroes, as @workers is only populated in
352
249
  # the master process.
250
+ # @!attribute [r] stats
353
251
  def stats
354
252
  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} }!
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)
358
273
  end
359
274
 
360
275
  def preload?
361
276
  @options[:preload_app]
362
277
  end
363
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
+
364
287
  # We do this in a separate method to keep the lambda scope
365
288
  # of the signals handlers as small as possible.
366
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
+
367
305
  Signal.trap "SIGCHLD" do
368
306
  wakeup!
369
307
  end
@@ -392,7 +330,7 @@ module Puma
392
330
 
393
331
  stop_workers
394
332
  stop
395
-
333
+ @events.fire_on_stopped!
396
334
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
397
335
  exit 0 # Clean exit, workers were stopped
398
336
  end
@@ -404,15 +342,25 @@ module Puma
404
342
 
405
343
  output_header "cluster"
406
344
 
407
- log "* Process workers: #{@options[:workers]}"
408
-
409
- before = Thread.list
345
+ # This is aligned with the output from Runner, see Runner#output_header
346
+ log "* Workers: #{@options[:workers]}"
410
347
 
411
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"
412
360
  log "* Preloading application"
413
361
  load_and_bind
414
362
 
415
- after = Thread.list
363
+ after = Thread.list.reject(&fork_safe)
416
364
 
417
365
  if after.size > before.size
418
366
  threads = (after - before)
@@ -426,14 +374,14 @@ module Puma
426
374
  end
427
375
  end
428
376
  else
429
- log "* Phased restart available"
377
+ log "* Restarts: (\u2714) hot (\u2714) phased"
430
378
 
431
- unless @launcher.config.app_configured?
379
+ unless @config.app_configured?
432
380
  error "No application configured, nothing to run"
433
381
  exit 1
434
382
  end
435
383
 
436
- @launcher.binder.parse @options[:binds], self
384
+ @launcher.binder.parse @options[:binds]
437
385
  end
438
386
 
439
387
  read, @wakeup = Puma::Util.pipe
@@ -447,12 +395,13 @@ module Puma
447
395
  #
448
396
  @check_pipe, @suicide_pipe = Puma::Util.pipe
449
397
 
450
- if daemon?
451
- log "* Daemonizing..."
452
- Process.daemon(true)
453
- else
454
- log "Use Ctrl-C to stop"
455
- 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
456
405
 
457
406
  redirect_io
458
407
 
@@ -464,7 +413,7 @@ module Puma
464
413
 
465
414
  @master_read, @worker_write = read, @wakeup
466
415
 
467
- @launcher.config.run_hooks :before_fork, nil
416
+ @config.run_hooks(:before_fork, nil, @log_writer)
468
417
 
469
418
  spawn_workers
470
419
 
@@ -472,51 +421,65 @@ module Puma
472
421
  stop
473
422
  end
474
423
 
475
- @launcher.events.fire_on_booted!
476
-
477
424
  begin
478
- force_check = false
425
+ booted = false
426
+ in_phased_restart = false
427
+ workers_not_booted = @options[:workers]
479
428
 
480
429
  while @status == :run
481
430
  begin
482
431
  if @phased_restart
483
432
  start_phased_restart
484
433
  @phased_restart = false
434
+ in_phased_restart = true
435
+ workers_not_booted = @options[:workers]
485
436
  end
486
437
 
487
- check_workers force_check
438
+ check_workers
488
439
 
489
- force_check = false
490
-
491
- res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
492
-
493
- if res
440
+ if read.wait_readable([0, @next_check - Time.now].max)
494
441
  req = read.read_nonblock(1)
495
442
 
443
+ @next_check = Time.now if req == "!"
496
444
  next if !req || req == "!"
497
445
 
498
446
  result = read.gets
499
447
  pid = result.to_i
500
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
+
501
455
  if w = @workers.find { |x| x.pid == pid }
502
456
  case req
503
457
  when "b"
504
458
  w.boot!
505
- log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
506
- 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
507
462
  when "e"
508
463
  # external term, see worker method, Signal.trap "SIGTERM"
509
- w.instance_variable_set :@term, true
464
+ w.term!
510
465
  when "t"
511
466
  w.term unless w.term?
512
- force_check = true
513
467
  when "p"
514
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
515
474
  end
516
475
  else
517
476
  log "! Out-of-sync worker list, no #{pid} worker"
518
477
  end
519
478
  end
479
+ if in_phased_restart && workers_not_booted.zero?
480
+ @events.fire_on_booted!
481
+ in_phased_restart = false
482
+ end
520
483
 
521
484
  rescue Interrupt
522
485
  @status = :stop
@@ -534,10 +497,20 @@ module Puma
534
497
 
535
498
  private
536
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
+
537
509
  # loops thru @workers, removing workers that exited, and calling
538
510
  # `#term` if needed
539
511
  def wait_workers
540
512
  @workers.reject! do |w|
513
+ next false if w.pid.nil?
541
514
  begin
542
515
  if Process.wait(w.pid, Process::WNOHANG)
543
516
  true
@@ -546,7 +519,29 @@ module Puma
546
519
  nil
547
520
  end
548
521
  rescue Errno::ECHILD
549
- true # child is already terminated
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
550
545
  end
551
546
  end
552
547
  end
@@ -3,7 +3,7 @@
3
3
  module Puma
4
4
  # Rack::CommonLogger forwards every request to the given +app+, and
5
5
  # logs a line in the
6
- # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
6
+ # {Apache common log format}[https://httpd.apache.org/docs/1.3/logs.html#common]
7
7
  # to the +logger+.
8
8
  #
9
9
  # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
@@ -16,7 +16,7 @@ module Puma
16
16
  # (which is called without arguments in order to make the error appear for
17
17
  # sure)
18
18
  class CommonLogger
19
- # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
19
+ # Common Log Format: https://httpd.apache.org/docs/1.3/logs.html#common
20
20
  #
21
21
  # lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
22
22
  #