puma 6.4.3 → 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 +387 -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 +38 -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 +157 -84
  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 +23 -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 +55 -36
  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'
@@ -64,6 +56,11 @@ module Puma
64
56
 
65
57
  TE_ERR_MSG = 'Invalid Transfer-Encoding'
66
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
+
67
64
  # The object used for a request with no body. All requests with
68
65
  # no body share this one object since it has no state.
69
66
  EmptyBody = NullIO.new
@@ -111,7 +108,8 @@ module Puma
111
108
  end
112
109
 
113
110
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
114
- :tempfile, :io_buffer, :http_content_length_limit_exceeded
111
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
112
+ :requests_served
115
113
 
116
114
  attr_writer :peerip, :http_content_length_limit
117
115
 
@@ -133,9 +131,9 @@ module Puma
133
131
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
134
132
  end
135
133
 
136
- # For the hijack protocol (allows us to just put the Client object
137
- # into the env)
138
- def call
134
+ # For the full hijack protocol, `env['rack.hijack']` is set to
135
+ # `client.method :full_hijack`
136
+ def full_hijack
139
137
  @hijacked = true
140
138
  env[HIJACK_IO] ||= @io
141
139
  end
@@ -150,29 +148,31 @@ module Puma
150
148
  end
151
149
 
152
150
  # Number of seconds until the timeout elapses.
151
+ # @!attribute [r] timeout
153
152
  def timeout
154
153
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
155
154
  end
156
155
 
157
- def reset(fast_check=true)
156
+ def reset
158
157
  @parser.reset
159
158
  @io_buffer.reset
160
159
  @read_header = true
161
- @read_proxy = !!@expect_proxy_proto
160
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
162
161
  @env = @proto_env.dup
163
- @body = nil
164
- @tempfile = nil
165
162
  @parsed_bytes = 0
166
163
  @ready = false
167
164
  @body_remain = 0
168
165
  @peerip = nil if @remote_addr_header
169
166
  @in_last_chunk = false
170
167
  @http_content_length_limit_exceeded = false
168
+ end
171
169
 
170
+ # only used with back-to-back requests contained in the buffer
171
+ def process_back_to_back_requests
172
172
  if @buffer
173
173
  return false unless try_to_parse_proxy_protocol
174
174
 
175
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
175
+ @parsed_bytes = parser_execute
176
176
 
177
177
  if @parser.finished?
178
178
  return setup_body
@@ -180,47 +180,67 @@ module Puma
180
180
  raise HttpParserError,
181
181
  "HEADER is longer than allowed, aborting client early."
182
182
  end
183
-
184
- return false
185
- else
186
- begin
187
- if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
188
- return try_to_finish
189
- end
190
- rescue IOError
191
- # swallow it
192
- end
193
-
194
183
  end
195
184
  end
196
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
+
197
192
  def close
193
+ tempfile_close
198
194
  begin
199
195
  @io.close
200
196
  rescue IOError, Errno::EBADF
201
- Puma::Util.purge_interrupt_queue
202
197
  end
203
198
  end
204
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
+
205
209
  # If necessary, read the PROXY protocol from the buffer. Returns
206
210
  # false if more data is needed.
207
211
  def try_to_parse_proxy_protocol
208
212
  if @read_proxy
209
213
  if @expect_proxy_proto == :v1
210
- if @buffer.include? "\r\n"
211
- if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
212
- if md[1]
213
- @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"
214
222
  end
215
- @buffer = md.post_match
223
+ return false
216
224
  end
217
- # if the buffer has a \r\n but doesn't have a PROXY protocol
218
- # request, this is just HTTP from a non-PROXY client; move on
225
+
219
226
  @read_proxy = false
220
- return @buffer.size > 0
221
- else
222
- 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"
223
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
224
244
  end
225
245
  end
226
246
  true
@@ -240,6 +260,7 @@ module Puma
240
260
 
241
261
  return read_body if in_data_phase
242
262
 
263
+ data = nil
243
264
  begin
244
265
  data = @io.read_nonblock(CHUNK_SIZE)
245
266
  rescue IO::WaitReadable
@@ -265,26 +286,28 @@ module Puma
265
286
 
266
287
  return false unless try_to_parse_proxy_protocol
267
288
 
268
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
289
+ @parsed_bytes = parser_execute
269
290
 
270
291
  if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
271
292
  @http_content_length_limit_exceeded = true
