puma 2.2.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 (136) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3334 -0
  3. data/LICENSE +23 -20
  4. data/README.md +387 -100
  5. data/bin/puma-wild +25 -0
  6. data/bin/pumactl +1 -1
  7. data/docs/5.0-Upgrade.md +98 -0
  8. data/docs/6.0-Upgrade.md +56 -0
  9. data/docs/7.0-Upgrade.md +52 -0
  10. data/docs/8.0-Upgrade.md +100 -0
  11. data/docs/architecture.md +74 -0
  12. data/docs/compile_options.md +55 -0
  13. data/docs/deployment.md +137 -0
  14. data/docs/fork_worker.md +41 -0
  15. data/docs/grpc.md +62 -0
  16. data/docs/images/favicon.svg +1 -0
  17. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  18. data/docs/images/puma-connection-flow.png +0 -0
  19. data/docs/images/puma-general-arch.png +0 -0
  20. data/docs/images/running-puma.svg +1 -0
  21. data/docs/images/standard-logo.svg +1 -0
  22. data/docs/java_options.md +54 -0
  23. data/docs/jungle/README.md +9 -0
  24. data/docs/jungle/rc.d/README.md +74 -0
  25. data/docs/jungle/rc.d/puma +61 -0
  26. data/docs/jungle/rc.d/puma.conf +10 -0
  27. data/docs/kubernetes.md +73 -0
  28. data/docs/nginx.md +5 -5
  29. data/docs/plugins.md +42 -0
  30. data/docs/rails_dev_mode.md +28 -0
  31. data/docs/restart.md +65 -0
  32. data/docs/signals.md +98 -0
  33. data/docs/stats.md +148 -0
  34. data/docs/systemd.md +253 -0
  35. data/docs/testing_benchmarks_local_files.md +150 -0
  36. data/docs/testing_test_rackup_ci_files.md +36 -0
  37. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  38. data/ext/puma_http11/extconf.rb +59 -2
  39. data/ext/puma_http11/http11_parser.c +319 -487
  40. data/ext/puma_http11/http11_parser.h +15 -13
  41. data/ext/puma_http11/http11_parser.java.rl +64 -94
  42. data/ext/puma_http11/http11_parser.rl +27 -24
  43. data/ext/puma_http11/http11_parser_common.rl +8 -8
  44. data/ext/puma_http11/mini_ssl.c +696 -38
  45. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  46. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  47. data/ext/puma_http11/org/jruby/puma/Http11.java +241 -145
  48. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +174 -221
  49. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
  50. data/ext/puma_http11/puma_http11.c +183 -175
  51. data/lib/puma/app/status.rb +77 -29
  52. data/lib/puma/binder.rb +349 -119
  53. data/lib/puma/cli.rb +163 -801
  54. data/lib/puma/client.rb +627 -144
  55. data/lib/puma/client_env.rb +171 -0
  56. data/lib/puma/cluster/worker.rb +183 -0
  57. data/lib/puma/cluster/worker_handle.rb +127 -0
  58. data/lib/puma/cluster.rb +634 -0
  59. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  60. data/lib/puma/commonlogger.rb +115 -0
  61. data/lib/puma/configuration.rb +420 -198
  62. data/lib/puma/const.rb +266 -115
  63. data/lib/puma/control_cli.rb +219 -120
  64. data/lib/puma/detect.rb +56 -2
  65. data/lib/puma/dsl.rb +1562 -0
  66. data/lib/puma/error_logger.rb +115 -0
  67. data/lib/puma/events.rb +44 -60
  68. data/lib/puma/io_buffer.rb +48 -5
  69. data/lib/puma/jruby_restart.rb +2 -51
  70. data/lib/puma/json_serialization.rb +96 -0
  71. data/lib/puma/launcher/bundle_pruner.rb +102 -0
  72. data/lib/puma/launcher.rb +501 -0
  73. data/lib/puma/log_writer.rb +153 -0
  74. data/lib/puma/minissl/context_builder.rb +96 -0
  75. data/lib/puma/minissl.rb +355 -37
  76. data/lib/puma/null_io.rb +79 -12
  77. data/lib/puma/plugin/systemd.rb +90 -0
  78. data/lib/puma/plugin/tmp_restart.rb +36 -0
  79. data/lib/puma/plugin.rb +111 -0
  80. data/lib/puma/rack/builder.rb +297 -0
  81. data/lib/puma/rack/urlmap.rb +93 -0
  82. data/lib/puma/rack_default.rb +21 -4
  83. data/lib/puma/reactor.rb +102 -133
  84. data/lib/puma/response.rb +532 -0
  85. data/lib/puma/runner.rb +211 -0
  86. data/lib/puma/sd_notify.rb +146 -0
  87. data/lib/puma/server.rb +582 -445
  88. data/lib/puma/server_plugin_control.rb +32 -0
  89. data/lib/puma/single.rb +72 -0
  90. data/lib/puma/state_file.rb +69 -0
  91. data/lib/puma/thread_pool.rb +389 -57
  92. data/lib/puma/util.rb +125 -0
  93. data/lib/puma.rb +80 -6
  94. data/lib/rack/handler/puma.rb +125 -47
  95. data/tools/Dockerfile +26 -0
  96. data/tools/trickletest.rb +44 -0
  97. metadata +88 -148
  98. data/COPYING +0 -55
  99. data/Gemfile +0 -10
  100. data/History.txt +0 -302
  101. data/Manifest.txt +0 -60
  102. data/Rakefile +0 -165
  103. data/TODO +0 -5
  104. data/docs/config.md +0 -0
  105. data/ext/puma_http11/ext_help.h +0 -15
  106. data/ext/puma_http11/io_buffer.c +0 -154
  107. data/lib/puma/accept_nonblock.rb +0 -23
  108. data/lib/puma/capistrano.rb +0 -34
  109. data/lib/puma/compat.rb +0 -11
  110. data/lib/puma/daemon_ext.rb +0 -20
  111. data/lib/puma/delegation.rb +0 -11
  112. data/lib/puma/java_io_buffer.rb +0 -45
  113. data/lib/puma/rack_patch.rb +0 -25
  114. data/puma.gemspec +0 -45
  115. data/test/test_app_status.rb +0 -88
  116. data/test/test_cli.rb +0 -171
  117. data/test/test_config.rb +0 -16
  118. data/test/test_http10.rb +0 -27
  119. data/test/test_http11.rb +0 -126
  120. data/test/test_integration.rb +0 -154
  121. data/test/test_iobuffer.rb +0 -38
  122. data/test/test_minissl.rb +0 -25
  123. data/test/test_null_io.rb +0 -31
  124. data/test/test_persistent.rb +0 -238
  125. data/test/test_puma_server.rb +0 -224
  126. data/test/test_rack_handler.rb +0 -10
  127. data/test/test_rack_server.rb +0 -141
  128. data/test/test_thread_pool.rb +0 -146
  129. data/test/test_unix_socket.rb +0 -39
  130. data/test/test_ws.rb +0 -89
  131. data/tools/jungle/init.d/README.md +0 -54
  132. data/tools/jungle/init.d/puma +0 -332
  133. data/tools/jungle/init.d/run-puma +0 -3
  134. data/tools/jungle/upstart/README.md +0 -61
  135. data/tools/jungle/upstart/puma-manager.conf +0 -31
  136. data/tools/jungle/upstart/puma.conf +0 -52
