puma 6.6.0 → 7.1.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +170 -5
  3. data/README.md +24 -32
  4. data/docs/fork_worker.md +5 -5
  5. data/docs/kubernetes.md +8 -6
  6. data/docs/restart.md +2 -2
  7. data/docs/signals.md +11 -11
  8. data/docs/stats.md +3 -2
  9. data/docs/systemd.md +1 -1
  10. data/ext/puma_http11/extconf.rb +2 -17
  11. data/ext/puma_http11/mini_ssl.c +18 -8
  12. data/ext/puma_http11/org/jruby/puma/Http11.java +10 -2
  13. data/ext/puma_http11/puma_http11.c +23 -11
  14. data/lib/puma/binder.rb +10 -8
  15. data/lib/puma/cli.rb +3 -5
  16. data/lib/puma/client.rb +95 -61
  17. data/lib/puma/cluster/worker.rb +9 -10
  18. data/lib/puma/cluster/worker_handle.rb +38 -7
  19. data/lib/puma/cluster.rb +41 -26
  20. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  21. data/lib/puma/commonlogger.rb +3 -3
  22. data/lib/puma/configuration.rb +89 -43
  23. data/lib/puma/const.rb +9 -10
  24. data/lib/puma/control_cli.rb +6 -2
  25. data/lib/puma/detect.rb +2 -0
  26. data/lib/puma/dsl.rb +135 -94
  27. data/lib/puma/error_logger.rb +3 -1
  28. data/lib/puma/events.rb +25 -10
  29. data/lib/puma/io_buffer.rb +8 -4
  30. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  31. data/lib/puma/launcher.rb +52 -48
  32. data/lib/puma/minissl.rb +0 -1
  33. data/lib/puma/plugin/systemd.rb +3 -3
  34. data/lib/puma/rack/urlmap.rb +1 -1
  35. data/lib/puma/reactor.rb +19 -4
  36. data/lib/puma/request.rb +45 -32
  37. data/lib/puma/runner.rb +8 -17
  38. data/lib/puma/server.rb +111 -61
  39. data/lib/puma/single.rb +5 -2
  40. data/lib/puma/state_file.rb +3 -2
  41. data/lib/puma/thread_pool.rb +47 -82
  42. data/lib/puma/util.rb +0 -7
  43. data/lib/puma.rb +10 -0
  44. data/lib/rack/handler/puma.rb +2 -2
  45. data/tools/Dockerfile +3 -1
  46. metadata +6 -4
@@ -29,7 +29,7 @@ public class Http11 extends RubyObject {
29
29
  public final static int MAX_REQUEST_URI_LENGTH = getConstLength("PUMA_REQUEST_URI_MAX_LENGTH", 1024 * 12);
30
30
  public final static String MAX_REQUEST_URI_LENGTH_ERR = "HTTP element REQUEST_URI is longer than the " + MAX_REQUEST_URI_LENGTH + " allowed length.";
31
31
  public final static int MAX_FRAGMENT_LENGTH = 1024;
32
- public final static String MAX_FRAGMENT_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 1024 allowed length.";
32
+ public final static String MAX_FRAGMENT_LENGTH_ERR = "HTTP element FRAGMENT is longer than the 1024 allowed length.";
33
33
  public final static int MAX_REQUEST_PATH_LENGTH = getConstLength("PUMA_REQUEST_PATH_MAX_LENGTH", 8192);
34
34
  public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the " + MAX_REQUEST_PATH_LENGTH + " allowed length.";
35
35
  public final static int MAX_QUERY_STRING_LENGTH = getConstLength("PUMA_QUERY_STRING_MAX_LENGTH", 10 * 1024);
@@ -109,6 +109,10 @@ public class Http11 extends RubyObject {
109
109
  return (RubyClass)runtime.getModule("Puma").getConstant("HttpParserError");
110
110
  }
111
111
 
112
+ private static boolean is_ows(int c) {
113
+ return c == ' ' || c == '\t';
114
+ }
115
+
112
116
  public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int field, int flen, int value, int vlen) {
113
117
  RubyString f;
114
118
  IRubyObject v;
@@ -127,7 +131,11 @@ public class Http11 extends RubyObject {
127
131
  }
128
132
  }
129
133
 
