puma 6.6.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +309 -5
  3. data/README.md +41 -42
  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 +5 -5
  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/jungle/README.md +1 -1
  15. data/docs/kubernetes.md +11 -16
  16. data/docs/plugins.md +2 -2
  17. data/docs/restart.md +2 -2
  18. data/docs/signals.md +21 -21
  19. data/docs/stats.md +4 -3
  20. data/docs/systemd.md +4 -4
  21. data/ext/puma_http11/extconf.rb +2 -17
  22. data/ext/puma_http11/http11_parser.java.rl +51 -65
  23. data/ext/puma_http11/mini_ssl.c +18 -8
  24. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  25. data/ext/puma_http11/org/jruby/puma/Http11.java +174 -102
  26. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  27. data/ext/puma_http11/puma_http11.c +122 -118
  28. data/lib/puma/app/status.rb +10 -2
  29. data/lib/puma/binder.rb +12 -10
  30. data/lib/puma/cli.rb +4 -6
  31. data/lib/puma/client.rb +205 -131
  32. data/lib/puma/client_env.rb +171 -0
  33. data/lib/puma/cluster/worker.rb +17 -17
  34. data/lib/puma/cluster/worker_handle.rb +38 -7
  35. data/lib/puma/cluster.rb +44 -30
  36. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  37. data/lib/puma/commonlogger.rb +3 -3
  38. data/lib/puma/configuration.rb +173 -58
  39. data/lib/puma/const.rb +11 -11
  40. data/lib/puma/control_cli.rb +7 -3
  41. data/lib/puma/detect.rb +13 -0
  42. data/lib/puma/dsl.rb +225 -108
  43. data/lib/puma/error_logger.rb +3 -1
  44. data/lib/puma/events.rb +25 -10
  45. data/lib/puma/io_buffer.rb +8 -4
  46. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  47. data/lib/puma/launcher.rb +57 -53
  48. data/lib/puma/log_writer.rb +8 -2
  49. data/lib/puma/minissl.rb +0 -1
  50. data/lib/puma/plugin/systemd.rb +3 -3
  51. data/lib/puma/rack/urlmap.rb +1 -1
  52. data/lib/puma/reactor.rb +19 -13
  53. data/lib/puma/{request.rb → response.rb} +56 -212
  54. data/lib/puma/runner.rb +9 -18
  55. data/lib/puma/server.rb +182 -97
  56. data/lib/puma/server_plugin_control.rb +32 -0
  57. data/lib/puma/single.rb +7 -4
  58. data/lib/puma/state_file.rb +3 -2
  59. data/lib/puma/thread_pool.rb +170 -99
  60. data/lib/puma/util.rb +0 -7
  61. data/lib/puma.rb +10 -0
  62. data/lib/rack/handler/puma.rb +3 -3
  63. data/tools/Dockerfile +15 -5
  64. metadata +19 -7
  65. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/server.rb CHANGED
@@ -11,16 +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
- # This method was private on Ruby 2.4 but became public on Ruby 2.5+:
22
- Thread.send(: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
@@ -32,8 +30,16 @@ module Puma
32
30
  #
33
31
  # Each `Puma::Server` will have one reactor and one thread pool.
34
32
  class Server
35
- include Puma::Const
36
- 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
37
43
 
38
44
  attr_reader :options
39
45
  attr_reader :thread
@@ -50,7 +56,6 @@ module Puma
50
56
  attr_accessor :app
51
57
  attr_accessor :binder
52
58
 
53
-
54
59
  # Create a server for the rack app +app+.
55
60
  #
56
61
  # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
@@ -77,6 +82,9 @@ module Puma
77
82
 
78
83
  @thread = nil
79
84
  @thread_pool = nil
85
+ @reactor = nil
86
+
87
+ @env_set_http_version = nil
80
88
 
81
89
  @options = if options.is_a?(UserFileDefaultOptions)
82
90
  options
@@ -94,10 +102,19 @@ module Puma
94
102
  @min_threads = @options[:min_threads]
95
103
  @max_threads = @options[:max_threads]
96
104
  @queue_requests = @options[:queue_requests]
97
- @max_fast_inline = @options[:max_fast_inline]
105
+ @max_keep_alive = @options[:max_keep_alive]
98
106
  @enable_keep_alives = @options[:enable_keep_alives]
107
+ @enable_keep_alives &&= @queue_requests
99
108
  @io_selector_backend = @options[:io_selector_backend]
100
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
101
118
 
102
119
  # make this a hash, since we prefer `key?` over `include?`
103
120
  @supported_http_methods =
@@ -114,7 +131,7 @@ module Puma
114
131
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
115
132
  @leak_stack_on_error = @options[:environment] ? temp : true
116
133
 
117
- @binder = Binder.new(log_writer)
134
+ @binder = Binder.new(log_writer, @options)
118
135
 
119
136
  ENV['RACK_ENV'] ||= "development"
120
137
 
@@ -165,7 +182,6 @@ module Puma
165
182
  begin
166
183
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if skt.kind_of? TCPSocket
167
184
  rescue IOError, SystemCallError
168
- Puma::Util.purge_interrupt_queue
169
185
  end
170
186
  end
171
187
 
@@ -174,7 +190,6 @@ module Puma
174
190
  begin
175
191
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if skt.kind_of? TCPSocket
176
192
  rescue IOError, SystemCallError
177
- Puma::Util.purge_interrupt_queue
178
193
  end
179
194
  end
180
195
  else
@@ -195,7 +210,6 @@ module Puma
195
210
  begin
196
211
  tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
197
212
  rescue IOError, SystemCallError
198
- Puma::Util.purge_interrupt_queue
199
213
  @precheck_closing = false
200
214
  false
201
215
  else
@@ -220,7 +234,6 @@ module Puma
220
234
  @thread_pool&.spawned
221
235
  end
222
236
 
223
-
224
237
  # This number represents the number of requests that
225
238
  # the server is capable of taking right now.
226
239
  #
@@ -233,11 +246,6 @@ module Puma
233
246
  @thread_pool&.pool_capacity
234
247
  end
235
248
 
236
- # @!attribute [r] busy_threads
237
- def busy_threads
238
- @thread_pool&.busy_threads
239
- end
240
-
241
249
  # Runs the server.
242
250
  #
243
251
  # If +background+ is true (the default) then a thread is spun
@@ -251,10 +259,16 @@ module Puma
251
259
 
252
260
  @status = :run
253
261
 
254
- @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
255
265
 
256
266
  if @queue_requests
257
- @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
+ }
258
272
  @reactor.run
259
273
  end
260
274
 
@@ -279,17 +293,20 @@ module Puma
279
293
  # This method is called from the Reactor thread when a queued Client receives data,
280
294
  # times out, or when the Reactor is shutting down.
281
295
  #
296
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
297
+ # from the server.
298
+ #
282
299
  # It is responsible for ensuring that a request has been completely received
283
300
  # before it starts to be processed by the ThreadPool. This may be known as read buffering.
284
301
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
285
302
  # such as nginx) then the application would be subject to a slow client attack.
286
303
  #
287
- # 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).
288
305
  #
289
306
  # The method checks to see if it has the full header and body with
290
307
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
291
308
  # then the request is passed to the ThreadPool (`@thread_pool << client`)
292
- # 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.
293
310
  # The Client is then removed from the reactor (return `true`).
294
311
  #
295
312
  # If a client object times out, a 408 response is written, its connection is closed,
@@ -313,11 +330,14 @@ module Puma
313
330
  end
314
331
  rescue StandardError => e
315
332
  client_error(e, client)
316
- client.close
333
+ close_client_safely(client)
317
334
  true
318
335
  end
319
336
 
320
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
+
321
341
  begin
322
342
  check = @check
323
343
  sockets = [check] + @binder.ios
@@ -364,8 +384,18 @@ module Puma
364
384
  if sock == check
365
385
  break if handle_check
366
386
  else
367
- pool.wait_until_not_full
368
- pool.wait_for_less_busy_worker(options[:wait_for_less_busy_worker]) if @clustered
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
369
399
 
370
400
  io = begin
371
401
  sock.accept_nonblock
@@ -373,11 +403,10 @@ module Puma
373
403
  next
374
404
  end
375
405
  drain += 1 if shutting_down?
376
- pool << Client.new(io, @binder.env(sock)).tap { |c|
377
- c.listener = sock
378
- c.http_content_length_limit = @http_content_length_limit
379
- c.send(addr_send_name, addr_value) if addr_value
380
- }
406
+
407
+ client = new_client(io, sock)
408
+ client.send(addr_send_name, addr_value) if addr_value
409
+ pool << client
381
410
  end
382
411
  end
383
412
  rescue IOError, Errno::EBADF
@@ -388,7 +417,7 @@ module Puma
388
417
  end
389
418
  end
390
419
 
391
- @log_writer.debug "Drained #{drain} additional connections." if drain
420
+ @log_writer.debug { "Drained #{drain} additional connections." } if drain
392
421
  @events.fire :state, @status
393
422
 
394
423
  if queue_requests
@@ -414,6 +443,16 @@ module Puma
414
443
  @events.fire :state, :done
415
444
  end
416
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
+
417
456
  # :nodoc:
418
457
  def handle_check
419
458
  cmd = @check.read(1)
@@ -436,25 +475,20 @@ module Puma
436
475
  # Given a connection on +client+, handle the incoming requests,
437
476
  # or queue the connection in the Reactor if no request is available.
438
477
  #
439
- # This method is called from a ThreadPool worker thread.
478
+ # This method is called from a ThreadPool processor thread.
440
479
  #
441
480
  # This method supports HTTP Keep-Alive so it may, depending on if the client
442
481
  # indicates that it supports keep alive, wait for another request before
443
482
  # returning.
444
483
  #
445
484
  # Return true if one or more requests were processed.
446
- def process_client(client)
447
- # Advertise this server into the thread
448
- Thread.current.puma_server = self
449
-
450
- clean_thread_locals = options[:clean_thread_locals]
485
+ def process_client(processor, client)
451
486
  close_socket = true
452
487
 
453
488
  requests = 0
454
489
 
455
490
  begin
456
- if @queue_requests &&
457
- !client.eagerly_finish
491
+ if @queue_requests && !client.eagerly_finish
458
492
 
459
493
  client.set_timeout(@first_data_timeout)
460
494
  if @reactor.add client
@@ -467,38 +501,40 @@ module Puma
467
501
  client.finish(@first_data_timeout)
468
502
  end
469
503
 
470
- while true
504
+ can_loop = true
505
+ while can_loop
506
+ can_loop = false
471
507
  @requests_count += 1
472
- case handle_request(client, requests + 1)
473
- when false
474
- break
508
+ case handle_request(processor, client, requests + 1)
509
+ when :close
475
510
  when :async
476
511
  close_socket = false
477
- break
478
- when true
479
- ThreadPool.clean_thread_locals if clean_thread_locals
480
-
512
+ when :keep_alive
481
513
  requests += 1
482
514
 
483
- # As an optimization, try to read the next request from the
484
- # socket for a short time before returning to the reactor.
485
- fast_check = @status == :run
486
-
487
- # Always pass the client back to the reactor after a reasonable
488
- # number of inline requests if there are other requests pending.
489
- fast_check = false if requests >= @max_fast_inline &&
490
- @thread_pool.backlog > 0
515
+ client.reset
491
516
 
492
- next_request_ready = with_force_shutdown(client) do
493
- 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 }
494
523
  end
495
524
 
496
- unless next_request_ready
497
- 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
498
535
  client.set_timeout @persistent_timeout
499
536
  if @reactor.add client
500
537
  close_socket = false
501
- break
502
538
  end
503
539
  end
504
540
  end
@@ -511,17 +547,21 @@ module Puma
511
547
  ensure
512
548
  client.io_buffer.reset
513
549
 
514
- begin
515
- client.close if close_socket
516
- rescue IOError, SystemCallError
517
- Puma::Util.purge_interrupt_queue
518
- # Already closed
519
- rescue StandardError => e
520
- @log_writer.unknown_error e, nil, "Client"
521
- end
550
+ close_client_safely(client) if close_socket
522
551
  end
523
552
  end
524
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
+
525
565
  # Triggers a client timeout if the thread-pool shuts down
526
566
  # during execution of the provided block.
527
567
  def with_force_shutdown(client, &block)
@@ -542,7 +582,7 @@ module Puma
542
582
  lowlevel_error(e, client.env)
543
583
  @log_writer.ssl_error e, client.io
544
584
  when HttpParserError
545
- response_to_error(client, requests, e, 400)
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
546
586
  @log_writer.parse_error e, client
547
587
  when HttpParserError501
548
588
  response_to_error(client, requests, e, 501)
@@ -575,7 +615,15 @@ module Puma
575
615
  end
576
616
 
577
617
  def response_to_error(client, requests, err, status_code)
578
- 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
579
627
  prepare_response(status, headers, res_body, requests, client)
580
628
  end
581
629
  private :response_to_error
@@ -583,50 +631,28 @@ module Puma
583
631
  # Wait for all outstanding requests to finish.
584
632
  #
585
633
  def graceful_shutdown
586
- if options[:shutdown_debug]
587
- threads = Thread.list
588
- total = threads.size
589
-
590
- pid = Process.pid
591
-
592
- $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
593
-
594
- threads.each_with_index do |t,i|
595
- $stdout.syswrite "#{pid}: Thread #{i+1}/#{total}: #{t.inspect}\n"
596
- $stdout.syswrite "#{pid}: #{t.backtrace.join("\n#{pid}: ")}\n\n"
597
- end
598
- $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
599
- end
600
-
601
634
  if @status != :restart
602
635
  @binder.close
603
636
  end
604
637
 
605
- if @thread_pool
606
- if timeout = options[:force_shutdown_after]
607
- @thread_pool.shutdown timeout.to_f
608
- else
609
- @thread_pool.shutdown
610
- end
611
- end
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
612
639
  end
613
640
 
614
641
  def notify_safely(message)
615
642
  @notify << message
616
643
  rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
617
644
  # The server, in another thread, is shutting down
618
- Puma::Util.purge_interrupt_queue
619
645
  rescue RuntimeError => e
620
646
  # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
621
647
  if e.message.include?('IOError')
622
- Puma::Util.purge_interrupt_queue
648
+ # ignore
623
649
  else
624
650
  raise e
625
651
  end
626
652
  end
627
653
  private :notify_safely
628
654
 
629
- # 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
630
656
  # off the request queue before finally exiting.
631
657
 
632
658
  def stop(sync=false)
@@ -650,7 +676,16 @@ module Puma
650
676
 
651
677
  # List of methods invoked by #stats.
652
678
  # @version 5.0.0
653
- STAT_METHODS = [:backlog, :running, :pool_capacity, :max_threads, :requests_count, :busy_threads].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
654
689
 
655
690
  # Returns a hash of stats about the running server for reporting purposes.
656
691
  # @version 5.0.0
@@ -660,9 +695,16 @@ module Puma
660
695
  stats = @thread_pool&.stats || {}
661
696
  stats[:max_threads] = @max_threads
662
697
  stats[:requests_count] = @requests_count
698
+ stats[:reactor_max] = @reactor.reactor_max if @reactor
699
+ reset_max
663
700
  stats
664
701
  end
665
702
 
703
+ def reset_max
704
+ @reactor.reactor_max = 0 if @reactor
705
+ @thread_pool&.reset_max
706
+ end
707
+
666
708
  # below are 'delegations' to binder
667
709
  # remove in Puma 7?
668
710
 
@@ -680,6 +722,49 @@ module Puma
680
722
  @binder.add_unix_listener path, umask, mode, backlog
681
723
  end
682
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
+
683
768
  # @!attribute [r] connected_ports
684
769
  def connected_ports
685
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