puma 4.3.3 → 5.3.2

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1348 -519
  3. data/LICENSE +23 -20
  4. data/README.md +74 -31
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +24 -20
  7. data/docs/compile_options.md +19 -0
  8. data/docs/deployment.md +15 -10
  9. data/docs/fork_worker.md +33 -0
  10. data/docs/jungle/README.md +9 -0
  11. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  12. data/{tools → docs}/jungle/rc.d/puma +2 -2
  13. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  14. data/docs/kubernetes.md +66 -0
  15. data/docs/nginx.md +1 -1
  16. data/docs/plugins.md +2 -2
  17. data/docs/rails_dev_mode.md +29 -0
  18. data/docs/restart.md +46 -23
  19. data/docs/signals.md +7 -6
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +27 -67
  22. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  23. data/ext/puma_http11/ext_help.h +1 -1
  24. data/ext/puma_http11/extconf.rb +22 -8
  25. data/ext/puma_http11/http11_parser.c +48 -48
  26. data/ext/puma_http11/http11_parser.h +1 -1
  27. data/ext/puma_http11/http11_parser.java.rl +1 -1
  28. data/ext/puma_http11/http11_parser.rl +4 -2
  29. data/ext/puma_http11/mini_ssl.c +211 -118
  30. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +5 -7
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  34. data/ext/puma_http11/puma_http11.c +32 -50
  35. data/lib/puma.rb +46 -0
  36. data/lib/puma/app/status.rb +48 -35
  37. data/lib/puma/binder.rb +177 -103
  38. data/lib/puma/cli.rb +11 -15
  39. data/lib/puma/client.rb +83 -76
  40. data/lib/puma/cluster.rb +184 -198
  41. data/lib/puma/cluster/worker.rb +183 -0
  42. data/lib/puma/cluster/worker_handle.rb +90 -0
  43. data/lib/puma/commonlogger.rb +2 -2
  44. data/lib/puma/configuration.rb +55 -49
  45. data/lib/puma/const.rb +13 -5
  46. data/lib/puma/control_cli.rb +93 -76
  47. data/lib/puma/detect.rb +24 -3
  48. data/lib/puma/dsl.rb +266 -92
  49. data/lib/puma/error_logger.rb +104 -0
  50. data/lib/puma/events.rb +55 -34
  51. data/lib/puma/io_buffer.rb +9 -2
  52. data/lib/puma/jruby_restart.rb +0 -58
  53. data/lib/puma/json.rb +96 -0
  54. data/lib/puma/launcher.rb +113 -45
  55. data/lib/puma/minissl.rb +114 -33
  56. data/lib/puma/minissl/context_builder.rb +6 -3
  57. data/lib/puma/null_io.rb +13 -1
  58. data/lib/puma/plugin.rb +1 -10
  59. data/lib/puma/queue_close.rb +26 -0
  60. data/lib/puma/rack/builder.rb +0 -4
  61. data/lib/puma/reactor.rb +85 -369
  62. data/lib/puma/request.rb +467 -0
  63. data/lib/puma/runner.rb +29 -58
  64. data/lib/puma/server.rb +267 -698
  65. data/lib/puma/single.rb +9 -65
  66. data/lib/puma/state_file.rb +8 -3
  67. data/lib/puma/systemd.rb +46 -0
  68. data/lib/puma/thread_pool.rb +119 -53
  69. data/lib/puma/util.rb +12 -0
  70. data/lib/rack/handler/puma.rb +2 -3
  71. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  72. metadata +28 -24
  73. data/docs/tcp_mode.md +0 -96
  74. data/ext/puma_http11/io_buffer.c +0 -155
  75. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  76. data/lib/puma/accept_nonblock.rb +0 -29
  77. data/lib/puma/tcp_logger.rb +0 -41
  78. data/tools/jungle/README.md +0 -19
  79. data/tools/jungle/init.d/README.md +0 -61
  80. data/tools/jungle/init.d/puma +0 -421
  81. data/tools/jungle/init.d/run-puma +0 -18
  82. data/tools/jungle/upstart/README.md +0 -61
  83. data/tools/jungle/upstart/puma-manager.conf +0 -31
  84. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/cli.rb CHANGED
@@ -80,7 +80,7 @@ module Puma
80
80
  @launcher.run
