puma 5.6.5 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +338 -14
  3. data/LICENSE +0 -0
  4. data/README.md +79 -29
  5. data/bin/puma-wild +1 -1
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +34 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +1 -3
  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/jungle/README.md +0 -0
  14. data/docs/jungle/rc.d/README.md +0 -0
  15. data/docs/jungle/rc.d/puma.conf +0 -0
  16. data/docs/kubernetes.md +12 -0
  17. data/docs/nginx.md +1 -1
  18. data/docs/plugins.md +0 -0
  19. data/docs/rails_dev_mode.md +0 -0
  20. data/docs/restart.md +1 -0
  21. data/docs/signals.md +0 -0
  22. data/docs/stats.md +0 -0
  23. data/docs/systemd.md +3 -6
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  27. data/ext/puma_http11/ext_help.h +0 -0
  28. data/ext/puma_http11/extconf.rb +16 -9
  29. data/ext/puma_http11/http11_parser.c +1 -1
  30. data/ext/puma_http11/http11_parser.h +1 -1
  31. data/ext/puma_http11/http11_parser.java.rl +2 -2
  32. data/ext/puma_http11/http11_parser.rl +2 -2
  33. data/ext/puma_http11/http11_parser_common.rl +2 -2
  34. data/ext/puma_http11/mini_ssl.c +127 -19
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  39. data/ext/puma_http11/puma_http11.c +17 -9
  40. data/lib/puma/app/status.rb +4 -4
  41. data/lib/puma/binder.rb +50 -53
  42. data/lib/puma/cli.rb +16 -18
  43. data/lib/puma/client.rb +100 -26
  44. data/lib/puma/cluster/worker.rb +18 -11
  45. data/lib/puma/cluster/worker_handle.rb +4 -1
  46. data/lib/puma/cluster.rb +102 -40
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +77 -59
  49. data/lib/puma/const.rb +129 -92
  50. data/lib/puma/control_cli.rb +15 -11
  51. data/lib/puma/detect.rb +7 -4
  52. data/lib/puma/dsl.rb +250 -56
  53. data/lib/puma/error_logger.rb +18 -9
  54. data/lib/puma/events.rb +6 -126
  55. data/lib/puma/io_buffer.rb +39 -4
  56. data/lib/puma/jruby_restart.rb +2 -1
  57. data/lib/puma/json_serialization.rb +0 -0
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +102 -175
  60. data/lib/puma/log_writer.rb +147 -0
  61. data/lib/puma/minissl/context_builder.rb +26 -12
  62. data/lib/puma/minissl.rb +104 -11
  63. data/lib/puma/null_io.rb +16 -2
  64. data/lib/puma/plugin/systemd.rb +90 -0
  65. data/lib/puma/plugin/tmp_restart.rb +1 -1
  66. data/lib/puma/plugin.rb +0 -0
  67. data/lib/puma/rack/builder.rb +6 -6
  68. data/lib/puma/rack/urlmap.rb +1 -1
  69. data/lib/puma/rack_default.rb +19 -4
  70. data/lib/puma/reactor.rb +19 -10
  71. data/lib/puma/request.rb +365 -170
  72. data/lib/puma/runner.rb +56 -20
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +137 -89
  75. data/lib/puma/single.rb +13 -11
  76. data/lib/puma/state_file.rb +3 -6
  77. data/lib/puma/thread_pool.rb +57 -19
  78. data/lib/puma/util.rb +0 -11
  79. data/lib/puma.rb +12 -11
  80. data/lib/rack/handler/puma.rb +113 -86
  81. data/tools/Dockerfile +2 -2
  82. data/tools/trickletest.rb +0 -0
  83. metadata +11 -6
  84. data/lib/puma/queue_close.rb +0 -26
  85. data/lib/puma/systemd.rb +0 -46
data/lib/puma/server.rb CHANGED
@@ -2,20 +2,19 @@
2
2
 
3
3
  require 'stringio'
4
4
 
