puma 7.2.0 → 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.
@@ -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.
@@ -46,10 +46,11 @@ module Puma
46
46
  # elsewhere, i.e. the connection has been hijacked by the Rack application.
47
47
  #
48
48
  # Finally, it'll return +true+ on keep-alive connections.
49
+ # @param processor [Puma::ThreadPool::ProcessorThread]
49
50
  # @param client [Puma::Client]
50
51
  # @param requests [Integer]
51
52
  # @return [:close, :keep_alive, :async]
52
- def handle_request(client, requests)
53
+ def handle_request(processor, client, requests)
53
54
  env = client.env
54
55
  io_buffer = client.io_buffer
55
56
  socket = client.io # io may be a MiniSSL::Socket
@@ -58,24 +59,6 @@ module Puma
58
59
 
59
60
  return :close if closed_socket?(socket)
60
61
 
61
- if client.http_content_length_limit_exceeded
62
- return prepare_response(413, {}, ["Payload Too Large"], requests, client)
63
- end
64
-
65
- normalize_env env, client
66
-
67
- env[PUMA_SOCKET] = socket
68
-
69
- if env[HTTPS_KEY] && socket.peercert
70
- env[PUMA_PEERCERT] = socket.peercert
71
- end
72
-
73
- env[HIJACK_P] = true
74
- env[HIJACK] = client.method :full_hijack
75
-
76
- env[RACK_INPUT] = client.body
77
- env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
78
-
79
62
  if @early_hints
80
63
  env[EARLY_HINTS] = lambda { |headers|
81
64
  begin
@@ -89,22 +72,11 @@ module Puma
89
72
  }
90
73
  end
91
74
 
92
- req_env_post_parse env
93
-
94
- # A rack extension. If the app writes #call'ables to this
95
- # array, we will invoke them when the request is done.
96
- #
97
- env[RACK_AFTER_REPLY] ||= []
98
- env[RACK_RESPONSE_FINISHED] ||= []
75
+ env["puma.mark_as_io_bound"] = -> { processor.mark_as_io_thread! }
99
76
 
100
77
  begin
101
- if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
102
- status, headers, app_body = @thread_pool.with_force_shutdown do
103
- @app.call(env)
104
- end
105
- else
106
- @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
107
- 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)
108
80
  end
109
81
 
110
82
  # app_body needs to always be closed, hold value in case lowlevel_error
@@ -136,7 +108,6 @@ module Puma
136
108
  prepare_response(status, headers, res_body, requests, client)
137
109
  ensure
138
110
  io_buffer.reset
139
- uncork_socket client.io
140
111
  app_body.close if app_body.respond_to? :close
141
112
  client&.tempfile_close
142
113
  if after_reply = env[RACK_AFTER_REPLY]
@@ -245,7 +216,8 @@ module Puma
245
216
 
246
217
  io_buffer << LINE_END
247
218
  fast_write_str socket, io_buffer.read_and_reset
248
- socket.flush
219
+
220
+ uncork_socket socket.flush
249
221
  return keep_alive ? :keep_alive : :close
250
222
  end
251
223
  else
@@ -264,34 +236,18 @@ module Puma
264
236
  # response_hijack.call
265
237
  if response_hijack
266
238
  fast_write_str socket, io_buffer.read_and_reset
267
- uncork_socket socket
239
+ uncork_socket socket.flush
268
240
  response_hijack.call socket
269
241
  return :async
270
242
  end
271
243
 
272
244
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
273
245
  body.close if close_body
246
+
274
247
  # if we're shutting down, close keep_alive connections
275
248
  !shutting_down? && keep_alive ? :keep_alive : :close
276
249
  end
277
250
 
278
- HTTP_ON_VALUES = { "on" => true, HTTPS => true }
279
- private_constant :HTTP_ON_VALUES
280
-
281
- # @param env [Hash] see Puma::Client#env, from request
282
- # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
283
- #
284
- def default_server_port(env)
285
- if HTTP_ON_VALUES[env[HTTPS_KEY]] ||
286
- env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
287
- env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
288
- env[HTTP_X_FORWARDED_SSL] == "on"
289
- PORT_443
290
- else
291
- PORT_80
292
- end
293
- end
294
-
295
251
  # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
