puma 4.3.12 → 6.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1591 -521
  3. data/LICENSE +23 -20
  4. data/README.md +130 -42
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +63 -26
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +60 -69
  9. data/docs/fork_worker.md +31 -0
  10. data/docs/jungle/README.md +9 -0
  11. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  12. data/{tools → docs}/jungle/rc.d/puma +2 -2
  13. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  14. data/docs/kubernetes.md +66 -0
  15. data/docs/nginx.md +1 -1
  16. data/docs/plugins.md +15 -15
  17. data/docs/rails_dev_mode.md +28 -0
  18. data/docs/restart.md +46 -23
  19. data/docs/signals.md +13 -11
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +85 -128
  22. data/docs/testing_benchmarks_local_files.md +150 -0
  23. data/docs/testing_test_rackup_ci_files.md +36 -0
  24. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  25. data/ext/puma_http11/ext_help.h +1 -1
  26. data/ext/puma_http11/extconf.rb +49 -12
  27. data/ext/puma_http11/http11_parser.c +46 -48
  28. data/ext/puma_http11/http11_parser.h +2 -2
  29. data/ext/puma_http11/http11_parser.java.rl +3 -3
  30. data/ext/puma_http11/http11_parser.rl +3 -3
  31. data/ext/puma_http11/http11_parser_common.rl +2 -2
  32. data/ext/puma_http11/mini_ssl.c +250 -93
  33. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  34. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  35. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
  36. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  37. data/ext/puma_http11/puma_http11.c +46 -57
  38. data/lib/puma/app/status.rb +52 -38
  39. data/lib/puma/binder.rb +232 -119
  40. data/lib/puma/cli.rb +33 -33
  41. data/lib/puma/client.rb +125 -87
  42. data/lib/puma/cluster/worker.rb +175 -0
  43. data/lib/puma/cluster/worker_handle.rb +97 -0
  44. data/lib/puma/cluster.rb +224 -229
  45. data/lib/puma/commonlogger.rb +2 -2
  46. data/lib/puma/configuration.rb +112 -87
  47. data/lib/puma/const.rb +25 -22
  48. data/lib/puma/control_cli.rb +99 -79
  49. data/lib/puma/detect.rb +31 -2
  50. data/lib/puma/dsl.rb +423 -110
  51. data/lib/puma/error_logger.rb +112 -0
  52. data/lib/puma/events.rb +16 -115
  53. data/lib/puma/io_buffer.rb +34 -2
  54. data/lib/puma/jruby_restart.rb +2 -59
  55. data/lib/puma/json_serialization.rb +96 -0
  56. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  57. data/lib/puma/launcher.rb +170 -148
  58. data/lib/puma/log_writer.rb +137 -0
  59. data/lib/puma/minissl/context_builder.rb +35 -19
  60. data/lib/puma/minissl.rb +213 -55
  61. data/lib/puma/null_io.rb +18 -1
  62. data/lib/puma/plugin/tmp_restart.rb +1 -1
  63. data/lib/puma/plugin.rb +3 -12
  64. data/lib/puma/rack/builder.rb +5 -9
  65. data/lib/puma/rack_default.rb +1 -1
  66. data/lib/puma/reactor.rb +85 -369
  67. data/lib/puma/request.rb +607 -0
  68. data/lib/puma/runner.rb +83 -77
  69. data/lib/puma/server.rb +305 -789
  70. data/lib/puma/single.rb +18 -74
  71. data/lib/puma/state_file.rb +45 -8
  72. data/lib/puma/systemd.rb +47 -0
  73. data/lib/puma/thread_pool.rb +137 -66
  74. data/lib/puma/util.rb +21 -4
  75. data/lib/puma.rb +54 -5
  76. data/lib/rack/handler/puma.rb +11 -12
  77. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  78. metadata +31 -23
  79. data/docs/tcp_mode.md +0 -96
  80. data/ext/puma_http11/io_buffer.c +0 -155
  81. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  82. data/lib/puma/accept_nonblock.rb +0 -29
  83. data/lib/puma/tcp_logger.rb +0 -41
  84. data/tools/jungle/README.md +0 -19
  85. data/tools/jungle/init.d/README.md +0 -61
  86. data/tools/jungle/init.d/puma +0 -421
  87. data/tools/jungle/init.d/run-puma +0 -18
  88. data/tools/jungle/upstart/README.md +0 -61
  89. data/tools/jungle/upstart/puma-manager.conf +0 -31
  90. data/tools/jungle/upstart/puma.conf +0 -69
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!{ "started_at": "#{@started_at.utc.iso8601}", "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
18
+ {
19
+ started_at: @started_at.utc.iso8601
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) if @server
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) if @server
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,15 @@ 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
113
57
 
114
- @launcher.events.fire_on_booted!
58
+ @events.fire_on_booted!
115
59
 
