puma 7.0.3-java → 7.1.0-java

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: 57482800b8ce7fafc070a24927cb3348799099a2925ba3d541b078cbb02eaf66
4
- data.tar.gz: 2eb44b3d0e0c1e81a9176bd2a04dd7f86e1e21b2c5765c74ca7b51cd25b1a71a
3
+ metadata.gz: c34ad6eb102937d5162b4ee769397333da4ad801d17fab6cc70226319ad94db2
4
+ data.tar.gz: 4c4fd1e10d19a1a156a8cd524bcf547e7b0c59cbac7868a21c3cd469ca60c96d
5
5
  SHA512:
6
- metadata.gz: 60b359191f2c5fd22932e65b506ac1f02946d02cbaa6b905a97d96972abbc8f15b6bb151d6feca804a202c72683000871a3c3023c8b36b5cf545cb92a01fab74
7
- data.tar.gz: b29236b5fcea83b4ec0ce75ca878da67466413d05e82b217dd1f5d5d16fe34b5e510c5f43b283ed816005c5b71a1451d0fcdc8581d455b4e5878ce85daabad7c
6
+ metadata.gz: 5f1f1026adb88d8bae2fb4008a94731b190af258c68270ab4f1c7578457f260612982d102c19b302b2f724b5e8bed7e774a6a3b8effd79d1ed23afa5349928cb
7
+ data.tar.gz: f01f2c0187729869c45940da06a5a496eaf404889baa0ee4cacbfb704a286b5d2f0852ffb0cf7671a2db8a5263850b83d4d381dc689f05ded5190c27b234afa8
data/History.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 7.1.0 / 2025-10-16
2
+
3
+ * Features
4
+ * Introduce `after_worker_shutdown` hook ([#3707])
5
+ * Reintroduce keepalive "fast inline" behavior. Provides faster (8x on JRuby & 1.4x on Ruby) pipeline processing ([#3794])
6
+
7
+ * Bugfixes
8
+ * Skip reading zero bytes when request body is buffered ([#3795])
9
+ * Fix `PUMA_LOG_CONFIG=1` logging twice with prune_bundler enabled ([#3778])
10
+ * Fix prune_bundler not showing in `PUMA_LOG_CONFIG=1` output ([#3779])
11
+ * Guard ThreadPool method call, which may be nil during shutdown ([#3791], [#3790])
12
+ * Set `Thread.current.puma_server` in Thread init code, not every request ([#3774])
13
+ * Fix race condition while deleting pidfile ([#3657])
14
+
15
+ ## 7.0.4 / 2025-09-23
16
+
17
+ * Bugfixes
18
+ * Fix SSL_shutdown error handling ([#3703])
19
+ * Strip whitespace from the beginnings of request header values. ([#3742])
20
+
21
+ * Performance
22
+ * puma_http11.c: Use interned UTF-8 strings for hash keys ([#3754])
23
+ * Move sleep cluster logic to its own class ([#3746], [#3740])
24
+
1
25
  ## 7.0.3 / 2025-09-13
2
26
 
3
27
  * Performance
@@ -26,7 +50,8 @@
26
50
  * Raise an ArgumentError if no block given to hooks ([#3377])
27
51
  * Don't set env['HTTP_VERSION'] for Rack > 3.1 ([#3711], [#3576])
28
52
  * Runner.rb - remove `ruby_engine` method, deprecated Nov-2024 ([#3701])
29
- * Set conditional config defaults after CLI options are parsed and config files are loaded ([#3297])
53
+ * Config `preload_app!` is now the default for clustered mode ([#3297])
54
+ * Config instance must be `clamp`-d before reading any values ([#3297])
30
55
  * Response headers set to lowercase ([#3704])
31
56
  * Update minimum Ruby version to 3.0 ([#3698])
32
57
  * Rename callback hooks ([#3438])
@@ -2234,6 +2259,20 @@ be added back in a future date when a java Puma::MiniSSL is added.
2234
2259
  * Bugfixes
2235
2260
  * Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
2236
2261
 
2262
+ [#3707]:https://github.com/puma/puma/pull/3707 "PR by @nerdrew, merged 2025-10-02"
2263
+ [#3794]:https://github.com/puma/puma/pull/3794 "PR by @schneems, merged 2025-10-16"
2264
+ [#3795]:https://github.com/puma/puma/pull/3795 "PR by @MSP-Greg, merged 2025-10-16"
2265
+ [#3778]:https://github.com/puma/puma/pull/3778 "PR by @joshuay03, merged 2025-10-16"
2266
+ [#3779]:https://github.com/puma/puma/pull/3779 "PR by @joshuay03, merged 2025-10-16"
2267
+ [#3791]:https://github.com/puma/puma/pull/3791 "PR by @MSP-Greg, merged 2025-10-09"
2268
+ [#3790]:https://github.com/puma/puma/issues/3790 "Issue by @eric-wtfoxtrot, closed 2025-10-09"
2269
+ [#3774]:https://github.com/puma/puma/pull/3774 "PR by @MSP-Greg, merged 2025-10-16"
2270
+ [#3657]:https://github.com/puma/puma/pull/3657 "PR by @marksmith, merged 2025-10-16"
2271
+ [#3703]:https://github.com/puma/puma/pull/3703 "PR by @marshall-lee, merged 2025-09-20"
2272
+ [#3742]:https://github.com/puma/puma/pull/3742 "PR by @kenballus, merged 2025-09-18"
2273
+ [#3754]:https://github.com/puma/puma/pull/3754 "PR by @byroot, merged 2025-09-18"
2274
+ [#3746]:https://github.com/puma/puma/pull/3746 "PR by @schneems, merged 2025-09-18"
2275
+ [#3740]:https://github.com/puma/puma/issues/3740 "Issue by @joshuay03, closed 2025-09-18"
2237
2276
  [#3748]:https://github.com/puma/puma/pull/3748 "PR by @MSP-Greg, merged 2025-09-14"
2238
2277
  [#3749]:https://github.com/puma/puma/pull/3749 "PR by @schneems, merged 2025-09-14"
2239
2278
  [#3736]:https://github.com/puma/puma/pull/3736 "PR by @MSP-Greg, merged 2025-09-08"
data/README.md CHANGED
@@ -142,8 +142,8 @@ Preloading can’t be used with phased restart, since phased restart kills and r
142
142
 
143
143
  #### Cluster mode hooks
144
144
 
145
- When using clustered mode, Puma's configuration DSL provides `before_fork` and `before_worker_boot`
146
- hooks to run code when the master process forks and child workers are booted respectively.
145
+ When using clustered mode, Puma's configuration DSL provides `before_fork`, `before_worker_boot`, and `after_worker_shutdown`
146
+ hooks to run code when the master process forks, the child workers are booted, and after each child worker exits respectively.
147
147
 
148
148
  It is recommended to use these hooks with `preload_app!`, otherwise constants loaded by your
149
149
  application (such as `Rails`) will not be available inside the hooks.
@@ -157,6 +157,11 @@ end
157
157
  before_worker_boot do
158
158
  # Add code to run inside the Puma worker process after forking.
159
159
  end
160
+
161
+ after_worker_shutdown do |worker_handle|
162
+ # Add code to run inside the Puma master process after a worker exits. `worker.process_status` can be used to get the
163
+ # `Process::Status` of the exited worker.
164
+ end
160
165
  ```
161
166
 
162
167
  In addition, there is an `before_refork` and `after_refork` hooks which are used only in [`fork_worker` mode](docs/fork_worker.md),
data/docs/kubernetes.md CHANGED
@@ -10,7 +10,7 @@ Assuming you already have a running cluster and docker image repository, you can
10
10
 
11
11
  A basic Dockerfile example:
12
12
 
13
- ```
13
+ ```Dockerfile
14
14
  FROM ruby:3.4.5-alpine # can be updated to newer ruby versions
15
15
  RUN apk update && apk add build-base # and any other packages you need
16
16
 
@@ -28,7 +28,7 @@ CMD bundle exec rackup -o 0.0.0.0
28
28
 
29
29
  A sample `deployment.yaml`:
30
30
 
31
- ```
31
+ ```yaml
32
32
  ---
33
33
  apiVersion: apps/v1
34
34
  kind: Deployment
data/docs/stats.md CHANGED
@@ -62,7 +62,7 @@ When Puma runs in single mode, these stats are available at the top level. When
62
62
  this is a "wholistic" stat reflecting the overall current state of work to be done and the capacity to do it.
63
63
  * pool_capacity: `how many threads are waiting to receive work` + `max_threads` - `running`. In a typical configuration where `min_threads`
64
64
  and `max_threads` are configured to the same number, this is simply `how many threads are waiting to receive work`. This number exists only as a stat
65
- and is not used for any internal decisions, unlike `busy_theads`, which is usually a more useful stat.
65
+ and is not used for any internal decisions, unlike `busy_threads`, which is usually a more useful stat.
66
66
  * max_threads: the maximum number of threads Puma is configured to spool per worker
67
67
  * requests_count: the number of requests this worker has served since starting
68
68
  * reactor_max: the maximum observed number of requests held in Puma's "reactor" which is used for asyncronously buffering request bodies. This stat is reset on every call, so it's the maximum value observed since the last stat call.
@@ -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);
data/lib/puma/client.rb CHANGED
@@ -500,40 +500,36 @@ module Puma
500
500
  # after this
501
501
  remain = @body_remain
502
502
 
503
- if remain > CHUNK_SIZE
504
- want = CHUNK_SIZE
505
- else
506
- want = remain
507
- 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
508
512
 
509
- begin
510
- chunk = @io.read_nonblock(want, @read_buffer)
511
- rescue IO::WaitReadable
512
- return false
513
- rescue SystemCallError, IOError
514
- raise ConnectionError, "Connection error detected during read"
515
- 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
516
520
 
517
- # No chunk means a closed socket
518
- unless chunk
519
- @body.close
520
- @buffer = nil
521
- set_ready
522
- raise EOFError
521
+ remain -= @body.write(chunk)
523
522
  end
524
523
 
525
- remain -= @body.write(chunk)
526
-
527
524
  if remain <= 0
528
525
  @body.rewind
529
526
  @buffer = nil
530
527
  set_ready
531
- return true
528
+ true
529
+ else
530
+ @body_remain = remain
531
+ false
532
532
  end
533
-
534
- @body_remain = remain
535
-
536
- false
537
533
  end
538
534
 
539
535
  def read_chunked_body
@@ -28,10 +28,10 @@ module Puma
28
28
  @worker_max = Array.new WORKER_MAX_KEYS.length, 0
29
29
  end
30
30
 
31
- 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
32
32
 
33
33
  # @version 5.0.0
34
- attr_writer :pid, :phase
34
+ attr_writer :pid, :phase, :process_status
35
35
 
36
36
  def booted?
37
37
  @stage == :booted
data/lib/puma/cluster.rb CHANGED
@@ -23,7 +23,7 @@ module Puma
23
23
  @next_check = Time.now
24
24
 
25
25
  @worker_max = [] # keeps track of 'max' stat values
26
- @phased_restart = false
26
+ @pending_phased_restart = false
27
27
  end
28
28
 
29
29
  # Returns the list of cluster worker handles.
@@ -238,7 +238,7 @@ module Puma
238
238
  def phased_restart(refork = false)
239
239
  return false if @options[:preload_app] && !refork
240
240
 
241
- @phased_restart = refork ? :refork : true
241
+ @pending_phased_restart = refork ? :refork : true
242
242
  wakeup!
243
243
 
244
244
  true
@@ -456,11 +456,11 @@ module Puma
456
456
  break
457
457
  end
458
458
 
459
- if @phased_restart
460
- start_phased_restart(@phased_restart == :refork)
459
+ if @pending_phased_restart
460
+ start_phased_restart(@pending_phased_restart == :refork)
461
461
 
462
- in_phased_restart = @phased_restart
463
- @phased_restart = false
462
+ in_phased_restart = @pending_phased_restart
463
+ @pending_phased_restart = false
464
464
 
465
465
  workers_not_booted = @options[:workers]
466
466
  # worker 0 is not restarted on refork
@@ -583,7 +583,9 @@ module Puma
583
583
  # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
584
584
  # 2. When `fork_worker` is enabled, some worker may not be direct children,
585
585
  # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
586
- if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
586
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
587
+ w.process_status = status
588
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
587
589
  true
588
590
  else
589
591
  w.term if w.term?
@@ -0,0 +1,91 @@
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"-ed
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
+ # ## Goal: 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 what the root cause is, 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), 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: 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 amount 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_delay
56
+
57
+ # Initialize happens once, `call` happens often. Perform 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 milliseconds
62
+ max_delay:
63
+ )
64
+ @on = max_delay > 0 && workers >= 2
65
+ @max_delay = max_delay.to_f
66
+
67
+ # Reach maximum delay when `max_threads * overload_multiplier` is reached in the system
68
+ @overload_multiplier = 25.0
69
+ end
70
+
71
+ def on?
72
+ @on
73
+ end
74
+
75
+ # We want the extreme values of this delay to be known (minimum and maximum) as well as
76
+ # a predictable curve between the two. i.e. no step functions or hard cliffs.
77
+ #
78
+ # Return value is always numeric. Returns 0 if there should be no delay.
79
+ def calculate(
80
+ # Number of threads working right now, plus number of requests in the todo list
81
+ busy_threads_plus_todo:,
82
+ # Maximum number of threads in the pool, note that the busy threads (alone) may go over this value at times
83
+ # if the pool needs to be reaped. The busy thread plus todo count may go over this value by a large amount.
84
+ max_threads:
85
+ )
86
+ max_value = @overload_multiplier * max_threads
87
+ # Approaches max delay when `busy_threads_plus_todo` approaches `max_value`
88
+ return max_delay * busy_threads_plus_todo.clamp(0, max_value) / max_value
89
+ end
90
+ end
91
+ end
@@ -155,6 +155,7 @@ module Puma
155
155
  out_of_band: [],
156
156
  # Number of seconds for another request within a persistent session.
157
157
  persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
158
+ prune_bundler: false,
158
159
  queue_requests: true,
159
160
  rackup: 'config.ru'.freeze,
160
161
  raise_exception_on_sigterm: true,
data/lib/puma/const.rb CHANGED
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "7.0.3"
104
- CODE_NAME = "Romantic Warrior"
103
+ PUMA_VERSION = VERSION = "7.1.0"
104
+ CODE_NAME = "Neon Witch"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
data/lib/puma/dsl.rb CHANGED
@@ -656,7 +656,8 @@ module Puma
656
656
  @options[:state] = path.to_s
657
657
  end
658
658
 
659
- # Use +permission+ to restrict permissions for the state file.
659
+ # Use +permission+ to restrict permissions for the state file. By convention,
660
+ # +permission+ is an octal number (e.g. `0640` or `0o640`).
660
661
  #
661
662
  # @example
662
663
  # state_permission 0600
@@ -818,6 +819,20 @@ module Puma
818
819
 
819
820
  alias_method :after_worker_boot, :after_worker_fork
820
821
 
822
+ # Code to run in the master right after a worker has stopped. The worker's
823
+ # index and Process::Status are passed as arguments.
824
+ #
825
+ # @note Cluster mode only.
826
+ #
827
+ # @example
828
+ # after_worker_shutdown do |worker_handle|
829
+ # puts 'Worker crashed' unless worker_handle.process_status.success?
830
+ # end
831
+ #
832
+ def after_worker_shutdown(&block)
833
+ process_hook :after_worker_shutdown, nil, block, cluster_only: true
834
+ end
835
+
821
836
  # Code to run after puma is booted (works for both single and cluster modes).
822
837
  #
823
838
  # @example
@@ -1207,13 +1222,19 @@ module Puma
1207
1222
  end
1208
1223
 
1209
1224
 
1210
- # Attempts to route traffic to less-busy workers by causing them to delay
1211
- # listening on the socket, allowing workers which are not processing any
1225
+ # Maximum delay of worker accept loop.
1226
+ #
1227
+ # Attempts to route traffic to less-busy workers by causing a busy worker to delay
1228
+ # listening on the socket, allowing workers which are not processing as many
1212
1229
  # requests to pick up new requests first.
1213
1230
  #
1214
1231
  # The default is 0.005 seconds.
1215
1232
  #
1216
- # Only works on MRI. For all other interpreters, this setting does nothing.
1233
+ # To turn off this feature, set the value to 0.
1234
+ #
1235
+ # @note Cluster mode with >= 2 workers only.
1236
+ #
1237
+ # @note Interpreters with forking support only.
1217
1238
  #
1218
1239
  # @see Puma::Server#handle_servers
1219
1240
  # @see Puma::ThreadPool#wait_for_less_busy_worker
data/lib/puma/launcher.rb CHANGED
@@ -42,26 +42,39 @@ module Puma
42
42
  # end
43
43
  # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
44
44
  def initialize(conf, launcher_args={})
45
- @runner = nil
46
- @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
47
- @events = launcher_args[:events] || Events.new
48
- @argv = launcher_args[:argv] || []
49
- @original_argv = @argv.dup
50
- @config = conf
51
-
52
- env = launcher_args.delete(:env) || ENV
45
+ ## Minimal initialization for a potential early restart (e.g. when pruning bundle)
53
46
 
47
+ @config = conf
54
48
  @config.clamp
49
+
55
50
  @options = @config.options
56
51
 
52
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
53
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
54
+ @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
55
+ @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
57
56
  @options[:log_writer] = @log_writer
58
57
  @options[:logger] = @log_writer if clustered?
59
58
 
59
+ @events = launcher_args[:events] || Events.new
60
+
61
+ @argv = launcher_args[:argv] || []
62
+ @original_argv = @argv.dup
63
+
64
+ ## End minimal initialization
65
+
66
+ generate_restart_data
67
+ Dir.chdir(@restart_dir)
68
+
69
+ prune_bundler!
70
+
71
+ env = launcher_args.delete(:env) || ENV
72
+
60
73
  # Advertise the Configuration
61
74
  Puma.cli_config = @config if defined?(Puma.cli_config)
62
75
  log_config if env['PUMA_LOG_CONFIG']
63
76
 
64
- @binder = Binder.new(@log_writer, @options)
77
+ @binder = Binder.new(@log_writer, @options)
65
78
  @binder.create_inherited_fds(env).each { |k| env.delete k }
66
79
  @binder.create_activated_fds(env).each { |k| env.delete k }
67
80
 
@@ -81,21 +94,10 @@ module Puma
81
94
  )
82
95
  end
83
96
 
84
- @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
85
- @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
86
-
87
- @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
88
-
89
- generate_restart_data
90
-
91
97
  if clustered? && !Puma.forkable?
92
98
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
93
99
  end
94
100
 
95
- Dir.chdir(@restart_dir)
96
-
97
- prune_bundler!
98
-
99
101
  @environment = @options[:environment] if @options[:environment]
100
102
  set_rack_environment
101
103
 
@@ -139,7 +141,10 @@ module Puma
139
141
  # Delete the configured pidfile
140
142
  def delete_pidfile
141
143
  path = @options[:pidfile]
142
- File.unlink(path) if path && File.exist?(path)
144
+ begin
145
+ File.unlink(path) if path
146
+ rescue Errno::ENOENT
147
+ end
143
148
  end
144
149
 
145
150
  # Begin async shutdown of the server
@@ -381,9 +386,9 @@ module Puma
381
386
  # using it.
382
387
  @restart_dir = Dir.pwd
383
388
 
384
- # Use the same trick as unicorn, namely favor PWD because
385
- # it will contain an unresolved symlink, useful for when
386
- # the pwd is /data/releases/current.
389
+ # Use the same trick as unicorn, namely favor PWD because
390
+ # it will contain an unresolved symlink, useful for when
391
+ # the pwd is /data/releases/current.
387
392
  elsif dir = ENV['PWD']
388
393
  s_env = File.stat(dir)
389
394
  s_pwd = File.stat(Dir.pwd)
Binary file
data/lib/puma/server.rb CHANGED
@@ -13,14 +13,12 @@ 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
19
20
 
20
21
  module Puma
21
- # Add `Thread#puma_server` and `Thread#puma_server=`
22
- Thread.attr_accessor(:puma_server)
23
-
24
22
  # The HTTP Server itself. Serves out a single Rack app.
25
23
  #
26
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -58,7 +56,6 @@ module Puma
58
56
  attr_accessor :app
59
57
  attr_accessor :binder
60
58
 
61
-
62
59
  # Create a server for the rack app +app+.
63
60
  #
64
61
  # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
@@ -110,6 +107,10 @@ module Puma
110
107
  @enable_keep_alives &&= @queue_requests
111
108
  @io_selector_backend = @options[:io_selector_backend]
112
109
  @http_content_length_limit = @options[:http_content_length_limit]
110
+ @cluster_accept_loop_delay = ClusterAcceptLoopDelay.new(
111
+ workers: @options[:workers],
112
+ max_delay: @options[:wait_for_less_busy_worker] || 0 # Real default is in Configuration::DEFAULTS, this is for unit testing
113
+ )
113
114
 
114
115
  if @options[:fiber_per_request]
115
116
  singleton_class.prepend(FiberPerRequest)
@@ -245,11 +246,6 @@ module Puma
245
246
  @thread_pool&.pool_capacity
246
247
  end
247
248
 
248
- # @!attribute [r] busy_threads
249
- def busy_threads
250
- @thread_pool&.busy_threads
251
- end
252
-
253
249
  # Runs the server.
254
250
  #
255
251
  # If +background+ is true (the default) then a thread is spun
@@ -263,10 +259,14 @@ module Puma
263
259
 
264
260
  @status = :run
265
261
 
266
- @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) { |client| process_client client }
267
263
 
268
264
  if @queue_requests
269
- @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
265
+ @reactor = Reactor.new(@io_selector_backend) { |c|
266
+ # Inversion of control, the reactor is calling a method on the server when it
267
+ # is done buffering a request or receives a new request from a keepalive connection.
268
+ self.reactor_wakeup(c)
269
+ }
270
270
  @reactor.run
271
271
  end
272
272
 
@@ -291,6 +291,9 @@ module Puma
291
291
  # This method is called from the Reactor thread when a queued Client receives data,
292
292
  # times out, or when the Reactor is shutting down.
293
293
  #
294
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
295
+ # from the server.
296
+ #
294
297
  # It is responsible for ensuring that a request has been completely received
295
298
  # before it starts to be processed by the ThreadPool. This may be known as read buffering.
296
299
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
@@ -325,7 +328,7 @@ module Puma
325
328
  end
326
329
  rescue StandardError => e
327
330
  client_error(e, client)
328
- client.close
331
+ close_client_safely(client)
329
332
  true
330
333
  end
331
334
 
@@ -339,7 +342,6 @@ module Puma
339
342
  pool = @thread_pool
340
343
  queue_requests = @queue_requests
341
344
  drain = options[:drain_on_shutdown] ? 0 : nil
342
- max_flt = @max_threads.to_f
343
345
 
344
346
  addr_send_name, addr_value = case options[:remote_address]
345
347
  when :value
@@ -384,15 +386,13 @@ module Puma
384
386
  # clients until the code is finished.
385
387
  pool.wait_while_out_of_band_running
386
388
 
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
389
+ # A well rested herd (cluster) runs faster
390
+ if @cluster_accept_loop_delay.on? && (busy_threads_plus_todo = pool.busy_threads) > 0
391
+ delay = @cluster_accept_loop_delay.calculate(
392
+ max_threads: @max_threads,
393
+ busy_threads_plus_todo: busy_threads_plus_todo
394
+ )
395
+ sleep(delay)
396
396
  end
397
397
 
398
398
  io = begin
@@ -401,11 +401,9 @@ module Puma
401
401
  next
402
402
  end
403
403
  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
- }
404
+ client = new_client(io, sock)
405
+ client.send(addr_send_name, addr_value) if addr_value
406
+ pool << client
409
407
  end
410
408
  end
411
409
  rescue IOError, Errno::EBADF
@@ -442,6 +440,14 @@ module Puma
442
440
  @events.fire :state, :done
443
441
  end
444
442
 
443
+ # :nodoc:
444
+ def new_client(io, sock)
445
+ client = Client.new(io, @binder.env(sock))
446
+ client.listener = sock
447
+ client.http_content_length_limit = @http_content_length_limit
448
+ client
449
+ end
450
+
445
451
  # :nodoc:
446
452
  def handle_check
447
453
  cmd = @check.read(1)
@@ -472,9 +478,6 @@ module Puma
472
478
  #
473
479
  # Return true if one or more requests were processed.
474
480
  def process_client(client)
475
- # Advertise this server into the thread
476
- Thread.current.puma_server = self
477
-
478
481
  close_socket = true
479
482
 
480
483
  requests = 0
@@ -493,31 +496,41 @@ module Puma
493
496
  client.finish(@first_data_timeout)
494
497
  end
495
498
 
496
- @requests_count += 1
497
- case handle_request(client, requests + 1)
498
- when false
499
- when :async
500
- close_socket = false
501
- when true
502
- requests += 1
499
+ can_loop = true
500
+ while can_loop
501
+ can_loop = false
502
+ @requests_count += 1
503
+ case handle_request(client, requests + 1)
504
+ when false
505
+ when :async
506
+ close_socket = false
507
+ when true
508
+ requests += 1
503
509
 
504
- client.reset
510
+ client.reset
505
511
 
506
- # This indicates data exists in the client read buffer and there may be
507
- # additional requests on it, so process them
508
- next_request_ready = if client.has_back_to_back_requests?
509
- with_force_shutdown(client) { client.process_back_to_back_requests }
510
- else
511
- with_force_shutdown(client) { client.eagerly_finish }
512
- end
512
+ # This indicates data exists in the client read buffer and there may be
513
+ # additional requests on it, so process them
514
+ next_request_ready = if client.has_back_to_back_requests?
515
+ with_force_shutdown(client) { client.process_back_to_back_requests }
516
+ else
517
+ with_force_shutdown(client) { client.eagerly_finish }
518
+ end
513
519
 
514
- if next_request_ready
515
- @thread_pool << client
516
- close_socket = false
517
- elsif @queue_requests
518
- client.set_timeout @persistent_timeout
519
- if @reactor.add client
520
- close_socket = false
520
+ if next_request_ready
521
+ # When Puma has spare threads, allow this one to be monopolized
522
+ # Perf optimization for https://github.com/puma/puma/issues/3788
523
+ if @thread_pool.waiting > 0
524
+ can_loop = true
525
+ else
526
+ @thread_pool << client
527
+ close_socket = false
528
+ end
529
+ elsif @queue_requests
530
+ client.set_timeout @persistent_timeout
531
+ if @reactor.add client
532
+ close_socket = false
533
+ end
521
534
  end
522
535
  end
523
536
  end
@@ -529,16 +542,21 @@ module Puma
529
542
  ensure
530
543
  client.io_buffer.reset
531
544
 
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
545
+ close_client_safely(client) if close_socket
539
546
  end
540
547
  end
541
548
 
549
+ # :nodoc:
550
+ def close_client_safely(client)
551
+ client.close
552
+ rescue IOError, SystemCallError
553
+ # Already closed
554
+ rescue MiniSSL::SSLError => e
555
+ @log_writer.ssl_error e, client.io
556
+ rescue StandardError => e
557
+ @log_writer.unknown_error e, nil, "Client"
558
+ end
559
+
542
560
  # Triggers a client timeout if the thread-pool shuts down
543
561
  # during execution of the provided block.
544
562
  def with_force_shutdown(client, &block)
@@ -692,7 +710,7 @@ module Puma
692
710
 
693
711
  def reset_max
694
712
  @reactor.reactor_max = 0 if @reactor
695
- @thread_pool.reset_max
713
+ @thread_pool&.reset_max
696
714
  end
697
715
 
698
716
  # below are 'delegations' to binder
@@ -32,10 +32,11 @@ module Puma
32
32
  "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
33
  end
34
34
  end
35
+
35
36
  if permission
36
- File.write path, contents, mode: 'wb:UTF-8'
37
- else
38
37
  File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
39
40
  end
40
41
  end
41
42
 
@@ -5,6 +5,10 @@ require 'thread'
5
5
  require_relative 'io_buffer'
6
6
 
7
7
  module Puma
8
+
9
+ # Add `Thread#puma_server` and `Thread#puma_server=`
10
+ Thread.attr_accessor(:puma_server)
11
+
8
12
  # Internal Docs for A simple thread pool management object.
9
13
  #
10
14
  # Each Puma "worker" has a thread pool to process requests.
@@ -33,7 +37,9 @@ module Puma
33
37
  # The block passed is the work that will be performed in each
34
38
  # thread.
35
39
  #
36
- def initialize(name, options = {}, &block)
40
+ def initialize(name, options = {}, server: nil, &block)
41
+ @server = server
42
+
37
43
  @not_empty = ConditionVariable.new
38
44
  @not_full = ConditionVariable.new
39
45
  @mutex = Mutex.new
@@ -134,6 +140,9 @@ module Puma
134
140
  trigger_before_thread_start_hooks
135
141
  th = Thread.new(@spawned) do |spawned|
136
142
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
143
+ # Advertise server into the thread
144
+ Thread.current.puma_server = @server
145
+
137
146
  todo = @todo
138
147
  block = @block
139
148
  mutex = @mutex
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.3
4
+ version: 7.1.0
5
5
  platform: java
6
6
  authors:
7
7
  - Evan Phoenix
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-14 00:00:00.000000000 Z
10
+ date: 2025-10-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,7 @@ files:
86
86
  - lib/puma/cluster.rb
87
87
  - lib/puma/cluster/worker.rb
88
88
  - lib/puma/cluster/worker_handle.rb
89
+ - lib/puma/cluster_accept_loop_delay.rb
89
90
  - lib/puma/commonlogger.rb
90
91
  - lib/puma/configuration.rb
91
92
  - lib/puma/const.rb
@@ -131,6 +132,7 @@ metadata:
131
132
  homepage_uri: https://puma.io
132
133
  source_code_uri: https://github.com/puma/puma
133
134
  rubygems_mfa_required: 'true'
135
+ msys2_mingw_dependencies: openssl
134
136
  rdoc_options: []
135
137
  require_paths:
136
138
  - lib