puma 6.4.3 → 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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +448 -8
  3. data/README.md +110 -51
  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/fork_worker.md +11 -1
  10. data/docs/grpc.md +62 -0
  11. data/docs/images/favicon.svg +1 -0
  12. data/docs/images/running-puma.svg +1 -0
  13. data/docs/images/standard-logo.svg +1 -0
  14. data/docs/java_options.md +54 -0
  15. data/docs/jungle/README.md +1 -1
  16. data/docs/kubernetes.md +11 -16
  17. data/docs/plugins.md +6 -2
  18. data/docs/restart.md +2 -2
  19. data/docs/signals.md +21 -21
  20. data/docs/stats.md +11 -5
  21. data/docs/systemd.md +14 -5
  22. data/ext/puma_http11/extconf.rb +20 -32
  23. data/ext/puma_http11/http11_parser.java.rl +51 -65
  24. data/ext/puma_http11/mini_ssl.c +29 -9
  25. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +194 -101
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  28. data/ext/puma_http11/puma_http11.c +125 -118
  29. data/lib/puma/app/status.rb +11 -3
  30. data/lib/puma/binder.rb +22 -12
  31. data/lib/puma/cli.rb +11 -9
  32. data/lib/puma/client.rb +233 -136
  33. data/lib/puma/client_env.rb +171 -0
  34. data/lib/puma/cluster/worker.rb +24 -21
  35. data/lib/puma/cluster/worker_handle.rb +38 -8
  36. data/lib/puma/cluster.rb +74 -48
  37. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  38. data/lib/puma/commonlogger.rb +3 -3
  39. data/lib/puma/configuration.rb +197 -64
  40. data/lib/puma/const.rb +23 -12
  41. data/lib/puma/control_cli.rb +11 -7
  42. data/lib/puma/detect.rb +13 -0
  43. data/lib/puma/dsl.rb +483 -127
  44. data/lib/puma/error_logger.rb +7 -5
  45. data/lib/puma/events.rb +25 -10
  46. data/lib/puma/io_buffer.rb +8 -4
  47. data/lib/puma/jruby_restart.rb +0 -16
  48. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  49. data/lib/puma/launcher.rb +76 -59
  50. data/lib/puma/log_writer.rb +17 -11
  51. data/lib/puma/minissl/context_builder.rb +1 -0
  52. data/lib/puma/minissl.rb +1 -1
  53. data/lib/puma/null_io.rb +26 -0
  54. data/lib/puma/plugin/systemd.rb +3 -3
  55. data/lib/puma/rack/urlmap.rb +1 -1
  56. data/lib/puma/reactor.rb +19 -13
  57. data/lib/puma/{request.rb → response.rb} +57 -209
  58. data/lib/puma/runner.rb +15 -17
  59. data/lib/puma/sd_notify.rb +1 -4
  60. data/lib/puma/server.rb +200 -104
  61. data/lib/puma/server_plugin_control.rb +32 -0
  62. data/lib/puma/single.rb +7 -4
  63. data/lib/puma/state_file.rb +3 -2
  64. data/lib/puma/thread_pool.rb +179 -96
  65. data/lib/puma/util.rb +0 -7
  66. data/lib/puma.rb +10 -0
  67. data/lib/rack/handler/puma.rb +11 -8
  68. data/tools/Dockerfile +15 -5
  69. metadata +26 -16
  70. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/server.rb CHANGED
@@ -11,13 +11,14 @@ 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
+ require_relative 'configuration'
16
+ require_relative 'cluster_accept_loop_delay'
15
17
 
16
18
  require 'socket'
17
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
18
20
 
19
21
  module Puma
20
-
21
22
  # The HTTP Server itself. Serves out a single Rack app.
22
23
  #
23
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -29,9 +30,18 @@ module Puma
29
30
  #
30
31
  # Each `Puma::Server` will have one reactor and one thread pool.
31
32
  class Server
32
- include Puma::Const
33
- include Request
33
+ module FiberPerRequest
34
+ def handle_request(processor, client, requests)
35
+ Fiber.new do
36
+ super
37
+ end.resume
38
+ end
39
+ end
40
+
41
+ include Const
42
+ include Response
34
43
 
44
+ attr_reader :options
35
45
  attr_reader :thread
36
46
  attr_reader :log_writer
37
47
  attr_reader :events
@@ -46,8 +56,6 @@ module Puma
46
56
  attr_accessor :app
47
57
  attr_accessor :binder
48
58
 