116
60
  begin
117
- server.run.join
61
+ server_thread.join
118
62
  rescue Interrupt
119
63
  # Swallow it
120
64
  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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sd_notify'
4
+
5
+ module Puma
6
+ class Systemd
7
+ def initialize(log_writer, events)
8
+ @log_writer = log_writer
9
+ @events = events
10
+ end
11
+
12
+ def hook_events
13
+ @events.on_booted { SdNotify.ready }
14
+ @events.on_stopped { SdNotify.stopping }
15
+ @events.on_restart { SdNotify.reloading }
16
+ end
17
+
18
+ def start_watchdog
19
+ return unless SdNotify.watchdog?
20
+
21
+ ping_f = watchdog_sleep_time
22
+
23
+ log "Pinging systemd watchdog every #{ping_f.round(1)} sec"
24
+ Thread.new do
25
+ loop do
26
+ sleep ping_f
27
+ SdNotify.watchdog
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def watchdog_sleep_time
35
+ usec = Integer(ENV["WATCHDOG_USEC"])
36
+
37
+ sec_f = usec / 1_000_000.0
38
+ # "It is recommended that a daemon sends a keep-alive notification message
39
+ # to the service manager every half of the time returned here."
40
+ sec_f / 2
41
+ end
42
+
43
+ def log(str)
44
+ @log_writer.log(str)
45
+ end
46
+ end
47
+ 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,20 @@ 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])
44
47
  @block = block
45
- @extra = extra
48
+ @extra = [::Puma::IOBuffer]
49
+ @out_of_band = options[:out_of_band]
50
+ @clean_thread_locals = options[:clean_thread_locals]
51
+ @reaping_time = options[:reaping_time]
52
+ @auto_trim_time = options[:auto_trim_time]
46
53
 
47
54
  @shutdown = false
48
55
 
49
56
  @trim_requested = 0
57
+ @out_of_band_pending = false
50
58
 
51
59
  @workers = []
52
60
 
@@ -54,17 +62,20 @@ module Puma
54
62
  @reaper = nil
55
63
 
56
64
  @mutex.synchronize do
57
- @min.times { spawn_thread }
65
+ @min.times do
66
+ spawn_thread
67
+ @not_full.wait(@mutex)
68
+ end
58
69
  end
59
70
 
60
- @clean_thread_locals = false
71
+ @force_shutdown = false
72
+ @shutdown_mutex = Mutex.new
61
73
  end
62
74
 
63
75
  attr_reader :spawned, :trim_requested, :waiting
64
- attr_accessor :clean_thread_locals
65
76
 
66
77
  def self.clean_thread_locals
67
- Thread.current.keys.each do |key| # rubocop: disable Performance/HashEachMethods
78
+ Thread.current.keys.each do |key| # rubocop: disable Style/HashEachMethods
68
79
  Thread.current[key] = nil unless key == :__recursive_key__
69
80
  end
70
81
  end
@@ -72,13 +83,20 @@ module Puma
72
83
  # How many objects have yet to be processed by the pool?
73
84
  #
74
85
  def backlog
75
- @mutex.synchronize { @todo.size }
86
+ with_mutex { @todo.size }
76
87
  end
77
88
 
89
+ # @!attribute [r] pool_capacity
78
90
  def pool_capacity
79
91
  waiting + (@max - spawned)
80
92
  end
81
93
 
94
+ # @!attribute [r] busy_threads
95
+ # @version 5.0.0
96
+ def busy_threads
97
+ with_mutex { @spawned - @waiting + @todo.size }
98
+ end
99
+
82
100
  # :nodoc:
83
101
  #
84
102
  # Must be called with @mutex held!
@@ -87,7 +105,7 @@ module Puma
87
105
  @spawned += 1
88
106
 
89
107
  th = Thread.new(@spawned) do |spawned|
90
- Puma.set_thread_name 'threadpool %03i' % spawned
108
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
91
109
  todo = @todo
92
110
  block = @block
93
111
  mutex = @mutex
@@ -99,48 +117,41 @@ module Puma
99
117
  while true
100
118
  work = nil
101
119
 
102
- continue = true
103
-
104
120
  mutex.synchronize do
105
121
  while todo.empty?
106
122
  if @trim_requested > 0
107
123
  @trim_requested -= 1
108
- continue = false
124
+ @spawned -= 1
125
+ @workers.delete th
109
126
  not_full.signal
110
- break
111
- end
112
-
113
- if @shutdown
114
- continue = false
115
- break
127
+ Thread.exit
116
128
  end
117
129
 
118
130
  @waiting += 1
131
+ if @out_of_band_pending && trigger_out_of_band_hook
132
+ @out_of_band_pending = false
133
+ end
119
134
  not_full.signal
