puma 7.0.4 → 8.0.2

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +162 -0
  3. data/README.md +25 -13
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/grpc.md +62 -0
  10. data/docs/images/favicon.svg +1 -0
  11. data/docs/images/running-puma.svg +1 -0
  12. data/docs/images/standard-logo.svg +1 -0
  13. data/docs/jungle/README.md +1 -1
  14. data/docs/kubernetes.md +5 -12
  15. data/docs/plugins.md +2 -2
  16. data/docs/signals.md +10 -10
  17. data/docs/stats.md +2 -2
  18. data/docs/systemd.md +3 -3
  19. data/ext/puma_http11/http11_parser.java.rl +51 -65
  20. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  21. data/ext/puma_http11/org/jruby/puma/Http11.java +168 -104
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  23. data/ext/puma_http11/puma_http11.c +101 -109
  24. data/lib/puma/app/status.rb +10 -2
  25. data/lib/puma/binder.rb +2 -2
  26. data/lib/puma/cli.rb +1 -1
  27. data/lib/puma/client.rb +138 -102
  28. data/lib/puma/client_env.rb +171 -0
  29. data/lib/puma/cluster/worker.rb +10 -9
  30. data/lib/puma/cluster/worker_handle.rb +2 -2
  31. data/lib/puma/cluster.rb +12 -11
  32. data/lib/puma/cluster_accept_loop_delay.rb +17 -18
  33. data/lib/puma/configuration.rb +87 -17
  34. data/lib/puma/const.rb +4 -3
  35. data/lib/puma/control_cli.rb +1 -1
  36. data/lib/puma/detect.rb +11 -0
  37. data/lib/puma/dsl.rb +115 -18
  38. data/lib/puma/launcher/bundle_pruner.rb +2 -4
  39. data/lib/puma/launcher.rb +35 -30
  40. data/lib/puma/log_writer.rb +8 -2
  41. data/lib/puma/reactor.rb +3 -12
  42. data/lib/puma/{request.rb → response.rb} +25 -194
  43. data/lib/puma/runner.rb +1 -1
  44. data/lib/puma/server.rb +103 -64
  45. data/lib/puma/server_plugin_control.rb +32 -0
  46. data/lib/puma/single.rb +2 -2
  47. data/lib/puma/state_file.rb +3 -2
  48. data/lib/puma/thread_pool.rb +139 -24
  49. data/lib/rack/handler/puma.rb +1 -1
  50. data/tools/Dockerfile +13 -5
  51. metadata +16 -5
  52. data/ext/puma_http11/ext_help.h +0 -15
@@ -12,7 +12,7 @@ module Puma
12
12
  # #handle_request, which is called in Server#process_client.
13
13
  # @version 5.0.3
14
14
  #
15
- module Request # :nodoc:
15
+ module Response # :nodoc:
16
16
 
17
17
  # Single element array body: smaller bodies are written to io_buffer first,
18
18
  # then a single write from io_buffer. Larger sizes are written separately.
@@ -36,43 +36,28 @@ module Puma
36
36
  # Takes the request contained in +client+, invokes the Rack application to construct
37
37
  # the response and writes it back to +client.io+.
38
38
  #
39
- # It'll return +false+ when the connection is closed, this doesn't mean
39
+ # It'll return +:close+ when the connection is closed, this doesn't mean
40
40
  # that the response wasn't successful.
41
41
  #
42
+ # It'll return +:keep_alive+ if the connection is a pipeline or keep-alive connection.
43
+ # Which may contain additional requests.
44
+ #
42
45
  # It'll return +:async+ if the connection remains open but will be handled
43
46
  # elsewhere, i.e. the connection has been hijacked by the Rack application.
44
47
  #
45
48
  # Finally, it'll return +true+ on keep-alive connections.
49
+ # @param processor [Puma::ThreadPool::ProcessorThread]
46
50
  # @param client [Puma::Client]
47
51
  # @param requests [Integer]