296
252
  # and body segments (called by `fast_write_response`).
297
253
  # Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
@@ -406,6 +362,7 @@ module Puma
406
362
  end
407
363
  end
408
364
  socket.flush
365
+ uncork_socket socket
409
366
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
410
367
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
411
368
  rescue Errno::EPIPE, SystemCallError, IOError
@@ -414,85 +371,6 @@ module Puma
414
371
 
415
372
  private :fast_write_str, :fast_write_response
416
373
 
417
- # Given a Hash +env+ for the request read from +client+, add
418
- # and fixup keys to comply with Rack's env guidelines.
419
- # @param env [Hash] see Puma::Client#env, from request
420
- # @param client [Puma::Client] only needed for Client#peerip
421
- #
422
- def normalize_env(env, client)
423
- if host = env[HTTP_HOST]
424
- # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
425
- if colon = host.rindex("]:") # IPV6 with port
426
- env[SERVER_NAME] = host[0, colon+1]
427
- env[SERVER_PORT] = host[colon+2, host.bytesize]
428
- elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
429
- env[SERVER_NAME] = host[0, colon]
430
- env[SERVER_PORT] = host[colon+1, host.bytesize]
431
- else
432
- env[SERVER_NAME] = host
433
- env[SERVER_PORT] = default_server_port(env)
434
- end
435
- else
436
- env[SERVER_NAME] = LOCALHOST
437
- env[SERVER_PORT] = default_server_port(env)
438
- end
439
-
440
- unless env[REQUEST_PATH]
441
- # it might be a dumbass full host request header
442
- uri = begin
443
- URI.parse(env[REQUEST_URI])
444
- rescue URI::InvalidURIError
445
- raise Puma::HttpParserError
446
- end
447
- env[REQUEST_PATH] = uri.path
448
-
449
- # A nil env value will cause a LintError (and fatal errors elsewhere),
450
- # so only set the env value if there actually is a value.
451
- env[QUERY_STRING] = uri.query if uri.query
452
- end
453
-
454
- env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
455
-
456
- # From https://www.ietf.org/rfc/rfc3875 :
457
- # "Script authors should be aware that the REMOTE_ADDR and
458
- # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
459
- # may not identify the ultimate source of the request.
460
- # They identify the client for the immediate request to the
461
- # server; that client may be a proxy, gateway, or other
462
- # intermediary acting on behalf of the actual source client."
463
- #
464
-
465
- unless env.key?(REMOTE_ADDR)
466
- begin
467
- addr = client.peerip
468
- rescue Errno::ENOTCONN
469
- # Client disconnects can result in an inability to get the
470
- # peeraddr from the socket; default to unspec.
471
- if client.peer_family == Socket::AF_INET6
472
- addr = UNSPECIFIED_IPV6
473
- else
474
- addr = UNSPECIFIED_IPV4
475
- end
476
- end
477
-
478
- # Set unix socket addrs to localhost
479
- if addr.empty?
480
- if client.peer_family == Socket::AF_INET6
481
- addr = LOCALHOST_IPV6
482
- else
483
- addr = LOCALHOST_IPV4
484
- end
485
- end
486
-
487
- env[REMOTE_ADDR] = addr
488
- end
489
-
490
- # The legacy HTTP_VERSION header can be sent as a client header.
491
- # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
492
- env[HTTP_VERSION] = env[SERVER_PROTOCOL] if @env_set_http_version
493
- end
494
- private :normalize_env
495
-
496
374
  # @param header_key [#to_s]
497
375
  # @return [Boolean]
498
376
  #
@@ -506,56 +384,6 @@ module Puma
506
384
  def illegal_header_value?(header_value)
507
385
  !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
508
386
  end