120
- not_empty.wait mutex
121
- @waiting -= 1
135
+ begin
136
+ not_empty.wait mutex
137
+ ensure
138
+ @waiting -= 1
139
+ end
122
140
  end
123
141
 
124
- work = todo.shift if continue
142
+ work = todo.shift
125
143
  end
126
144
 
127
- break unless continue
128
-
129
145
  if @clean_thread_locals
130
146
  ThreadPool.clean_thread_locals
131
147
  end
132
148
 
133
149
  begin
134
- block.call(work, *extra)
150
+ @out_of_band_pending = true if block.call(work, *extra)
135
151
  rescue Exception => e
136
152
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
137
153
  end
138
154
  end
139
-
140
- mutex.synchronize do
141
- @spawned -= 1
142
- @workers.delete th
143
- end
144
155
  end
145
156
 
146
157
  @workers << th
@@ -150,9 +161,32 @@ module Puma
150
161
 
151
162
  private :spawn_thread
152
163
 
164
+ # @version 5.0.0
165
+ def trigger_out_of_band_hook
166
+ return false unless @out_of_band&.any?
167
+
168
+ # we execute on idle hook when all threads are free
169
+ return false unless @spawned == @waiting
170
+
171
+ @out_of_band.each(&:call)
172
+ true
173
+ rescue Exception => e
174
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
175
+ true
176
+ end
177
+
178
+ private :trigger_out_of_band_hook
179
+
180
+ # @version 5.0.0
181
+ def with_mutex(&block)
182
+ @mutex.owned? ?
183
+ yield :
184
+ @mutex.synchronize(&block)
185
+ end
186
+
153
187
  # Add +work+ to the todo list for a Thread to pickup and process.
154
188
  def <<(work)
155
- @mutex.synchronize do
189
+ with_mutex do
156
190
  if @shutdown
157
191
  raise "Unable to add work while shutting down"
158
192
  end
@@ -191,13 +225,10 @@ module Puma
191
225
  # then the `@todo` array would stay the same size as the reactor works
192
226
  # to try to buffer the request. In that scenario the next call to this
193
227
  # method would not block and another request would be added into the reactor
194
- # by the server. This would continue until a fully bufferend request
228
+ # by the server. This would continue until a fully buffered request
195
229
  # makes it through the reactor and can then be processed by the thread pool.
196
- #
197
- # Returns the current number of busy threads, or +nil+ if shutting down.
198
- #
199
230
  def wait_until_not_full
200
- @mutex.synchronize do
231
+ with_mutex do
201
232
  while true
202
233
  return if @shutdown
203
234
 
@@ -205,21 +236,42 @@ module Puma
205
236
  # is work queued that cannot be handled by waiting
206
237
  # threads, then accept more work until we would
207
238
  # spin up the max number of threads.
208
- busy_threads = @spawned - @waiting + @todo.size
209
- return busy_threads if @max > busy_threads
239
+ return if busy_threads < @max
210
240
 
211
241
  @not_full.wait @mutex
212
242
  end
213
243
  end
214
244
  end
215
245
 
216
- # If too many threads are in the pool, tell one to finish go ahead
246
+ # @version 5.0.0
247
+ def wait_for_less_busy_worker(delay_s)
248
+ return unless delay_s && delay_s > 0
249
+
250
+ # Ruby MRI does GVL, this can result
251
+ # in processing contention when multiple threads
252
+ # (requests) are running concurrently
253
+ return unless Puma.mri?
254
+
255
+ with_mutex do
256
+ return if @shutdown
257
+
258
+ # do not delay, if we are not busy
259
+ return unless busy_threads > 0
260
+
261
+ # this will be signaled once a request finishes,
262
+ # which can happen earlier than delay
263
+ @not_full.wait @mutex, delay_s
264
+ end
265
+ end
266
+
267
+ # If there are any free threads in the pool, tell one to go ahead
217
268
  # and exit. If +force+ is true, then a trim request is requested
218
269
  # even if all threads are being utilized.
219
270
  #
220
271
  def trim(force=false)
221
- @mutex.synchronize do
222
- if (force or @waiting > 0) and @spawned - @trim_requested > @min
272
+ with_mutex do
273
+ free = @waiting - @todo.size
274
+ if (force or free > 0) and @spawned - @trim_requested > @min
223
275
  @trim_requested += 1
224
276
  @not_empty.signal
225
277
  end
@@ -229,7 +281,7 @@ module Puma
229
281
  # If there are dead threads in the pool make them go away while decreasing
230
282
  # spawned counter so that new healthy threads could be created again.
231
283
  def reap
232
- @mutex.synchronize do
284
+ with_mutex do
233
285
  dead_workers = @workers.reject(&:alive?)
234
286
 
235
287
  dead_workers.each do |worker|
@@ -270,26 +322,43 @@ module Puma
270
322
  end
271
323
  end
272
324
 
273
- def auto_trim!(timeout=30)
274
- @auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
325
+ def auto_trim!(timeout=@auto_trim_time)
326
+ @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
275
327
  @auto_trim.start!
276
328
  end
277
329
 
278
- def auto_reap!(timeout=5)
279
- @reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
330
+ def auto_reap!(timeout=@reaping_time)
331
+ @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
280
332
  @reaper.start!
281
333
  end
282
334
 
335
+ # Allows ThreadPool::ForceShutdown to be raised within the
336
+ # provided block if the thread is forced to shutdown during execution.
337
+ def with_force_shutdown
338
+ t = Thread.current
339
+ @shutdown_mutex.synchronize do
340
+ raise ForceShutdown if @force_shutdown
341
+ t[:with_force_shutdown] = true
342
+ end
343
+ yield
344
+ ensure
345
+ t[:with_force_shutdown] = false
346
+ end
347
+
283
348
  # Tell all threads in the pool to exit and wait for them to finish.
349
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
350
+ # Next, wait an extra +grace+ seconds then force-kill remaining threads.
351
+ # Finally, wait +kill_grace+ seconds for remaining threads to exit.
284
352
  #
285
353
  def shutdown(timeout=-1)
286
- threads = @mutex.synchronize do
354
+ threads = with_mutex do
287
355
  @shutdown = true
356
+ @trim_requested = @spawned
288
357
  @not_empty.broadcast
289
358
  @not_full.broadcast
290
359
 
291
- @auto_trim.stop if @auto_trim
292
- @reaper.stop if @reaper
360
+ @auto_trim&.stop
361
+ @reaper&.stop
293
362
  # dup workers so that we join them all safely
294
363
  @workers.dup
295
364
  end
@@ -298,27 +367,29 @@ module Puma
298
367
  # Wait for threads to finish without force shutdown.
299
368
  threads.each(&:join)
300
369
  else
301
- # Wait for threads to finish after n attempts (+timeout+).
302
- # If threads are still running, it will forcefully kill them.
303
- timeout.times do
304
- threads.delete_if do |t|
305
- t.join 1
306
- end
307
-
308
- if threads.empty?
309
- break
310
- else
311
- sleep 1
370
+ join = ->(inner_timeout) do
371
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
372
+ threads.reject! do |t|
373
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
374
+ t.join inner_timeout - elapsed
312
375
  end
313
376
  end
314
377
 
315
- threads.each do |t|
316
- t.raise ForceShutdown
317
- end
378
+ # Wait +timeout+ seconds for threads to finish.
379
+ join.call(timeout)
318
380
 
319
- threads.each do |t|
320
- t.join SHUTDOWN_GRACE_TIME
381
+ # If threads are still running, raise ForceShutdown and wait to finish.
382
+ @shutdown_mutex.synchronize do
383
+ @force_shutdown = true
384
+ threads.each do |t|
385
+ t.raise ForceShutdown if t[:with_force_shutdown]
386
+ end
321
387
  end
388
+ join.call(SHUTDOWN_GRACE_TIME)
389
+
390
+ # If threads are _still_ running, forcefully kill them and wait to finish.
391
+ threads.each(&:kill)
392
+ join.call(1)
322
393
  end
323
394
 
324
395
  @spawned = 0
data/lib/puma/util.rb CHANGED
@@ -10,18 +10,34 @@ module Puma
10
10
  IO.pipe
11
11
  end
12
12
 
13
- # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
14
- # 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
15
23
  if defined?(::Encoding)
24
+ def escape(s, encoding = Encoding::UTF_8)
25
+ URI.encode_www_form_component(s, encoding)
26
+ end
27
+
16
28
  def unescape(s, encoding = Encoding::UTF_8)
17
29
  URI.decode_www_form_component(s, encoding)
18
30
  end
19
31
  else
32
+ def escape(s, encoding = nil)
33
+ URI.encode_www_form_component(s, encoding)
34
+ end
35
+
20
36
  def unescape(s, encoding = nil)
21
37
  URI.decode_www_form_component(s, encoding)
22
38
  end
23
39
  end
24
- module_function :unescape
40
+ module_function :unescape, :escape
25
41
 
26
42
  DEFAULT_SEP = /[&;] */n
27
43
 
@@ -50,7 +66,7 @@ module Puma
50
66
  end
51
67
  end
52
68
 
53
- return params
69
+ params
54
70
  end
55
71
 
56
72
  # A case-insensitive Hash that preserves the original case of a
@@ -72,6 +88,7 @@ module Puma
72
88
  end
73
89
  end
74
90
 
91
+ # @!attribute [r] to_hash
75
92
  def to_hash
76
93
  hash = {}
77
94
  each { |k,v| hash[k] = v }