puma 6.4.1 → 7.2.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +407 -8
  3. data/README.md +109 -49
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +11 -1
  6. data/docs/java_options.md +54 -0
  7. data/docs/jungle/README.md +1 -1
  8. data/docs/kubernetes.md +11 -16
  9. data/docs/plugins.md +6 -2
  10. data/docs/restart.md +2 -2
  11. data/docs/signals.md +21 -21
  12. data/docs/stats.md +11 -5
  13. data/docs/systemd.md +14 -5
  14. data/ext/puma_http11/extconf.rb +20 -32
  15. data/ext/puma_http11/mini_ssl.c +29 -9
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +40 -9
  17. data/ext/puma_http11/puma_http11.c +125 -118
  18. data/lib/puma/app/status.rb +11 -3
  19. data/lib/puma/binder.rb +21 -11
  20. data/lib/puma/cli.rb +10 -8
  21. data/lib/puma/client.rb +183 -83
  22. data/lib/puma/cluster/worker.rb +24 -21
  23. data/lib/puma/cluster/worker_handle.rb +38 -8
  24. data/lib/puma/cluster.rb +73 -47
  25. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  26. data/lib/puma/commonlogger.rb +3 -3
  27. data/lib/puma/configuration.rb +131 -60
  28. data/lib/puma/const.rb +31 -12
  29. data/lib/puma/control_cli.rb +10 -6
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +411 -121
  32. data/lib/puma/error_logger.rb +7 -5
  33. data/lib/puma/events.rb +25 -10
  34. data/lib/puma/io_buffer.rb +8 -4
  35. data/lib/puma/jruby_restart.rb +0 -16
  36. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  37. data/lib/puma/launcher.rb +73 -55
  38. data/lib/puma/log_writer.rb +9 -9
  39. data/lib/puma/minissl/context_builder.rb +1 -0
  40. data/lib/puma/minissl.rb +1 -1
  41. data/lib/puma/null_io.rb +26 -0
  42. data/lib/puma/plugin/systemd.rb +3 -3
  43. data/lib/puma/rack/urlmap.rb +1 -1
  44. data/lib/puma/reactor.rb +19 -13
  45. data/lib/puma/request.rb +71 -39
  46. data/lib/puma/runner.rb +15 -17
  47. data/lib/puma/sd_notify.rb +1 -4
  48. data/lib/puma/server.rb +134 -73
  49. data/lib/puma/single.rb +7 -4
  50. data/lib/puma/state_file.rb +3 -2
  51. data/lib/puma/thread_pool.rb +57 -80
  52. data/lib/puma/util.rb +0 -7
  53. data/lib/puma.rb +10 -0
  54. data/lib/rack/handler/puma.rb +10 -7
  55. data/tools/Dockerfile +15 -5
  56. metadata +14 -15
  57. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/client.rb CHANGED
@@ -1,13 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class IO
4
- # We need to use this for a jruby work around on both 1.8 and 1.9.
5
- # So this either creates the constant (on 1.8), or harmlessly
6
- # reopens it (on 1.9).
7
- module WaitReadable
8
- end
9
- end
10
-
11
3
  require_relative 'detect'
12
4
  require_relative 'io_buffer'
13
5
  require 'tempfile'
@@ -51,11 +43,24 @@ module Puma
51
43
  CHUNK_VALID_ENDING = Const::LINE_END
52
44
  CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
53
45
 
46
+ # The maximum number of bytes we'll buffer looking for a valid
47
+ # chunk header.
48
+ MAX_CHUNK_HEADER_SIZE = 4096
49
+
50
+ # The maximum amount of excess data the client sends
51
+ # using chunk size extensions before we abort the connection.
52
+ MAX_CHUNK_EXCESS = 16 * 1024
53
+
54
54
  # Content-Length header value validation
55
55
  CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
56
56
 
57
57
  TE_ERR_MSG = 'Invalid Transfer-Encoding'
58
58
 
59
+ # See:
60
+ # https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
61
+ # https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
62
+ STRIP_OWS = /\A[ \t]+|[ \t]+\z/
63
+
59
64
  # The object used for a request with no body. All requests with
