puma 2.1.1 → 7.2.1

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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3273 -0
  3. data/LICENSE +23 -20
  4. data/README.md +388 -100
  5. data/bin/puma-wild +25 -0
  6. data/bin/pumactl +1 -1
  7. data/docs/architecture.md +74 -0
  8. data/docs/compile_options.md +55 -0
  9. data/docs/deployment.md +137 -0
  10. data/docs/fork_worker.md +41 -0
  11. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  12. data/docs/images/puma-connection-flow.png +0 -0
  13. data/docs/images/puma-general-arch.png +0 -0
  14. data/docs/java_options.md +54 -0
  15. data/docs/jungle/README.md +9 -0
  16. data/docs/jungle/rc.d/README.md +74 -0
  17. data/docs/jungle/rc.d/puma +61 -0
  18. data/docs/jungle/rc.d/puma.conf +10 -0
  19. data/docs/kubernetes.md +73 -0
  20. data/docs/nginx.md +5 -5
  21. data/docs/plugins.md +42 -0
  22. data/docs/rails_dev_mode.md +28 -0
  23. data/docs/restart.md +65 -0
  24. data/docs/signals.md +98 -0
  25. data/docs/stats.md +148 -0
  26. data/docs/systemd.md +253 -0
  27. data/docs/testing_benchmarks_local_files.md +150 -0
  28. data/docs/testing_test_rackup_ci_files.md +36 -0
  29. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  30. data/ext/puma_http11/extconf.rb +59 -2
  31. data/ext/puma_http11/http11_parser.c +319 -487
  32. data/ext/puma_http11/http11_parser.h +15 -13
  33. data/ext/puma_http11/http11_parser.java.rl +27 -43
  34. data/ext/puma_http11/http11_parser.rl +27 -24
  35. data/ext/puma_http11/http11_parser_common.rl +8 -8
  36. data/ext/puma_http11/mini_ssl.c +696 -38
  37. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  38. data/ext/puma_http11/org/jruby/puma/Http11.java +145 -113
  39. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +139 -172
  40. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
  41. data/ext/puma_http11/puma_http11.c +183 -175
  42. data/lib/puma/app/status.rb +77 -29
  43. data/lib/puma/binder.rb +349 -119
  44. data/lib/puma/cli.rb +164 -769
  45. data/lib/puma/client.rb +591 -132
  46. data/lib/puma/cluster/worker.rb +183 -0
  47. data/lib/puma/cluster/worker_handle.rb +127 -0
  48. data/lib/puma/cluster.rb +634 -0
  49. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  50. data/lib/puma/commonlogger.rb +115 -0
  51. data/lib/puma/configuration.rb +370 -202
  52. data/lib/puma/const.rb +266 -115
  53. data/lib/puma/control_cli.rb +219 -120
  54. data/lib/puma/detect.rb +45 -2
  55. data/lib/puma/dsl.rb +1496 -0
  56. data/lib/puma/error_logger.rb +115 -0
  57. data/lib/puma/events.rb +44 -60
  58. data/lib/puma/io_buffer.rb +48 -5
  59. data/lib/puma/jruby_restart.rb +2 -51
  60. data/lib/puma/json_serialization.rb +96 -0
  61. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  62. data/lib/puma/launcher.rb +502 -0
  63. data/lib/puma/log_writer.rb +147 -0
  64. data/lib/puma/minissl/context_builder.rb +96 -0
  65. data/lib/puma/minissl.rb +355 -37
  66. data/lib/puma/null_io.rb +79 -12
  67. data/lib/puma/plugin/systemd.rb +90 -0
  68. data/lib/puma/plugin/tmp_restart.rb +36 -0
  69. data/lib/puma/plugin.rb +111 -0
  70. data/lib/puma/rack/builder.rb +297 -0
  71. data/lib/puma/rack/urlmap.rb +93 -0
  72. data/lib/puma/rack_default.rb +21 -4
  73. data/lib/puma/reactor.rb +102 -133
  74. data/lib/puma/request.rb +703 -0
  75. data/lib/puma/runner.rb +211 -0
  76. data/lib/puma/sd_notify.rb +146 -0
  77. data/lib/puma/server.rb +544 -436
  78. data/lib/puma/single.rb +72 -0
  79. data/lib/puma/state_file.rb +69 -0
  80. data/lib/puma/thread_pool.rb +282 -56
  81. data/lib/puma/util.rb +125 -0
  82. data/lib/puma.rb +80 -6
  83. data/lib/rack/handler/puma.rb +125 -47
  84. data/tools/Dockerfile +26 -0
  85. data/tools/trickletest.rb +44 -0
  86. metadata +77 -148
  87. data/COPYING +0 -55
  88. data/Gemfile +0 -10
  89. data/History.txt +0 -291
  90. data/Manifest.txt +0 -60
  91. data/Rakefile +0 -121
  92. data/TODO +0 -5
  93. data/docs/config.md +0 -0
  94. data/ext/puma_http11/ext_help.h +0 -15
  95. data/ext/puma_http11/io_buffer.c +0 -154
  96. data/lib/puma/accept_nonblock.rb +0 -23
  97. data/lib/puma/capistrano.rb +0 -34
  98. data/lib/puma/compat.rb +0 -11
  99. data/lib/puma/daemon_ext.rb +0 -20
  100. data/lib/puma/delegation.rb +0 -11
  101. data/lib/puma/java_io_buffer.rb +0 -45
  102. data/lib/puma/rack_patch.rb +0 -25
  103. data/puma.gemspec +0 -45
  104. data/test/test_app_status.rb +0 -88
  105. data/test/test_cli.rb +0 -171
  106. data/test/test_config.rb +0 -16
  107. data/test/test_http10.rb +0 -27
  108. data/test/test_http11.rb +0 -126
  109. data/test/test_integration.rb +0 -154
  110. data/test/test_iobuffer.rb +0 -38
  111. data/test/test_minissl.rb +0 -25
  112. data/test/test_null_io.rb +0 -31
  113. data/test/test_persistent.rb +0 -238
  114. data/test/test_puma_server.rb +0 -207
  115. data/test/test_rack_handler.rb +0 -10
  116. data/test/test_rack_server.rb +0 -141
  117. data/test/test_thread_pool.rb +0 -146
  118. data/test/test_unix_socket.rb +0 -39
  119. data/test/test_ws.rb +0 -89
  120. data/tools/jungle/init.d/README.md +0 -54
  121. data/tools/jungle/init.d/puma +0 -332
  122. data/tools/jungle/init.d/run-puma +0 -3
  123. data/tools/jungle/upstart/README.md +0 -61
  124. data/tools/jungle/upstart/puma-manager.conf +0 -31
  125. data/tools/jungle/upstart/puma.conf +0 -52
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runner'
4
+ require_relative 'detect'
5
+ require_relative 'plugin'
6
+
7
+ module Puma
8
+ # This class is instantiated by the `Puma::Launcher` and used
9
+ # to boot and serve a Ruby application when no puma "workers" are needed
10
+ # i.e. only using "threaded" mode. For example `$ puma -t 1:5`
11
+ #
12
+ # At the core of this class is running an instance of `Puma::Server` which
13
+ # gets created via the `start_server` method from the `Puma::Runner` class
14
+ # that this inherits from.
15
+ class Single < Runner
16
+ # @!attribute [r] stats
17
+ def stats
18
+ {
19
+ started_at: utc_iso8601(@started_at)
20
+ }.merge(@server&.stats || {}).merge(super)
21
+ end
22
+
23
+ def restart
24
+ @server&.begin_restart
25
+ end
26
+
27
+ def stop
28
+ @server&.stop false
29
+ end
30
+
31
+ def halt
32
+ @server&.halt
33
+ end
34
+
35
+ def stop_blocked
36
+ log "- Gracefully stopping, waiting for requests to finish"
37
+ @control&.stop true
38
+ @server&.stop true
39
+ end
40
+
41
+ def run
42
+ output_header "single"
43
+
44
+ load_and_bind
45
+
46
+ Plugins.fire_background
47
+
48
+ @launcher.write_state
49
+
50
+ start_control
51
+
52
+ @server = start_server
53
+ server_thread = @server.run
54
+
55
+ log "Use Ctrl-C to stop"
56
+
57
+ warn_ruby_mn_threads
58
+
59
+ redirect_io
60
+
61
+ @events.fire_after_booted!
62
+
63
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
64
+
65
+ begin
66
+ server_thread.join
67
+ rescue Interrupt
68
+ # Swallow it
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+
5
+ # Puma::Launcher uses StateFile to write a yaml file for use with Puma::ControlCLI.
6
+ #
7
+ # In previous versions of Puma, YAML was used to read/write the state file.
8
+ # Since Puma is similar to Bundler/RubyGems in that it may load before one's app
9
+ # does, minimizing the dependencies that may be shared with the app is desired.
10
+ #
11
+ # At present, it only works with numeric and string values. It is still a valid
12
+ # yaml file, and the CI tests parse it with Psych.
13
+ #
14
+ class StateFile
15
+
16
+ ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
+
18
+ def initialize
19
+ @options = {}
20
+ end
21
+
22
+ def save(path, permission = nil)
23
+ contents = +"---\n"
24
+ @options.each do |k,v|
25
+ next unless ALLOWED_FIELDS.include? k
26
+ case v
27
+ when Numeric
28
+ contents << "#{k}: #{v}\n"
29
+ when String
30
+ next if v.strip.empty?
31
+ contents << (k == 'running_from' || v.to_s.include?(' ') ?
32
+ "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
+ end
34
+ end
35
+
36
+ if permission
37
+ File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
40
+ end
41
+ end
42
+
43
+ def load(path)
44
+ File.read(path).lines.each do |line|
45
+ next if line.start_with? '#'
46
+ k,v = line.split ':', 2
47
+ next unless v && ALLOWED_FIELDS.include?(k)
48
+ v = v.strip
49
+ @options[k] =
50
+ case v
51
+ when '' then nil
52
+ when /\A\d+\z/ then v.to_i
53
+ when /\A\d+\.\d+\z/ then v.to_f
54
+ else v.gsub(/\A"|"\z/, '')
55
+ end
56
+ end
57
+ end
58
+
59
+ ALLOWED_FIELDS.each do |f|
60
+ define_method f.to_sym do
61
+ @options[f]
62
+ end
63
+
64
+ define_method :"#{f}=" do |v|
65
+ @options[f] = v
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,9 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
5
+ require_relative 'io_buffer'
6
+
3
7
  module Puma
4
- # A simple thread pool management object.
8
+
9
+ # Add `Thread#puma_server` and `Thread#puma_server=`
10
+ Thread.attr_accessor(:puma_server)
11
+
12
+ # Internal Docs for A simple thread pool management object.
13
+ #
14
+ # Each Puma "worker" has a thread pool to process requests.
15
+ #
16
+ # First a connection to a client is made in `Puma::Server`. It is wrapped in a
17
+ # `Puma::Client` instance and then passed to the `Puma::Reactor` to ensure
18
+ # the whole request is buffered into memory. Once the request is ready, it is passed into
19
+ # a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
5
20
  #
21
+ # Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
22
+ # and processes it.
6
23
  class ThreadPool
24
+ class ForceShutdown < RuntimeError
25
+ end
26
+
27
+ # How long, after raising the ForceShutdown of a thread during
28
+ # forced shutdown mode, to wait for the thread to try and finish
29
+ # up its work before leaving the thread to die on the vine.
30
+ SHUTDOWN_GRACE_TIME = 5 # seconds
31
+
32
+ attr_reader :out_of_band_running
7
33
 
8
34
  # Maintain a minimum of +min+ and maximum of +max+ threads
9
35
  # in the pool.
@@ -11,39 +37,97 @@ module Puma
11
37
  # The block passed is the work that will be performed in each
12
38
  # thread.
13
39
  #
14
- def initialize(min, max, *extra, &blk)
15
- @cond = ConditionVariable.new
16
- @mutex = Mutex.new
40
+ def initialize(name, options = {}, server: nil, &block)
41
+ @server = server
17
42
 
18
- @todo = []
43
+ @not_empty = ConditionVariable.new
44
+ @not_full = ConditionVariable.new
45
+ @mutex = Mutex.new
46
+ @todo = Queue.new
19
47
 
48
+ @backlog_max = 0
20
49
  @spawned = 0
21
50
  @waiting = 0
22
51
 
23
- @min = min
24
- @max = max
25
- @block = blk
26
- @extra = extra
52
+ @name = name
53
+ @min = Integer(options[:min_threads])
54
+ @max = Integer(options[:max_threads])
55
+ # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
56
+ # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
57
+ # makes stubbing constants difficult.
58
+ @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
59
+ @block = block
60
+ @out_of_band = options[:out_of_band]
61
+ @out_of_band_running = false
62
+ @out_of_band_condvar = ConditionVariable.new
63
+ @before_thread_start = options[:before_thread_start]
64
+ @before_thread_exit = options[:before_thread_exit]
65
+ @reaping_time = options[:reaping_time]
66
+ @auto_trim_time = options[:auto_trim_time]
27
67
 
28
68
  @shutdown = false
29
69
 
30
70
  @trim_requested = 0
71
+ @out_of_band_pending = false
31
72
 
32
73
  @workers = []
33
74
 
34
75
  @auto_trim = nil
76
+ @reaper = nil
35
77
 
36
78
  @mutex.synchronize do
37
- min.times { spawn_thread }
79
+ @min.times do
80
+ spawn_thread
81
+ @not_full.wait(@mutex)
82
+ end
83
+ end
84
+
85
+ @force_shutdown = false
86
+ @shutdown_mutex = Mutex.new
87
+ end
88
+
89
+ attr_reader :spawned, :trim_requested, :waiting
90
+
91
+ # generate stats hash so as not to perform multiple locks
92
+ # @return [Hash] hash containing stat info from ThreadPool
93
+ def stats
94
+ with_mutex do
95
+ temp = @backlog_max
96
+ @backlog_max = 0
97
+ { backlog: @todo.size,
98
+ running: @spawned,
99
+ pool_capacity: @waiting + (@max - @spawned),
100
+ busy_threads: @spawned - @waiting + @todo.size,
101
+ backlog_max: temp
102
+ }
38
103
  end
39
104
  end
40
105
 
41
- attr_reader :spawned, :trim_requested
106
+ def reset_max
107
+ with_mutex { @backlog_max = 0 }
108
+ end
42
109
 
43
110
  # How many objects have yet to be processed by the pool?
44
111
  #
45
112
  def backlog
46
- @mutex.synchronize { @todo.size }
113
+ with_mutex { @todo.size }
114
+ end
115
+
116
+ # The maximum size of the backlog
117
+ #
118
+ def backlog_max
119
+ with_mutex { @backlog_max }
120
+ end
121
+
122
+ # @!attribute [r] pool_capacity
123
+ def pool_capacity
124
+ waiting + (@max - spawned)
125
+ end
126
+
127
+ # @!attribute [r] busy_threads
128
+ # @version 5.0.0
129
+ def busy_threads
130
+ with_mutex { @spawned - @waiting + @todo.size }
47
131
  end
48
132
 
49
133
  # :nodoc:
@@ -53,48 +137,52 @@ module Puma
53
137
  def spawn_thread
54
138
  @spawned += 1
55
139
 
56
- th = Thread.new do
140
+ trigger_before_thread_start_hooks
141
+ th = Thread.new(@spawned) do |spawned|
142
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
143
+ # Advertise server into the thread
144
+ Thread.current.puma_server = @server
145
+
57
146
  todo = @todo
58
147
  block = @block
59
148
  mutex = @mutex
60
- cond = @cond
61
-
62
- extra = @extra.map { |i| i.new }
149
+ not_empty = @not_empty
150
+ not_full = @not_full
63
151
 
64
152
  while true
65
153
  work = nil
66
154
 
67
- continue = true
68
-
69
155
  mutex.synchronize do
70
156
  while todo.empty?
71
157
  if @trim_requested > 0
72
158
  @trim_requested -= 1
73
- continue = false
74
- break
75
- end
76
-
77
- if @shutdown
78
- continue = false
79
- break
159
+ @spawned -= 1
160
+ @workers.delete th
161
+ not_full.signal
162
+ trigger_before_thread_exit_hooks
163
+ Thread.exit
80
164
  end
81
165
 
82
166
  @waiting += 1
83
- cond.wait mutex
84
- @waiting -= 1
167
+ if @out_of_band_pending && trigger_out_of_band_hook
168
+ @out_of_band_pending = false
169
+ end
170
+ not_full.signal
171
+ begin
172
+ not_empty.wait mutex
173
+ ensure
174
+ @waiting -= 1
175
+ end
85
176
  end
86
177
 
87
- work = todo.pop if continue
178
+ work = todo.shift
88
179
  end
89
180
 
90
- break unless continue
91
-
92
- block.call(work, *extra)
93
- end
94
-
95
- mutex.synchronize do
96
- @spawned -= 1
97
- @workers.delete th
181
+ begin
182
+ @out_of_band_pending = true if block.call(work)
183
+ rescue Exception => e
184
+ STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
185
+ end
98
186
  end
99
187
  end
100
188
 
@@ -105,40 +193,126 @@ module Puma
105
193
 
106
194
  private :spawn_thread
107
195
 
196
+ def trigger_before_thread_start_hooks
197
+ return unless @before_thread_start&.any?
198
+
199
+ @before_thread_start.each do |b|
200
+ begin
201
+ b[:block].call
202
+ rescue Exception => e
203
+ STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
204
+ end
205
+ end
206
+ nil
207
+ end
208
+
209
+ private :trigger_before_thread_start_hooks
210
+
211
+ def trigger_before_thread_exit_hooks
212
+ return unless @before_thread_exit&.any?
213
+
214
+ @before_thread_exit.each do |b|
215
+ begin
216
+ b[:block].call
217
+ rescue Exception => e
218
+ STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
219
+ end
220
+ end
221
+ nil
222
+ end
223
+
224
+ private :trigger_before_thread_exit_hooks
225
+
226
+ # @version 5.0.0
227
+ def trigger_out_of_band_hook
228
+ return false unless @out_of_band&.any?
229
+
230
+ # we execute on idle hook when all threads are free
231
+ return false unless @spawned == @waiting
232
+ @out_of_band_running = true
233
+ @out_of_band.each { |b| b[:block].call }
234
+ true
235
+ rescue Exception => e
236
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
237
+ true
238
+ ensure
239
+ @out_of_band_running = false
240
+ @out_of_band_condvar.broadcast
241
+ end
242
+
243
+ private :trigger_out_of_band_hook
244
+
245
+ def wait_while_out_of_band_running
246
+ return unless @out_of_band_running
247
+
248
+ with_mutex do
249
+ @out_of_band_condvar.wait(@mutex) while @out_of_band_running
250
+ end
251
+ end
252
+
253
+ # @version 5.0.0
254
+ def with_mutex(&block)
255
+ @mutex.owned? ?
256
+ yield :
257
+ @mutex.synchronize(&block)
258
+ end
259
+
108
260
  # Add +work+ to the todo list for a Thread to pickup and process.
109
261
  def <<(work)
110
- @mutex.synchronize do
262
+ with_mutex do
111
263
  if @shutdown
112
264
  raise "Unable to add work while shutting down"
113
265
  end
114
266
 
115
267
  @todo << work
268
+ t = @todo.size
269
+ @backlog_max = t if t > @backlog_max
116
270
 
117
- if @waiting == 0 and @spawned < @max
271
+ if @waiting < @todo.size and @spawned < @max
118
272
  spawn_thread
119
273
  end
120
274
 
121
- @cond.signal
275
+ @not_empty.signal
122
276
  end
123
277
  end
124
278
 
125
- # If too many threads are in the pool, tell one to finish go ahead
279
+ # If there are any free threads in the pool, tell one to go ahead
126
280
  # and exit. If +force+ is true, then a trim request is requested
127
281
  # even if all threads are being utilized.
128
282
  #
129
283
  def trim(force=false)
130
- @mutex.synchronize do
131
- if (force or @waiting > 0) and @spawned - @trim_requested > @min
284
+ with_mutex do
285
+ free = @waiting - @todo.size
286
+ if (force or free > 0) and @spawned - @trim_requested > @min
132
287
  @trim_requested += 1
133
- @cond.signal
288
+ @not_empty.signal
134
289
  end
135
290
  end
136
291
  end
137
292
 
138
- class AutoTrim
139
- def initialize(pool, timeout)
293
+ # If there are dead threads in the pool make them go away while decreasing
294
+ # spawned counter so that new healthy threads could be created again.
295
+ def reap
296
+ with_mutex do
297
+ dead_workers = @workers.reject(&:alive?)
298
+
299
+ dead_workers.each do |worker|
300
+ worker.kill
301
+ @spawned -= 1
302
+ end
303
+
304
+ @workers.delete_if do |w|
305
+ dead_workers.include?(w)
306
+ end
307
+ end
308
+ end
309
+
310
+ class Automaton
311
+ def initialize(pool, timeout, thread_name, message)
140
312
  @pool = pool
141
313
  @timeout = timeout
314
+ @thread_name = thread_name
315
+ @message = message
142
316
  @running = false
143
317
  end
144
318
 
@@ -146,8 +320,9 @@ module Puma
146
320
  @running = true
147
321
 
148
322
  @thread = Thread.new do
323
+ Puma.set_thread_name @thread_name
149
324
  while @running
150
- @pool.trim
325
+ @pool.public_send(@message)
151
326
  sleep @timeout
152
327
  end
153
328
  end
@@ -159,24 +334,75 @@ module Puma
159
334
  end
160
335
  end
161
336
 
162
- def auto_trim!(timeout=5)
163
- @auto_trim = AutoTrim.new(self, timeout)
337
+ def auto_trim!(timeout=@auto_trim_time)
338
+ @auto_trim = Automaton.new(self, timeout, "#{@name} tp trim", :trim)
164
339
  @auto_trim.start!
165
340
  end
166
341
 
342
+ def auto_reap!(timeout=@reaping_time)
343
+ @reaper = Automaton.new(self, timeout, "#{@name} tp reap", :reap)
344
+ @reaper.start!
345
+ end
346
+
347
+ # Allows ThreadPool::ForceShutdown to be raised within the
348
+ # provided block if the thread is forced to shutdown during execution.
349
+ def with_force_shutdown
350
+ t = Thread.current
351
+ @shutdown_mutex.synchronize do
352
+ raise ForceShutdown if @force_shutdown
353
+ t[:with_force_shutdown] = true
354
+ end
355
+ yield
356
+ ensure
357
+ t[:with_force_shutdown] = false
358
+ end
359
+
167
360
  # Tell all threads in the pool to exit and wait for them to finish.
361
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
362
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
363
+ # threads. Finally, wait 1 second for remaining threads to exit.
168
364
  #
169
- def shutdown
170
- @mutex.synchronize do
365
+ def shutdown(timeout=-1)
366
+ threads = with_mutex do
171
367
  @shutdown = true
172
- @cond.broadcast
173
-
174
- @auto_trim.stop if @auto_trim
368
+ @trim_requested = @spawned
369
+ @not_empty.broadcast
370
+ @not_full.broadcast
371
+
372
+ @auto_trim&.stop
373
+ @reaper&.stop
374
+ # dup workers so that we join them all safely
375
+ @workers.dup
175
376
  end
176
377
 
177
- # Use this instead of #each so that we don't stop in the middle
178
- # of each and see a mutated object mid #each
179
- @workers.first.join until @workers.empty?
378
+ if timeout == -1
379
+ # Wait for threads to finish without force shutdown.
380
+ threads.each(&:join)
381
+ else
382
+ join = ->(inner_timeout) do
383
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
384
+ threads.reject! do |t|
385
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
386
+ t.join inner_timeout - elapsed
387
+ end
388
+ end
389
+
390
+ # Wait +timeout+ seconds for threads to finish.
391
+ join.call(timeout)
392
+
393
+ # If threads are still running, raise ForceShutdown and wait to finish.
394
+ @shutdown_mutex.synchronize do
395
+ @force_shutdown = true
396
+ threads.each do |t|
397
+ t.raise ForceShutdown if t[:with_force_shutdown]
398
+ end
399
+ end
400
+ join.call(@shutdown_grace_time)
401
+
402
+ # If threads are _still_ running, forcefully kill them and wait to finish.
403
+ threads.each(&:kill)
404
+ join.call(1)
405
+ end
180
406
 
181
407
  @spawned = 0
182
408
  @workers = []