509
- private :illegal_header_key?, :illegal_header_value?
510
-
511
- # Fixup any headers with `,` in the name to have `_` now. We emit
512
- # headers with `,` in them during the parse phase to avoid ambiguity
513
- # with the `-` to `_` conversion for critical headers. But here for
514
- # compatibility, we'll convert them back. This code is written to
515
- # avoid allocation in the common case (ie there are no headers
516
- # with `,` in their names), that's why it has the extra conditionals.
517
- #
518
- # @note If a normalized version of a `,` header already exists, we ignore
519
- # the `,` version. This prevents clobbering headers managed by proxies
520
- # but not by clients (Like X-Forwarded-For).
521
- #
522
- # @param env [Hash] see Puma::Client#env, from request, modifies in place
523
- # @version 5.0.3
524
- #
525
- def req_env_post_parse(env)
526
- to_delete = nil
527
- to_add = nil
528
-
529
- env.each do |k,v|
530
- if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
531
- if to_delete
532
- to_delete << k
533
- else
534
- to_delete = [k]
535
- end
536
-
537
- new_k = k.tr(",", "_")
538
- if env.key?(new_k)
539
- next
540
- end
541
-
542
- unless to_add
543
- to_add = {}
544
- end
545
-
546
- to_add[new_k] = v
547
- end
548
- end
549
-
550
- if to_delete # rubocop:disable Style/SafeNavigation
551
- to_delete.each { |k| env.delete(k) }
552
- end
553
-
554
- if to_add
555
- env.merge! to_add
556
- end
557
- end
558
- private :req_env_post_parse
559
387
 
560
388
  # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
561
389
  # @param headers [Hash] the headers returned by the Rack application
@@ -652,7 +480,8 @@ module Puma
652
480
  headers.each do |k, vs|
653
481
  next if illegal_header_key?(k)
654
482
 
655
- case k.downcase
483
+ key = k.downcase
484
+ case key
656
485
  when CONTENT_LENGTH2
657
486
  next if illegal_header_value?(vs)
658
487
  # nil.to_i is 0, nil&.to_i is nil
@@ -679,10 +508,10 @@ module Puma
679
508
  if ary
680
509
  ary.each do |v|
681
510
  next if illegal_header_value?(v)
682
- io_buffer.append k.downcase, colon, v, line_ending
511
+ io_buffer.append key, colon, v, line_ending
683
512
  end
684
513
  else
685
- io_buffer.append k.downcase, colon, line_ending
514
+ io_buffer.append key, colon, line_ending
686
515
  end
687
516
  end
688
517
 
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
 
@@ -31,15 +31,15 @@ module Puma
31
31
  # Each `Puma::Server` will have one reactor and one thread pool.
32
32
  class Server
33
33
  module FiberPerRequest
34
- def handle_request(client, requests)
34
+ def handle_request(processor, client, requests)
35
35
  Fiber.new do
36
36
  super
37
37
  end.resume
38
38
  end
39
39
  end
40
40
 
41
- include Puma::Const
42
- include Request
41
+ include Const
42
+ include Response
43
43
 
44
44
  attr_reader :options
45
45
  attr_reader :thread
@@ -259,7 +259,9 @@ module Puma
259
259
 
260
260
  @status = :run
261
261
 
262
- @thread_pool = ThreadPool.new(thread_name, options, server: self) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) do |processor, client|
263
+ process_client(processor, client)
264
+ end
263
265
 
264
266
  if @queue_requests