272
293
  end
273
294
 
274
295
  if @parser.finished?
275
- return setup_body
296
+ setup_body
276
297
  elsif @parsed_bytes >= MAX_HEADER
277
298
  raise HttpParserError,
278
299
  "HEADER is longer than allowed, aborting client early."
300
+ else
301
+ false
279
302
  end
280
-
281
- false
282
303
  end
283
304
 
284
305
  def eagerly_finish
285
306
  return true if @ready
286
- return false unless @to_io.wait_readable(0)
287
- 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
288
311
  end
289
312
 
290
313
  def finish(timeout)
@@ -292,6 +315,44 @@ module Puma
292
315
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
293
316
  end
294
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
+
295
356
  def timeout!
296
357
  write_error(408) if in_data_phase
297
358
  raise ConnectionError
@@ -366,17 +427,18 @@ module Puma
366
427
  if te
367
428
  te_lwr = te.downcase
368
429
  if te.include? ','
369
- te_ary = te_lwr.split ','
430
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
370
431
  te_count = te_ary.count CHUNKED
371
432
  te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
372
- if te_ary.last == CHUNKED && te_count == 1 && te_valid
373
- @env.delete TRANSFER_ENCODING2
374
- return setup_chunked_body body
375
- elsif te_count >= 1
433
+ if te_count > 1
376
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}'"
377
437
  elsif !te_valid
378
438
  raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
379
439
  end
440
+ @env.delete TRANSFER_ENCODING2
441
+ return setup_chunked_body body
380
442
  elsif te_lwr == CHUNKED
381
443
  @env.delete TRANSFER_ENCODING2
382
444
  return setup_chunked_body body
@@ -403,18 +465,33 @@ module Puma
403
465
  return true
404
466
  end
405
467
 
406
- remain = cl.to_i - body.bytesize
468
+ content_length = cl.to_i
469
+
470
+ remain = content_length - body.bytesize
407
471
 
408
472
  if remain <= 0
409
- @body = StringIO.new(body)
410
- @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
411
488
  set_ready
412
489
  return true
413
490
  end
414
491
 
415
492
  if remain > MAX_BODY
416
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
417
- @body.unlink
493
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
494
+ File.unlink @body.path unless IS_WINDOWS
418
495
  @body.binmode
419
496
  @tempfile = @body
420
497
  else
@@ -439,46 +516,42 @@ module Puma
439
516
  # after this
440
517
  remain = @body_remain
441
518
 
442
- if remain > CHUNK_SIZE
443
- want = CHUNK_SIZE
444
- else
445
- want = remain
446
- 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
447
528
 
448
- begin
449
- chunk = @io.read_nonblock(want, @read_buffer)
450
- rescue IO::WaitReadable
451
- return false
452
- rescue SystemCallError, IOError
453
- raise ConnectionError, "Connection error detected during read"
454
- 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
455
536
 
456
- # No chunk means a closed socket
457
- unless chunk
458
- @body.close
459
- @buffer = nil
460
- set_ready
461
- raise EOFError
537
+ remain -= @body.write(chunk)
462
538
  end
463
539
 
464
- remain -= @body.write(chunk)
465
-
466
540
  if remain <= 0
467
541
  @body.rewind
468
542
  @buffer = nil
469
543
  set_ready
470
- return true
544
+ true
545
+ else
546
+ @body_remain = remain
547
+ false
471
548
  end
472
-
473
- @body_remain = remain
474
-
475
- false
476
549
  end
477
550
 
478
551
  def read_chunked_body
479
552
  while true
480
553
  begin
481
- chunk = @io.read_nonblock(4096, @read_buffer)
554
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
482
555
  rescue IO::WaitReadable
483
556
  return false
484
557
  rescue SystemCallError, IOError
@@ -506,8 +579,8 @@ module Puma
506
579
  @prev_chunk = ""
507
580
  @excess_cr = 0
508
581
 
509
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
510
- @body.unlink
582
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
583
+ File.unlink @body.path unless IS_WINDOWS
511
584
  @body.binmode
512
585
  @tempfile = @body
513
586
  @chunked_content_length = 0
@@ -627,7 +700,7 @@ module Puma
627
700
  @partial_part_left = len - part.size
628
701
  end
629
702
  else
630
- if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
703
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
631
704
  raise HttpParserError, "maximum size of chunk header exceeded"
632
705
  end
633
706
 
@@ -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