5
- require 'puma/thread_pool'
6
- require 'puma/const'
7
- require 'puma/events'
8
- require 'puma/null_io'
9
- require 'puma/reactor'
10
- require 'puma/client'
11
- require 'puma/binder'
12
- require 'puma/util'
13
- require 'puma/io_buffer'
14
- require 'puma/request'
5
+ require_relative 'thread_pool'
6
+ require_relative 'const'
7
+ require_relative 'log_writer'
8
+ require_relative 'events'
9
+ require_relative 'null_io'
10
+ require_relative 'reactor'
11
+ require_relative 'client'
12
+ require_relative 'binder'
13
+ require_relative 'util'
14
+ require_relative 'request'
15
15
 
16
16
  require 'socket'
17
- require 'io/wait'
18
- require 'forwardable'
17
+ require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
19
18
 
20
19
  module Puma
21
20
 
@@ -30,39 +29,30 @@ module Puma
30
29
  #
31
30
  # Each `Puma::Server` will have one reactor and one thread pool.
32
31
  class Server
33
-
34
32
  include Puma::Const
35
33
  include Request
36
- extend Forwardable
37
34
 
38
35
  attr_reader :thread
36
+ attr_reader :log_writer
39
37
  attr_reader :events
40
38
  attr_reader :min_threads, :max_threads # for #stats
41
39
  attr_reader :requests_count # @version 5.0.0
42
- attr_reader :log_writer # to help with backports
43
40
 
44
41
  # @todo the following may be deprecated in the future
45
42
  attr_reader :auto_trim_time, :early_hints, :first_data_timeout,
46
43
  :leak_stack_on_error,
47
44
  :persistent_timeout, :reaping_time
48
45
 
49
- # @deprecated v6.0.0
50
- attr_writer :auto_trim_time, :early_hints, :first_data_timeout,
51
- :leak_stack_on_error, :min_threads, :max_threads,
52
- :persistent_timeout, :reaping_time
53
-
54
46
  attr_accessor :app
55
47
  attr_accessor :binder
56
48
 
57
- def_delegators :@binder, :add_tcp_listener, :add_ssl_listener,
58
- :add_unix_listener, :connected_ports
59
-
60
- ThreadLocalKey = :puma_server
49
+ THREAD_LOCAL_KEY = :puma_server
61
50
 
62
51
  # Create a server for the rack app +app+.
63
52
  #
64
- # +events+ is an object which will be called when certain error events occur
65
- # to be handled. See Puma::Events for the list of current methods to implement.
53
+ # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
54
+ #
55
+ # +events+ is a Puma::Events object used to notify application status events.
66
56
  #
67
57
  # Server#run returns a thread that you can join on to wait for the server
68
58
  # to do its work.
@@ -71,35 +61,56 @@ module Puma
71
61
  # and have default values set via +fetch+. Normally the values are set via
72
62
  # `::Puma::Configuration.puma_default_options`.
73
63
  #
74
- def initialize(app, events=Events.stdio, options={})
64
+ # @note The `events` parameter is set to nil, and set to `Events.new` in code.
65
+ # Often `options` needs to be passed, but `events` does not. Using nil allows
66
+ # calling code to not require events.rb.
67
+ #
68
+ def initialize(app, events = nil, options = {})
75
69
  @app = app
76
- @events = events
77
- @log_writer = events
70
+ @events = events || Events.new
78
71
 
79
72
  @check, @notify = nil
80
73
  @status = :stop
81
74
 
82
- @auto_trim_time = 30
83
- @reaping_time = 1
84
-
85
75
  @thread = nil
86
76
  @thread_pool = nil
87
77
 
88
- @options = options
78
+ @options = if options.is_a?(UserFileDefaultOptions)
79
+ options
80
+ else
81
+ UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
82
+ end
89
83
 