60
65
  # no body share this one object since it has no state.
61
66
  EmptyBody = NullIO.new
@@ -103,7 +108,8 @@ module Puma
103
108
  end
104
109
 
105
110
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
106
- :tempfile, :io_buffer, :http_content_length_limit_exceeded
111
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
112
+ :requests_served
107
113
 
108
114
  attr_writer :peerip, :http_content_length_limit
109
115
 
@@ -125,9 +131,9 @@ module Puma
125
131
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
126
132
  end
127
133
 
128
- # For the hijack protocol (allows us to just put the Client object
129
- # into the env)
130
- def call
134
+ # For the full hijack protocol, `env['rack.hijack']` is set to
135
+ # `client.method :full_hijack`
136
+ def full_hijack
131
137
  @hijacked = true
132
138
  env[HIJACK_IO] ||= @io
133
139
  end
@@ -142,29 +148,31 @@ module Puma
142
148
  end
143
149
 
144
150
  # Number of seconds until the timeout elapses.
151
+ # @!attribute [r] timeout
145
152
  def timeout
146
153
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
147
154
  end
148
155
 
149
- def reset(fast_check=true)
156
+ def reset
150
157
  @parser.reset
151
158
  @io_buffer.reset
152
159
  @read_header = true
153
- @read_proxy = !!@expect_proxy_proto
160
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
154
161
  @env = @proto_env.dup
155
- @body = nil
156
- @tempfile = nil
157
162
  @parsed_bytes = 0
158
163
  @ready = false
159
164
  @body_remain = 0
160
165
  @peerip = nil if @remote_addr_header
161
166
  @in_last_chunk = false
162
167
  @http_content_length_limit_exceeded = false
168
+ end
163
169
 
170
+ # only used with back-to-back requests contained in the buffer
171
+ def process_back_to_back_requests
164
172
  if @buffer
165
173
  return false unless try_to_parse_proxy_protocol
166
174
 
167
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
175
+ @parsed_bytes = parser_execute
168
176
 
169
177
  if @parser.finished?
170
178
  return setup_body
@@ -172,47 +180,67 @@ module Puma
172
180
  raise HttpParserError,
173
181
  "HEADER is longer than allowed, aborting client early."
174
182
  end
175
-
176
- return false
177
- else
178
- begin
179
- if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
180
- return try_to_finish
181
- end
182
- rescue IOError
183
- # swallow it
184
- end
185
-
186
183
  end
187
184
  end
188
185
 
186
+ # if a client sends back-to-back requests, the buffer may contain one or more
187
+ # of them.
188
+ def has_back_to_back_requests?
189
+ !(@buffer.nil? || @buffer.empty?)
190
+ end
191
+
189
192
  def close
193
+ tempfile_close
190
194
  begin
191
195
  @io.close
192
196
  rescue IOError, Errno::EBADF
193
- Puma::Util.purge_interrupt_queue
194
197
  end
195
198
  end
196
199
 
200
+ def tempfile_close
201
+ tf_path = @tempfile&.path
202
+ @tempfile&.close
203
+ File.unlink(tf_path) if tf_path
204
+ @tempfile = nil
205
+ @body = nil
206
+ rescue Errno::ENOENT, IOError
207
+ end
208
+
197
209
  # If necessary, read the PROXY protocol from the buffer. Returns
198
210
  # false if more data is needed.
199
211
  def try_to_parse_proxy_protocol
200
212
  if @read_proxy
201
213
  if @expect_proxy_proto == :v1
202
- if @buffer.include? "\r\n"
203
- if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
204
- if md[1]
205
- @peerip = md[1].split(" ")[0]
214
+ crlf_index = @buffer.index "\r\n"
215
+
216
+ unless crlf_index
217
+ if "PROXY ".start_with? @buffer
218
+ return false
219
+ elsif @buffer.start_with? "PROXY "
220
+ if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
221
+ raise ConnectionError, "PROXY protocol v1 line is too long"
206
222
  end
207
- @buffer = md.post_match
223
+ return false
208
224
  end