48
- # @return [Boolean,:async]
49
- #
50
- def handle_request(client, requests)
52
+ # @return [:close, :keep_alive, :async]
53
+ def handle_request(processor, client, requests)
51
54
  env = client.env
52
55
  io_buffer = client.io_buffer
53
56
  socket = client.io # io may be a MiniSSL::Socket
54
57
  app_body = nil
55
58
  error = nil
56
59
 
57
- return false if closed_socket?(socket)
58
-
59
- if client.http_content_length_limit_exceeded
60
- return prepare_response(413, {}, ["Payload Too Large"], requests, client)
61
- end
62
-
63
- normalize_env env, client
64
-
65
- env[PUMA_SOCKET] = socket
66
-
67
- if env[HTTPS_KEY] && socket.peercert
68
- env[PUMA_PEERCERT] = socket.peercert
69
- end
70
-
71
- env[HIJACK_P] = true
72
- env[HIJACK] = client.method :full_hijack
73
-
74
- env[RACK_INPUT] = client.body
75
- env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
60
+ return :close if closed_socket?(socket)
76
61
 
77
62
  if @early_hints
78
63
  env[EARLY_HINTS] = lambda { |headers|
@@ -87,22 +72,11 @@ module Puma
87
72
  }
88
73
  end
89
74
 
90
- req_env_post_parse env
91
-
92
- # A rack extension. If the app writes #call'ables to this
93
- # array, we will invoke them when the request is done.
94
- #
95
- env[RACK_AFTER_REPLY] ||= []
96
- env[RACK_RESPONSE_FINISHED] ||= []
75
+ env["puma.mark_as_io_bound"] = -> { processor.mark_as_io_thread! }
97
76
 
98
77
  begin
99
- if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
100
- status, headers, app_body = @thread_pool.with_force_shutdown do
101
- @app.call(env)
102
- end
103
- else
104
- @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
105
- status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
78
+ status, headers, app_body = @thread_pool.with_force_shutdown do
79
+ @app.call(env)
106
80
  end
107
81
 
108
82
  # app_body needs to always be closed, hold value in case lowlevel_error
@@ -134,7 +108,6 @@ module Puma
134
108
  prepare_response(status, headers, res_body, requests, client)
135
109
  ensure
136
110
  io_buffer.reset
137
- uncork_socket client.io
138
111
  app_body.close if app_body.respond_to? :close
139
112
  client&.tempfile_close
140
113
  if after_reply = env[RACK_AFTER_REPLY]
@@ -167,13 +140,13 @@ module Puma
167
140
  # a call to `Server#lowlevel_error`
168
141
  # @param requests [Integer] number of inline requests handled
169
142
  # @param client [Puma::Client]
170
- # @return [Boolean,:async] keep-alive status or `:async`
143
+ # @return [:close, :keep_alive, :async]
171
144
  def prepare_response(status, headers, res_body, requests, client)
172
145
  env = client.env
173
146
  socket = client.io
174
147
  io_buffer = client.io_buffer
175
148
 
176
- return false if closed_socket?(socket)
149
+ return :close if closed_socket?(socket)
177
150
 
178
151
  # Close the connection after a reasonable number of inline requests
179
152
  force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
@@ -243,8 +216,9 @@ module Puma
243
216
 
244
217
  io_buffer << LINE_END
245
218
  fast_write_str socket, io_buffer.read_and_reset
246
- socket.flush
247
- return keep_alive
219
+
220
+ uncork_socket socket.flush
221
+ return keep_alive ? :keep_alive : :close
248
222
  end
249
223
  else
250
224
  if content_length
@@ -262,32 +236,16 @@ module Puma
262
236
  # response_hijack.call
263
237
  if response_hijack
264
238
  fast_write_str socket, io_buffer.read_and_reset
265
- uncork_socket socket
239
+ uncork_socket socket.flush
266
240
  response_hijack.call socket
267
241
  return :async
268
242
  end
269
243
 
270
244
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
271
245
  body.close if close_body