90
- @early_hints = options.fetch :early_hints, nil
91
- @first_data_timeout = options.fetch :first_data_timeout, FIRST_DATA_TIMEOUT
92
- @min_threads = options.fetch :min_threads, 0
93
- @max_threads = options.fetch :max_threads , (Puma.mri? ? 5 : 16)
94
- @persistent_timeout = options.fetch :persistent_timeout, PERSISTENT_TIMEOUT
95
- @queue_requests = options.fetch :queue_requests, true
96
- @max_fast_inline = options.fetch :max_fast_inline, MAX_FAST_INLINE
97
- @io_selector_backend = options.fetch :io_selector_backend, :auto
84
+ @clustered = (@options.fetch :workers, 0) > 0
85
+ @worker_write = @options[:worker_write]
86
+ @log_writer = @options.fetch :log_writer, LogWriter.stdio
87
+ @early_hints = @options[:early_hints]
88
+ @first_data_timeout = @options[:first_data_timeout]
89
+ @persistent_timeout = @options[:persistent_timeout]
90
+ @idle_timeout = @options[:idle_timeout]
91
+ @min_threads = @options[:min_threads]
92
+ @max_threads = @options[:max_threads]
93
+ @queue_requests = @options[:queue_requests]
94
+ @max_fast_inline = @options[:max_fast_inline]
95
+ @io_selector_backend = @options[:io_selector_backend]
96
+ @http_content_length_limit = @options[:http_content_length_limit]
97
+
98
+ # make this a hash, since we prefer `key?` over `include?`
99
+ @supported_http_methods =
100
+ if @options[:supported_http_methods] == :any
101
+ :any
102
+ else
103
+ if (ary = @options[:supported_http_methods])
104
+ ary
105
+ else
106
+ SUPPORTED_HTTP_METHODS
107
+ end.sort.product([nil]).to_h.freeze
108
+ end
98
109
 
99
110
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
100
111
  @leak_stack_on_error = @options[:environment] ? temp : true
101
112
 
102
- @binder = Binder.new(events)
113
+ @binder = Binder.new(log_writer)
103
114
 
104
115
  ENV['RACK_ENV'] ||= "development"
105
116
 
@@ -108,6 +119,8 @@ module Puma
108
119
  @precheck_closing = true
109
120
 
110
121
  @requests_count = 0
122
+
123
+ @idle_timeout_reached = false
111
124
  end
112
125
 
113
126
  def inherit_binder(bind)
@@ -117,7 +130,7 @@ module Puma
117
130
  class << self
118
131
  # @!attribute [r] current
119
132
  def current
120
- Thread.current[ThreadLocalKey]
133
+ Thread.current[THREAD_LOCAL_KEY]
121
134
  end
122
135
 
123
136
  # :nodoc:
@@ -195,12 +208,12 @@ module Puma
195
208
 
196
209
  # @!attribute [r] backlog
197
210
  def backlog
198
- @thread_pool and @thread_pool.backlog
211
+ @thread_pool&.backlog
199
212
  end
200
213
 
201
214
  # @!attribute [r] running
202
215
  def running
203
- @thread_pool and @thread_pool.spawned
216
+ @thread_pool&.spawned
204
217
  end
205
218
 
206
219
 
@@ -213,7 +226,7 @@ module Puma
213
226
  # value would be 4 until it finishes processing.
214
227
  # @!attribute [r] pool_capacity
215
228
  def pool_capacity
216
- @thread_pool and @thread_pool.pool_capacity
229
+ @thread_pool&.pool_capacity
217
230
  end
218
231
 
219
232
  # Runs the server.
@@ -229,29 +242,16 @@ module Puma
229
242
 
230
243
  @status = :run
231
244
 
232
- @thread_pool = ThreadPool.new(
233
- thread_name,
234
- @min_threads,
235
- @max_threads,
236
- ::Puma::IOBuffer,
237
- &method(:process_client)
238
- )
239
-
240
- @thread_pool.out_of_band_hook = @options[:out_of_band]
241
- @thread_pool.clean_thread_locals = @options[:clean_thread_locals]
245
+ @thread_pool = ThreadPool.new(thread_name, @options) { |client| process_client client }
242
246
 
243
247
  if @queue_requests
244
- @reactor = Reactor.new(@io_selector_backend, &method(:reactor_wakeup))
248
+ @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
245
249
  @reactor.run
246
250
  end
247
251
 
248
- if @reaping_time
249
- @thread_pool.auto_reap!(@reaping_time)
250
- end
251
252
 
252
- if @auto_trim_time
253
- @thread_pool.auto_trim!(@auto_trim_time)
254
- end
253
+ @thread_pool.auto_reap! if @options[:reaping_time]
254
+ @thread_pool.auto_trim! if @options[:auto_trim_time]
255
255
 
