puma 6.0.0 → 6.6.0

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +392 -13
  3. data/LICENSE +0 -0
  4. data/README.md +135 -29
  5. data/bin/puma-wild +0 -0
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +0 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +11 -1
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/java_options.md +54 -0
  14. data/docs/jungle/README.md +0 -0
  15. data/docs/jungle/rc.d/README.md +0 -0
  16. data/docs/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +12 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +4 -0
  20. data/docs/rails_dev_mode.md +0 -0
  21. data/docs/restart.md +1 -0
  22. data/docs/signals.md +2 -2
  23. data/docs/stats.md +8 -3
  24. data/docs/systemd.md +13 -7
  25. data/docs/testing_benchmarks_local_files.md +0 -0
  26. data/docs/testing_test_rackup_ci_files.md +0 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  28. data/ext/puma_http11/ext_help.h +0 -0
  29. data/ext/puma_http11/extconf.rb +21 -14
  30. data/ext/puma_http11/http11_parser.c +0 -0
  31. data/ext/puma_http11/http11_parser.h +0 -0
  32. data/ext/puma_http11/http11_parser.java.rl +0 -0
  33. data/ext/puma_http11/http11_parser.rl +0 -0
  34. data/ext/puma_http11/http11_parser_common.rl +0 -0
  35. data/ext/puma_http11/mini_ssl.c +107 -10
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +30 -7
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +0 -0
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +2 -1
  40. data/ext/puma_http11/puma_http11.c +4 -1
  41. data/lib/puma/app/status.rb +1 -1
  42. data/lib/puma/binder.rb +26 -15
  43. data/lib/puma/cli.rb +13 -5
  44. data/lib/puma/client.rb +113 -26
  45. data/lib/puma/cluster/worker.rb +14 -6
  46. data/lib/puma/cluster/worker_handle.rb +4 -5
  47. data/lib/puma/cluster.rb +93 -22
  48. data/lib/puma/commonlogger.rb +21 -14
  49. data/lib/puma/configuration.rb +42 -22
  50. data/lib/puma/const.rb +149 -89
  51. data/lib/puma/control_cli.rb +16 -9
  52. data/lib/puma/detect.rb +5 -4
  53. data/lib/puma/dsl.rb +432 -40
  54. data/lib/puma/error_logger.rb +6 -5
  55. data/lib/puma/events.rb +0 -0
  56. data/lib/puma/io_buffer.rb +10 -0
  57. data/lib/puma/jruby_restart.rb +0 -16
  58. data/lib/puma/json_serialization.rb +0 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +0 -0
  60. data/lib/puma/launcher.rb +29 -29
  61. data/lib/puma/log_writer.rb +23 -13
  62. data/lib/puma/minissl/context_builder.rb +4 -0
  63. data/lib/puma/minissl.rb +23 -0
  64. data/lib/puma/null_io.rb +42 -2
  65. data/lib/puma/plugin/systemd.rb +90 -0
  66. data/lib/puma/plugin/tmp_restart.rb +0 -0
  67. data/lib/puma/plugin.rb +0 -0
  68. data/lib/puma/rack/builder.rb +2 -2
  69. data/lib/puma/rack/urlmap.rb +1 -1
  70. data/lib/puma/rack_default.rb +18 -3
  71. data/lib/puma/reactor.rb +17 -8
  72. data/lib/puma/request.rb +207 -126
  73. data/lib/puma/runner.rb +26 -4
  74. data/lib/puma/sd_notify.rb +146 -0
  75. data/lib/puma/server.rb +121 -49
  76. data/lib/puma/single.rb +3 -1
  77. data/lib/puma/state_file.rb +2 -2
  78. data/lib/puma/thread_pool.rb +56 -9
  79. data/lib/puma/util.rb +1 -1
  80. data/lib/puma.rb +1 -3
  81. data/lib/rack/handler/puma.rb +116 -86
  82. data/tools/Dockerfile +2 -2
  83. data/tools/trickletest.rb +0 -0
  84. metadata +12 -13
  85. data/lib/puma/systemd.rb +0 -47
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Puma
6
+ # The MIT License
7
+ #
8
+ # Copyright (c) 2017-2022 Agis Anastasopoulos
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ # this software and associated documentation files (the "Software"), to deal in
12
+ # the Software without restriction, including without limitation the rights to
13
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ # the Software, and to permit persons to whom the Software is furnished to do so,
15
+ # subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in all
18
+ # copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #
27
+ # This is a copy of https://github.com/agis/ruby-sdnotify as of commit cca575c
28
+ # The only changes made was "rehoming" it within the Puma module to avoid
29
+ # namespace collisions and applying standard's code formatting style.
30
+ #
31
+ # SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
32
+ # notify systemd about state changes. Methods of this package are no-op on
33
+ # non-systemd systems (eg. Darwin).
34
+ #
35
+ # The API maps closely to the original implementation of sd_notify(3),
36
+ # therefore be sure to check the official man pages prior to using SdNotify.
37
+ #
38
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
39
+ module SdNotify
40
+ # Exception raised when there's an error writing to the notification socket
41
+ class NotifyError < RuntimeError; end
42
+
43
+ READY = "READY=1"
44
+ RELOADING = "RELOADING=1"
45
+ STOPPING = "STOPPING=1"
46
+ STATUS = "STATUS="
47
+ ERRNO = "ERRNO="
48
+ MAINPID = "MAINPID="
49
+ WATCHDOG = "WATCHDOG=1"
50
+ FDSTORE = "FDSTORE=1"
51
+
52
+ def self.ready(unset_env=false)
53
+ notify(READY, unset_env)
54
+ end
55
+
56
+ def self.reloading(unset_env=false)
57
+ notify(RELOADING, unset_env)
58
+ end
59
+
60
+ def self.stopping(unset_env=false)
61
+ notify(STOPPING, unset_env)
62
+ end
63
+
64
+ # @param status [String] a custom status string that describes the current
65
+ # state of the service
66
+ def self.status(status, unset_env=false)
67
+ notify("#{STATUS}#{status}", unset_env)
68
+ end
69
+
70
+ # @param errno [Integer]
71
+ def self.errno(errno, unset_env=false)
72
+ notify("#{ERRNO}#{errno}", unset_env)
73
+ end
74
+
75
+ # @param pid [Integer]
76
+ def self.mainpid(pid, unset_env=false)
77
+ notify("#{MAINPID}#{pid}", unset_env)
78
+ end
79
+
80
+ def self.watchdog(unset_env=false)
81
+ notify(WATCHDOG, unset_env)
82
+ end
83
+
84
+ def self.fdstore(unset_env=false)
85
+ notify(FDSTORE, unset_env)
86
+ end
87
+
88
+ # @param [Boolean] true if the service manager expects watchdog keep-alive
89
+ # notification messages to be sent from this process.
90
+ #
91
+ # If the $WATCHDOG_USEC environment variable is set,
92
+ # and the $WATCHDOG_PID variable is unset or set to the PID of the current
93
+ # process
94
+ #
95
+ # @note Unlike sd_watchdog_enabled(3), this method does not mutate the
96
+ # environment.
97
+ def self.watchdog?
98
+ wd_usec = ENV["WATCHDOG_USEC"]
99
+ wd_pid = ENV["WATCHDOG_PID"]
100
+
101
+ return false if !wd_usec
102
+
103
+ begin
104
+ wd_usec = Integer(wd_usec)
105
+ rescue
106
+ return false
107
+ end
108
+
109
+ return false if wd_usec <= 0
110
+ return true if !wd_pid || wd_pid == $$.to_s
111
+
112
+ false
113
+ end
114
+
115
+ # Notify systemd with the provided state, via the notification socket, if
116
+ # any.
117
+ #
118
+ # Generally this method will be used indirectly through the other methods
119
+ # of the library.
120
+ #
121
+ # @param state [String]
122
+ # @param unset_env [Boolean]
123
+ #
124
+ # @return [Fixnum, nil] the number of bytes written to the notification
125
+ # socket or nil if there was no socket to report to (eg. the program wasn't
126
+ # started by systemd)
127
+ #
128
+ # @raise [NotifyError] if there was an error communicating with the systemd
129
+ # socket
130
+ #
131
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
132
+ def self.notify(state, unset_env=false)
133
+ sock = ENV["NOTIFY_SOCKET"]
134
+
135
+ return nil if !sock
136
+
137
+ ENV.delete("NOTIFY_SOCKET") if unset_env
138
+
139
+ begin
140
+ Addrinfo.unix(sock, :DGRAM).connect { |s| s.write state }
141
+ rescue StandardError => e
142
+ raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/puma/server.rb CHANGED
@@ -11,15 +11,16 @@ require_relative 'reactor'
11
11
  require_relative 'client'