272
- # if we're shutting down, close keep_alive connections
273
- !shutting_down? && keep_alive
274
- end
275
-
276
- HTTP_ON_VALUES = { "on" => true, HTTPS => true }
277
- private_constant :HTTP_ON_VALUES
278
246
 
279
- # @param env [Hash] see Puma::Client#env, from request
280
- # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
281
- #
282
- def default_server_port(env)
283
- if HTTP_ON_VALUES[env[HTTPS_KEY]] ||
284
- env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
285
- env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
286
- env[HTTP_X_FORWARDED_SSL] == "on"
287
- PORT_443
288
- else
289
- PORT_80
290
- end
247
+ # if we're shutting down, close keep_alive connections
248
+ !shutting_down? && keep_alive ? :keep_alive : :close
291
249
  end
292
250
 
293
251
  # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
@@ -404,6 +362,7 @@ module Puma
404
362
  end
405
363
  end
406
364
  socket.flush
365
+ uncork_socket socket
407
366
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
408
367
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
409
368
  rescue Errno::EPIPE, SystemCallError, IOError
@@ -412,85 +371,6 @@ module Puma
412
371
 
413
372
  private :fast_write_str, :fast_write_response
414
373
 
415
- # Given a Hash +env+ for the request read from +client+, add
416
- # and fixup keys to comply with Rack's env guidelines.
417
- # @param env [Hash] see Puma::Client#env, from request
418
- # @param client [Puma::Client] only needed for Client#peerip
419
- #
420
- def normalize_env(env, client)
421
- if host = env[HTTP_HOST]
422
- # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
423
- if colon = host.rindex("]:") # IPV6 with port
424
- env[SERVER_NAME] = host[0, colon+1]
425
- env[SERVER_PORT] = host[colon+2, host.bytesize]
426
- elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
427
- env[SERVER_NAME] = host[0, colon]
428
- env[SERVER_PORT] = host[colon+1, host.bytesize]
429
- else
430
- env[SERVER_NAME] = host
431
- env[SERVER_PORT] = default_server_port(env)
432
- end
433
- else
434
- env[SERVER_NAME] = LOCALHOST
435
- env[SERVER_PORT] = default_server_port(env)
436
- end
437
-
438
- unless env[REQUEST_PATH]
439
- # it might be a dumbass full host request header
440
- uri = begin
441
- URI.parse(env[REQUEST_URI])
442
- rescue URI::InvalidURIError
443
- raise Puma::HttpParserError
444
- end
445
- env[REQUEST_PATH] = uri.path
446
-
447
- # A nil env value will cause a LintError (and fatal errors elsewhere),
448
- # so only set the env value if there actually is a value.
449
- env[QUERY_STRING] = uri.query if uri.query
450
- end
451
-
452
- env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
453
-
454
- # From https://www.ietf.org/rfc/rfc3875 :
455
- # "Script authors should be aware that the REMOTE_ADDR and
456
- # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
457
- # may not identify the ultimate source of the request.
458
- # They identify the client for the immediate request to the
459
- # server; that client may be a proxy, gateway, or other
460
- # intermediary acting on behalf of the actual source client."
461
- #
462
-
463
- unless env.key?(REMOTE_ADDR)
464
- begin
465
- addr = client.peerip
466
- rescue Errno::ENOTCONN
467
- # Client disconnects can result in an inability to get the
468
- # peeraddr from the socket; default to unspec.
469
- if client.peer_family == Socket::AF_INET6
470
- addr = UNSPECIFIED_IPV6
471
- else
472
- addr = UNSPECIFIED_IPV4
473
- end
474
- end
475
-
476
- # Set unix socket addrs to localhost
477
- if addr.empty?
478
- if client.peer_family == Socket::AF_INET6
479
- addr = LOCALHOST_IPV6
480
- else
481
- addr = LOCALHOST_IPV4
482
- end
483
- end
484
-
485
- env[REMOTE_ADDR] = addr
486
- end
487
-
488
- # The legacy HTTP_VERSION header can be sent as a client header.
489
- # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
490
- env[HTTP_VERSION] = env[SERVER_PROTOCOL] if @env_set_http_version
491
- end
492
- private :normalize_env
493
-
494
374
  # @param header_key [#to_s]