256
256
  @check, @notify = Puma::Util.pipe unless @notify
257
257
 
@@ -330,8 +330,28 @@ module Puma
330
330
 
331
331
  while @status == :run || (drain && shutting_down?)
332
332
  begin
333
- ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
334
- break unless ios
333
+ ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
334
+ unless ios
335
+ unless shutting_down?
336
+ @idle_timeout_reached = true
337
+
338
+ if @clustered
339
+ @worker_write << "i#{Process.pid}\n" rescue nil
340
+ next
341
+ else
342
+ @log_writer.log "- Idle timeout reached"
343
+ @status = :stop
344
+ end
345
+ end
346
+
347
+ break
348
+ end
349
+
350
+ if @idle_timeout_reached && @clustered
351
+ @idle_timeout_reached = false
352
+ @worker_write << "i#{Process.pid}\n" rescue nil
353
+ end
354
+
335
355
  ios.first.each do |sock|
336
356
  if sock == check
337
357
  break if handle_check
@@ -347,6 +367,7 @@ module Puma
347
367
  drain += 1 if shutting_down?
348
368
  pool << Client.new(io, @binder.env(sock)).tap { |c|
349
369
  c.listener = sock
370
+ c.http_content_length_limit = @http_content_length_limit
350
371
  c.send(addr_send_name, addr_value) if addr_value
351
372
  }
352
373
  end
@@ -355,27 +376,27 @@ module Puma
355
376
  # In the case that any of the sockets are unexpectedly close.
356
377
  raise
357
378
  rescue StandardError => e
358
- @events.unknown_error e, nil, "Listen loop"
379
+ @log_writer.unknown_error e, nil, "Listen loop"
359
380
  end
360
381
  end
361
382
 
362
- @events.debug "Drained #{drain} additional connections." if drain
383
+ @log_writer.debug "Drained #{drain} additional connections." if drain
363
384
  @events.fire :state, @status
364
385
 
365
386
  if queue_requests
366
387
  @queue_requests = false
367
388
  @reactor.shutdown
368
389
  end
390
+
369
391
  graceful_shutdown if @status == :stop || @status == :restart
370
392
  rescue Exception => e
371
- @events.unknown_error e, nil, "Exception handling servers"
393
+ @log_writer.unknown_error e, nil, "Exception handling servers"
372
394
  ensure
373
- # RuntimeError is Ruby 2.2 issue, can't modify frozen IOError
374
395
  # Errno::EBADF is infrequently raised
375
396
  [@check, @notify].each do |io|
376
397
  begin
377
398
  io.close unless io.closed?
378
- rescue Errno::EBADF, RuntimeError
399
+ rescue Errno::EBADF
379
400
  end
380
401
  end
381
402
  @notify = nil
@@ -414,9 +435,9 @@ module Puma
414
435
  # returning.
415
436
  #
416
437
  # Return true if one or more requests were processed.
417
- def process_client(client, buffer)
438
+ def process_client(client)
418
439
  # Advertise this server into the thread
419
- Thread.current[ThreadLocalKey] = self
440
+ Thread.current[THREAD_LOCAL_KEY] = self
420
441
 
421
442
  clean_thread_locals = @options[:clean_thread_locals]
422
443
  close_socket = true
@@ -440,15 +461,13 @@ module Puma
440
461
 
441
462
  while true
442
463
  @requests_count += 1
443
- case handle_request(client, buffer, requests + 1)
464
+ case handle_request(client, requests + 1)
444
465
  when false
445
466
  break
446
467
  when :async
447
468
  close_socket = false
448
469
  break
449
470
  when true
450
- buffer.reset
451
-
452
471
  ThreadPool.clean_thread_locals if clean_thread_locals
453
472
 
454
473
  requests += 1
@@ -478,11 +497,11 @@ module Puma
478
497
  end
479
498
  true
480
499
  rescue StandardError => e
481
- client_error(e, client)
500
+ client_error(e, client, requests)
482
501
  # The ensure tries to close +client+ down
483
502
  requests > 0
484
503
  ensure
485
- buffer.reset
504
+ client.io_buffer.reset
486
505
 