49
- THREAD_LOCAL_KEY = :puma_server
50
-
51
59
  # Create a server for the rack app +app+.
52
60
  #
53
61
  # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
@@ -74,6 +82,9 @@ module Puma
74
82
 
75
83
  @thread = nil
76
84
  @thread_pool = nil
85
+ @reactor = nil
86
+
87
+ @env_set_http_version = nil
77
88
 
78
89
  @options = if options.is_a?(UserFileDefaultOptions)
79
90
  options
@@ -91,9 +102,19 @@ module Puma
91
102
  @min_threads = @options[:min_threads]
92
103
  @max_threads = @options[:max_threads]
93
104
  @queue_requests = @options[:queue_requests]
94
- @max_fast_inline = @options[:max_fast_inline]
105
+ @max_keep_alive = @options[:max_keep_alive]
106
+ @enable_keep_alives = @options[:enable_keep_alives]
107
+ @enable_keep_alives &&= @queue_requests
95
108
  @io_selector_backend = @options[:io_selector_backend]
96
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
+ )
114
+
115
+ if @options[:fiber_per_request]
116
+ singleton_class.prepend(FiberPerRequest)
117
+ end
97
118
 
98
119
  # make this a hash, since we prefer `key?` over `include?`
99
120
  @supported_http_methods =
@@ -110,7 +131,7 @@ module Puma
110
131
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
111
132
  @leak_stack_on_error = @options[:environment] ? temp : true
112
133
 
113
- @binder = Binder.new(log_writer)
134
+ @binder = Binder.new(log_writer, @options)
114
135
 
115
136
  ENV['RACK_ENV'] ||= "development"
116
137
 
@@ -130,7 +151,7 @@ module Puma
130
151
  class << self
131
152
  # @!attribute [r] current
132
153
  def current
133
- Thread.current[THREAD_LOCAL_KEY]
154
+ Thread.current.puma_server
134
155
  end
135
156
 
136
157
  # :nodoc:
@@ -161,7 +182,6 @@ module Puma
161
182
  begin
162
183
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if skt.kind_of? TCPSocket
163
184
  rescue IOError, SystemCallError
164
- Puma::Util.purge_interrupt_queue
165
185
  end
166
186
  end
167
187
 
@@ -170,7 +190,6 @@ module Puma
170
190
  begin
171
191
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if skt.kind_of? TCPSocket
172
192
  rescue IOError, SystemCallError
173
- Puma::Util.purge_interrupt_queue
174
193
  end
175
194
  end
176
195
  else
@@ -191,7 +210,6 @@ module Puma
191
210
  begin
192
211
  tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
193
212
  rescue IOError, SystemCallError
194
- Puma::Util.purge_interrupt_queue
195
213
  @precheck_closing = false
196
214
  false
197
215
  else
@@ -216,7 +234,6 @@ module Puma
216
234
  @thread_pool&.spawned
217
235
  end
218
236
 
219
-
220
237
  # This number represents the number of requests that
221
238
  # the server is capable of taking right now.
222
239
  #
@@ -242,16 +259,21 @@ module Puma
242
259
 
243
260
  @status = :run
244
261
 
245
- @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
246
265
 
247
266
  if @queue_requests
248
- @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
267
+ @reactor = Reactor.new(@io_selector_backend) { |c|
268
+ # Inversion of control, the reactor is calling a method on the server when it
269
+ # is done buffering a request or receives a new request from a keepalive connection.
270
+ self.reactor_wakeup(c)
271
+ }
249
272
  @reactor.run
250
273
  end
251
274
 
252
-
253
- @thread_pool.auto_reap! if @options[:reaping_time]
254
- @thread_pool.auto_trim! if @options[:auto_trim_time]
275
+ @thread_pool.auto_reap! if options[:reaping_time]
276
+ @thread_pool.auto_trim! if @min_threads != @max_threads && options[:auto_trim_time]
255
277
 
256
278
  @check, @notify = Puma::Util.pipe unless @notify
257
279
 
@@ -271,17 +293,20 @@ module Puma
271
293
  # This method is called from the Reactor thread when a queued Client receives data,
272
294
  # times out, or when the Reactor is shutting down.
273
295
  #
296
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
297
+ # from the server.
298
+ #
274
299
  # It is responsible for ensuring that a request has been completely received
275
300
  # before it starts to be processed by the ThreadPool. This may be known as read buffering.
276
301
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
277
302
  # such as nginx) then the application would be subject to a slow client attack.
278
303
  #
279
- # 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).
280
305
  #