@@ -1,9 +1,81 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
5
+ require_relative 'io_buffer'
6
+ require_relative 'server_plugin_control'
7
+
3
8
  module Puma
4
- # A simple thread pool management object.
9
+
10
+ # Add `Thread#puma_server` and `Thread#puma_server=`
11
+ Thread.attr_accessor(:puma_server)
12
+
13
+ # Internal Docs for A simple thread pool management object.
5
14
  #
15
+ # Each Puma "worker" has a thread pool to process requests.
16
+ #
17
+ # First a connection to a client is made in `Puma::Server`. It is wrapped in a
18
+ # `Puma::Client` instance and then passed to the `Puma::Reactor` to ensure
19
+ # the whole request is buffered into memory. Once the request is ready, it is passed into
20
+ # a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
21
+ #
22
+ # Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
23
+ # and processes it.
6
24
  class ThreadPool
25
+ class ForceShutdown < RuntimeError
26
+ end
27
+
28
+ class ProcessorThread
29
+ attr_accessor :thread
30
+ attr_writer :marked_as_io_thread
31
+
32
+ def initialize(pool)
33
+ @pool = pool
34
+ @thread = nil
35
+ @marked_as_io_thread = false
36
+ end
37
+
38
+ def mark_as_io_thread!
39
+ unless @marked_as_io_thread
40
+ @marked_as_io_thread = true
41
+
42
+ # Immediately signal the pool that it can spawn a new thread
43
+ # if there's some work in the queue.
44
+ @pool.spawn_thread_if_needed
45
+ end
46
+ end
47
+
48
+ def marked_as_io_thread?
49
+ @marked_as_io_thread
50
+ end
51
+
52
+ def alive?
53
+ @thread&.alive?
54
+ end
55
+
56
+ def join(...)
57
+ @thread.join(...)
58
+ end
59
+
60
+ def kill(...)
61
+ @thread.kill(...)
62
+ end
63
+
64
+ def [](key)
65
+ @thread[key]
66
+ end
67
+
68
+ def raise(...)
69
+ @thread.raise(...)
70
+ end
71
+ end
72
+
73
+ # How long, after raising the ForceShutdown of a thread during
74
+ # forced shutdown mode, to wait for the thread to try and finish
75
+ # up its work before leaving the thread to die on the vine.
76
+ SHUTDOWN_GRACE_TIME = 5 # seconds
77
+
78
+ attr_reader :out_of_band_running
7
79
 