487
506
  begin
488
507
  client.close if close_socket
@@ -490,7 +509,7 @@ module Puma
490
509
  Puma::Util.purge_interrupt_queue
491
510
  # Already closed
492
511
  rescue StandardError => e
493
- @events.unknown_error e, nil, "Client"
512
+ @log_writer.unknown_error e, nil, "Client"
494
513
  end
495
514
  end
496
515
  end
@@ -506,23 +525,23 @@ module Puma
506
525
  # :nocov:
507
526
 
508
527
  # Handle various error types thrown by Client I/O operations.
509
- def client_error(e, client)
528
+ def client_error(e, client, requests = 1)
510
529
  # Swallow, do not log
511
530
  return if [ConnectionError, EOFError].include?(e.class)
512
531
 
513
- lowlevel_error(e, client.env)
514
532
  case e
515
533
  when MiniSSL::SSLError
516
- @events.ssl_error e, client.io
534
+ lowlevel_error(e, client.env)
535
+ @log_writer.ssl_error e, client.io
517
536
  when HttpParserError
518
- client.write_error(400)
519
- @events.parse_error e, client
537
+ response_to_error(client, requests, e, 400)
538
+ @log_writer.parse_error e, client
520
539
  when HttpParserError501
521
- client.write_error(501)
522
- @events.parse_error e, client
540
+ response_to_error(client, requests, e, 501)
541
+ @log_writer.parse_error e, client
523
542
  else
524
- client.write_error(500)
525
- @events.unknown_error e, nil, "Read"
543
+ response_to_error(client, requests, e, 500)
544
+ @log_writer.unknown_error e, nil, "Read"
526
545
  end
527
546
  end
528
547
 
@@ -543,10 +562,17 @@ module Puma
543
562
  backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