12
12
  require_relative 'binder'
13
13
  require_relative 'util'
14
- require_relative 'io_buffer'
15
14
  require_relative 'request'
16
15
 
17
16
  require 'socket'
18
17
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
19
- require 'forwardable'
20
18
 
21
19
  module Puma
22
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
+
23
24
  # The HTTP Server itself. Serves out a single Rack app.
24
25
  #
25
26
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -33,8 +34,8 @@ module Puma
33
34
  class Server
34
35
  include Puma::Const
35
36
  include Request
36
- extend Forwardable
37
37
 
38
+ attr_reader :options
38
39
  attr_reader :thread
39
40
  attr_reader :log_writer
40
41
  attr_reader :events
@@ -49,10 +50,6 @@ module Puma
49
50
  attr_accessor :app
50
51
  attr_accessor :binder
51
52
 
52
- def_delegators :@binder, :add_tcp_listener, :add_ssl_listener,
53
- :add_unix_listener, :connected_ports
54
-
55
- ThreadLocalKey = :puma_server
56
53
 
57
54
  # Create a server for the rack app +app+.
58
55
  #
@@ -87,15 +84,32 @@ module Puma
87
84
  UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
88
85
  end
89
86
 
90
- @log_writer = @options.fetch :log_writer, LogWriter.stdio
91
- @early_hints = @options[:early_hints]
92
- @first_data_timeout = @options[:first_data_timeout]
93
- @min_threads = @options[:min_threads]
94
- @max_threads = @options[:max_threads]
95
- @persistent_timeout = @options[:persistent_timeout]
96
- @queue_requests = @options[:queue_requests]
97
- @max_fast_inline = @options[:max_fast_inline]
98
- @io_selector_backend = @options[:io_selector_backend]
87
+ @clustered = (@options.fetch :workers, 0) > 0
88
+ @worker_write = @options[:worker_write]
89
+ @log_writer = @options.fetch :log_writer, LogWriter.stdio
90
+ @early_hints = @options[:early_hints]
91
+ @first_data_timeout = @options[:first_data_timeout]
92
+ @persistent_timeout = @options[:persistent_timeout]
93
+ @idle_timeout = @options[:idle_timeout]
94
+ @min_threads = @options[:min_threads]
95
+ @max_threads = @options[:max_threads]
96
+ @queue_requests = @options[:queue_requests]
97
+ @max_fast_inline = @options[:max_fast_inline]
98
+ @enable_keep_alives = @options[:enable_keep_alives]
99
+ @io_selector_backend = @options[:io_selector_backend]
100
+ @http_content_length_limit = @options[:http_content_length_limit]
101
+
102
+ # make this a hash, since we prefer `key?` over `include?`
103
+ @supported_http_methods =
104
+ if @options[:supported_http_methods] == :any
105
+ :any
106
+ else
107
+ if (ary = @options[:supported_http_methods])
108
+ ary
109
+ else
110
+ SUPPORTED_HTTP_METHODS
111
+ end.sort.product([nil]).to_h.freeze
112
+ end
99
113
 