495
375
  # @return [Boolean]
496
376
  #
@@ -504,56 +384,6 @@ module Puma
504
384
  def illegal_header_value?(header_value)
505
385
  !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
506
386
  end
507
- private :illegal_header_key?, :illegal_header_value?
508
-
509
- # Fixup any headers with `,` in the name to have `_` now. We emit
510
- # headers with `,` in them during the parse phase to avoid ambiguity
511
- # with the `-` to `_` conversion for critical headers. But here for
512
- # compatibility, we'll convert them back. This code is written to
513
- # avoid allocation in the common case (ie there are no headers
514
- # with `,` in their names), that's why it has the extra conditionals.
515
- #
516
- # @note If a normalized version of a `,` header already exists, we ignore
517
- # the `,` version. This prevents clobbering headers managed by proxies
518
- # but not by clients (Like X-Forwarded-For).
519
- #
520
- # @param env [Hash] see Puma::Client#env, from request, modifies in place
521
- # @version 5.0.3
522
- #
523
- def req_env_post_parse(env)
524
- to_delete = nil
525
- to_add = nil
526
-
527
- env.each do |k,v|
528
- if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
529
- if to_delete
530
- to_delete << k
531
- else
532
- to_delete = [k]
533
- end
534
-
535
- new_k = k.tr(",", "_")
536
- if env.key?(new_k)
537
- next
538
- end
539
-
540
- unless to_add
541
- to_add = {}
542
- end
543
-
544
- to_add[new_k] = v
545
- end
546
- end
547
-
548
- if to_delete # rubocop:disable Style/SafeNavigation
549
- to_delete.each { |k| env.delete(k) }
550
- end
551
-
552
- if to_add
553
- env.merge! to_add
554
- end
555
- end
556
- private :req_env_post_parse
557
387
 
558
388
  # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
559
389
  # @param headers [Hash] the headers returned by the Rack application
@@ -650,7 +480,8 @@ module Puma
650
480
  headers.each do |k, vs|
651
481
  next if illegal_header_key?(k)
652
482
 
653
- case k.downcase
483
+ key = k.downcase
484
+ case key
654
485
  when CONTENT_LENGTH2
655
486
  next if illegal_header_value?(vs)
656
487
  # nil.to_i is 0, nil&.to_i is nil
@@ -677,10 +508,10 @@ module Puma
677
508
  if ary
678
509
  ary.each do |v|
679
510
  next if illegal_header_value?(v)
680
- io_buffer.append k.downcase, colon, v, line_ending
511
+ io_buffer.append key, colon, v, line_ending
681
512
  end
682
513
  else
683
- io_buffer.append k.downcase, colon, line_ending
514
+ io_buffer.append key, colon, line_ending
684
515
  end
685
516
  end
686
517
 
data/lib/puma/runner.rb CHANGED
@@ -70,7 +70,7 @@ module Puma
70
70
  token = nil if token.empty? || token == 'none'
71
71
  end
72
72
 
73
- app = Puma::App::Status.new @launcher, token
73
+ app = Puma::App::Status.new @launcher, token: token, data_only: @options[:control_data_only]
74
74
 
75
75
  # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
76
76
  # Use `nil` for events, no hooks in control server
data/lib/puma/server.rb CHANGED
@@ -11,7 +11,7 @@ require_relative 'reactor'
11
11
  require_relative 'client'
12
12
  require_relative 'binder'
13
13
  require_relative 'util'
14
- require_relative 'request'
14
+ require_relative 'response'
15
15
  require_relative 'configuration'
16
16
  require_relative 'cluster_accept_loop_delay'
17
17
 
@@ -19,9 +19,6 @@ require 'socket'
19
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
20
20
 
21
21
  module Puma
