puma 3.12.6 → 6.3.0

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1806 -451
  3. data/LICENSE +23 -20
  4. data/README.md +217 -65
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +59 -21
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +69 -58
  9. data/docs/fork_worker.md +31 -0
  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 +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/docs/kubernetes.md +66 -0
  17. data/docs/nginx.md +2 -2
  18. data/docs/plugins.md +22 -12
  19. data/docs/rails_dev_mode.md +28 -0
  20. data/docs/restart.md +47 -22
  21. data/docs/signals.md +13 -11
  22. data/docs/stats.md +142 -0
  23. data/docs/systemd.md +94 -120
  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 +2 -2
  27. data/ext/puma_http11/ext_help.h +1 -1
  28. data/ext/puma_http11/extconf.rb +61 -3
  29. data/ext/puma_http11/http11_parser.c +103 -117
  30. data/ext/puma_http11/http11_parser.h +2 -2
  31. data/ext/puma_http11/http11_parser.java.rl +22 -38
  32. data/ext/puma_http11/http11_parser.rl +3 -3
  33. data/ext/puma_http11/http11_parser_common.rl +6 -6
  34. data/ext/puma_http11/mini_ssl.c +389 -99
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +248 -92
  39. data/ext/puma_http11/puma_http11.c +49 -57
  40. data/lib/puma/app/status.rb +71 -49
  41. data/lib/puma/binder.rb +244 -150
  42. data/lib/puma/cli.rb +38 -34
  43. data/lib/puma/client.rb +388 -244
  44. data/lib/puma/cluster/worker.rb +180 -0
  45. data/lib/puma/cluster/worker_handle.rb +97 -0
  46. data/lib/puma/cluster.rb +261 -243
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +116 -88
  49. data/lib/puma/const.rb +154 -104
  50. data/lib/puma/control_cli.rb +115 -70
  51. data/lib/puma/detect.rb +33 -2
  52. data/lib/puma/dsl.rb +764 -134
  53. data/lib/puma/error_logger.rb +113 -0
  54. data/lib/puma/events.rb +16 -112
  55. data/lib/puma/io_buffer.rb +42 -5
  56. data/lib/puma/jruby_restart.rb +2 -59
  57. data/lib/puma/json_serialization.rb +96 -0
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +184 -133
  60. data/lib/puma/log_writer.rb +147 -0
  61. data/lib/puma/minissl/context_builder.rb +93 -0
  62. data/lib/puma/minissl.rb +263 -70
  63. data/lib/puma/null_io.rb +18 -1
  64. data/lib/puma/plugin/systemd.rb +90 -0
  65. data/lib/puma/plugin/tmp_restart.rb +3 -1
  66. data/lib/puma/plugin.rb +7 -13
  67. data/lib/puma/rack/builder.rb +9 -11
  68. data/lib/puma/rack/urlmap.rb +2 -0
  69. data/lib/puma/rack_default.rb +21 -4
  70. data/lib/puma/reactor.rb +93 -315
  71. data/lib/puma/request.rb +671 -0
  72. data/lib/puma/runner.rb +94 -69
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +327 -772
  75. data/lib/puma/single.rb +20 -74
  76. data/lib/puma/state_file.rb +45 -8
  77. data/lib/puma/thread_pool.rb +146 -92
  78. data/lib/puma/util.rb +22 -10
  79. data/lib/puma.rb +60 -5
  80. data/lib/rack/handler/puma.rb +116 -90
  81. data/tools/Dockerfile +16 -0
  82. data/tools/trickletest.rb +0 -1
  83. metadata +54 -32
  84. data/ext/puma_http11/io_buffer.c +0 -155
  85. data/lib/puma/accept_nonblock.rb +0 -23
  86. data/lib/puma/compat.rb +0 -14
  87. data/lib/puma/convenient.rb +0 -25
  88. data/lib/puma/daemon_ext.rb +0 -33
  89. data/lib/puma/delegation.rb +0 -13
  90. data/lib/puma/java_io_buffer.rb +0 -47
  91. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  92. data/lib/puma/tcp_logger.rb +0 -41
  93. data/tools/jungle/README.md +0 -19
  94. data/tools/jungle/init.d/README.md +0 -61
  95. data/tools/jungle/init.d/puma +0 -421
  96. data/tools/jungle/init.d/run-puma +0 -18
  97. data/tools/jungle/upstart/README.md +0 -61
  98. data/tools/jungle/upstart/puma-manager.conf +0 -31
  99. data/tools/jungle/upstart/puma.conf +0 -69
  100. /data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
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
@@ -13,90 +13,35 @@ module Puma
13
13
  # gets created via the `start_server` method from the `Puma::Runner` class