544
563
  [status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
545
564
  else
546
- [status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
565
+ [status, {}, [""]]
547
566
  end
548
567
  end
549
568
 
569
+ def response_to_error(client, requests, err, status_code)
570
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
571
+ prepare_response(status, headers, res_body, requests, client)
572
+ client.write_error(status_code)
573
+ end
574
+ private :response_to_error
575
+
550
576
  # Wait for all outstanding requests to finish.
551
577
  #
552
578
  def graceful_shutdown
@@ -580,7 +606,7 @@ module Puma
580
606
 
581
607
  def notify_safely(message)
582
608
  @notify << message
583
- rescue IOError, NoMethodError, Errno::EPIPE
609
+ rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
584
610
  # The server, in another thread, is shutting down
585
611
  Puma::Util.purge_interrupt_queue
586
612
  rescue RuntimeError => e
@@ -625,5 +651,27 @@ module Puma
625
651
  def stats
626
652
  STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
627
653
  end
654
+
655
+ # below are 'delegations' to binder
656
+ # remove in Puma 7?
657
+
658
+
659
+ def add_tcp_listener(host, port, optimize_for_latency = true, backlog = 1024)
660
+ @binder.add_tcp_listener host, port, optimize_for_latency, backlog
661
+ end
662
+
663
+ def add_ssl_listener(host, port, ctx, optimize_for_latency = true,
664
+ backlog = 1024)
665
+ @binder.add_ssl_listener host, port, ctx, optimize_for_latency, backlog
666
+ end
667
+
668
+ def add_unix_listener(path, umask = nil, mode = nil, backlog = 1024)
669
+ @binder.add_unix_listener path, umask, mode, backlog
670
+ end
671
+
672
+ # @!attribute [r] connected_ports
673
+ def connected_ports
674
+ @binder.connected_ports
675
+ end
628
676
  end
629
677
  end
data/lib/puma/single.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/runner'
4
- require 'puma/detect'
5
- require 'puma/plugin'
3
+ require_relative 'runner'
4
+ require_relative 'detect'
5
+ require_relative 'plugin'
6
6
 
7
7
  module Puma
8
8
  # This class is instantiated by the `Puma::Launcher` and used
@@ -16,26 +16,26 @@ module Puma
16
16
  # @!attribute [r] stats
17
17
  def stats
18
18
  {
19
- started_at: @started_at.utc.iso8601
20
- }.merge(@server.stats)
19
+ started_at: utc_iso8601(@started_at)
20
+ }.merge(@server.stats).merge(super)
21
21
  end
22
22
 
23
23
  def restart
24
- @server.begin_restart
24
+ @server&.begin_restart
25
25
  end
26
26
 
27
27
  def stop
28
- @server.stop(false) if @server
28
+ @server&.stop false
29
29
  end
30
30
 
31
31
  def halt
32
- @server.halt
32
+ @server&.halt
33
33
  end
34
34
 
35
35
  def stop_blocked
36
36
  log "- Gracefully stopping, waiting for requests to finish"
37
- @control.stop(true) if @control
38
- @server.stop(true) if @server
37
+ @control&.stop true
38
+ @server&.stop true
39
39
  end
40
40
 
41
41
  def run
@@ -55,7 +55,9 @@ module Puma
55
55
  log "Use Ctrl-C to stop"
56
56
  redirect_io
57
57
 
58
- @launcher.events.fire_on_booted!
58
+ @events.fire_on_booted!
59
+
60
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
59
61
 
60
62
  begin
61
63
  server_thread.join
@@ -15,15 +15,12 @@ module Puma
15
15
 
16
16
  ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
17
 
18
- # @deprecated 6.0.0
19
- FIELDS = ALLOWED_FIELDS
20
-
21
18
  def initialize
22
19
  @options = {}
23
20
  end
24
21
 
25
22
  def save(path, permission = nil)
26
- contents = "---\n".dup
23
+ contents = +"---\n"
27
24
  @options.each do |k,v|
28
25
  next unless ALLOWED_FIELDS.include? k
29
26
  case v
@@ -59,11 +56,11 @@ module Puma
59
56
  end
60
57
 
61
58
  ALLOWED_FIELDS.each do |f|
62
- define_method f do
59
+ define_method f.to_sym do
63
60
  @options[f]
64
61
  end
65
62
 
66
- define_method "#{f}=" do |v|
63
+ define_method :"#{f}=" do |v|
67
64
  @options[f] = v
68
65
  end
69
66
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'thread'
4
4
 
5
+ require_relative 'io_buffer'
6
+
5
7
  module Puma
6
8
  # Internal Docs for A simple thread pool management object.
7
9
  #
@@ -29,7 +31,7 @@ module Puma
29
31
  # The block passed is the work that will be performed in each
30
32
  # thread.
31
33
  #
32
- def initialize(name, min, max, *extra, &block)
34
+ def initialize(name, options = {}, &block)
33
35
  @not_empty = ConditionVariable.new
34
36
  @not_full = ConditionVariable.new
35
37
  @mutex = Mutex.new
@@ -40,10 +42,19 @@ module Puma
40
42
  @waiting = 0
41
43
 
42
44
  @name = name
43
- @min = Integer(min)
44
- @max = Integer(max)
45
+ @min = Integer(options[:min_threads])
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)
45
51
  @block = block
46
- @extra = extra
52
+ @out_of_band = options[:out_of_band]
53
+ @clean_thread_locals = options[:clean_thread_locals]
54
+ @before_thread_start = options[:before_thread_start]
55
+ @before_thread_exit = options[:before_thread_exit]
56
+ @reaping_time = options[:reaping_time]
57
+ @auto_trim_time = options[:auto_trim_time]
47
58
 
48
59
  @shutdown = false
49
60
 
@@ -62,14 +73,11 @@ module Puma
62
73
  end
63
74
  end
64
75
 
65
- @clean_thread_locals = false
66
76
  @force_shutdown = false
67
77
  @shutdown_mutex = Mutex.new
68
78
  end
69
79
 
70
80
  attr_reader :spawned, :trim_requested, :waiting
71
- attr_accessor :clean_thread_locals
72
- attr_accessor :out_of_band_hook # @version 5.0.0
73
81
 
74
82
  def self.clean_thread_locals
75
83
  Thread.current.keys.each do |key| # rubocop: disable Style/HashEachMethods
@@ -101,6 +109,7 @@ module Puma
101
109
  def spawn_thread
102
110
  @spawned += 1
103
111
 