130
- while (vlen > 0 && Character.isWhitespace(buffer.get(value + vlen - 1))) vlen--;
134
+ while (vlen > 0 && is_ows(buffer.get(value + vlen - 1))) vlen--;
135
+ while (vlen > 0 && is_ows(buffer.get(value))) {
136
+ vlen--;
137
+ value++;
138
+ }
131
139
 
132
140
  if (b.equals(CONTENT_LENGTH_BYTELIST) || b.equals(CONTENT_TYPE_BYTELIST)) {
133
141
  f = RubyString.newString(runtime, b);
@@ -7,6 +7,7 @@
7
7
  #define RSTRING_NOT_MODIFIED 1
8
8
 
9
9
  #include "ruby.h"
10
+ #include "ruby/encoding.h"
10
11
  #include "ext_help.h"
11
12
  #include <assert.h>
12
13
  #include <string.h>
@@ -48,8 +49,11 @@ static VALUE global_request_path;
48
49
  #define VALIDATE_MAX_LENGTH(len, N) if(len > MAX_##N##_LENGTH) { rb_raise(eHttpParserError, MAX_##N##_LENGTH_ERR, len); }
49
50
 
50
51
  /** Defines global strings in the init method. */
51
- #define DEF_GLOBAL(N, val) global_##N = rb_str_new2(val); rb_global_variable(&global_##N)
52
-
52
+ static inline void DEF_GLOBAL(VALUE *var, const char *cstr)
53
+ {
54
+ rb_global_variable(var);
55
+ *var = rb_enc_interned_str_cstr(cstr, rb_utf8_encoding());
56
+ }
53
57
 
54
58
  /* Defines the maximum allowed lengths for various input elements.*/
55
59
  #ifndef PUMA_REQUEST_URI_MAX_LENGTH
@@ -134,13 +138,13 @@ static void init_common_fields(void)
134
138
  memcpy(tmp, HTTP_PREFIX, HTTP_PREFIX_LEN);
135
139
 
136
140
  for(i = 0; i < ARRAY_SIZE(common_http_fields); cf++, i++) {
141
+ rb_global_variable(&cf->value);
137
142
  if(cf->raw) {
138
143
  cf->value = rb_str_new(cf->name, cf->len);
139
144
  } else {
140
145
  memcpy(tmp + HTTP_PREFIX_LEN, cf->name, cf->len + 1);
141
146
  cf->value = rb_str_new(tmp, HTTP_PREFIX_LEN + cf->len);
142
147
  }
143
- rb_global_variable(&cf->value);
144
148
  }
145
149
  }
146
150
 
@@ -155,6 +159,10 @@ static VALUE find_common_field_value(const char *field, size_t flen)
155
159
  return Qnil;
156
160
  }
157
161
 
162
+ static int is_ows(const char c) {
163
+ return c == ' ' || c == '\t';
164
+ }
165
+
158
166
  void http_field(puma_parser* hp, const char *field, size_t flen,
159
167
  const char *value, size_t vlen)