8
80
  # Maintain a minimum of +min+ and maximum of +max+ threads
9
81
  # in the pool.
@@ -11,39 +83,102 @@ module Puma
11
83
  # The block passed is the work that will be performed in each
12
84
  # thread.
13
85
  #
14
- def initialize(min, max, *extra, &blk)
15
- @cond = ConditionVariable.new
16
- @mutex = Mutex.new
86
+ def initialize(name, options = {}, server: nil, &block)
87
+ @server = server
17
88
 
18
- @todo = []
89
+ @not_empty = ConditionVariable.new
90
+ @not_full = ConditionVariable.new
91
+ @mutex = Mutex.new
92
+ @todo = Queue.new
19
93
 
94
+ @backlog_max = 0
20
95
  @spawned = 0
21
96
  @waiting = 0
22
97
 
23
- @min = min
24
- @max = max
25
- @block = blk
26
- @extra = extra
98
+ @name = name
99
+ @min = Integer(options[:min_threads])
100
+ @max = Integer(options[:max_threads])
101
+ @max_io_threads = Integer(options[:max_io_threads] || 0)
102
+
103
+ # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
104
+ # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
105
+ # makes stubbing constants difficult.
106
+ @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
107
+ @shutdown_debug = options[:shutdown_debug]
108
+ @block = block
109
+ @out_of_band = options[:out_of_band]
110
+ @out_of_band_running = false
111
+ @out_of_band_condvar = ConditionVariable.new
112
+ @before_thread_start = options[:before_thread_start]
113
+ @before_thread_exit = options[:before_thread_exit]
114
+ @reaping_time = options[:reaping_time]
115
+ @auto_trim_time = options[:auto_trim_time]
27
116
 
28
117
  @shutdown = false
29
118
 
30
119
  @trim_requested = 0
120
+ @out_of_band_pending = false
31
121
 
32
- @workers = []
122
+ @processors = []
33
123
 
34
124
  @auto_trim = nil
125
+ @reaper = nil
35
126
 
36
127
  @mutex.synchronize do
37
- min.times { spawn_thread }
128
+ @min.times do
129
+ spawn_thread
130
+ @not_full.wait(@mutex)
131
+ end
38
132
  end
133
+
134
+ @force_shutdown = false
135
+ @shutdown_mutex = Mutex.new
39
136
  end
40
137
 
41
- attr_reader :spawned, :trim_requested
138
+ attr_reader :spawned, :trim_requested, :waiting
139
+ attr_accessor :min, :max
140
+
141
+ # generate stats hash so as not to perform multiple locks
142
+ # @return [Hash] hash containing stat info from ThreadPool
143
+ def stats
144
+ with_mutex do
145
+ temp = @backlog_max
146
+ @backlog_max = 0
147
+ { backlog: @todo.size,
148
+ running: @spawned,
149
+ pool_capacity: pool_capacity,
150
+ busy_threads: @spawned - @waiting + @todo.size,
151
+ io_threads: @processors.count(&:marked_as_io_thread?),
152
+ backlog_max: temp
153
+ }
154
+ end
155
+ end
156
+
157
+ def reset_max
158
+ with_mutex { @backlog_max = 0 }
159
+ end
42
160
 