281
306
  # The method checks to see if it has the full header and body with
282
307
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
283
308
  # then the request is passed to the ThreadPool (`@thread_pool << client`)
284
- # 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.
285
310
  # The Client is then removed from the reactor (return `true`).
286
311
  #
287
312
  # If a client object times out, a 408 response is written, its connection is closed,
@@ -305,25 +330,28 @@ module Puma
305
330
  end
306
331
  rescue StandardError => e
307
332
  client_error(e, client)
308
- client.close
333
+ close_client_safely(client)
309
334
  true
310
335
  end
311
336
 
312
337
  def handle_servers
338
+ @env_set_http_version = Object.const_defined?(:Rack) && ::Rack.respond_to?(:release) &&
339
+ Gem::Version.new(::Rack.release) < Gem::Version.new('3.1.0')
340
+
313
341
  begin
314
342
  check = @check
315
343
  sockets = [check] + @binder.ios
316
344
  pool = @thread_pool
317
345
  queue_requests = @queue_requests
318
- drain = @options[:drain_on_shutdown] ? 0 : nil
346
+ drain = options[:drain_on_shutdown] ? 0 : nil
319
347
 
320
- addr_send_name, addr_value = case @options[:remote_address]
348
+ addr_send_name, addr_value = case options[:remote_address]
321
349
  when :value
322
- [:peerip=, @options[:remote_address_value]]
350
+ [:peerip=, options[:remote_address_value]]
323
351
  when :header
324
- [:remote_addr_header=, @options[:remote_address_header]]
352
+ [:remote_addr_header=, options[:remote_address_header]]
325
353
  when :proxy_protocol
326
- [:expect_proxy_proto=, @options[:remote_address_proxy_protocol]]
354
+ [:expect_proxy_proto=, options[:remote_address_proxy_protocol]]
327
355
  else
328
356
  [nil, nil]
329
357
  end
@@ -336,7 +364,7 @@ module Puma
336
364
  @idle_timeout_reached = true
337
365
 
338
366
  if @clustered
339
- @worker_write << "i#{Process.pid}\n" rescue nil
367
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
340
368
  next
341
369
  else
342
370
  @log_writer.log "- Idle timeout reached"
@@ -349,15 +377,25 @@ module Puma
349
377
 
350
378
  if @idle_timeout_reached && @clustered
351
379
  @idle_timeout_reached = false
352
- @worker_write << "i#{Process.pid}\n" rescue nil
380
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
353
381
  end
354
382
 
355
383
  ios.first.each do |sock|
356
384
  if sock == check
357
385
  break if handle_check
358
386
  else
359
- pool.wait_until_not_full
360
- pool.wait_for_less_busy_worker(@options[:wait_for_less_busy_worker])
387
+ # if ThreadPool out_of_band code is running, we don't want to add
388
+ # clients until the code is finished.
389
+ pool.wait_while_out_of_band_running
390
+
391
+ # A well rested herd (cluster) runs faster
392
+ if @cluster_accept_loop_delay.on? && (busy_threads_plus_todo = pool.busy_threads) > 0
393
+ delay = @cluster_accept_loop_delay.calculate(
394
+ max_threads: @max_threads,
395
+ busy_threads_plus_todo: busy_threads_plus_todo
396
+ )
397
+ sleep(delay)
398
+ end
361
399
 
362
400
  io = begin
363
401
  sock.accept_nonblock
@@ -365,11 +403,10 @@ module Puma
365
403
  next
366
404
  end
367
405
  drain += 1 if shutting_down?
368
- pool << Client.new(io, @binder.env(sock)).tap { |c|
369
- c.listener = sock
370
- c.http_content_length_limit = @http_content_length_limit
371
- c.send(addr_send_name, addr_value) if addr_value
372
- }
406
+
407
+ client = new_client(io, sock)
408
+ client.send(addr_send_name, addr_value) if addr_value
409
+ pool << client
373
410
  end
374
411
  end
375
412
  rescue IOError, Errno::EBADF
@@ -380,7 +417,7 @@ module Puma
380
417
  end
381
418
  end
382
419
 
383
- @log_writer.debug "Drained #{drain} additional connections." if drain
420
+ @log_writer.debug { "Drained #{drain} additional connections." } if drain
384
421
  @events.fire :state, @status
385
422
 
386
423
  if queue_requests
@@ -406,6 +443,16 @@ module Puma
406
443
  @events.fire :state, :done
407
444
  end
408
445
 