14
14
  # that this inherits from.
15
15
  class Single < Runner
16
+ # @!attribute [r] stats
16
17
  def stats
17
- b = @server.backlog || 0
18
- r = @server.running || 0
19
- t = @server.pool_capacity || 0
20
- m = @server.max_threads || 0
21
- %Q!{ "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
18
+ {
19
+ started_at: utc_iso8601(@started_at)
20
+ }.merge(@server.stats).merge(super)
22
21
  end
23
22
 
24
23
  def restart
25
- @server.begin_restart
24
+ @server&.begin_restart
26
25
  end
27
26
 
28
27
  def stop
29
- @server.stop false
28
+ @server&.stop false
30
29
  end
31
30
 
32
31
  def halt
33
- @server.halt
32
+ @server&.halt
34
33
  end
35
34
 
36
35
  def stop_blocked
37
36
  log "- Gracefully stopping, waiting for requests to finish"
38
- @control.stop(true) if @control
39
- @server.stop(true)
40
- end
41
-
42
- def jruby_daemon?
43
- daemon? and Puma.jruby?
44
- end
45
-
46
- def jruby_daemon_start
47
- require 'puma/jruby_restart'
48
- JRubyRestart.daemon_start(@restart_dir, @launcher.restart_args)
37
+ @control&.stop true
38
+ @server&.stop true
49
39
  end
50
40
 
51
41
  def run
52
- already_daemon = false
53
-
54
- if jruby_daemon?
55
- require 'puma/jruby_restart'
56
-
57
- if JRubyRestart.daemon?
58
- # load and bind before redirecting IO so errors show up on stdout/stderr
59
- load_and_bind
60
- redirect_io
61
- end
62
-
63
- already_daemon = JRubyRestart.daemon_init
64
- end
65
-
66
42
  output_header "single"
67
43
 
68
- if jruby_daemon?
69
- if already_daemon
70
- JRubyRestart.perm_daemonize
71
- else
72
- pid = nil
73
-
74
- Signal.trap "SIGUSR2" do
75
- log "* Started new process #{pid} as daemon..."
76
-
77
- # Must use exit! so we don't unwind and run the ensures
78
- # that will be run by the new child (such as deleting the
79
- # pidfile)
80
- exit!(true)
81
- end
82
-
83
- Signal.trap "SIGCHLD" do
84
- log "! Error starting new process as daemon, exiting"
85
- exit 1
86
- end
87
-
88
- jruby_daemon_start
89
- sleep
90
- end
91
- else
92
- if daemon?
93
- log "* Daemonizing..."
94
- Process.daemon(true)
95
- redirect_io
96
- end
97
-
98
- load_and_bind
99
- end
44
+ load_and_bind
100
45
 
101
46
  Plugins.fire_background
102
47
 
@@ -105,16 +50,17 @@ module Puma
105
50
  start_control
106
51
 
107
52
  @server = server = start_server
53
+ server_thread = server.run
108
54
 
109
- unless daemon?
110
- log "Use Ctrl-C to stop"
111
- redirect_io
112
- end
55
+ log "Use Ctrl-C to stop"
56
+ redirect_io
57
+
58
+ @events.fire_on_booted!
113
59
 
114
- @launcher.events.fire_on_booted!
60
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
115
61
 
116
62
  begin
117
- server.run.join
63
+ server_thread.join
118
64
  rescue Interrupt
119
65
  # Swallow it
120
66
  end
@@ -1,24 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
-
5
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
+ #
6
14
  class StateFile
15
+
16
+ ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
+
7
18
  def initialize
8
19
  @options = {}
9
20
  end
10
21
 
11
- def save(path)
12
- File.write path, YAML.dump(@options)
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
+ if permission
36
+ File.write path, contents, mode: 'wb:UTF-8'
37
+ else
38
+ File.write path, contents, mode: 'wb:UTF-8', perm: permission
39
+ end
13
40
  end
14
41
 
15
42
  def load(path)
16
- @options = YAML.load File.read(path)
43
+ File.read(path).lines.each do |line|
44
+ next if line.start_with? '#'
45
+ k,v = line.split ':', 2
46
+ next unless v && ALLOWED_FIELDS.include?(k)
47
+ v = v.strip
48
+ @options[k] =
49
+ case v
50
+ when '' then nil
51
+ when /\A\d+\z/ then v.to_i
52
+ when /\A\d+\.\d+\z/ then v.to_f
53
+ else v.gsub(/\A"|"\z/, '')
54
+ end
55
+ end
17
56
  end
18
57
 
19
- FIELDS = %w!control_url control_auth_token pid!
20
-
21
- FIELDS.each do |f|
58
+ ALLOWED_FIELDS.each do |f|
22
59
  define_method f do
23
60
  @options[f]
24
61
  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
  #
@@ -13,7 +15,7 @@ module Puma
13
15
  # a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
14
16
  #
15
17
  # Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
16
- # and proceses it.
18
+ # and processes it.
17
19
  class ThreadPool
18
20
  class ForceShutdown < RuntimeError
19
21
  end
@@ -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(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
@@ -39,14 +41,23 @@ module Puma
39
41
  @spawned = 0
40
42
  @waiting = 0
41
43
 
42
- @min = Integer(min)
43
- @max = Integer(max)
44
+ @name = name
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)
44
51
  @block = block
45
- @extra = extra
52
+ @out_of_band = options[:out_of_band]
53
+ @clean_thread_locals = options[:clean_thread_locals]
54
+ @reaping_time = options[:reaping_time]
55
+ @auto_trim_time = options[:auto_trim_time]
46
56
 
47
57
  @shutdown = false
48
58
 
49
59
  @trim_requested = 0
60
+ @out_of_band_pending = false
50
61
 
51
62
  @workers = []
52
63
 
@@ -54,17 +65,20 @@ module Puma
54
65
  @reaper = nil
55
66
 
56
67
  @mutex.synchronize do
57
- @min.times { spawn_thread }
68
+ @min.times do
69
+ spawn_thread
70
+ @not_full.wait(@mutex)
71
+ end
58
72
  end
59
73
 
60
- @clean_thread_locals = false
74
+ @force_shutdown = false
75
+ @shutdown_mutex = Mutex.new
61
76
  end
62
77
 
63
78
  attr_reader :spawned, :trim_requested, :waiting
64
- attr_accessor :clean_thread_locals
65
79
 
66
80
  def self.clean_thread_locals
67
- Thread.current.keys.each do |key| # rubocop: disable Performance/HashEachMethods
81
+ Thread.current.keys.each do |key| # rubocop: disable Style/HashEachMethods
68
82
  Thread.current[key] = nil unless key == :__recursive_key__
69
83
  end
70
84
  end
@@ -72,13 +86,20 @@ module Puma
72
86
  # How many objects have yet to be processed by the pool?
73
87
  #
74
88
  def backlog
75
- @mutex.synchronize { @todo.size }
89
+ with_mutex { @todo.size }
76
90
  end
77
91
 
92
+ # @!attribute [r] pool_capacity
78
93
  def pool_capacity
79
94
  waiting + (@max - spawned)
80
95
  end
81
96
 
97
+ # @!attribute [r] busy_threads
98
+ # @version 5.0.0
99
+ def busy_threads
100
+ with_mutex { @spawned - @waiting + @todo.size }
101
+ end
102
+
82
103
  # :nodoc:
83
104
  #
84
105
  # Must be called with @mutex held!
@@ -87,61 +108,51 @@ module Puma
87
108
  @spawned += 1
88
109
 
89
110
  th = Thread.new(@spawned) do |spawned|
90
- # Thread name is new in Ruby 2.3
91
- Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=)
111
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
92
112
  todo = @todo