43
161
  # How many objects have yet to be processed by the pool?
44
162
  #
45
163
  def backlog
46
- @mutex.synchronize { @todo.size }
164
+ with_mutex { @todo.size }
165
+ end
166
+
167
+ # The maximum size of the backlog
168
+ #
169
+ def backlog_max
170
+ with_mutex { @backlog_max }
171
+ end
172
+
173
+ # @!attribute [r] pool_capacity
174
+ def pool_capacity
175
+ (waiting + (@max - spawned)).clamp(0, Float::INFINITY)
176
+ end
177
+
178
+ # @!attribute [r] busy_threads
179
+ # @version 5.0.0
180
+ def busy_threads
181
+ with_mutex { @spawned - @waiting + @todo.size }
47
182
  end
48
183
 
49
184
  # :nodoc:
@@ -53,92 +188,210 @@ module Puma
53
188
  def spawn_thread
54
189
  @spawned += 1
55
190
 
56
- th = Thread.new do
191
+ trigger_before_thread_start_hooks
192
+ processor = ProcessorThread.new(self)
193
+ processor.thread = Thread.new(processor, @spawned) do |processor, spawned|
194
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
195
+ # Advertise server into the thread
196
+ Thread.current.puma_server = @server
197
+
57
198
  todo = @todo
58
199
  block = @block
59
200
  mutex = @mutex
60
- cond = @cond
61
-
62
- extra = @extra.map { |i| i.new }
201
+ not_empty = @not_empty
202
+ not_full = @not_full
63
203
 
64
204
  while true
65
205
  work = nil
66
206
 
67
- continue = true
68
-
69
207
  mutex.synchronize do
208
+ if processor.marked_as_io_thread?
209
+ if @processors.count { |t| !t.marked_as_io_thread? } < @max
210
+ # We're not at max processor threads, so the io thread can rejoin the normal population.
211
+ processor.marked_as_io_thread = false
212
+ else
213
+ # We're already at max threads, so we exit the extra io thread.
214
+ @processors.delete(processor)
215
+ trigger_before_thread_exit_hooks
216
+ Thread.exit
217
+ end
218
+ end
219
+
70
220
  while todo.empty?
71
221
  if @trim_requested > 0
72
222
  @trim_requested -= 1
73
- continue = false
74
- break
75
- end
76
-
77
- if @shutdown
78
- continue = false
79
- break
223
+ @spawned -= 1
224
+ @processors.delete(processor)
225
+ not_full.signal
226
+ trigger_before_thread_exit_hooks
227
+ Thread.exit
80
228
  end
81
229
 
82
230
  @waiting += 1
83
- cond.wait mutex
84
- @waiting -= 1
231
+ if @out_of_band_pending && trigger_out_of_band_hook
232
+ @out_of_band_pending = false
233
+ end
234
+ not_full.signal
235
+ begin
236
+ not_empty.wait mutex
237
+ ensure
238
+ @waiting -= 1
239
+ end
85
240
  end
86
241
 
87
- work = todo.pop if continue
242
+ work = todo.shift
88
243
  end
89
244
 
90
- break unless continue
245
+ begin
246
+ @out_of_band_pending = true if block.call(processor, work)
247
+ rescue Exception => e
248
+ STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
249
+ end
250
+ end
251
+ end
252
+
253
+ @processors << processor
91
254
 
92
- block.call(work, *extra)
255
+ processor
256
+ end
257
+
258
+ private :spawn_thread
259
+
260
+ def trigger_before_thread_start_hooks
261
+ return unless @before_thread_start&.any?
262
+
263
+ @before_thread_start.each do |b|
264
+ begin
265
+ b[:block].call(ServerPluginControl.new(@server))
266
+ rescue Exception => e
267
+ STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
93
268
  end
269
+ end
270
+ nil
271
+ end
94
272
 