446
+ # :nodoc:
447
+ def new_client(io, sock)
448
+ client = Client.new(io, @binder.env(sock))
449
+ client.listener = sock
450
+ client.env_set_http_version = @env_set_http_version
451
+ client.http_content_length_limit = @http_content_length_limit
452
+ client.supported_http_methods = @supported_http_methods
453
+ client
454
+ end
455
+
409
456
  # :nodoc:
410
457
  def handle_check
411
458
  cmd = @check.read(1)
@@ -428,25 +475,20 @@ module Puma
428
475
  # Given a connection on +client+, handle the incoming requests,
429
476
  # or queue the connection in the Reactor if no request is available.
430
477
  #
431
- # This method is called from a ThreadPool worker thread.
478
+ # This method is called from a ThreadPool processor thread.
432
479
  #
433
480
  # This method supports HTTP Keep-Alive so it may, depending on if the client
434
481
  # indicates that it supports keep alive, wait for another request before
435
482
  # returning.
436
483
  #
437
484
  # Return true if one or more requests were processed.
438
- def process_client(client)
439
- # Advertise this server into the thread
440
- Thread.current[THREAD_LOCAL_KEY] = self
441
-
442
- clean_thread_locals = @options[:clean_thread_locals]
485
+ def process_client(processor, client)
443
486
  close_socket = true
444
487
 
445
488
  requests = 0
446
489
 
447
490
  begin
448
- if @queue_requests &&
449
- !client.eagerly_finish
491
+ if @queue_requests && !client.eagerly_finish
450
492
 
451
493
  client.set_timeout(@first_data_timeout)
452
494
  if @reactor.add client
@@ -459,38 +501,40 @@ module Puma
459
501
  client.finish(@first_data_timeout)
460
502
  end
461
503
 
462
- while true
504
+ can_loop = true
505
+ while can_loop
506
+ can_loop = false
463
507
  @requests_count += 1
464
- case handle_request(client, requests + 1)
465
- when false
466
- break
508
+ case handle_request(processor, client, requests + 1)
509
+ when :close
467
510
  when :async
468
511
  close_socket = false
469
- break
470
- when true
471
- ThreadPool.clean_thread_locals if clean_thread_locals
472
-
512
+ when :keep_alive
473
513
  requests += 1
474
514
 
475
- # As an optimization, try to read the next request from the
476
- # socket for a short time before returning to the reactor.
477
- fast_check = @status == :run
478
-
479
- # Always pass the client back to the reactor after a reasonable
480
- # number of inline requests if there are other requests pending.
481
- fast_check = false if requests >= @max_fast_inline &&
482
- @thread_pool.backlog > 0
515
+ client.reset
483
516
 
484
- next_request_ready = with_force_shutdown(client) do
485
- client.reset(fast_check)
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 }
486
523
  end
487
524
 
488
- unless next_request_ready
489
- break unless @queue_requests
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
490
535
  client.set_timeout @persistent_timeout
491
536
  if @reactor.add client
492
537
  close_socket = false
493
- break
494
538
  end
495
539
  end
496
540
  end
@@ -503,17 +547,21 @@ module Puma
503
547
  ensure
504
548
  client.io_buffer.reset
505
549
 
506
- begin
507
- client.close if close_socket
508
- rescue IOError, SystemCallError
509
- Puma::Util.purge_interrupt_queue
510
- # Already closed
511
- rescue StandardError => e
512
- @log_writer.unknown_error e, nil, "Client"
513
- end
550
+ close_client_safely(client) if close_socket
514
551
  end
515
552
  end
516
553
 
554
+ # :nodoc:
555
+ def close_client_safely(client)
556
+ client.close
557
+ rescue IOError, SystemCallError
558
+ # Already closed
559
+ rescue MiniSSL::SSLError => e
560
+ @log_writer.ssl_error e, client.io
561
+ rescue StandardError => e
562
+ @log_writer.unknown_error e, nil, "Client"
563
+ end
564
+
517
565
  # Triggers a client timeout if the thread-pool shuts down
518
566
  # during execution of the provided block.
519
567
  def with_force_shutdown(client, &block)
@@ -534,7 +582,7 @@ module Puma
534
582
  lowlevel_error(e, client.env)
535
583
  @log_writer.ssl_error e, client.io
536
584
  when HttpParserError
537
- response_to_error(client, requests, e, 400)
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
538
586
  @log_writer.parse_error e, client
539
587
  when HttpParserError501
540
588
  response_to_error(client, requests, e, 501)