93
113
  block = @block
94
114
  mutex = @mutex
95
115
  not_empty = @not_empty
96
116
  not_full = @not_full
97
117
 
98
- extra = @extra.map { |i| i.new }
99
-
100
118
  while true
101
119
  work = nil
102
120
 
103
- continue = true
104
-
105
121
  mutex.synchronize do
106
122
  while todo.empty?
107
123
  if @trim_requested > 0
108
124
  @trim_requested -= 1
109
- continue = false
125
+ @spawned -= 1
126
+ @workers.delete th
110
127
  not_full.signal
111
- break
112
- end
113
-
114
- if @shutdown
115
- continue = false
116
- break
128
+ Thread.exit
117
129
  end
118
130
 
119
131
  @waiting += 1
132
+ if @out_of_band_pending && trigger_out_of_band_hook
133
+ @out_of_band_pending = false
134
+ end
120
135
  not_full.signal
121
- not_empty.wait mutex
122
- @waiting -= 1
136
+ begin
137
+ not_empty.wait mutex
138
+ ensure
139
+ @waiting -= 1
140
+ end
123
141
  end
124
142
 
125
- work = todo.shift if continue
143
+ work = todo.shift
126
144
  end
127
145
 
128
- break unless continue
129
-
130
146
  if @clean_thread_locals