160
168
  {
@@ -181,7 +189,11 @@ void http_field(puma_parser* hp, const char *field, size_t flen,
181
189
  f = rb_str_new(hp->buf, new_size);
182
190
  }
183
191
 
184
- while (vlen > 0 && isspace(value[vlen - 1])) vlen--;
192
+ while (vlen > 0 && is_ows(value[vlen - 1])) vlen--;
193
+ while (vlen > 0 && is_ows(value[0])) {
194
+ vlen--;
195
+ value++;
196
+ }
185
197
 
186
198
  /* check for duplicate header */
187
199
  v = rb_hash_aref(hp->request, f);
@@ -468,15 +480,15 @@ void Init_puma_http11(void)
468
480
  VALUE mPuma = rb_define_module("Puma");
469
481
  VALUE cHttpParser = rb_define_class_under(mPuma, "HttpParser", rb_cObject);
470
482
 
471
- DEF_GLOBAL(request_method, "REQUEST_METHOD");
472
- DEF_GLOBAL(request_uri, "REQUEST_URI");
473
- DEF_GLOBAL(fragment, "FRAGMENT");
474
- DEF_GLOBAL(query_string, "QUERY_STRING");
475
- DEF_GLOBAL(server_protocol, "SERVER_PROTOCOL");
476
- DEF_GLOBAL(request_path, "REQUEST_PATH");
483
+ DEF_GLOBAL(&global_request_method, "REQUEST_METHOD");
484
+ DEF_GLOBAL(&global_request_uri, "REQUEST_URI");
485
+ DEF_GLOBAL(&global_fragment, "FRAGMENT");
486
+ DEF_GLOBAL(&global_query_string, "QUERY_STRING");
487
+ DEF_GLOBAL(&global_server_protocol, "SERVER_PROTOCOL");
488
+ DEF_GLOBAL(&global_request_path, "REQUEST_PATH");
477
489
 
478
- eHttpParserError = rb_define_class_under(mPuma, "HttpParserError", rb_eStandardError);
479
490
  rb_global_variable(&eHttpParserError);
491
+ eHttpParserError = rb_define_class_under(mPuma, "HttpParserError", rb_eStandardError);
480
492
 
481
493
  rb_define_alloc_func(cHttpParser, HttpParser_alloc);
482
494
  rb_define_method(cHttpParser, "initialize", HttpParser_init, 0);
data/lib/puma/binder.rb CHANGED
@@ -5,7 +5,6 @@ require 'socket'
5
5
 
6
6
  require_relative 'const'
7
7
  require_relative 'util'
8
- require_relative 'configuration'
9
8
 
10
9
  module Puma
11
10
 
@@ -19,9 +18,9 @@ module Puma
19
18
 
20
19
  RACK_VERSION = [1,6].freeze
21
20
 
22
- def initialize(log_writer, conf = Configuration.new, env: ENV)
21
+ def initialize(log_writer, options, env: ENV)
23
22
  @log_writer = log_writer
24
- @conf = conf
23
+ @options = options
25
24
  @listeners = []
26
25
  @inherited_fds = {}
27
26
  @activated_sockets = {}
@@ -31,10 +30,10 @@ module Puma
31
30
  @proto_env = {
32
31
  "rack.version".freeze => RACK_VERSION,
33
32
  "rack.errors".freeze => log_writer.stderr,
34
- "rack.multithread".freeze => conf.options[:max_threads] > 1,
35
- "rack.multiprocess".freeze => conf.options[:workers] >= 1,
33
+ "rack.multithread".freeze => options[:max_threads] > 1,
34
+ "rack.multiprocess".freeze => options[:workers] >= 1,
36
35
  "rack.run_once".freeze => false,
37
- RACK_URL_SCHEME => conf.options[:rack_url_scheme],
36
+ RACK_URL_SCHEME => options[:rack_url_scheme],
38
37
  "SCRIPT_NAME".freeze => env['SCRIPT_NAME'] || "",
39
38
 
40
39
  # I'd like to set a default CONTENT_TYPE here but some things
@@ -44,7 +43,10 @@ module Puma
44
43
 
45
44
  "QUERY_STRING".freeze => "",
46
45
  SERVER_SOFTWARE => PUMA_SERVER_STRING,
47
- GATEWAY_INTERFACE => CGI_VER
46
+ GATEWAY_INTERFACE => CGI_VER,
47
+
48
+ RACK_AFTER_REPLY => nil,
49
+ RACK_RESPONSE_FINISHED => nil,
48
50
  }
49
51
 
50
52
  @envs = {}
@@ -243,7 +245,7 @@ module Puma
243
245
  cert_key.each do |v|
244
246
  if params[v]&.start_with?('store:')
245
247
  index = Integer(params.delete(v).split('store:').last)
246
- params["#{v}_pem"] = @conf.options[:store][index]
248
+ params["#{v}_pem"] = @options[:store][index]
247
249
  end
248
250
  end
249
251
  MiniSSL::ContextBuilder.new(params, @log_writer).context
data/lib/puma/cli.rb CHANGED
@@ -39,10 +39,8 @@ module Puma
39
39
  @control_url = nil
40
40
  @control_options = {}
41
41
 
42
- setup_options env
43
-
44
42
  begin
45
- @parser.parse! @argv
43
+ setup_options env
46
44
 
47
45
  if file = @argv.shift
48
46
  @conf.configure do |user_config, file_config|
@@ -93,7 +91,7 @@ module Puma
93
91
  #
94
92
 
95
93
  def setup_options(env = ENV)
96
- @conf = Configuration.new({}, {events: @events}, env) do |user_config, file_config|
94
+ @conf = Configuration.new({}, { events: @events }, env) do |user_config, file_config|
97
95
  @parser = OptionParser.new do |o|
98
96
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
99
97
  user_config.bind arg
@@ -240,7 +238,7 @@ module Puma
240
238
  $stdout.puts o
241
239
  exit 0
242
240
  end
243
- end
241
+ end.parse! @argv
244
242
  end
245
243
  end
246
244
  end
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,11 +148,12 @@ 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
@@ -166,11 +165,14 @@ module Puma
166
165
  @peerip = nil if @remote_addr_header
167
166
  @in_last_chunk = false
168
167
  @http_content_length_limit_exceeded = false
168
+ end
169
169
 
170
+ # only used with back-to-back requests contained in the buffer
171
+ def process_back_to_back_requests
170
172
  if @buffer
171
173
  return false unless try_to_parse_proxy_protocol
172
174
 
173
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
175
+ @parsed_bytes = parser_execute
174
176
 
175
177
  if @parser.finished?
176
178
  return setup_body
@@ -178,25 +180,20 @@ module Puma
178
180
  raise HttpParserError,
179
181
  "HEADER is longer than allowed, aborting client early."
180
182
  end
181
-
182
- return false
183
- else
184
- begin
185
- if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
186
- return try_to_finish
187
- end
188
- rescue IOError
189
- # swallow it
190
- end
191
183
  end
192
184
  end
193
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
+
194
192
  def close
195
193
  tempfile_close
196
194
  begin
197
195
  @io.close
198
196
  rescue IOError, Errno::EBADF
199
- Puma::Util.purge_interrupt_queue
200
197
  end
201
198
  end
202
199
 
@@ -273,26 +270,28 @@ module Puma
273
270
 
274
271
  return false unless try_to_parse_proxy_protocol
275
272
 
276
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
273
+ @parsed_bytes = parser_execute
277
274
 
278
275
  if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
279
276
  @http_content_length_limit_exceeded = true
280
277
  end
281
278
 
282
279
  if @parser.finished?
283
- return setup_body
280
+ setup_body
284
281
  elsif @parsed_bytes >= MAX_HEADER
285
282
  raise HttpParserError,
286
283
  "HEADER is longer than allowed, aborting client early."
284
+ else
285
+ false
287
286
  end
288
-
289
- false
290
287
  end
291
288
 
292
289
  def eagerly_finish
293
290
  return true if @ready
294
- return false unless @to_io.wait_readable(0)
295
- try_to_finish
291
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
292
+ return true if try_to_finish
293
+ end
294
+ false
296
295
  end
297
296
 
298
297
  def finish(timeout)
@@ -300,6 +299,44 @@ module Puma
300
299
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
301
300
  end
302
301
 
302
+ # Wraps `@parser.execute` and adds meaningful error messages
303
+ # @return [Integer] bytes of buffer read by parser
304
+ #
305
+ def parser_execute
306
+ @parser.execute(@env, @buffer, @parsed_bytes)
307
+ rescue => e
308
+ @env[HTTP_CONNECTION] = 'close'
309
+ raise e unless HttpParserError === e && e.message.include?('non-SSL')
310
+
311
+ req, _ = @buffer.split "\r\n\r\n"
312
+ request_line, headers = req.split "\r\n", 2
313
+
314
+ # below checks for request issues and changes error message accordingly
315
+ if !@env.key? REQUEST_METHOD
316
+ if request_line.count(' ') != 2
317
+ # maybe this is an SSL connection ?
318
+ raise e
319
+ else
320
+ method = request_line[/\A[^ ]+/]
321
+ raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
322
+ end
323
+ elsif !@env.key? REQUEST_PATH
324
+ path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
325
+ raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
326
+ elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
327
+ query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
328
+ raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
329
+ elsif !@env.key? SERVER_PROTOCOL
330
+ # protocol is bad
331
+ text = request_line[/[^ ]*\z/]
332
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
333
+ elsif !headers.empty?
334
+ # headers are bad
335
+ hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
336
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
337
+ end
338
+ end
339
+
303
340
  def timeout!
304
341
  write_error(408) if in_data_phase
305
342
  raise ConnectionError
@@ -374,17 +411,18 @@ module Puma
374
411
  if te
375
412
  te_lwr = te.downcase
376
413
  if te.include? ','
377
- te_ary = te_lwr.split ','
414
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
378
415
  te_count = te_ary.count CHUNKED
379
416
  te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
380
- if te_ary.last == CHUNKED && te_count == 1 && te_valid
381
- @env.delete TRANSFER_ENCODING2
382
- return setup_chunked_body body
383
- elsif te_count >= 1
417
+ if te_count > 1
384
418
  raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
419
+ elsif te_ary.last != CHUNKED
420
+ raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
385
421
  elsif !te_valid
386
422
  raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
387
423
  end
424
+ @env.delete TRANSFER_ENCODING2
425
+ return setup_chunked_body body
388
426
  elsif te_lwr == CHUNKED
389
427
  @env.delete TRANSFER_ENCODING2
390
428
  return setup_chunked_body body
@@ -462,40 +500,36 @@ module Puma
462
500
  # after this
463
501
  remain = @body_remain
464
502
 
465
- if remain > CHUNK_SIZE
466
- want = CHUNK_SIZE
467
- else
468
- want = remain
469
- end
503
+ # don't bother with reading zero bytes
504
+ unless remain.zero?
505
+ begin
506
+ chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
507
+ rescue IO::WaitReadable
508
+ return false
509
+ rescue SystemCallError, IOError
510
+ raise ConnectionError, "Connection error detected during read"
511
+ end
470
512
 
471
- begin
472
- chunk = @io.read_nonblock(want, @read_buffer)
473
- rescue IO::WaitReadable
474
- return false
475
- rescue SystemCallError, IOError
476
- raise ConnectionError, "Connection error detected during read"
477
- end
513
+ # No chunk means a closed socket
514
+ unless chunk
515
+ @body.close
516
+ @buffer = nil
517
+ set_ready
518
+ raise EOFError
519
+ end
478
520
 
479
- # No chunk means a closed socket
480
- unless chunk
481
- @body.close
482
- @buffer = nil
483
- set_ready
484
- raise EOFError
521
+ remain -= @body.write(chunk)
485
522
  end
486
523
 
487
- remain -= @body.write(chunk)
488
-
489
524
  if remain <= 0
490
525
  @body.rewind
491
526
  @buffer = nil
492
527
  set_ready
493
- return true
528
+ true
529
+ else
530
+ @body_remain = remain
531
+ false
494
532
  end
495
-
496
- @body_remain = remain
497
-
498
- false
499
533
  end
500
534
 
501
535
  def read_chunked_body
@@ -110,7 +110,6 @@ module Puma
110
110
  begin
111
111
  @worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
112
112
  rescue SystemCallError, IOError
113
- Puma::Util.purge_interrupt_queue
114
113
  STDERR.puts "Master seems to have exited, exiting."
115
114
  return
116
115
  end
@@ -128,16 +127,16 @@ module Puma
128
127
 
129
128
  while true
130
129
  begin
131
- b = server.backlog || 0
132
- r = server.running || 0
133
- t = server.pool_capacity || 0
134
- m = server.max_threads || 0
135
- rc = server.requests_count || 0
136
- bt = server.busy_threads || 0
137
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads":#{m}, "requests_count":#{rc}, "busy_threads":#{bt} }\n!
138
- io << payload
130
+ payload = base_payload.dup
131
+
132
+ hsh = server.stats
133
+ hsh.each do |k, v|
134
+ payload << %Q! "#{k}":#{v || 0},!
135
+ end
136
+ # sub call properly adds 'closing' string
137
+ io << payload.sub(/,\z/, " }\n")
138
+ server.reset_max
139
139
  rescue IOError
140
- Puma::Util.purge_interrupt_queue
141
140
  break
142
141
  end
143
142
  sleep @options[:worker_check_interval]
@@ -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
@@ -51,12 +54,40 @@ module Puma
51
54
  @term
52
55
  end
53
56
 
54
- STATUS_PATTERN = /{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads":(?<max_threads>\d*), "requests_count":(?<requests_count>\d*), "busy_threads":(?<busy_threads>\d*) }/
55
- private_constant :STATUS_PATTERN
56
-
57
57
  def ping!(status)
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
83
+ end
58
84
  @last_checkin = Time.now
59
- @last_status = status.match(STATUS_PATTERN).named_captures.map { |c_name, c| [c_name.to_sym, c.to_i] }.to_h
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 }
60
91
  end
61
92
 
62
93
  # @see Puma::Cluster#check_workers