22
- # Add `Thread#puma_server` and `Thread#puma_server=`
23
- Thread.attr_accessor(:puma_server)
24
-
25
22
  # The HTTP Server itself. Serves out a single Rack app.
26
23
  #
27
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -34,15 +31,15 @@ module Puma
34
31
  # Each `Puma::Server` will have one reactor and one thread pool.
35
32
  class Server
36
33
  module FiberPerRequest
37
- def handle_request(client, requests)
34
+ def handle_request(processor, client, requests)
38
35
  Fiber.new do
39
36
  super
40
37
  end.resume
41
38
  end
42
39
  end
43
40
 
44
- include Puma::Const
45
- include Request
41
+ include Const
42
+ include Response
46
43
 
47
44
  attr_reader :options
48
45
  attr_reader :thread
@@ -262,7 +259,9 @@ module Puma
262
259
 
263
260
  @status = :run
264
261
 
265
- @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) do |processor, client|
263
+ process_client(processor, client)
264
+ end
266
265
 
267
266
  if @queue_requests
268
267
  @reactor = Reactor.new(@io_selector_backend) { |c|
@@ -302,12 +301,12 @@ module Puma
302
301
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
303
302
  # such as nginx) then the application would be subject to a slow client attack.
304
303
  #
305
- # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
304
+ # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/main/docs/architecture.md).
306
305
  #
307
306
  # The method checks to see if it has the full header and body with
308
307
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
309
308
  # then the request is passed to the ThreadPool (`@thread_pool << client`)
310
- # so that a "worker thread" can pick up the request and begin to execute application logic.
309
+ # so that a "processor thread" can pick up the request and begin to execute application logic.
311
310
  # The Client is then removed from the reactor (return `true`).
312
311
  #
313
312
  # If a client object times out, a 408 response is written, its connection is closed,
@@ -404,6 +403,7 @@ module Puma
404
403
  next
405
404
  end
406
405
  drain += 1 if shutting_down?
406
+
407
407
  client = new_client(io, sock)
408
408
  client.send(addr_send_name, addr_value) if addr_value
409
409
  pool << client
@@ -417,7 +417,7 @@ module Puma
417
417
  end
418
418
  end
419
419
 
420
- @log_writer.debug "Drained #{drain} additional connections." if drain
420
+ @log_writer.debug { "Drained #{drain} additional connections." } if drain
421
421
  @events.fire :state, @status
422
422
 
423
423
  if queue_requests
@@ -447,7 +447,9 @@ module Puma
447
447
  def new_client(io, sock)
448
448
  client = Client.new(io, @binder.env(sock))
449
449
  client.listener = sock
450
+ client.env_set_http_version = @env_set_http_version
450
451
  client.http_content_length_limit = @http_content_length_limit
452
+ client.supported_http_methods = @supported_http_methods
451
453
  client
452
454
  end
453
455
 
@@ -473,17 +475,14 @@ module Puma
473
475
  # Given a connection on +client+, handle the incoming requests,
474
476
  # or queue the connection in the Reactor if no request is available.
475
477
  #
476
- # This method is called from a ThreadPool worker thread.
478
+ # This method is called from a ThreadPool processor thread.
477
479
  #
478
480
  # This method supports HTTP Keep-Alive so it may, depending on if the client
479
481
  # indicates that it supports keep alive, wait for another request before
480
482
  # returning.
481
483
  #
482
484
  # Return true if one or more requests were processed.
483
- def process_client(client)
484
- # Advertise this server into the thread
485
- Thread.current.puma_server = self
486
-
485
+ def process_client(processor, client)
487
486
  close_socket = true
488
487
 
489
488
  requests = 0
@@ -502,31 +501,41 @@ module Puma
502
501
  client.finish(@first_data_timeout)
503
502
  end
504
503
 
505
- @requests_count += 1
506
- case handle_request(client, requests + 1)
507
- when false
508
- when :async
509
- close_socket = false
510
- when true
511
- requests += 1
504
+ can_loop = true
505
+ while can_loop
506
+ can_loop = false
507
+ @requests_count += 1
508
+ case handle_request(processor, client, requests + 1)
509
+ when :close
510
+ when :async
511
+ close_socket = false
512
+ when :keep_alive
513
+ requests += 1
512
514
 
513
- client.reset
515
+ client.reset
514
516
 
515
- # This indicates data exists in the client read buffer and there may be
516
- # additional requests on it, so process them
517
- next_request_ready = if client.has_back_to_back_requests?
518
- with_force_shutdown(client) { client.process_back_to_back_requests }
519
- else
520
- with_force_shutdown(client) { client.eagerly_finish }
521
- end
517
+ # This indicates data exists in the client read buffer and there may be
518
+ # additional requests on it, so process them
519
+ next_request_ready = if client.has_back_to_back_requests?
520
+ with_force_shutdown(client) { client.process_back_to_back_requests }
521
+ else
522
+ with_force_shutdown(client) { client.eagerly_finish }
523
+ end
522
524
 
523
- if next_request_ready
524
- @thread_pool << client
525
- close_socket = false
526
- elsif @queue_requests
527
- client.set_timeout @persistent_timeout
528
- if @reactor.add client
529
- close_socket = false
525
+ if next_request_ready
526
+ # When Puma has spare threads, allow this one to be monopolized
527
+ # Perf optimization for https://github.com/puma/puma/issues/3788
528
+ if @thread_pool.waiting > 0
529
+ can_loop = true
530
+ else
531
+ @thread_pool << client
532
+ close_socket = false
533
+ end
534
+ elsif @queue_requests
535
+ client.set_timeout @persistent_timeout
536
+ if @reactor.add client
537
+ close_socket = false
538
+ end
530
539
  end
531
540
  end
532
541
  end
@@ -573,7 +582,7 @@ module Puma
573
582
  lowlevel_error(e, client.env)
574
583
  @log_writer.ssl_error e, client.io
575
584
  when HttpParserError
576
- response_to_error(client, requests, e, 400)
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
577
586
  @log_writer.parse_error e, client
578
587
  when HttpParserError501
579
588
  response_to_error(client, requests, e, 501)
@@ -606,7 +615,15 @@ module Puma
606
615
  end
607
616
 
608
617
  def response_to_error(client, requests, err, status_code)
609
- status, headers, res_body = lowlevel_error(err, client.env, status_code)
618
+ # @todo remove sometime later
619
+ if status_code == 413
620
+ status = 413
621
+ res_body = ["Payload Too Large"]
622
+ headers = {}
623
+ headers[CONTENT_LENGTH2] = 17
624
+ else
625
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
626
+ end
610
627
  prepare_response(status, headers, res_body, requests, client)
611
628
  end
612
629
  private :response_to_error
@@ -614,32 +631,11 @@ module Puma
614
631
  # Wait for all outstanding requests to finish.
615
632
  #
616
633
  def graceful_shutdown
617
- if options[:shutdown_debug]
618
- threads = Thread.list
619
- total = threads.size
620
-
621
- pid = Process.pid
622
-
623
- $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
624
-
625
- threads.each_with_index do |t,i|
626
- $stdout.syswrite "#{pid}: Thread #{i+1}/#{total}: #{t.inspect}\n"
627
- $stdout.syswrite "#{pid}: #{t.backtrace.join("\n#{pid}: ")}\n\n"
628
- end
629
- $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
630
- end
631
-
632
634
  if @status != :restart
633
635
  @binder.close
634
636
  end
635
637
 
636
- if @thread_pool
637
- if timeout = options[:force_shutdown_after]
638
- @thread_pool.shutdown timeout.to_f
639
- else
640
- @thread_pool.shutdown
641
- end
642
- end
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
643
639
  end
644
640
 
645
641
  def notify_safely(message)
@@ -656,7 +652,7 @@ module Puma
656
652
  end
657
653
  private :notify_safely
658
654
 
659
- # Stops the acceptor thread and then causes the worker threads to finish
655
+ # Stops the acceptor thread and then causes the processor threads to finish
660
656
  # off the request queue before finally exiting.
661
657
 
662
658
  def stop(sync=false)
@@ -706,7 +702,7 @@ module Puma
706
702
 
707
703
  def reset_max
708
704
  @reactor.reactor_max = 0 if @reactor
709
- @thread_pool.reset_max
705
+ @thread_pool&.reset_max
710
706
  end
711
707
 
712
708
  # below are 'delegations' to binder
@@ -726,6 +722,49 @@ module Puma
726
722
  @binder.add_unix_listener path, umask, mode, backlog
727
723
  end
728
724
 
725
+ # Updates the minimum and maximum number of threads in the thread pool.
726
+ #
727
+ # This method allows dynamic adjustment of the thread pool size while the server
728
+ # is running. It validates the provided values and updates both the thread pool
729
+ # and the server's thread configuration.
730
+ #
731
+ # @param min [Integer] The minimum number of threads to maintain in the pool.
732
+ # Defaults to the current minimum if not specified. Must be greater than 0
733
+ # and less than or equal to max.
734
+ # @param max [Integer] The maximum number of threads allowed in the pool.
735
+ # Defaults to the current maximum if not specified. Must be greater than or
736
+ # equal to min.
737
+ #
738
+ # @return [void]
739
+ #
740
+ # @note If validation fails, a warning message is logged and no changes are made.
741
+ #
742
+ # @example Update both min and max threads
743
+ # server.update_thread_pool_min_max(min: 2, max: 8)
744
+ #
745
+ # @example Update only the minimum threads
746
+ # server.update_thread_pool_min_max(min: 4)
747
+ #
748
+ # @example Update only the maximum threads
749
+ # server.update_thread_pool_min_max(max: 16)
750
+ #
751
+ def update_thread_pool_min_max(min: @min_threads, max: @max_threads)
752
+ if min > max
753
+ @log_writer.log "`min' value cannot be greater than `max' value."
754
+ return
755
+ end
756
+
757
+ if min < 0
758
+ @log_writer.log "`min' value cannot be less than 0"
759
+ return
760
+ end
761
+
762
+ @thread_pool&.with_mutex do
763
+ @thread_pool.min, @thread_pool.max = min, max
764
+ @min_threads, @max_threads = min, max
765
+ end
766
+ end
767
+
729
768
  # @!attribute [r] connected_ports
730
769
  def connected_ports
731
770
  @binder.connected_ports
@@ -0,0 +1,32 @@
1
+ module Puma
2
+ # ServerPluginControl provides a control interface for server plugins to
3
+ # interact with and manage server settings dynamically.
4
+ #
5
+ # This class acts as a facade between plugins and the Puma server,
6
+ # allowing plugins to safely modify server configuration and thread pool
7
+ # settings without direct access to the server's internal state.
8
+ #
9
+ class ServerPluginControl
10
+ def initialize(server)
11
+ @server = server
12
+ end
13
+
14
+ # Returns the maximum number of threads in the thread pool.
15
+ def max_threads
16
+ @server.max_threads
17
+ end
18
+
19
+ # Returns the minimum number of threads in the thread pool.
20
+ def min_threads
21
+ @server.min_threads
22
+ end
23
+
24
+ # Updates the minimum and maximum number of threads in the thread pool.
25
+ #
26
+ # @see Puma::Server#update_thread_pool_min_max
27
+ #
28
+ def update_thread_pool_min_max(min: max_threads, max: min_threads)
29
+ @server.update_thread_pool_min_max(min: min, max: max)
30
+ end
31
+ end
32
+ end
data/lib/puma/single.rb CHANGED
@@ -49,8 +49,8 @@ module Puma
49
49
 
50
50
  start_control
51
51
 
52
- @server = server = start_server
53
- server_thread = server.run
52
+ @server = start_server
53
+ server_thread = @server.run
54
54
 
55
55
  log "Use Ctrl-C to stop"
56
56
 
@@ -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