@@ -548,7 +596,7 @@ module Puma
548
596
  # A fallback rack response if +@app+ raises as exception.
549
597
  #
550
598
  def lowlevel_error(e, env, status=500)
551
- if handler = @options[:lowlevel_error_handler]
599
+ if handler = options[:lowlevel_error_handler]
552
600
  if handler.arity == 1
553
601
  return handler.call(e)
554
602
  elsif handler.arity == 2
@@ -567,59 +615,44 @@ module Puma
567
615
  end
568
616
 
569
617
  def response_to_error(client, requests, err, status_code)
570
- 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
571
627
  prepare_response(status, headers, res_body, requests, client)
572
- client.write_error(status_code)
573
628
  end
574
629
  private :response_to_error
575
630
 
576
631
  # Wait for all outstanding requests to finish.
577
632
  #
578
633
  def graceful_shutdown
579
- if @options[:shutdown_debug]
580
- threads = Thread.list
581
- total = threads.size
582
-
583
- pid = Process.pid
584
-
585
- $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
586
-
587
- threads.each_with_index do |t,i|
588
- $stdout.syswrite "#{pid}: Thread #{i+1}/#{total}: #{t.inspect}\n"
589
- $stdout.syswrite "#{pid}: #{t.backtrace.join("\n#{pid}: ")}\n\n"
590
- end
591
- $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
592
- end
593
-
594
634
  if @status != :restart
595
635
  @binder.close
596
636
  end
597
637
 
598
- if @thread_pool
599
- if timeout = @options[:force_shutdown_after]
600
- @thread_pool.shutdown timeout.to_f
601
- else
602
- @thread_pool.shutdown
603
- end
604
- end
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
605
639
  end
606
640
 
607
641
  def notify_safely(message)
608
642
  @notify << message
609
643
  rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
610
644
  # The server, in another thread, is shutting down
611
- Puma::Util.purge_interrupt_queue
612
645
  rescue RuntimeError => e
613
646
  # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
614
647
  if e.message.include?('IOError')
615
- Puma::Util.purge_interrupt_queue
648
+ # ignore
616
649
  else
617
650
  raise e
618
651
  end
619
652
  end
620
653
  private :notify_safely
621
654
 
622
- # 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
623
656
  # off the request queue before finally exiting.
624
657
 
625
658
  def stop(sync=false)
@@ -643,13 +676,33 @@ module Puma
643
676
 
644
677
  # List of methods invoked by #stats.
645
678
  # @version 5.0.0
646
- STAT_METHODS = [:backlog, :running, :pool_capacity, :max_threads, :requests_count].freeze
679
+ STAT_METHODS = [
680
+ :backlog,
681
+ :running,
682
+ :pool_capacity,
683
+ :busy_threads,
684
+ :backlog_max,
685
+ :max_threads,
686
+ :requests_count,
687
+ :reactor_max,
688
+ ].freeze
647
689
 
648
690
  # Returns a hash of stats about the running server for reporting purposes.
649
691
  # @version 5.0.0
650
692
  # @!attribute [r] stats
693
+ # @return [Hash] hash containing stat info from `Server` and `ThreadPool`
651
694
  def stats
652
- STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
695
+ stats = @thread_pool&.stats || {}
696
+ stats[:max_threads] = @max_threads
697
+ stats[:requests_count] = @requests_count
698
+ stats[:reactor_max] = @reactor.reactor_max if @reactor
699
+ reset_max
700
+ stats
701
+ end
702
+
703
+ def reset_max
704
+ @reactor.reactor_max = 0 if @reactor
705
+ @thread_pool&.reset_max
653
706
  end
654
707
 
655
708
  # below are 'delegations' to binder
@@ -669,6 +722,49 @@ module Puma
669
722
  @binder.add_unix_listener path, umask, mode, backlog
670
723
  end
671
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
+
672
768
  # @!attribute [r] connected_ports
673
769
  def connected_ports
674
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
@@ -17,7 +17,7 @@ module Puma
17
17
  def stats
18
18
  {
19
19
  started_at: utc_iso8601(@started_at)
20
- }.merge(@server.stats).merge(super)
20
+ }.merge(@server&.stats || {}).merge(super)
21
21
  end
22
22
 
23
23
  def restart
@@ -49,13 +49,16 @@ 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
+
57
+ warn_ruby_mn_threads
58
+
56
59
  redirect_io
57
60
 
58
- @events.fire_on_booted!
61
+ @events.fire_after_booted!
59
62
 
60
63
  debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
61
64
 
@@ -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