112
+ trigger_before_thread_start_hooks
104
113
  th = Thread.new(@spawned) do |spawned|
105
114
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
106
115
  todo = @todo
@@ -109,8 +118,6 @@ module Puma
109
118
  not_empty = @not_empty
110
119
  not_full = @not_full
111
120
 
112
- extra = @extra.map { |i| i.new }
113
-
114
121
  while true
115
122
  work = nil
116
123
 
@@ -121,6 +128,7 @@ module Puma
121
128
  @spawned -= 1
122
129
  @workers.delete th
123
130
  not_full.signal
131
+ trigger_before_thread_exit_hooks
124
132
  Thread.exit
125
133
  end
126
134
 
@@ -144,7 +152,7 @@ module Puma
144
152
  end
145
153
 
146
154
  begin
147
- @out_of_band_pending = true if block.call(work, *extra)
155
+ @out_of_band_pending = true if block.call(work)
148
156
  rescue Exception => e
149
157
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
150
158
  end
@@ -158,14 +166,44 @@ module Puma
158
166
 
159
167
  private :spawn_thread
160
168
 
169
+ def trigger_before_thread_start_hooks
170
+ return unless @before_thread_start&.any?
171
+
172
+ @before_thread_start.each do |b|
173
+ begin
174
+ b.call
175
+ rescue Exception => e
176
+ STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
177
+ end
178
+ end
179
+ nil
180
+ end
181
+
182
+ private :trigger_before_thread_start_hooks
183
+
184
+ def trigger_before_thread_exit_hooks
185
+ return unless @before_thread_exit&.any?
186
+
187
+ @before_thread_exit.each do |b|
188
+ begin
189
+ b.call
190
+ rescue Exception => e
191
+ STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
192
+ end
193
+ end
194
+ nil
195
+ end
196
+
197
+ private :trigger_before_thread_exit_hooks
198
+
161
199
  # @version 5.0.0
162
200
  def trigger_out_of_band_hook
163
- return false unless out_of_band_hook && out_of_band_hook.any?
201
+ return false unless @out_of_band&.any?
164
202
 
165
203
  # we execute on idle hook when all threads are free
166
204
  return false unless @spawned == @waiting
167
205
 
168
- out_of_band_hook.each(&:call)
206
+ @out_of_band.each(&:call)
169
207
  true
170
208
  rescue Exception => e
171
209
  STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
@@ -319,12 +357,12 @@ module Puma
319
357
  end
320
358
  end
321
359
 
322
- def auto_trim!(timeout=30)
360
+ def auto_trim!(timeout=@auto_trim_time)
323
361
  @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
324
362
  @auto_trim.start!
325
363
  end
326
364
 
327
- def auto_reap!(timeout=5)
365
+ def auto_reap!(timeout=@reaping_time)
328
366
  @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
329
367
  @reaper.start!
330
368
  end
@@ -344,8 +382,8 @@ module Puma
344
382
 
345
383
  # Tell all threads in the pool to exit and wait for them to finish.
346
384
  # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
347
- # Next, wait an extra +grace+ seconds then force-kill remaining threads.
348
- # Finally, wait +kill_grace+ seconds for remaining threads to exit.
385
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
386
+ # threads. Finally, wait 1 second for remaining threads to exit.
349
387
  #
350
388
  def shutdown(timeout=-1)
351
389
  threads = with_mutex do
@@ -354,8 +392,8 @@ module Puma
354
392
  @not_empty.broadcast
355
393
  @not_full.broadcast
356
394
 
357
- @auto_trim.stop if @auto_trim
358
- @reaper.stop if @reaper
395
+ @auto_trim&.stop
396
+ @reaper&.stop
359
397
  # dup workers so that we join them all safely
360
398
  @workers.dup
361
399
  end
@@ -382,7 +420,7 @@ module Puma
382
420
  t.raise ForceShutdown if t[:with_force_shutdown]
383
421
  end
384
422
  end
385
- join.call(SHUTDOWN_GRACE_TIME)
423
+ join.call(@shutdown_grace_time)
386
424
 
387
425
  # If threads are _still_ running, forcefully kill them and wait to finish.
388
426
  threads.each(&:kill)