81
81
  end
82
82
 
83
- private
83
+ private
84
84
  def unsupported(str)
85
85
  @events.error(str)
86
86
  raise UnsupportedOption
@@ -104,6 +104,10 @@ module Puma
104
104
  user_config.bind arg
105
105
  end
106
106
 
107
+ o.on "--bind-to-activated-sockets [only]", "Bind to all activated sockets" do |arg|
108
+ user_config.bind_to_activated_sockets(arg || true)
109
+ end
110
+
107
111
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
108
112
  file_config.load arg
109
113
  end
@@ -112,21 +116,11 @@ module Puma
112
116
  configure_control_url(arg)
113
117
  end
114
118
 
115
- # alias --control-url for backwards-compatibility
116
- o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
- configure_control_url(arg)
118
- end
119
-
120
119
  o.on "--control-token TOKEN",
121
120
  "The token to use as authentication for the control server" do |arg|
122
121
  @control_options[:auth_token] = arg
123
122
  end
124
123
 
125
- o.on "-d", "--daemon", "Daemonize the server into the background" do
126
- user_config.daemonize
127
- user_config.quiet
128
- end
129
-
130
124
  o.on "--debug", "Log lowlevel debugging information" do
131
125
  user_config.debug
132
126
  end
@@ -140,6 +134,12 @@ module Puma
140
134
  user_config.environment arg
141
135
  end
142
136
 
137
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
138
+ "Fork new workers from existing worker. Cluster mode only",
139
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
140
+ user_config.fork_worker(*args.compact)
141
+ end
142
+
143
143
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
144
  $LOAD_PATH.unshift(*arg.split(':'))
145
145
  end
@@ -192,10 +192,6 @@ module Puma
192
192
  end
193
193
  end
194
194
 
195
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
196
- user_config.tcp_mode!
197
- end
198
-
199
195
  o.on "--early-hints", "Enable early hints support" do
200
196
  user_config.early_hints
201
197
  end
data/lib/puma/client.rb CHANGED
@@ -69,6 +69,7 @@ module Puma
69
69
  @hijacked = false
70
70
 
71
71
  @peerip = nil
72
+ @listener = nil
72
73
  @remote_addr_header = nil
73
74
 
74
75
  @body_remain = 0
@@ -81,10 +82,17 @@ module Puma
81
82
 
82
83
  attr_writer :peerip
83
84
 
84
- attr_accessor :remote_addr_header
85
+ attr_accessor :remote_addr_header, :listener
85
86
 
86
87
  def_delegators :@io, :closed?
87
88
 
89
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
90
+ # used for MiniSSL::Socket
91
+ def io_ok?
92
+ @to_io.is_a?(::BasicSocket) && !closed?
93
+ end
94
+
95
+ # @!attribute [r] inspect
88
96
  def inspect
89
97
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
90
98
  end
@@ -96,12 +104,18 @@ module Puma
96
104
  env[HIJACK_IO] ||= @io
97
105
  end
98
106
 
107
+ # @!attribute [r] in_data_phase
99
108
  def in_data_phase
100
109
  !@read_header
101
110
  end
102
111
 
103
112
  def set_timeout(val)
104
- @timeout_at = Time.now + val
113
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
114
+ end
115
+
116
+ # Number of seconds until the timeout elapses.
117
+ def timeout
118
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
105
119
  end
106
120
 
107
121
  def reset(fast_check=true)
@@ -113,7 +127,7 @@ module Puma
113
127
  @parsed_bytes = 0
114
128
  @ready = false
115
129
  @body_remain = 0
116
- @peerip = nil
130
+ @peerip = nil if @remote_addr_header
117
131
  @in_last_chunk = false
118
132
 
119
133
  if @buffer
@@ -153,9 +167,11 @@ module Puma
153
167
 
154
168
  begin
155
169
  data = @io.read_nonblock(CHUNK_SIZE)
156
- rescue Errno::EAGAIN
170
+ rescue IO::WaitReadable
157
171
  return false
158
- rescue SystemCallError, IOError, EOFError
172
+ rescue EOFError
173
+ # Swallow error, don't log
174
+ rescue SystemCallError, IOError
159
175
  raise ConnectionError, "Connection error detected during read"
160
176
  end
161
177
 
@@ -184,68 +200,20 @@ module Puma
184
200
  false
185
201
  end
186
202
 
187
- if IS_JRUBY
188
- def jruby_start_try_to_finish
189
- return read_body unless @read_header
190
-
191
- begin
192
- data = @io.sysread_nonblock(CHUNK_SIZE)
193
- rescue OpenSSL::SSL::SSLError => e
194
- return false if e.kind_of? IO::WaitReadable
195
- raise e
196
- end
197
-
198
- # No data means a closed socket
199
- unless data
200
- @buffer = nil
201
- set_ready
202
- raise EOFError
203
- end
204
-
205
- if @buffer
206
- @buffer << data
207
- else
208
- @buffer = data
209
- end
210
-
211
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
212
-
213
- if @parser.finished?
214
- return setup_body
215
- elsif @parsed_bytes >= MAX_HEADER
216
- raise HttpParserError,
217
- "HEADER is longer than allowed, aborting client early."
218
- end
219
-
220
- false
221
- end
222
-
223
- def eagerly_finish
224
- return true if @ready
225
-
226
- if @io.kind_of? OpenSSL::SSL::SSLSocket
227
- return true if jruby_start_try_to_finish
228
- end
229
-
230
- return false unless IO.select([@to_io], nil, nil, 0)
231
- try_to_finish
232
- end
233
-
234
- else
203
+ def eagerly_finish
204
+ return true if @ready
205
+ return false unless IO.select([@to_io], nil, nil, 0)
206
+ try_to_finish
207
+ end
235
208
 
236
- def eagerly_finish
237
- return true if @ready
238
- return false unless IO.select([@to_io], nil, nil, 0)
239
- try_to_finish
240
- end
241
- end # IS_JRUBY
209
+ def finish(timeout)
210
+ return if @ready
211
+ IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
212
+ end
242
213
 
243
- def finish
244
- return true if @ready
245
- until try_to_finish
246
- IO.select([@to_io], nil, nil)
247
- end
248
- true
214
+ def timeout!
215
+ write_error(408) if in_data_phase
216
+ raise ConnectionError
249
217
  end
250
218
 
251
219
  def write_error(status_code)
@@ -259,7 +227,7 @@ module Puma
259
227
  return @peerip if @peerip
260
228
 
261
229
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
230
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
263
231
  @peerip = hdr
264
232
  return hdr
265
233
  end
@@ -267,6 +235,15 @@ module Puma
267
235
  @peerip ||= @io.peeraddr.last
268
236
  end
269
237
 
238
+ # Returns true if the persistent connection can be closed immediately
239
+ # without waiting for the configured idle/shutdown timeout.
240
+ # @version 5.0.0
241
+ #
242
+ def can_close?
243
+ # Allow connection to close if we're not in the middle of parsing a request.
244
+ @parsed_bytes == 0
245
+ end
246
+
270
247
  private
271
248
 
272
249
  def setup_body
@@ -285,8 +262,16 @@ module Puma
285
262
 
286
263
  te = @env[TRANSFER_ENCODING2]
287
264
 
288
- if te && CHUNKED.casecmp(te) == 0
289
- return setup_chunked_body(body)
265
+ if te
266
+ if te.include?(",")
267
+ te.split(",").each do |part|
268
+ if CHUNKED.casecmp(part.strip) == 0
269
+ return setup_chunked_body(body)
270
+ end
271
+ end
272
+ elsif CHUNKED.casecmp(te) == 0
273
+ return setup_chunked_body(body)
274
+ end
290
275
  end
291
276
 
292
277
  @chunked_body = false
@@ -311,6 +296,7 @@ module Puma
311
296
 
312
297
  if remain > MAX_BODY
313
298
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
299
+ @body.unlink
314
300
  @body.binmode
315
301
  @tempfile = @body
316
302
  else
@@ -343,7 +329,7 @@ module Puma
343
329
 
344
330
  begin
345
331
  chunk = @io.read_nonblock(want)
346
- rescue Errno::EAGAIN
332
+ rescue IO::WaitReadable
347
333
  return false
348
334
  rescue SystemCallError, IOError
349
335
  raise ConnectionError, "Connection error detected during read"
@@ -389,7 +375,10 @@ module Puma
389
375
  raise EOFError
390
376
  end
391
377
 
392
- return true if decode_chunk(chunk)
378
+ if decode_chunk(chunk)
379
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
380
+ return true
381
+ end
393
382
  end
394
383
  end
395
384
 
@@ -399,22 +388,40 @@ module Puma
399
388
  @prev_chunk = ""
400
389
 
401
390
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
391
+ @body.unlink
402
392
  @body.binmode
403
393
  @tempfile = @body
394
+ @chunked_content_length = 0
404
395
 
405
- return decode_chunk(body)
396
+ if decode_chunk(body)
397
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
398
+ return true
399
+ end
400
+ end
401
+
402
+ # @version 5.0.0
403
+ def write_chunk(str)
404
+ @chunked_content_length += @body.write(str)
406
405
  end
407
406
 
408
407
  def decode_chunk(chunk)
409
408
  if @partial_part_left > 0
410
409
  if @partial_part_left <= chunk.size
411
410
  if @partial_part_left > 2
412
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
411
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
413
412
  end
414
413
  chunk = chunk[@partial_part_left..-1]
415
414
  @partial_part_left = 0
416
415
  else
417
- @body << chunk if @partial_part_left > 2 # don't include the last \r\n
416
+ if @partial_part_left > 2
417
+ if @partial_part_left == chunk.size + 1
418
+ # Don't include the last \r
419
+ write_chunk(chunk[0..(@partial_part_left-3)])
420
+ else
421
+ # don't include the last \r\n
422
+ write_chunk(chunk)
423
+ end
424
+ end
418
425
  @partial_part_left -= chunk.size
419
426
  return false
420
427
  end
@@ -461,12 +468,12 @@ module Puma
461
468
 
462
469
  case
463
470
  when got == len
464
- @body << part[0..-3] # to skip the ending \r\n
471
+ write_chunk(part[0..-3]) # to skip the ending \r\n
465
472
  when got <= len - 2
466
- @body << part
473
+ write_chunk(part)
467
474
  @partial_part_left = len - part.size
468
475
  when got == len - 1 # edge where we get just \r but not \n
469
- @body << part[0..-2]
476
+ write_chunk(part[0..-2])
470
477
  @partial_part_left = len - part.size
471
478
  end
472
479
  else
data/lib/puma/cluster.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'puma/runner'
4
4
  require 'puma/util'
5
5
  require 'puma/plugin'
6
+ require 'puma/cluster/worker_handle'
7
+ require 'puma/cluster/worker'
6
8
 
7
9
  require 'time'
8
10
 
@@ -11,10 +13,6 @@ 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`.
@@ -24,9 +22,8 @@ module Puma
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
 
@@ -37,7 +34,7 @@ module Puma
37
34
  begin
38
35
  loop do
39
36
  wait_workers
40
- break if @workers.empty?
37
+ break if @workers.reject {|w| w.pid.nil?}.empty?
41
38
  sleep 0.2
42
39
  end
43
40
  rescue Interrupt
@@ -46,6 +43,7 @@ module Puma
46
43
  end
47
44
 
48
45
  def start_phased_restart
46
+ @events.fire_on_restart!
49
47
  @phase += 1
50
48
  log "- Starting phased worker restart, phase: #{@phase}"
51
49
 
@@ -62,95 +60,49 @@ module Puma
62
60
  @workers.each { |x| x.hup }
63
61
  end
64
62
 
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
63
  def spawn_workers
129
64
  diff = @options[:workers] - @workers.size
130
65
  return if diff < 1
131
66
 
132
67
  master = Process.pid
68
+ if @options[:fork_worker]
69
+ @fork_writer << "-1\n"
70
+ end
133
71
 
134
72
  diff.times do
135
73
  idx = next_worker_index
136
- @launcher.config.run_hooks :before_worker_fork, idx
137
74
 
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
75
+ if @options[:fork_worker] && idx != 0
76
+ @fork_writer << "#{idx}\n"
77
+ pid = nil
78
+ else
79
+ pid = spawn_worker(idx, master)
143
80
  end
144
81
 
145
82
  debug "Spawned worker: #{pid}"
146
- @workers << Worker.new(idx, pid, @phase, @options)
83
+ @workers << WorkerHandle.new(idx, pid, @phase, @options)
84
+ end
147
85
 
148
- @launcher.config.run_hooks :after_worker_fork, idx
86
+ if @options[:fork_worker] &&
87
+ @workers.all? {|x| x.phase == @phase}
88
+
89
+ @fork_writer << "0\n"
149
90
  end
91
+ end
150
92
 
151
- if diff > 0
152
- @phased_state = :idle
93
+ # @version 5.0.0
94
+ def spawn_worker(idx, master)
95
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
96
+
97
+ pid = fork { worker(idx, master) }
98
+ if !pid
99
+ log "! Complete inability to spawn new workers detected"
100
+ log "! Seppuku is the only choice."
101
+ exit! 1
153
102
  end
103
+
104
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
105
+ pid
154
106
  end
155
107
 
156
108
  def cull_workers
@@ -163,11 +115,12 @@ module Puma
163
115
  debug "Workers to cull: #{workers_to_cull.inspect}"
164
116
 
165
117
  workers_to_cull.each do |worker|
166
- log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
118
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
167
119
  worker.term
168
120
  end
169
121
  end
170
122
 
123
+ # @!attribute [r] next_worker_index
171
124
  def next_worker_index
172
125
  all_positions = 0...@options[:workers]
173
126
  occupied_positions = @workers.map { |w| w.index }
@@ -179,26 +132,12 @@ module Puma
179
132
  @workers.count { |w| !w.booted? } == 0
180
133
  end
181
134
 
182
- def check_workers(force=false)
183
- return if !force && @next_check && @next_check >= Time.now
135
+ def check_workers
136
+ return if @next_check >= Time.now
184
137
 
185
138
  @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
186
139
 
187
- any = false
188
-
189
- @workers.each do |w|
190
- next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
191
- if w.ping_timeout?(@options[:worker_timeout])
192
- log "! Terminating timed out worker: #{w.pid}"
193
- w.kill
194
- any = true
195
- end
196
- end
197
-
198
- # If we killed any timed out workers, try to catch them
199
- # during this loop by giving the kernel time to kill them.
200
- sleep 1 if any
201
-
140
+ timeout_workers
202
141
  wait_workers
203
142
  cull_workers
204
143
  spawn_workers
@@ -211,17 +150,18 @@ module Puma
211
150
  w = @workers.find { |x| x.phase != @phase }
212
151
 
213
152
  if w
214
- if @phased_state == :idle
215
- @phased_state = :waiting
216
- log "- Stopping #{w.pid} for phased upgrade..."
217
- end
218
-
153
+ log "- Stopping #{w.pid} for phased upgrade..."
219
154
  unless w.term?
220
155
  w.term
221
156
  log "- #{w.signal} sent to #{w.pid}..."
222
157
  end
223
158
  end
224
159
  end
160
+
161
+ @next_check = [
162
+ @workers.reject(&:term?).map(&:ping_timeout).min,
163
+ @next_check
164
+ ].compact.min
225
165
  end
226
166
 
227
167
  def wakeup!
@@ -235,80 +175,25 @@ module Puma
235
175
  end
236
176
 
237
177
  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
178
  @workers = []
179
+
245
180
  @master_read.close
246
181
  @suicide_pipe.close
182
+ @fork_writer.close
247
183
 
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
184
+ pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
185
+ if @options[:fork_worker]
186
+ pipes[:fork_pipe] = @fork_pipe
187
+ pipes[:wakeup] = @wakeup
263
188
  end
264
189
 
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
302
- end
303
-
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
190
+ server = start_server if preload?
191
+ new_worker = Worker.new index: index,
192
+ master: master,
193
+ launcher: @launcher,
194
+ pipes: pipes,
195
+ server: server
196
+ new_worker.run
312
197
  end
313
198
 
314
199
  def restart
@@ -350,20 +235,61 @@ module Puma
350
235
 
351
236
  # Inside of a child process, this will return all zeroes, as @workers is only populated in
352
237
  # the master process.
238
+ # @!attribute [r] stats
353
239
  def stats
354
240
  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} }!
241
+ worker_status = @workers.map do |w|
242
+ {
243
+ started_at: w.started_at.utc.iso8601,
244
+ pid: w.pid,
245
+ index: w.index,
246
+ phase: w.phase,
247
+ booted: w.booted?,
248
+ last_checkin: w.last_checkin.utc.iso8601,
249
+ last_status: w.last_status,
250
+ }
251
+ end
252
+
253
+ {
254
+ started_at: @started_at.utc.iso8601,
255
+ workers: @workers.size,
256
+ phase: @phase,
257
+ booted_workers: worker_status.count { |w| w[:booted] },
258
+ old_workers: old_worker_count,
259
+ worker_status: worker_status,
260
+ }
358
261
  end
359
262
 
360
263
  def preload?
361
264
  @options[:preload_app]
362
265
  end
363
266
 
267
+ # @version 5.0.0
268
+ def fork_worker!
269
+ if (worker = @workers.find { |w| w.index == 0 })
270
+ worker.phase += 1
271
+ end
272
+ phased_restart
273
+ end
274
+
364
275
  # We do this in a separate method to keep the lambda scope
365
276
  # of the signals handlers as small as possible.
366
277
  def setup_signals
278
+ if @options[:fork_worker]
279
+ Signal.trap "SIGURG" do
280
+ fork_worker!
281
+ end
282
+
283
+ # Auto-fork after the specified number of requests.
284
+ if (fork_requests = @options[:fork_worker].to_i) > 0
285
+ @launcher.events.register(:ping!) do |w|
286
+ fork_worker! if w.index == 0 &&
287
+ w.phase == 0 &&
288
+ w.last_status[:requests_count] >= fork_requests
289
+ end
290
+ end
291
+ end
292
+
367
293
  Signal.trap "SIGCHLD" do
368
294
  wakeup!
369
295
  end
@@ -392,7 +318,7 @@ module Puma
392
318
 
393
319
  stop_workers
394
320
  stop
395
-
321
+ @events.fire_on_stopped!
396
322
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
397
323
  exit 0 # Clean exit, workers were stopped
398
324
  end
@@ -404,15 +330,25 @@ module Puma
404
330
 
405
331
  output_header "cluster"
406
332
 
407
- log "* Process workers: #{@options[:workers]}"
408
-
409
- before = Thread.list
333
+ # This is aligned with the output from Runner, see Runner#output_header
334
+ log "* Workers: #{@options[:workers]}"
410
335
 
411
336
  if preload?
337
+ # Threads explicitly marked as fork safe will be ignored. Used in Rails,
338
+ # but may be used by anyone. Note that we need to explicit
339
+ # Process::Waiter check here because there's a bug in Ruby 2.6 and below
340
+ # where calling thread_variable_get on a Process::Waiter will segfault.
341
+ # We can drop that clause once those versions of Ruby are no longer
342
+ # supported.
343
+ fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
344
+
345
+ before = Thread.list.reject(&fork_safe)
346
+
347
+ log "* Restarts: (\u2714) hot (\u2716) phased"
412
348
  log "* Preloading application"
413
349
  load_and_bind
414
350
 
415
- after = Thread.list
351
+ after = Thread.list.reject(&fork_safe)
416
352
 
417
353
  if after.size > before.size
418
354
  threads = (after - before)
@@ -426,7 +362,7 @@ module Puma
426
362
  end
427
363
  end
428
364
  else
429
- log "* Phased restart available"
365
+ log "* Restarts: (\u2714) hot (\u2714) phased"
430
366
 
431
367
  unless @launcher.config.app_configured?
432
368
  error "No application configured, nothing to run"
@@ -447,12 +383,13 @@ module Puma
447
383
  #
448
384
  @check_pipe, @suicide_pipe = Puma::Util.pipe
449
385
 
450
- if daemon?
451
- log "* Daemonizing..."
452
- Process.daemon(true)
453
- else
454
- log "Use Ctrl-C to stop"
455
- end
386
+ # Separate pipe used by worker 0 to receive commands to
387
+ # fork new worker processes.
388
+ @fork_pipe, @fork_writer = Puma::Util.pipe
389
+
390
+ log "Use Ctrl-C to stop"
391
+
392
+ single_worker_warning
456
393
 
457
394
  redirect_io
458
395
 
@@ -464,7 +401,8 @@ module Puma
464
401
 
465
402
  @master_read, @worker_write = read, @wakeup
466
403
 
467
- @launcher.config.run_hooks :before_fork, nil
404
+ @launcher.config.run_hooks :before_fork, nil, @launcher.events
405
+ Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
468
406
 
469
407
  spawn_workers
470
408
 
@@ -472,51 +410,67 @@ module Puma
472
410
  stop
473
411
  end
474
412
 
475
- @launcher.events.fire_on_booted!
476
-
477
413
  begin
478
- force_check = false
414
+ booted = false
415
+ in_phased_restart = false
416
+ workers_not_booted = @options[:workers]
479
417
 
480
418
  while @status == :run
481
419
  begin
482
420
  if @phased_restart
483
421
  start_phased_restart
484
422
  @phased_restart = false
423
+ in_phased_restart = true
424
+ workers_not_booted = @options[:workers]
485
425
  end
486
426
 
487
- check_workers force_check
488
-
489
- force_check = false
427
+ check_workers
490
428
 
491
- res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
429
+ res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
492
430
 
493
431
  if res
494
432
  req = read.read_nonblock(1)
495
433
 
434
+ @next_check = Time.now if req == "!"
496
435
  next if !req || req == "!"
497
436
 
498
437
  result = read.gets
499
438
  pid = result.to_i
500
439
 
440
+ if req == "b" || req == "f"
441
+ pid, idx = result.split(':').map(&:to_i)
442
+ w = @workers.find {|x| x.index == idx}
443
+ w.pid = pid if w.pid.nil?
444
+ end
445
+
501
446
  if w = @workers.find { |x| x.pid == pid }
502
447
  case req
503
448
  when "b"
504
449
  w.boot!
505
- log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
506
- force_check = true
450
+ log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
451
+ @next_check = Time.now
452
+ workers_not_booted -= 1
507
453
  when "e"
508
454
  # external term, see worker method, Signal.trap "SIGTERM"
509
455
  w.instance_variable_set :@term, true
510
456
  when "t"
511
457
  w.term unless w.term?
512
- force_check = true
513
458
  when "p"
514
459
  w.ping!(result.sub(/^\d+/,'').chomp)
460
+ @launcher.events.fire(:ping!, w)
461
+ if !booted && @workers.none? {|worker| worker.last_status.empty?}
462
+ @launcher.events.fire_on_booted!
463
+ booted = true
464
+ end
515
465
  end
516
466
  else
517
467
  log "! Out-of-sync worker list, no #{pid} worker"
518
468
  end
519
469
  end
470
+ if in_phased_restart && workers_not_booted.zero?
471
+ @events.fire_on_booted!
472
+ in_phased_restart = false
473
+ end
520
474
 
521
475
  rescue Interrupt
522
476
  @status = :stop
@@ -534,10 +488,20 @@ module Puma
534
488
 
535
489
  private
536
490
 
491
+ def single_worker_warning
492
+ return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
493
+
494
+ log "! WARNING: Detected running cluster mode with 1 worker."
495
+ log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
496
+ log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
497
+ log "! Set the `silence_single_worker_warning` option to silence this warning message."
498
+ end
499
+
537
500
  # loops thru @workers, removing workers that exited, and calling
538
501
  # `#term` if needed
539
502
  def wait_workers
540
503
  @workers.reject! do |w|
504
+ next false if w.pid.nil?
541
505
  begin
542
506
  if Process.wait(w.pid, Process::WNOHANG)
543
507
  true
@@ -546,7 +510,29 @@ module Puma
546
510
  nil
547
511
  end
548
512
  rescue Errno::ECHILD
549
- true # child is already terminated
513
+ begin
514
+ Process.kill(0, w.pid)
515
+ # child still alive but has another parent (e.g., using fork_worker)
516
+ w.term if w.term?
517
+ false
518
+ rescue Errno::ESRCH, Errno::EPERM
519
+ true # child is already terminated
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ # @version 5.0.0
526
+ def timeout_workers
527
+ @workers.each do |w|
528
+ if !w.term? && w.ping_timeout <= Time.now
529
+ details = if w.booted?
530
+ "(worker failed to check in within #{@options[:worker_timeout]} seconds)"
531
+ else
532
+ "(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
533
+ end
534
+ log "! Terminating timed out worker #{details}: #{w.pid}"
535
+ w.kill
550
536
  end
551
537
  end
552
538
  end