209
- # if the buffer has a \r\n but doesn't have a PROXY protocol
210
- # request, this is just HTTP from a non-PROXY client; move on
225
+
211
226
  @read_proxy = false
212
- return @buffer.size > 0
213
- else
214
- return false
227
+ return true
228
+ end
229
+
230
+ if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
231
+ raise ConnectionError, "PROXY protocol v1 line is too long"
215
232
  end
233
+
234
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
235
+ if md[1]
236
+ @peerip = md[1].split(" ")[0]
237
+ end
238
+ @buffer = md.post_match
239
+ end
240
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
241
+ # request, this is just HTTP from a non-PROXY client; move on
242
+ @read_proxy = false
243
+ return @buffer.size > 0
216
244
  end
217
245
  end
218
246
  true
@@ -232,6 +260,7 @@ module Puma
232
260
 
233
261
  return read_body if in_data_phase
234
262
 
263
+ data = nil
235
264
  begin
236
265
  data = @io.read_nonblock(CHUNK_SIZE)
237
266
  rescue IO::WaitReadable
@@ -257,26 +286,28 @@ module Puma
257
286
 
258
287
  return false unless try_to_parse_proxy_protocol
259
288
 
260
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
289
+ @parsed_bytes = parser_execute
261
290
 
262
291
  if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
263
292
  @http_content_length_limit_exceeded = true
264
293
  end
265
294
 
266
295
  if @parser.finished?
267
- return setup_body
296
+ setup_body
268
297
  elsif @parsed_bytes >= MAX_HEADER
269
298
  raise HttpParserError,
270
299
  "HEADER is longer than allowed, aborting client early."
300
+ else
301
+ false
271
302
  end
272
-
273
- false
274
303
  end
275
304
 
276
305
  def eagerly_finish
277
306
  return true if @ready
278
- return false unless @to_io.wait_readable(0)
279
- try_to_finish
307
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
308
+ return true if try_to_finish
309
+ end
310
+ false
280
311
  end
281
312
 
282
313
  def finish(timeout)
@@ -284,6 +315,44 @@ module Puma
284
315
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
285
316
  end
286
317
 
318
+ # Wraps `@parser.execute` and adds meaningful error messages
319
+ # @return [Integer] bytes of buffer read by parser
320
+ #
321
+ def parser_execute
322
+ @parser.execute(@env, @buffer, @parsed_bytes)
323
+ rescue => e
324
+ @env[HTTP_CONNECTION] = 'close'
325
+ raise e unless HttpParserError === e && e.message.include?('non-SSL')
326
+
327
+ req, _ = @buffer.split "\r\n\r\n"
328
+ request_line, headers = req.split "\r\n", 2
329
+
330
+ # below checks for request issues and changes error message accordingly
331
+ if !@env.key? REQUEST_METHOD
332
+ if request_line.count(' ') != 2
333
+ # maybe this is an SSL connection ?
334
+ raise e
335
+ else
336
+ method = request_line[/\A[^ ]+/]
337
+ raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
338
+ end
339
+ elsif !@env.key? REQUEST_PATH
340
+ path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
341
+ raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
342
+ elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
343
+ query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
344
+ raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
345
+ elsif !@env.key? SERVER_PROTOCOL
346
+ # protocol is bad
347
+ text = request_line[/[^ ]*\z/]
348
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
349
+ elsif !headers.empty?
350
+ # headers are bad
351
+ hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
352
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
353
+ end
354
+ end
355
+
287
356
  def timeout!
288
357
  write_error(408) if in_data_phase
289
358
  raise ConnectionError
@@ -358,17 +427,18 @@ module Puma
358
427
  if te
359
428
  te_lwr = te.downcase
360
429
  if te.include? ','
361
- te_ary = te_lwr.split ','
430
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
362
431
  te_count = te_ary.count CHUNKED
363
432
  te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
364
- if te_ary.last == CHUNKED && te_count == 1 && te_valid
365
- @env.delete TRANSFER_ENCODING2
366
- return setup_chunked_body body
367
- elsif te_count >= 1
433
+ if te_count > 1
368
434
  raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
435
+ elsif te_ary.last != CHUNKED
436
+ raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
369
437
  elsif !te_valid