95
- mutex.synchronize do
96
- @spawned -= 1
97
- @workers.delete th
273
+ private :trigger_before_thread_start_hooks
274
+
275
+ def trigger_before_thread_exit_hooks
276
+ return unless @before_thread_exit&.any?
277
+
278
+ @before_thread_exit.each do |b|
279
+ begin
280
+ b[:block].call
281
+ rescue Exception => e
282
+ STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
98
283
  end
99
284
  end
285
+ nil
286
+ end
287
+
288
+ private :trigger_before_thread_exit_hooks
289
+
290
+ # @version 5.0.0
291
+ def trigger_out_of_band_hook
292
+ return false unless @out_of_band&.any?
293
+
294
+ # we execute on idle hook when all threads are free
295
+ return false unless @spawned == @waiting
296
+ @out_of_band_running = true
297
+ @out_of_band.each { |b| b[:block].call }
298
+ true
299
+ rescue Exception => e
300
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
301
+ true
302
+ ensure
303
+ @out_of_band_running = false
304
+ @out_of_band_condvar.broadcast
305
+ end
306
+
307
+ private :trigger_out_of_band_hook
308
+
309
+ def wait_while_out_of_band_running
310
+ return unless @out_of_band_running
100
311
 
101
- @workers << th
312
+ with_mutex do
313
+ @out_of_band_condvar.wait(@mutex) while @out_of_band_running
314
+ end
315
+ end
102
316
 
103
- th
317
+ # @version 5.0.0
318
+ def with_mutex(&block)
319
+ @mutex.owned? ?
320
+ yield :
321
+ @mutex.synchronize(&block)
104
322
  end
105
323
 
106
- private :spawn_thread
324
+ # :nodoc:
325
+ #
326
+ # Must be called with @mutex held!
327
+ #
328
+ def can_spawn_processor?
329
+ io_processors_count = @processors.count(&:marked_as_io_thread?)
330
+ extra_io_processors_count = io_processors_count > @max_io_threads ? io_processors_count - @max_io_threads : 0
331
+ (@spawned - io_processors_count) < (@max - extra_io_processors_count)
332
+ end
107
333
 
108
334
  # Add +work+ to the todo list for a Thread to pickup and process.
109
335
  def <<(work)
110
- @mutex.synchronize do
336
+ with_mutex do
111
337
  if @shutdown
112
338
  raise "Unable to add work while shutting down"
113
339
  end
114
340
 
115
341
  @todo << work
342
+ t = @todo.size
343
+ @backlog_max = t if t > @backlog_max
116
344
 
117
- if @waiting == 0 and @spawned < @max
345
+ if @waiting < @todo.size and can_spawn_processor?
118
346
  spawn_thread
119
347
  end
120
348
 
121
- @cond.signal
349
+ @not_empty.signal
122
350
  end
351
+ self
123
352
  end
124
353
 
125
- # If too many threads are in the pool, tell one to finish go ahead
354
+ def spawn_thread_if_needed # :nodoc:
355
+ with_mutex do
356
+ if @waiting < @todo.size and can_spawn_processor?
357
+ spawn_thread
358
+ end
359
+ end
360
+ end
361
+
362
+ # If there are any free threads in the pool, tell one to go ahead
126
363
  # and exit. If +force+ is true, then a trim request is requested
127
364
  # even if all threads are being utilized.
128
365
  #
129
366
  def trim(force=false)
130
- @mutex.synchronize do
131
- if (force or @waiting > 0) and @spawned - @trim_requested > @min
367
+ with_mutex do
368
+ free = @waiting - @todo.size
369
+ if (force or free > 0) and @spawned - @trim_requested > @min
132
370
  @trim_requested += 1
133
- @cond.signal
371
+ @not_empty.signal
134
372
  end
135
373
  end
136
374
  end
137
375
 
138
- class AutoTrim
139
- def initialize(pool, timeout)
376
+ # If there are dead threads in the pool make them go away while decreasing
377
+ # spawned counter so that new healthy threads could be created again.
378
+ def reap
379
+ with_mutex do
380
+ @processors, dead_processors = @processors.partition(&:alive?)
381
+
382
+ dead_processors.each do |processor|
383
+ processor.kill
384
+ @spawned -= 1
385
+ end
386
+ end
387
+ end
388
+
389
+ class Automaton
390
+ def initialize(pool, timeout, thread_name, message)
140
391
  @pool = pool
