puma 7.0.2 → 7.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27238c0a5b2fe438167c0f56d89da2bded024ea017cdb903be3517cab2e826b6
4
- data.tar.gz: d18f43c13332384208ea7e941e8ff01f6fdbd2ff74b36b978200737b4b8ac125
3
+ metadata.gz: 13d2ebef5879f5ab45b75cadd20e4a72b643ff9f5e6af2cbcf69d655ec4475b8
4
+ data.tar.gz: 1ce847fac6f3e5f893814462b47792db32f1cb0960b9c8619d7ad26c4057bfec
5
5
  SHA512:
6
- metadata.gz: 5381aede9f16b8bb80d8f45ce6c892ee337b1ce9528a9073bf3a0369bd86f475eab5e3c77e3f217e082939d2fdc680bed9fd44ff9e817b5c0d3bfd8ee74be810
7
- data.tar.gz: 588e29319e59120aa21853ccd88e9ecec589cd488e6284e6fe8d036d363fa77e8bc5997e70f1d9d7b07ba02b9735bee70873bf7b1b454f07475aaa374ebd96af
6
+ metadata.gz: e67841b0a72c35c6667745e0aad11b152dbd46dc952b51ec811100281f8d93e45bc70fd16861b848918790b2eb0b0b290fa493f82beb0e4ed818abdffcb2f9fc
7
+ data.tar.gz: cecf1704d003e0f0299811a6c16310bb79fd7ee5ff21ce991bb2b82430f2d31e348a65b4054e57656255af10ac62489e4c4df889367a6c0b967d540522717ea7
data/History.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 7.0.4 / 2025-09-23
2
+
3
+ * Bugfixes
4
+ * Fix SSL_shutdown error handling ([#3703])
5
+ * Strip whitespace from the beginnings of request header values. ([#3742])
6
+
7
+ * Performance
8
+ * puma_http11.c: Use interned UTF-8 strings for hash keys ([#3754])
9
+ * Move sleep cluster logic to its own class ([#3746], [#3740])
10
+
11
+ ## 7.0.3 / 2025-09-13
12
+
13
+ * Performance
14
+ * server.rb - process_client - add ka to todo if readable & complete ([#3748])
15
+
16
+ * Bugfixes
17
+ * Convert PUMA_PERSISTENT_TIMEOUT to an Integer ([#3749])
18
+
1
19
  ## 7.0.2 / 2025-09-08
2
20
 
3
21
  * Bugfixes
@@ -18,7 +36,8 @@
18
36
  * Raise an ArgumentError if no block given to hooks ([#3377])
19
37
  * Don't set env['HTTP_VERSION'] for Rack > 3.1 ([#3711], [#3576])
20
38
  * Runner.rb - remove `ruby_engine` method, deprecated Nov-2024 ([#3701])
21
- * Set conditional config defaults after CLI options are parsed and config files are loaded ([#3297])
39
+ * Config `preload_app!` is now the default for clustered mode ([#3297])
40
+ * Config instance must be `clamp`-d before reading any values ([#3297])
22
41
  * Response headers set to lowercase ([#3704])
23
42
  * Update minimum Ruby version to 3.0 ([#3698])
24
43
  * Rename callback hooks ([#3438])
@@ -2226,6 +2245,13 @@ be added back in a future date when a java Puma::MiniSSL is added.
2226
2245
  * Bugfixes
2227
2246
  * Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
2228
2247
 
2248
+ [#3703]:https://github.com/puma/puma/pull/3703 "PR by @marshall-lee, merged 2025-09-20"
2249
+ [#3742]:https://github.com/puma/puma/pull/3742 "PR by @kenballus, merged 2025-09-18"
2250
+ [#3754]:https://github.com/puma/puma/pull/3754 "PR by @byroot, merged 2025-09-18"
2251
+ [#3746]:https://github.com/puma/puma/pull/3746 "PR by @schneems, merged 2025-09-18"
2252
+ [#3740]:https://github.com/puma/puma/issues/3740 "Issue by @joshuay03, closed 2025-09-18"
2253
+ [#3748]:https://github.com/puma/puma/pull/3748 "PR by @MSP-Greg, merged 2025-09-14"
2254
+ [#3749]:https://github.com/puma/puma/pull/3749 "PR by @schneems, merged 2025-09-14"
2229
2255
  [#3736]:https://github.com/puma/puma/pull/3736 "PR by @MSP-Greg, merged 2025-09-08"
2230
2256
  [#3734]:https://github.com/puma/puma/issues/3734 "Issue by @espen, closed 2025-09-08"
2231
2257
  [#3729]:https://github.com/puma/puma/pull/3729 "PR by @bensheldon, merged 2025-09-08"
@@ -660,14 +660,29 @@ VALUE engine_shutdown(VALUE self) {
660
660
 
661
661
  TypedData_Get_Struct(self, ms_conn, &engine_data_type, conn);
662
662
 
663
+ if (SSL_in_init(conn->ssl)) {
664
+ // Avoid "shutdown while in init" error
665
+ // See https://github.com/openssl/openssl/blob/openssl-3.5.2/ssl/ssl_lib.c#L2827-L2828
666
+ return Qtrue;
667
+ }
668
+
663
669
  ERR_clear_error();
664
670
 
665
671
  ok = SSL_shutdown(conn->ssl);
666
- if (ok == 0) {
667
- return Qfalse;
672
+ // See https://github.com/openssl/openssl/blob/openssl-3.5.2/ssl/ssl_lib.c#L2792-L2797
673
+ // for description of SSL_shutdown return values.
674
+ switch (ok) {
675
+ case 0:
676
+ // "close notify" alert is sent by us.
677
+ return Qfalse;
678
+ case 1:
679
+ // "close notify" alert was received from peer.
680
+ return Qtrue;
681
+ default:
682
+ raise_error(conn->ssl, ok);
668
683
  }
669
684
 
670
- return Qtrue;
685
+ return Qnil;
671
686
  }
672
687
 
673
688
  VALUE engine_init(VALUE self) {
@@ -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);
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ # Calculate a delay value for sleeping when running in clustered mode
5
+ #
6
+ # The main reason this is a class is so it can be unit tested independently.
7
+ # This makes modification easier in the future if we can encode properties of the
8
+ # delay into a test instead of relying on end-to-end testing only.
9
+ #
10
+ # This is an imprecise mechanism to address specific goals:
11
+ #
12
+ # - Evenly distribute requests across all workers at start
13
+ # - Evenly distribute CPU resources across all workers
14
+ #
15
+ # ## Goal: Distribute requests across workers at start
16
+ #
17
+ # There was a perf bug in Puma where one worker would wake up slightly before the rest and accept
18
+ # all the requests on the socket even though it didn't have enough resources to process all of them.
19
+ # This was originally fixed by never calling accept when a worker had more requests than threads
20
+ # already https://github.com/puma/puma/pull/3678/files/2736ebddb3fc8528e5150b5913fba251c37a8bf7#diff-a95f46e7ce116caddc9b9a9aa81004246d5210d5da5f4df90a818c780630166bL251-L291
21
+ #
22
+ # With the introduction of true keepalive support, there are two ways a request can come in:
23
+ # - A new request from a new client comes into the socket and it must be "accept"-d
24
+ # - A keepalive request is served and the connection is retained. Another request is then accepted
25
+ #
26
+ # Ideally the server handles requests in the order they come in, and ideally it doesn't accept more requests than it can handle.
27
+ # These goals are contradictory, because when the server is at maximum capacity due to keepalive connections, it could mean we
28
+ # block all new requests, even if those came in before the new request on the older keepalive connection.
29
+ #
30
+ # ## Distribute CPU resources across all workers
31
+ #
32
+ # - This issue was opened https://github.com/puma/puma/issues/2078
33
+ #
34
+ # There are several entangled issues and it's not exactly clear the root cause, but the observable outcome
35
+ # was that performance was better with a small sleep, and that eventually became the default.
36
+ #
37
+ # An attempt to describe why this works is here: https://github.com/puma/puma/issues/2078#issuecomment-3287032470.
38
+ #
39
+ # Summarizing: The delay is for tuning the rate at which "accept" is called on the socket.
40
+ # Puma works by calling "accept" nonblock on the socket in a loop. When there are multiple workers,
41
+ # (processes) then they will "race" to accept a request at roughly the same rate. However if one
42
+ # worker has all threads busy processing requests, then accepting a new request might "steal" it from
43
+ # a less busy worker. If a worker has no work to do, it should loop as fast as possible.
44
+ #
45
+ # ## Solution(s): Distribute requests across workers at start
46
+ #
47
+ # For now, both goals are framed as "load balancing" across workers (processes) and achieved through
48
+ # the same mechanism of sleeping longer to delay busier workers. Rather than the prior Puma 6.x
49
+ # and earlier behavior of using a binary on/off sleep value, we increase it an amound proportional
50
+ # to the load the server is under. Capping the maximum delay to the scenario where all threads are busy
51
+ # and the todo list has reached a multiplier of the maximum number of threads.
52
+ #
53
+ # Private: API may change unexpectedly
54
+ class ClusterAcceptLoopDelay
55
+ attr_reader :max_threads, :max_delay
56
+
57
+ # Initialize happens once, `call` happens often. Push global calculations here
58
+ def initialize(
59
+ # Number of workers in the cluster
60
+ workers: ,
61
+ # Maximum delay in seconds i.e. 0.005 is 5 microseconds
62
+ max_delay: # In seconds i.e. 0.005 is 5 microseconds
63
+
64
+ )
65
+ @on = max_delay > 0 && workers >= 2
66
+ @max_delay = max_delay.to_f
67
+
68
+ # Reach maximum delay when `max_threads * overload_multiplier` is reached in the system
69
+ @overload_multiplier = 25.0
70
+ end
71
+
72
+ def on?
73
+ @on
74
+ end
75
+
76
+ # We want the extreme values of this delay to be known (minimum and maximum) as well as
77
+ # a predictable curve between the two. i.e. no step functions or hard cliffs.
78
+ #
79
+ # Return value is always numeric. Returns 0 if there should be no delay
80
+ def calculate(
81
+ # Number of threads working right now, plus number of requests in the todo list
82
+ busy_threads_plus_todo:,
83
+ # Maximum number of threads in the pool, note that the busy threads (alone) may go over this value at times
84
+ # if the pool needs to be reaped. The busy thread plus todo count may go over this value by a large amount
85
+ max_threads:
86
+ )
87
+ max_value = @overload_multiplier * max_threads
88
+ # Approaches max delay when `busy_threads_plus_todo` approaches `max_value`
89
+ return max_delay * busy_threads_plus_todo.clamp(0, max_value) / max_value
90
+ end
91
+ end
92
+ end
@@ -154,7 +154,7 @@ module Puma
154
154
  mutate_stdout_and_stderr_to_sync_on_write: true,
155
155
  out_of_band: [],
156
156
  # Number of seconds for another request within a persistent session.
157
- persistent_timeout: ENV.fetch('PUMA_PERSISTENT_TIMEOUT', 65),
157
+ persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
158
158
  queue_requests: true,
159
159
  rackup: 'config.ru'.freeze,
160
160
  raise_exception_on_sigterm: true,
@@ -236,6 +236,7 @@ module Puma
236
236
  def puma_options_from_env(env = ENV)
237
237
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
238
238
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
239
+ persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
239
240
  workers = if env['WEB_CONCURRENCY'] == 'auto'
240
241
  require_processor_counter
241
242
  ::Concurrent.available_processor_count
@@ -246,6 +247,7 @@ module Puma
246
247
  {
247
248
  min_threads: min && min != "" && Integer(min),
248
249
  max_threads: max && max != "" && Integer(max),
250
+ persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
249
251
  workers: workers && workers != "" && Integer(workers),
250
252
  environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
251
253
  }
data/lib/puma/const.rb CHANGED
@@ -100,7 +100,7 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "7.0.2"
103
+ PUMA_VERSION = VERSION = "7.0.4"
104
104
  CODE_NAME = "Romantic Warrior"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
data/lib/puma/server.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'binder'
13
13
  require_relative 'util'
14
14
  require_relative 'request'
15
15
  require_relative 'configuration'
16
+ require_relative 'cluster_accept_loop_delay'
16
17
 
17
18
  require 'socket'
18
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
@@ -58,7 +59,6 @@ module Puma
58
59
  attr_accessor :app
59
60
  attr_accessor :binder
60
61
 
61
-
62
62
  # Create a server for the rack app +app+.
63
63
  #
64
64
  # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
@@ -110,6 +110,10 @@ module Puma
110
110
  @enable_keep_alives &&= @queue_requests
111
111
  @io_selector_backend = @options[:io_selector_backend]
112
112
  @http_content_length_limit = @options[:http_content_length_limit]
113
+ @cluster_accept_loop_delay = ClusterAcceptLoopDelay.new(
114
+ workers: @options[:workers],
115
+ max_delay: @options[:wait_for_less_busy_worker] || 0 # Real default is in Configuration::DEFAULTS, this is for unit testing
116
+ )
113
117
 
114
118
  if @options[:fiber_per_request]
115
119
  singleton_class.prepend(FiberPerRequest)
@@ -245,11 +249,6 @@ module Puma
245
249
  @thread_pool&.pool_capacity
246
250
  end
247
251
 
248
- # @!attribute [r] busy_threads
249
- def busy_threads
250
- @thread_pool&.busy_threads
251
- end
252
-
253
252
  # Runs the server.
254
253
  #
255
254
  # If +background+ is true (the default) then a thread is spun
@@ -266,7 +265,11 @@ module Puma
266
265
  @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
267
266
 
268
267
  if @queue_requests
269
- @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
268
+ @reactor = Reactor.new(@io_selector_backend) { |c|
269
+ # Inversion of control, the reactor is calling a method on the server when it
270
+ # is done buffering a request or receives a new request from a keepalive connection.
271
+ self.reactor_wakeup(c)
272
+ }
270
273
  @reactor.run
271
274
  end
272
275
 
@@ -291,6 +294,9 @@ module Puma
291
294
  # This method is called from the Reactor thread when a queued Client receives data,
292
295
  # times out, or when the Reactor is shutting down.
293
296
  #
297
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
298
+ # from the server.
299
+ #
294
300
  # It is responsible for ensuring that a request has been completely received
295
301
  # before it starts to be processed by the ThreadPool. This may be known as read buffering.
296
302
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
@@ -325,7 +331,7 @@ module Puma
325
331
  end
326
332
  rescue StandardError => e
327
333
  client_error(e, client)
328
- client.close
334
+ close_client_safely(client)
329
335
  true
330
336
  end
331
337
 
@@ -339,7 +345,6 @@ module Puma
339
345
  pool = @thread_pool
340
346
  queue_requests = @queue_requests
341
347
  drain = options[:drain_on_shutdown] ? 0 : nil
342
- max_flt = @max_threads.to_f
343
348
 
344
349
  addr_send_name, addr_value = case options[:remote_address]
345
350
  when :value
@@ -384,15 +389,13 @@ module Puma
384
389
  # clients until the code is finished.
385
390
  pool.wait_while_out_of_band_running
386
391
 
387
- # only use delay when clustered and busy
388
- if pool.busy_threads >= @max_threads
389
- if @clustered
390
- delay = 0.0001 * ((@reactor&.reactor_size || 0) + pool.busy_threads * 1.5)/max_flt
391
- sleep delay
392
- else
393
- # use small sleep for busy single worker
394
- sleep 0.0001
395
- end
392
+ # A well rested herd (cluster) runs faster
393
+ if @cluster_accept_loop_delay.on? && (busy_threads_plus_todo = pool.busy_threads) > 0
394
+ delay = @cluster_accept_loop_delay.calculate(
395
+ max_threads: @max_threads,
396
+ busy_threads_plus_todo: busy_threads_plus_todo
397
+ )
398
+ sleep(delay)
396
399
  end
397
400
 
398
401
  io = begin
@@ -401,11 +404,9 @@ module Puma
401
404
  next
402
405
  end
403
406
  drain += 1 if shutting_down?
404
- pool << Client.new(io, @binder.env(sock)).tap { |c|
405
- c.listener = sock
406
- c.http_content_length_limit = @http_content_length_limit
407
- c.send(addr_send_name, addr_value) if addr_value
408
- }
407
+ client = new_client(io, sock)
408
+ client.send(addr_send_name, addr_value) if addr_value
409
+ pool << client
409
410
  end
410
411
  end
411
412
  rescue IOError, Errno::EBADF
@@ -442,6 +443,14 @@ module Puma
442
443
  @events.fire :state, :done
443
444
  end
444
445
 
446
+ # :nodoc:
447
+ def new_client(io, sock)
448
+ client = Client.new(io, @binder.env(sock))
449
+ client.listener = sock
450
+ client.http_content_length_limit = @http_content_length_limit
451
+ client
452
+ end
453
+
445
454
  # :nodoc:
446
455
  def handle_check
447
456
  cmd = @check.read(1)
@@ -508,7 +517,7 @@ module Puma
508
517
  next_request_ready = if client.has_back_to_back_requests?
509
518
  with_force_shutdown(client) { client.process_back_to_back_requests }
510
519
  else
511
- nil
520
+ with_force_shutdown(client) { client.eagerly_finish }
512
521
  end
513
522
 
514
523
  if next_request_ready
@@ -529,16 +538,21 @@ module Puma
529
538
  ensure
530
539
  client.io_buffer.reset
531
540
 
532
- begin
533
- client.close if close_socket
534
- rescue IOError, SystemCallError
535
- # Already closed
536
- rescue StandardError => e
537
- @log_writer.unknown_error e, nil, "Client"
538
- end
541
+ close_client_safely(client) if close_socket
539
542
  end
540
543
  end
541
544
 
545
+ # :nodoc:
546
+ def close_client_safely(client)
547
+ client.close
548
+ rescue IOError, SystemCallError
549
+ # Already closed
550
+ rescue MiniSSL::SSLError => e
551
+ @log_writer.ssl_error e, client.io
552
+ rescue StandardError => e
553
+ @log_writer.unknown_error e, nil, "Client"
554
+ end
555
+
542
556
  # Triggers a client timeout if the thread-pool shuts down
543
557
  # during execution of the provided block.
544
558
  def with_force_shutdown(client, &block)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.2
4
+ version: 7.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix
@@ -87,6 +87,7 @@ files:
87
87
  - lib/puma/cluster.rb
88
88
  - lib/puma/cluster/worker.rb
89
89
  - lib/puma/cluster/worker_handle.rb
90
+ - lib/puma/cluster_accept_loop_delay.rb
90
91
  - lib/puma/commonlogger.rb
91
92
  - lib/puma/configuration.rb
92
93
  - lib/puma/const.rb