265
267
  @reactor = Reactor.new(@io_selector_backend) { |c|
@@ -304,7 +306,7 @@ module Puma
304
306
  # The method checks to see if it has the full header and body with
305
307
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
306
308
  # then the request is passed to the ThreadPool (`@thread_pool << client`)
307
- # 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.
308
310
  # The Client is then removed from the reactor (return `true`).
309
311
  #
310
312
  # If a client object times out, a 408 response is written, its connection is closed,
@@ -401,6 +403,7 @@ module Puma
401
403
  next
402
404
  end
403
405
  drain += 1 if shutting_down?
406
+
404
407
  client = new_client(io, sock)
405
408
  client.send(addr_send_name, addr_value) if addr_value
406
409
  pool << client
@@ -414,7 +417,7 @@ module Puma
414
417
  end
415
418
  end
416
419
 
417
- @log_writer.debug "Drained #{drain} additional connections." if drain
420
+ @log_writer.debug { "Drained #{drain} additional connections." } if drain
418
421
  @events.fire :state, @status
419
422
 
420
423
  if queue_requests
@@ -444,7 +447,9 @@ module Puma
444
447
  def new_client(io, sock)
445
448
  client = Client.new(io, @binder.env(sock))
446
449
  client.listener = sock
450
+ client.env_set_http_version = @env_set_http_version
447
451
  client.http_content_length_limit = @http_content_length_limit
452
+ client.supported_http_methods = @supported_http_methods
448
453
  client
449
454
  end
450
455
 
@@ -470,14 +475,14 @@ module Puma
470
475
  # Given a connection on +client+, handle the incoming requests,
471
476
  # or queue the connection in the Reactor if no request is available.
472
477
  #
473
- # This method is called from a ThreadPool worker thread.
478
+ # This method is called from a ThreadPool processor thread.
474
479
  #
475
480
  # This method supports HTTP Keep-Alive so it may, depending on if the client
476
481
  # indicates that it supports keep alive, wait for another request before
477
482
  # returning.
478
483
  #
479
484
  # Return true if one or more requests were processed.
480
- def process_client(client)
485
+ def process_client(processor, client)
481
486
  close_socket = true
482
487
 
483
488
  requests = 0
@@ -500,7 +505,7 @@ module Puma
500
505
  while can_loop
501
506
  can_loop = false
502
507
  @requests_count += 1
503
- case handle_request(client, requests + 1)
508
+ case handle_request(processor, client, requests + 1)
504
509
  when :close
505
510
  when :async
506
511
  close_socket = false
@@ -577,7 +582,7 @@ module Puma
577
582
  lowlevel_error(e, client.env)
578
583
  @log_writer.ssl_error e, client.io
579
584
  when HttpParserError
580
- response_to_error(client, requests, e, 400)
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
581
586
  @log_writer.parse_error e, client
582
587
  when HttpParserError501
583
588
  response_to_error(client, requests, e, 501)
@@ -610,7 +615,15 @@ module Puma
610
615
  end
611
616
 
612
617
  def response_to_error(client, requests, err, status_code)
613
- 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
614
627
  prepare_response(status, headers, res_body, requests, client)
615
628
  end
616
629
  private :response_to_error
@@ -618,32 +631,11 @@ module Puma
618
631
  # Wait for all outstanding requests to finish.
619
632
  #
620
633
  def graceful_shutdown
621
- if options[:shutdown_debug]
622
- threads = Thread.list
623
- total = threads.size
624
-
625
- pid = Process.pid
626
-
627
- $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
628
-
629
- threads.each_with_index do |t,i|
630
- $stdout.syswrite "#{pid}: Thread #{i+1}/#{total}: #{t.inspect}\n"
631
- $stdout.syswrite "#{pid}: #{t.backtrace.join("\n#{pid}: ")}\n\n"
632
- end
633
- $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
634
- end
635
-
636
634
  if @status != :restart
637
635
  @binder.close
638
636
  end
639
637
 
640
- if @thread_pool
641
- if timeout = options[:force_shutdown_after]
642
- @thread_pool.shutdown timeout.to_f
643
- else
644
- @thread_pool.shutdown
645
- end
646
- end
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
647
639
  end
648
640
 
649
641
  def notify_safely(message)
@@ -660,7 +652,7 @@ module Puma
660
652
  end
661
653
  private :notify_safely
662
654
 
663
- # 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
664
656
  # off the request queue before finally exiting.
665
657
 
666
658
  def stop(sync=false)
@@ -730,6 +722,49 @@ module Puma
730
722
  @binder.add_unix_listener path, umask, mode, backlog
731
723
  end
732
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
+
733
768
  # @!attribute [r] connected_ports
734
769
  def connected_ports
735
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