370
438
  raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
371
439
  end
440
+ @env.delete TRANSFER_ENCODING2
441
+ return setup_chunked_body body
372
442
  elsif te_lwr == CHUNKED
373
443
  @env.delete TRANSFER_ENCODING2
374
444
  return setup_chunked_body body
@@ -395,18 +465,33 @@ module Puma
395
465
  return true
396
466
  end
397
467
 
398
- remain = cl.to_i - body.bytesize
468
+ content_length = cl.to_i
469
+
470
+ remain = content_length - body.bytesize
399
471
 
400
472
  if remain <= 0
401
- @body = StringIO.new(body)
402
- @buffer = nil
473
+ # Part of the body is a pipelined request OR garbage. We'll deal with that later.
474
+ if content_length == 0
475
+ @body = EmptyBody
476
+ if body.empty?
477
+ @buffer = nil
478
+ else
479
+ @buffer = body
480
+ end
481
+ elsif remain == 0
482
+ @body = StringIO.new body
483
+ @buffer = nil
484
+ else
485
+ @body = StringIO.new(body[0,content_length])
486
+ @buffer = body[content_length..-1]
487
+ end
403
488
  set_ready
404
489
  return true
405
490
  end
406
491
 
407
492
  if remain > MAX_BODY
408
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
409
- @body.unlink
493
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
494
+ File.unlink @body.path unless IS_WINDOWS
410
495
  @body.binmode
411
496
  @tempfile = @body
412
497
  else
@@ -431,46 +516,42 @@ module Puma
431
516
  # after this
432
517
  remain = @body_remain
433
518
 
434
- if remain > CHUNK_SIZE
435
- want = CHUNK_SIZE
436
- else
437
- want = remain
438
- end
519
+ # don't bother with reading zero bytes
520
+ unless remain.zero?
521
+ begin
522
+ chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
523
+ rescue IO::WaitReadable
524
+ return false
525
+ rescue SystemCallError, IOError
526
+ raise ConnectionError, "Connection error detected during read"
527
+ end
439
528
 
440
- begin
441
- chunk = @io.read_nonblock(want, @read_buffer)
442
- rescue IO::WaitReadable
443
- return false
444
- rescue SystemCallError, IOError
445
- raise ConnectionError, "Connection error detected during read"
446
- end
529
+ # No chunk means a closed socket
530
+ unless chunk
531
+ @body.close
532
+ @buffer = nil
533
+ set_ready
534
+ raise EOFError
535
+ end
447
536
 
448
- # No chunk means a closed socket
449
- unless chunk
450
- @body.close
451
- @buffer = nil
452
- set_ready
453
- raise EOFError
537
+ remain -= @body.write(chunk)
454
538
  end
455
539
 
456
- remain -= @body.write(chunk)
457
-
458
540
  if remain <= 0
459
541
  @body.rewind
460
542
  @buffer = nil
461
543
  set_ready
462
- return true
544
+ true
545
+ else
546
+ @body_remain = remain
547
+ false
463
548
  end
464
-
465
- @body_remain = remain
466
-
467
- false
468
549
  end
469
550
 
470
551
  def read_chunked_body
471
552
  while true
472
553
  begin
473
- chunk = @io.read_nonblock(4096, @read_buffer)
554
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
474
555
  rescue IO::WaitReadable
475
556
  return false
476
557
  rescue SystemCallError, IOError
@@ -496,9 +577,10 @@ module Puma
496
577
  @chunked_body = true
497
578
  @partial_part_left = 0
498
579
  @prev_chunk = ""
580
+ @excess_cr = 0
499
581
 
500
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
501
- @body.unlink
582
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
583
+ File.unlink @body.path unless IS_WINDOWS
502
584
  @body.binmode
503
585
  @tempfile = @body
504
586
  @chunked_content_length = 0
@@ -577,6 +659,20 @@ module Puma
577
659
  end
578
660
  end
579
661
 