100
114
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
101
115
  @leak_stack_on_error = @options[:environment] ? temp : true
@@ -109,6 +123,8 @@ module Puma
109
123
  @precheck_closing = true
110
124
 
111
125
  @requests_count = 0
126
+
127
+ @idle_timeout_reached = false
112
128
  end
113
129
 
114
130
  def inherit_binder(bind)
@@ -118,7 +134,7 @@ module Puma
118
134
  class << self
119
135
  # @!attribute [r] current
120
136
  def current
121
- Thread.current[ThreadLocalKey]
137
+ Thread.current.puma_server
122
138
  end
123
139
 
124
140
  # :nodoc:
@@ -217,6 +233,11 @@ module Puma
217
233
  @thread_pool&.pool_capacity
218
234
  end
219
235
 
236
+ # @!attribute [r] busy_threads
237
+ def busy_threads
238
+ @thread_pool&.busy_threads
239
+ end
240
+
220
241
  # Runs the server.
221
242
  #
222
243
  # If +background+ is true (the default) then a thread is spun
@@ -230,16 +251,15 @@ module Puma
230
251
 
231
252
  @status = :run
232
253
 
233
- @thread_pool = ThreadPool.new(thread_name, @options) { |a, b| process_client a, b }
254
+ @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
234
255
 