141
392
  @timeout = timeout
393
+ @thread_name = thread_name
394
+ @message = message
142
395
  @running = false
143
396
  end
144
397
 
@@ -146,8 +399,9 @@ module Puma
146
399
  @running = true
147
400
 
148
401
  @thread = Thread.new do
402
+ Puma.set_thread_name @thread_name
149
403
  while @running
150
- @pool.trim
404
+ @pool.public_send(@message)
151
405
  sleep @timeout
152
406
  end
153
407
  end
@@ -159,27 +413,105 @@ module Puma
159
413
  end
160
414
  end
161
415
 
162
- def auto_trim!(timeout=5)
163
- @auto_trim = AutoTrim.new(self, timeout)
416
+ def auto_trim!(timeout=@auto_trim_time)
417
+ @auto_trim = Automaton.new(self, timeout, "#{@name} tp trim", :trim)
164
418
  @auto_trim.start!
165
419
  end
166
420
 
421
+ def auto_reap!(timeout=@reaping_time)
422
+ @reaper = Automaton.new(self, timeout, "#{@name} tp reap", :reap)
423
+ @reaper.start!
424
+ end
425
+
426
+ # Allows ThreadPool::ForceShutdown to be raised within the
427
+ # provided block if the thread is forced to shutdown during execution.
428
+ def with_force_shutdown
429
+ t = Thread.current
430
+ @shutdown_mutex.synchronize do
431
+ raise ForceShutdown if @force_shutdown
432
+ t[:with_force_shutdown] = true
433
+ end
434
+ yield
435
+ ensure
436
+ t[:with_force_shutdown] = false
437
+ end
438
+
167
439
  # Tell all threads in the pool to exit and wait for them to finish.
440
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
441
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
442
+ # threads. Finally, wait 1 second for remaining threads to exit.
168
443
  #
169
- def shutdown
170
- @mutex.synchronize do
444
+ def shutdown(timeout)
445
+ threads = with_mutex do
171
446
  @shutdown = true
172
- @cond.broadcast
447
+ @trim_requested = @spawned
448
+ @not_empty.broadcast
449
+ @not_full.broadcast
450
+
451
+ @auto_trim&.stop
452
+ @reaper&.stop
453
+ # dup processors so that we join them all safely
454
+ @processors.dup
455
+ end
173
456
 
174
- @auto_trim.stop if @auto_trim
457
+ if @shutdown_debug == true
458
+ shutdown_debug("Shutdown initiated")
175
459
  end
176
460
 
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?
461
+ if timeout == -1
462
+ # Wait for threads to finish without force shutdown.
463
+ threads.each(&:join)
464
+ else
465
+ join = ->(inner_timeout) do
466
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
467
+ threads.reject! do |t|
468
+ remaining = inner_timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
469
+ remaining > 0 && t.join(remaining)
470
+ end
471
+ end
472
+
473
+ # Wait +timeout+ seconds for threads to finish.
474
+ join.call(timeout)
475
+ if @shutdown_debug == :on_force && !threads.empty?
476
+ shutdown_debug("Shutdown timeout exceeded")
477
+ end
478
+
479
+ # If threads are still running, raise ForceShutdown and wait to finish.
480
+ @shutdown_mutex.synchronize do
481
+ @force_shutdown = true
482
+ threads.each do |t|
483
+ t.raise ForceShutdown if t[:with_force_shutdown]
484
+ end
485
+ end
486
+ join.call(@shutdown_grace_time)
487
+ if @shutdown_debug == :on_force && !threads.empty?
488
+ shutdown_debug("Shutdown grace timeout exceeded")
489
+ end
490
+
491
+ # If threads are _still_ running, forcefully kill them and wait to finish.
492
+ threads.each(&:kill)
493
+ join.call(1)
494
+ end
180
495
 
181
496
  @spawned = 0