662
+ # Track the excess as a function of the size of the
663
+ # header vs the size of the actual data. Excess can
664
+ # go negative (and is expected to) when the body is
665
+ # significant.
666
+ # The additional of chunk_hex.size and 2 compensates
667
+ # for a client sending 1 byte in a chunked body over
668
+ # a long period of time, making sure that that client
669
+ # isn't accidentally eventually punished.
670
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
671
+
672
+ if @excess_cr >= MAX_CHUNK_EXCESS
673
+ raise HttpParserError, "Maximum chunk excess detected"
674
+ end
675
+
580
676
  len += 2
581
677
 
582
678
  part = io.read(len)
@@ -604,6 +700,10 @@ module Puma
604
700
  @partial_part_left = len - part.size
605
701
  end
606
702
  else
703
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
704
+ raise HttpParserError, "maximum size of chunk header exceeded"
705
+ end
706
+
607
707
  @prev_chunk = line
608
708
  return false
609
709
  end
@@ -14,7 +14,7 @@ module Puma
14
14
  class Worker < Puma::Runner # :nodoc:
15
15
  attr_reader :index, :master
16
16
 
17
- def initialize(index:, master:, launcher:, pipes:, server: nil)
17
+ def initialize(index:, master:, launcher:, pipes:, app: nil)
18
18
  super(launcher)
19
19
 
20
20
  @index = index
@@ -23,7 +23,8 @@ module Puma
23
23
  @worker_write = pipes[:worker_write]
24
24
  @fork_pipe = pipes[:fork_pipe]
25
25
  @wakeup = pipes[:wakeup]
26
- @server = server
26
+ @app = app
27
+ @server = nil
27
28
  @hook_data = {}
28
29
  end
29
30
 
@@ -57,7 +58,7 @@ module Puma
57
58
  @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
58
59
 
59
60
  begin
60
- server = @server ||= start_server
61
+ @server = start_server
61
62
  rescue Exception => e
62
63
  log "! Unable to start worker"
63
64
  log e
@@ -85,36 +86,37 @@ module Puma
85
86
  if idx == -1 # stop server
86
87
  if restart_server.length > 0
87
88
  restart_server.clear
88
- server.begin_restart(true)
89
+ @server.begin_restart(true)
89
90
  @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
90
91
  end
92
+ elsif idx == -2 # refork cycle is done
93
+ @config.run_hooks(:after_refork, nil, @log_writer, @hook_data)
91
94
  elsif idx == 0 # restart server
92
95
  restart_server << true << false
93
96
  else # fork worker
94
97
  worker_pids << pid = spawn_worker(idx)
95
- @worker_write << "f#{pid}:#{idx}\n" rescue nil
98
+ @worker_write << "#{PIPE_FORK}#{pid}:#{idx}\n" rescue nil
96
99
  end
97
100
  end
98
101
  end
99
102
  end
100
103
 
101
104
  Signal.trap "SIGTERM" do
102
- @worker_write << "e#{Process.pid}\n" rescue nil
105
+ @worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
103
106
  restart_server.clear
104
- server.stop
107
+ @server.stop
105
108
  restart_server << false
106
109
  end
107
110
 
108
111
  begin
109
- @worker_write << "b#{Process.pid}:#{index}\n"
112
+ @worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
110
113
  rescue SystemCallError, IOError
111
- Puma::Util.purge_interrupt_queue
112
114
  STDERR.puts "Master seems to have exited, exiting."
113
115
  return
114
116
  end
115
117
 
116
118
  while restart_server.pop
117
- server_thread = server.run
119
+ server_thread = @server.run
118
120
 
119
121
  if @log_writer.debug? && index == 0
120
122
  debug_loaded_extensions "Loaded Extensions - worker 0:"
@@ -122,19 +124,20 @@ module Puma
122
124
 
123
125
  stat_thread ||= Thread.new(@worker_write) do |io|
124
126
  Puma.set_thread_name "stat pld"
125
- base_payload = "p#{Process.pid}"
127
+ base_payload = "#{PIPE_PING}#{Process.pid}"
126
128
 
127
129
  while true
128
130
  begin