235
256
  if @queue_requests
236
257
  @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
237
258
  @reactor.run
238
259
  end
239
260
 
240
-
241
- @thread_pool.auto_reap! if @options[:reaping_time]
242
- @thread_pool.auto_trim! if @options[:auto_trim_time]
261
+ @thread_pool.auto_reap! if options[:reaping_time]
262
+ @thread_pool.auto_trim! if @min_threads != @max_threads && options[:auto_trim_time]
243
263
 
244
264
  @check, @notify = Puma::Util.pipe unless @notify
245
265
 
@@ -303,29 +323,49 @@ module Puma
303
323
  sockets = [check] + @binder.ios
304
324
  pool = @thread_pool
305
325
  queue_requests = @queue_requests
306
- drain = @options[:drain_on_shutdown] ? 0 : nil
326
+ drain = options[:drain_on_shutdown] ? 0 : nil
307
327
 
308
- addr_send_name, addr_value = case @options[:remote_address]
328
+ addr_send_name, addr_value = case options[:remote_address]
309
329
  when :value
310
- [:peerip=, @options[:remote_address_value]]
330
+ [:peerip=, options[:remote_address_value]]
311
331
  when :header
312
- [:remote_addr_header=, @options[:remote_address_header]]
332
+ [:remote_addr_header=, options[:remote_address_header]]
313
333
  when :proxy_protocol
314
- [:expect_proxy_proto=, @options[:remote_address_proxy_protocol]]
334
+ [:expect_proxy_proto=, options[:remote_address_proxy_protocol]]
315
335
  else
316
336
  [nil, nil]
317
337
  end
318
338
 
319
339
  while @status == :run || (drain && shutting_down?)
320
340
  begin
321
- ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
322
- break unless ios
341
+ ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
342
+ unless ios
343
+ unless shutting_down?
344
+ @idle_timeout_reached = true
345
+
346
+ if @clustered
347
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
348
+ next
349
+ else
350
+ @log_writer.log "- Idle timeout reached"
351
+ @status = :stop
352
+ end
353
+ end
354
+
355
+ break
356
+ end
357
+
358
+ if @idle_timeout_reached && @clustered
359
+ @idle_timeout_reached = false
360
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
361
+ end
362
+
323
363
  ios.first.each do |sock|
324
364
  if sock == check
325
365
  break if handle_check
326
366
  else
327
367
  pool.wait_until_not_full
328
- pool.wait_for_less_busy_worker(@options[:wait_for_less_busy_worker])
368
+ pool.wait_for_less_busy_worker(options[:wait_for_less_busy_worker]) if @clustered
329
369
 
330
370
  io = begin
331
371
  sock.accept_nonblock
@@ -335,6 +375,7 @@ module Puma
335
375
  drain += 1 if shutting_down?
336
376
  pool << Client.new(io, @binder.env(sock)).tap { |c|
337
377
  c.listener = sock
378
+ c.http_content_length_limit = @http_content_length_limit
338
379
  c.send(addr_send_name, addr_value) if addr_value
339
380
  }
340
381
  end
@@ -354,6 +395,7 @@ module Puma
354
395
  @queue_requests = false
355
396
  @reactor.shutdown
356
397
  end
398
+
357
399
  graceful_shutdown if @status == :stop || @status == :restart
358
400
  rescue Exception => e
359
401
  @log_writer.unknown_error e, nil, "Exception handling servers"
@@ -401,11 +443,11 @@ module Puma
401
443
  # returning.
402
444
  #
403
445
  # Return true if one or more requests were processed.