182
- @workers = []
497
+ @processors = []
498
+ end
499
+
500
+ private
501
+
502
+ def shutdown_debug(message)
503
+ pid = Process.pid
504
+ threads = Thread.list
505
+
506
+ $stdout.syswrite "#{pid}: #{message}\n"
507
+ $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
508
+
509
+ threads.each_with_index do |thread, index|
510
+ $stdout.syswrite "#{pid}: Thread #{index + 1}/#{threads.size}: #{thread.inspect}\n"
511
+ $stdout.syswrite "#{pid}: #{(thread.backtrace || []).join("\n#{pid}: ")}\n\n"
512
+ end
513
+
514
+ $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
183
515
  end
184
516
  end
185
517
  end
data/lib/puma/util.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri/common'
4
+
1
5
  module Puma
2
6
  module Util
3
7
  module_function
@@ -5,5 +9,126 @@ module Puma
5
9
  def pipe
6
10
  IO.pipe
7
11
  end
12
+
13
+ # Escapes and unescapes a URI escaped string with
14
+ # +encoding+. +encoding+ will be the target encoding of the string
15
+ # returned, and it defaults to UTF-8
16
+ if defined?(::Encoding)
17
+ def escape(s, encoding = Encoding::UTF_8)
18
+ URI.encode_www_form_component(s, encoding)
19
+ end
20
+
21
+ def unescape(s, encoding = Encoding::UTF_8)
22
+ URI.decode_www_form_component(s, encoding)
23
+ end
24
+ else
25
+ def escape(s, encoding = nil)
26
+ URI.encode_www_form_component(s, encoding)
27
+ end
28
+
29
+ def unescape(s, encoding = nil)
30
+ URI.decode_www_form_component(s, encoding)
31
+ end
32
+ end
33
+ module_function :unescape, :escape
34
+
35
+ DEFAULT_SEP = /[&;] */n
36
+
37
+ # Stolen from Mongrel, with some small modifications:
38
+ # Parses a query string by breaking it up at the '&'
39
+ # and ';' characters. You can also use this to parse
40
+ # cookies by changing the characters used in the second
41
+ # parameter (which defaults to '&;').
42
+ def parse_query(qs, d = nil, &unescaper)
43
+ unescaper ||= method(:unescape)
44
+
45
+ params = {}
46
+
47
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
48
+ next if p.empty?
49
+ k, v = p.split('=', 2).map(&unescaper)
50
+
51
+ if cur = params[k]
52
+ if cur.class == Array
53
+ params[k] << v
54
+ else
55
+ params[k] = [cur, v]
56
+ end
57
+ else
58
+ params[k] = v
59
+ end
60
+ end
61
+
62
+ params
63
+ end
64
+
65
+ # A case-insensitive Hash that preserves the original case of a
66
+ # header when set.
67
+ class HeaderHash < Hash
68
+ def self.new(hash={})
69
+ HeaderHash === hash ? hash : super(hash)
70
+ end
71
+
72
+ def initialize(hash={})
73
+ super()
74
+ @names = {}
75
+ hash.each { |k, v| self[k] = v }
76
+ end
77
+
78
+ def each
79
+ super do |k, v|
80
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
81
+ end
82
+ end
83
+
84
+ # @!attribute [r] to_hash
85
+ def to_hash
86
+ hash = {}
87
+ each { |k,v| hash[k] = v }
88
+ hash
89
+ end
90
+
91
+ def [](k)
92
+ super(k) || super(@names[k.downcase])
93
+ end
94
+
95
+ def []=(k, v)
96
+ canonical = k.downcase
97
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
98
+ @names[k] = @names[canonical] = k
99
+ super k, v
100
+ end
101
+
102
+ def delete(k)
103
+ canonical = k.downcase
104
+ result = super @names.delete(canonical)
105
+ @names.delete_if { |name,| name.downcase == canonical }
106
+ result
107
+ end
108
+
109
+ def include?(k)
110
+ @names.include?(k) || @names.include?(k.downcase)
111
+ end
112
+
113
+ alias_method :has_key?, :include?
114
+ alias_method :member?, :include?
115
+ alias_method :key?, :include?
116
+
117
+ def merge!(other)
118
+ other.each { |k, v| self[k] = v }
119
+ self
120
+ end
121
+
122
+ def merge(other)
123
+ hash = dup
124
+ hash.merge! other
125
+ end
126
+
127
+ def replace(other)
128
+ clear
129
+ other.each { |k, v| self[k] = v }
130
+ self
131
+ end
132
+ end
8
133
  end
9
134
  end