129
- b = server.backlog || 0
130
- r = server.running || 0
131
- t = server.pool_capacity || 0
132
- m = server.max_threads || 0
133
- rc = server.requests_count || 0
134
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
135
- io << payload
131
+ payload = base_payload.dup
132
+
133
+ hsh = @server.stats
134
+ hsh.each do |k, v|
135
+ payload << %Q! "#{k}":#{v || 0},!
136
+ end
137
+ # sub call properly adds 'closing' string
138
+ io << payload.sub(/,\z/, " }\n")
139
+ @server.reset_max
136
140
  rescue IOError
137
- Puma::Util.purge_interrupt_queue
138
141
  break
139
142
  end
140
143
  sleep @options[:worker_check_interval]
@@ -147,7 +150,7 @@ module Puma
147
150
  # exiting until any background operations are completed
148
151
  @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
149
152
  ensure
150
- @worker_write << "t#{Process.pid}\n" rescue nil
153
+ @worker_write << "#{PIPE_TERM}#{Process.pid}\n" rescue nil
151
154
  @worker_write.close
152
155
  end
153
156
 
@@ -162,7 +165,7 @@ module Puma
162
165
  launcher: @launcher,
163
166
  pipes: { check_pipe: @check_pipe,
164
167
  worker_write: @worker_write },
165
- server: @server
168
+ app: @app
166
169
  new_worker.run
167
170
  end
168
171
 
@@ -4,13 +4,15 @@ module Puma
4
4
  class Cluster < Runner
5
5
  #—————————————————————— DO NOT USE — this class is for internal use only ———
6
6
 
7
-
8
7
  # This class represents a worker process from the perspective of the puma
9
8
  # master process. It contains information about the process and its health
10
9
  # and it exposes methods to control the process via IPC. It does not
11
10
  # include the actual logic executed by the worker process itself. For that,
12
11
  # see Puma::Cluster::Worker.
13
12
  class WorkerHandle # :nodoc:
13
+ # array of stat 'max' keys
14
+ WORKER_MAX_KEYS = [:backlog_max, :reactor_max]
15
+
14
16
  def initialize(idx, pid, phase, options)
15
17
  @index = idx
16
18
  @pid = pid
@@ -23,12 +25,13 @@ module Puma
23
25
  @last_checkin = Time.now
24
26
  @last_status = {}
25
27
  @term = false
28
+ @worker_max = Array.new WORKER_MAX_KEYS.length, 0
26
29
  end
27
30
 
28
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
31
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at, :process_status
29
32
 
30
33
  # @version 5.0.0
31
- attr_writer :pid, :phase
34
+ attr_writer :pid, :phase, :process_status
32
35
 
33
36
  def booted?
34
37
  @stage == :booted
@@ -52,12 +55,39 @@ module Puma
52
55
  end
53
56
 
54
57
  def ping!(status)
55
- @last_checkin = Time.now
56
- captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
57
- @last_status = captures.names.inject({}) do |hash, key|
58
- hash[key.to_sym] = captures[key].to_i
59
- hash
58
+ hsh = {}
59
+ k, v = nil, nil
60
+ status.tr('}{"', '').strip.split(", ") do |kv|
61
+ cntr = 0
62
+ kv.split(':') do |t|
63
+ if cntr == 0
64
+ k = t
65
+ cntr = 1
66
+ else
67
+ v = t
68
+ end
69
+ end
70
+ hsh[k.to_sym] = v.to_i
71
+ end
72
+
73
+ # check stat max values, we can't signal workers to reset the max values,
74
+ # so we do so here
75
+ WORKER_MAX_KEYS.each_with_index do |key, idx|
76
+ next unless hsh[key]
77
+
78
+ if hsh[key] < @worker_max[idx]
79
+ hsh[key] = @worker_max[idx]
80
+ else
81
+ @worker_max[idx] = hsh[key]
82
+ end
60
83
  end
84
+ @last_checkin = Time.now
85
+ @last_status = hsh
86
+ end
87
+
88
+ # Resets max values to zero. Called whenever `Cluster#stats` is called
89
+ def reset_max
90
+ WORKER_MAX_KEYS.length.times { |idx| @worker_max[idx] = 0 }
61
91
  end
62
92
 
63
93
  # @see Puma::Cluster#check_workers