131
147
  ThreadPool.clean_thread_locals
132
148
  end
133
149
 
134
150
  begin
135
- block.call(work, *extra)
151
+ @out_of_band_pending = true if block.call(work)
136
152
  rescue Exception => e
137
153
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
138
154
  end
139
155
  end
140
-
141
- mutex.synchronize do
142
- @spawned -= 1
143
- @workers.delete th
144
- end
145
156
  end
146
157
 
147
158
  @workers << th
@@ -151,9 +162,32 @@ module Puma
151
162
 
152
163
  private :spawn_thread
153
164
 
165
+ # @version 5.0.0
166
+ def trigger_out_of_band_hook
167
+ return false unless @out_of_band&.any?
168
+
169
+ # we execute on idle hook when all threads are free
170
+ return false unless @spawned == @waiting
171
+
172
+ @out_of_band.each(&:call)
173
+ true
174
+ rescue Exception => e
175
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
176
+ true
177
+ end
178
+
179
+ private :trigger_out_of_band_hook
180
+
181
+ # @version 5.0.0
182
+ def with_mutex(&block)
183
+ @mutex.owned? ?
184
+ yield :
185
+ @mutex.synchronize(&block)
186
+ end
187
+
154
188
  # Add +work+ to the todo list for a Thread to pickup and process.
155
189
  def <<(work)
156
- @mutex.synchronize do
190
+ with_mutex do
157
191
  if @shutdown
158
192
  raise "Unable to add work while shutting down"
159
193
  end
@@ -190,12 +224,12 @@ module Puma
190
224
  # request, it might not be added to the `@todo` array right away.
191
225
  # For example if a slow client has only sent a header, but not a body
192
226
  # then the `@todo` array would stay the same size as the reactor works
193
- # to try to buffer the request. In tha scenario the next call to this
227
+ # to try to buffer the request. In that scenario the next call to this
194
228
  # method would not block and another request would be added into the reactor
195
- # by the server. This would continue until a fully bufferend request
229
+ # by the server. This would continue until a fully buffered request
196
230
  # makes it through the reactor and can then be processed by the thread pool.
197
231
  def wait_until_not_full
198
- @mutex.synchronize do
232
+ with_mutex do
199
233
  while true
200
234
  return if @shutdown
201
235
 
@@ -203,20 +237,42 @@ module Puma
203
237
  # is work queued that cannot be handled by waiting
204
238
  # threads, then accept more work until we would
205
239
  # spin up the max number of threads.
206
- return if @todo.size - @waiting < @max - @spawned
240
+ return if busy_threads < @max
207
241
 
208
242
  @not_full.wait @mutex
209
243
  end
210
244
  end
211
245
  end
212
246
 
213
- # If too many threads are in the pool, tell one to finish go ahead
247
+ # @version 5.0.0
248
+ def wait_for_less_busy_worker(delay_s)
249
+ return unless delay_s && delay_s > 0
250
+
251
+ # Ruby MRI does GVL, this can result
252
+ # in processing contention when multiple threads
253
+ # (requests) are running concurrently
254
+ return unless Puma.mri?
255
+
256
+ with_mutex do
257
+ return if @shutdown
258
+
259
+ # do not delay, if we are not busy
260
+ return unless busy_threads > 0
261
+
262
+ # this will be signaled once a request finishes,
263
+ # which can happen earlier than delay
264
+ @not_full.wait @mutex, delay_s
265
+ end
266
+ end
267
+
268
+ # If there are any free threads in the pool, tell one to go ahead
214
269
  # and exit. If +force+ is true, then a trim request is requested
215
270
  # even if all threads are being utilized.
216
271
  #
217
272
  def trim(force=false)
218
- @mutex.synchronize do
219
- if (force or @waiting > 0) and @spawned - @trim_requested > @min
273
+ with_mutex do
274
+ free = @waiting - @todo.size
275
+ if (force or free > 0) and @spawned - @trim_requested > @min
220
276
  @trim_requested += 1
221
277
  @not_empty.signal
222
278
  end
@@ -226,7 +282,7 @@ module Puma
226
282
  # If there are dead threads in the pool make them go away while decreasing
227
283
  # spawned counter so that new healthy threads could be created again.
228
284
  def reap
229
- @mutex.synchronize do
285
+ with_mutex do
230
286
  dead_workers = @workers.reject(&:alive?)
231
287
 
232
288
  dead_workers.each do |worker|
@@ -240,10 +296,12 @@ module Puma
240
296
  end
241
297
  end
242
298
 
243
- class AutoTrim
244
- def initialize(pool, timeout)
299
+ class Automaton
300
+ def initialize(pool, timeout, thread_name, message)
245
301
  @pool = pool
246
302
  @timeout = timeout
303
+ @thread_name = thread_name
304
+ @message = message
247
305
  @running = false
248
306
  end
249
307
 
@@ -251,8 +309,9 @@ module Puma
251
309
  @running = true
252
310
 
253
311
  @thread = Thread.new do
312
+ Puma.set_thread_name @thread_name
254
313
  while @running
255
- @pool.trim
314
+ @pool.public_send(@message)
256
315
  sleep @timeout
257
316
  end
258
317
  end
@@ -264,50 +323,43 @@ module Puma
264
323
  end
265
324
  end
266
325
 
267
- def auto_trim!(timeout=30)
268
- @auto_trim = AutoTrim.new(self, timeout)
326
+ def auto_trim!(timeout=@auto_trim_time)
327
+ @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
269
328
  @auto_trim.start!
270
329
  end
271
330
 
272
- class Reaper
273
- def initialize(pool, timeout)
274
- @pool = pool
275
- @timeout = timeout
276
- @running = false
277
- end
278
-
279
- def start!
280
- @running = true
281
-
282
- @thread = Thread.new do
283
- while @running
284
- @pool.reap
285
- sleep @timeout
286
- end
287
- end
288
- end
289
-
290
- def stop
291
- @running = false
292
- @thread.wakeup
293
- end
331
+ def auto_reap!(timeout=@reaping_time)
332
+ @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
333
+ @reaper.start!
294
334
  end
295
335
 
296
- def auto_reap!(timeout=5)
297
- @reaper = Reaper.new(self, timeout)
298
- @reaper.start!
336
+ # Allows ThreadPool::ForceShutdown to be raised within the
337
+ # provided block if the thread is forced to shutdown during execution.
338
+ def with_force_shutdown
339
+ t = Thread.current
340
+ @shutdown_mutex.synchronize do
341
+ raise ForceShutdown if @force_shutdown
342
+ t[:with_force_shutdown] = true
343
+ end
344
+ yield
345
+ ensure
346
+ t[:with_force_shutdown] = false
299
347
  end
300
348
 
301
349
  # Tell all threads in the pool to exit and wait for them to finish.
350
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
351
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
352
+ # threads. Finally, wait 1 second for remaining threads to exit.
302
353
  #
303
354
  def shutdown(timeout=-1)
304
- threads = @mutex.synchronize do
355
+ threads = with_mutex do
305
356
  @shutdown = true
357
+ @trim_requested = @spawned
306
358
  @not_empty.broadcast
307
359
  @not_full.broadcast
308
360
 
309
- @auto_trim.stop if @auto_trim
310
- @reaper.stop if @reaper
361
+ @auto_trim&.stop
362
+ @reaper&.stop
311
363
  # dup workers so that we join them all safely
312
364
  @workers.dup
313
365
  end
@@ -316,27 +368,29 @@ module Puma
316
368
  # Wait for threads to finish without force shutdown.
317
369
  threads.each(&:join)
318
370
  else
319
- # Wait for threads to finish after n attempts (+timeout+).
320
- # If threads are still running, it will forcefully kill them.
321
- timeout.times do
322
- threads.delete_if do |t|
323
- t.join 1
324
- end
325
-
326
- if threads.empty?
327
- break
328
- else
329
- sleep 1
371
+ join = ->(inner_timeout) do
372
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
373
+ threads.reject! do |t|
374
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
375
+ t.join inner_timeout - elapsed
330
376
  end
331
377
  end
332
378
 
333
- threads.each do |t|
334
- t.raise ForceShutdown
335
- end
379
+ # Wait +timeout+ seconds for threads to finish.
380
+ join.call(timeout)
336
381
 
337
- threads.each do |t|
338
- t.join SHUTDOWN_GRACE_TIME
382
+ # If threads are still running, raise ForceShutdown and wait to finish.
383
+ @shutdown_mutex.synchronize do
384
+ @force_shutdown = true
385
+ threads.each do |t|
386
+ t.raise ForceShutdown if t[:with_force_shutdown]
387
+ end
339
388
  end
389
+ join.call(@shutdown_grace_time)
390
+
391
+ # If threads are _still_ running, forcefully kill them and wait to finish.
392
+ threads.each(&:kill)
393
+ join.call(1)
340
394
  end
341
395
 
342
396
  @spawned = 0
data/lib/puma/util.rb CHANGED
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
- major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
3
2
 
4
- if major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
5
- require 'puma/rack/backports/uri/common_193'
6
- else
7
- require 'uri/common'
8
- end
3
+ require 'uri/common'
9
4
 
10
5
  module Puma
11
6
  module Util
@@ -15,18 +10,34 @@ module Puma
15
10
  IO.pipe
16
11
  end
17
12
 
18
- # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
19
- # target encoding of the string returned, and it defaults to UTF-8
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
15
+ # Additional context: https://github.com/puma/puma/pull/1345
16
+ def purge_interrupt_queue
17
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
18
+ end
19
+
20
+ # Escapes and unescapes a URI escaped string with
21
+ # +encoding+. +encoding+ will be the target encoding of the string
22
+ # returned, and it defaults to UTF-8
20
23
  if defined?(::Encoding)
24
+ def escape(s, encoding = Encoding::UTF_8)
25
+ URI.encode_www_form_component(s, encoding)
26
+ end
27
+
21
28
  def unescape(s, encoding = Encoding::UTF_8)
22
29
  URI.decode_www_form_component(s, encoding)
23
30
  end
24
31
  else
32
+ def escape(s, encoding = nil)
33
+ URI.encode_www_form_component(s, encoding)
34
+ end
35
+
25
36
  def unescape(s, encoding = nil)
26
37
  URI.decode_www_form_component(s, encoding)
27
38
  end
28
39
  end
29
- module_function :unescape
40
+ module_function :unescape, :escape
30
41
 
31
42
  DEFAULT_SEP = /[&;] */n
32
43
 
@@ -55,7 +66,7 @@ module Puma
55
66
  end
56
67
  end
57
68
 
58
- return params
69
+ params
59
70
  end
60
71
 
61
72
  # A case-insensitive Hash that preserves the original case of a
@@ -77,6 +88,7 @@ module Puma
77
88
  end
78
89
  end
79
90
 
91
+ # @!attribute [r] to_hash
80
92
  def to_hash
81
93
  hash = {}
82
94
  each { |k,v| hash[k] = v }