404
- def process_client(client, buffer)
446
+ def process_client(client)
405
447
  # Advertise this server into the thread
406
- Thread.current[ThreadLocalKey] = self
448
+ Thread.current.puma_server = self
407
449
 
408
- clean_thread_locals = @options[:clean_thread_locals]
450
+ clean_thread_locals = options[:clean_thread_locals]
409
451
  close_socket = true
410
452
 
411
453
  requests = 0
@@ -427,15 +469,13 @@ module Puma
427
469
 
428
470
  while true
429
471
  @requests_count += 1
430
- case handle_request(client, buffer, requests + 1)
472
+ case handle_request(client, requests + 1)
431
473
  when false
432
474
  break
433
475
  when :async
434
476
  close_socket = false
435
477
  break
436
478
  when true
437
- buffer.reset
438
-
439
479
  ThreadPool.clean_thread_locals if clean_thread_locals
440
480
 
441
481
  requests += 1
@@ -465,11 +505,11 @@ module Puma
465
505
  end
466
506
  true
467
507
  rescue StandardError => e
468
- client_error(e, client)
508
+ client_error(e, client, requests)
469
509
  # The ensure tries to close +client+ down
470
510
  requests > 0
471
511
  ensure
472
- buffer.reset
512
+ client.io_buffer.reset
473
513
 
474
514
  begin
475
515
  client.close if close_socket
@@ -493,22 +533,22 @@ module Puma
493
533
  # :nocov:
494
534
 
495
535
  # Handle various error types thrown by Client I/O operations.
496
- def client_error(e, client)
536
+ def client_error(e, client, requests = 1)
497
537
  # Swallow, do not log
498
538
  return if [ConnectionError, EOFError].include?(e.class)
499
539
 
500
- lowlevel_error(e, client.env)
501
540
  case e
502
541
  when MiniSSL::SSLError
542
+ lowlevel_error(e, client.env)
503
543
  @log_writer.ssl_error e, client.io
504
544
  when HttpParserError
505
- client.write_error(400)
545
+ response_to_error(client, requests, e, 400)
506
546
  @log_writer.parse_error e, client
507
547
  when HttpParserError501
508
- client.write_error(501)
548
+ response_to_error(client, requests, e, 501)
509
549
  @log_writer.parse_error e, client
510
550
  else
511
- client.write_error(500)
551
+ response_to_error(client, requests, e, 500)
512
552
  @log_writer.unknown_error e, nil, "Read"
513
553
  end
514
554
  end
@@ -516,7 +556,7 @@ module Puma
516
556
  # A fallback rack response if +@app+ raises as exception.
517
557
  #
518
558
  def lowlevel_error(e, env, status=500)
519
- if handler = @options[:lowlevel_error_handler]
559
+ if handler = options[:lowlevel_error_handler]
520
560
  if handler.arity == 1
521
561
  return handler.call(e)
522
562
  elsif handler.arity == 2
@@ -530,14 +570,20 @@ module Puma
530
570
  backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
531
571
  [status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
532
572
  else
533
- [status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
573
+ [status, {}, [""]]
534
574
  end
535
575
  end
536
576
 
577
+ def response_to_error(client, requests, err, status_code)
578
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
579
+ prepare_response(status, headers, res_body, requests, client)
580
+ end
581
+ private :response_to_error
582
+
537
583
  # Wait for all outstanding requests to finish.
538
584
  #
539
585
  def graceful_shutdown
540
- if @options[:shutdown_debug]
586
+ if options[:shutdown_debug]
541
587
  threads = Thread.list
542
588
  total = threads.size
543
589
 
@@ -557,7 +603,7 @@ module Puma
557
603
  end
558
604
 
559
605
  if @thread_pool
560
- if timeout = @options[:force_shutdown_after]
606
+ if timeout = options[:force_shutdown_after]
561
607
  @thread_pool.shutdown timeout.to_f
562
608
  else
563
609
  @thread_pool.shutdown
@@ -567,7 +613,7 @@ module Puma
567
613
 
568
614
  def notify_safely(message)
569
615
  @notify << message
570
- rescue IOError, NoMethodError, Errno::EPIPE
616
+ rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
571
617
  # The server, in another thread, is shutting down
572
618
  Puma::Util.purge_interrupt_queue
573
619
  rescue RuntimeError => e
@@ -604,13 +650,39 @@ module Puma
604
650
 
605
651
  # List of methods invoked by #stats.
606
652
  # @version 5.0.0
607
- STAT_METHODS = [:backlog, :running, :pool_capacity, :max_threads, :requests_count].freeze
653
+ STAT_METHODS = [:backlog, :running, :pool_capacity, :max_threads, :requests_count, :busy_threads].freeze
608
654
 
609
655
  # Returns a hash of stats about the running server for reporting purposes.
610
656
  # @version 5.0.0
611
657
  # @!attribute [r] stats
658
+ # @return [Hash] hash containing stat info from `Server` and `ThreadPool`
612
659
  def stats
613
- STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
660
+ stats = @thread_pool&.stats || {}
661
+ stats[:max_threads] = @max_threads
662
+ stats[:requests_count] = @requests_count
663
+ stats
664
+ end
665
+
666
+ # below are 'delegations' to binder
667
+ # remove in Puma 7?
668
+
669
+
670
+ def add_tcp_listener(host, port, optimize_for_latency = true, backlog = 1024)
671
+ @binder.add_tcp_listener host, port, optimize_for_latency, backlog
672
+ end
673
+
674
+ def add_ssl_listener(host, port, ctx, optimize_for_latency = true,
675
+ backlog = 1024)
676
+ @binder.add_ssl_listener host, port, ctx, optimize_for_latency, backlog
677
+ end
678
+
679
+ def add_unix_listener(path, umask = nil, mode = nil, backlog = 1024)
680
+ @binder.add_unix_listener path, umask, mode, backlog
681
+ end
682
+
683
+ # @!attribute [r] connected_ports
684
+ def connected_ports
685
+ @binder.connected_ports
614
686
  end
615
687
  end
616
688
  end
data/lib/puma/single.rb CHANGED
@@ -16,7 +16,7 @@ module Puma
16
16
  # @!attribute [r] stats
17
17
  def stats
18
18
  {
19
- started_at: @started_at.utc.iso8601
19
+ started_at: utc_iso8601(@started_at)
20
20
  }.merge(@server.stats).merge(super)
21
21
  end
22
22
 
@@ -57,6 +57,8 @@ module Puma
57
57
 
58
58
  @events.fire_on_booted!
59
59
 
60
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
61
+
60
62
  begin
61
63
  server_thread.join
62
64
  rescue Interrupt
@@ -56,11 +56,11 @@ module Puma
56
56
  end
57
57
 
58
58
  ALLOWED_FIELDS.each do |f|
59
- define_method f do
59
+ define_method f.to_sym do
60
60
  @options[f]
61
61
  end
62
62
 
63
- define_method "#{f}=" do |v|
63
+ define_method :"#{f}=" do |v|
64
64
  @options[f] = v
65
65
  end
66
66
  end
@@ -44,10 +44,15 @@ module Puma
44
44
  @name = name
45
45
  @min = Integer(options[:min_threads])
46
46
  @max = Integer(options[:max_threads])
47
+ # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
48
+ # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
49
+ # makes stubbing constants difficult.
50
+ @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
47
51
  @block = block
48
- @extra = [::Puma::IOBuffer]
49
52
  @out_of_band = options[:out_of_band]
50
53
  @clean_thread_locals = options[:clean_thread_locals]
54
+ @before_thread_start = options[:before_thread_start]
55
+ @before_thread_exit = options[:before_thread_exit]
51
56
  @reaping_time = options[:reaping_time]
52
57
  @auto_trim_time = options[:auto_trim_time]
53
58
 
@@ -80,6 +85,18 @@ module Puma
80
85
  end
81
86
  end
82
87
 
88
+ # generate stats hash so as not to perform multiple locks
89
+ # @return [Hash] hash containing stat info from ThreadPool
90
+ def stats
91
+ with_mutex do
92
+ { backlog: @todo.size,
93
+ running: @spawned,
94
+ pool_capacity: @waiting + (@max - @spawned),
95
+ busy_threads: @spawned - @waiting + @todo.size
96
+ }
97
+ end
98
+ end
99
+
83
100
  # How many objects have yet to be processed by the pool?
84
101
  #
85
102
  def backlog
@@ -104,6 +121,7 @@ module Puma
104
121
  def spawn_thread
105
122
  @spawned += 1
106
123
 
124
+ trigger_before_thread_start_hooks
107
125
  th = Thread.new(@spawned) do |spawned|
108
126
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
109
127
  todo = @todo
@@ -112,8 +130,6 @@ module Puma
112
130
  not_empty = @not_empty
113
131
  not_full = @not_full
114
132
 
115
- extra = @extra.map { |i| i.new }
116
-
117
133
  while true
118
134
  work = nil
119
135
 
@@ -124,6 +140,7 @@ module Puma
124
140
  @spawned -= 1
125
141
  @workers.delete th
126
142
  not_full.signal
143
+ trigger_before_thread_exit_hooks
127
144
  Thread.exit
128
145
  end
129
146
 
@@ -147,7 +164,7 @@ module Puma
147
164
  end
148
165
 
149
166
  begin
150
- @out_of_band_pending = true if block.call(work, *extra)
167
+ @out_of_band_pending = true if block.call(work)
151
168
  rescue Exception => e
152
169
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
153
170
  end
@@ -161,6 +178,36 @@ module Puma
161
178
 
162
179
  private :spawn_thread
163
180
 
181
+ def trigger_before_thread_start_hooks
182
+ return unless @before_thread_start&.any?
183
+
184
+ @before_thread_start.each do |b|
185
+ begin
186
+ b.call
187
+ rescue Exception => e
188
+ STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
189
+ end
190
+ end
191
+ nil
192
+ end
193
+
194
+ private :trigger_before_thread_start_hooks
195
+
196
+ def trigger_before_thread_exit_hooks
197
+ return unless @before_thread_exit&.any?
198
+
199
+ @before_thread_exit.each do |b|
200
+ begin
201
+ b.call
202
+ rescue Exception => e
203
+ STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
204
+ end
205
+ end
206
+ nil
207
+ end
208
+
209
+ private :trigger_before_thread_exit_hooks
210
+
164
211
  # @version 5.0.0
165
212
  def trigger_out_of_band_hook
166
213
  return false unless @out_of_band&.any?
@@ -323,12 +370,12 @@ module Puma
323
370
  end
324
371
 
325
372
  def auto_trim!(timeout=@auto_trim_time)
326
- @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
373
+ @auto_trim = Automaton.new(self, timeout, "#{@name} tp trim", :trim)
327
374
  @auto_trim.start!
328
375
  end
329
376
 
330
377
  def auto_reap!(timeout=@reaping_time)
331
- @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
378
+ @reaper = Automaton.new(self, timeout, "#{@name} tp reap", :reap)
332
379
  @reaper.start!
333
380
  end
334
381
 
@@ -347,8 +394,8 @@ module Puma
347
394
 
348
395
  # Tell all threads in the pool to exit and wait for them to finish.
349
396
  # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
350
- # Next, wait an extra +grace+ seconds then force-kill remaining threads.
351
- # Finally, wait +kill_grace+ seconds for remaining threads to exit.
397
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
398
+ # threads. Finally, wait 1 second for remaining threads to exit.
352
399
  #
353
400
  def shutdown(timeout=-1)
354
401
  threads = with_mutex do
@@ -385,7 +432,7 @@ module Puma
385
432
  t.raise ForceShutdown if t[:with_force_shutdown]
386
433
  end
387
434
  end
388
- join.call(SHUTDOWN_GRACE_TIME)
435
+ join.call(@shutdown_grace_time)
389
436
 
390
437
  # If threads are _still_ running, forcefully kill them and wait to finish.
391
438
  threads.each(&:kill)
data/lib/puma/util.rb CHANGED
@@ -11,7 +11,7 @@ module Puma
11
11
  end
12
12
 
13
13
  # An instance method on Thread has been provided to address https://bugs.ruby-lang.org/issues/13632,
14
- # which currently effects some older versions of Ruby: 2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1
14
+ # which currently affects some older versions of Ruby: 2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1
15
15
  # Additional context: https://github.com/puma/puma/pull/1345
16
16
  def purge_interrupt_queue
17
17
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue