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.
- checksums.yaml +7 -0
- data/History.md +3334 -0
- data/LICENSE +23 -20
- data/README.md +387 -100
- data/bin/puma-wild +25 -0
- data/bin/pumactl +1 -1
- data/docs/5.0-Upgrade.md +98 -0
- data/docs/6.0-Upgrade.md +56 -0
- data/docs/7.0-Upgrade.md +52 -0
- data/docs/8.0-Upgrade.md +100 -0
- data/docs/architecture.md +74 -0
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +137 -0
- data/docs/fork_worker.md +41 -0
- data/docs/grpc.md +62 -0
- data/docs/images/favicon.svg +1 -0
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/images/running-puma.svg +1 -0
- data/docs/images/standard-logo.svg +1 -0
- data/docs/java_options.md +54 -0
- data/docs/jungle/README.md +9 -0
- data/docs/jungle/rc.d/README.md +74 -0
- data/docs/jungle/rc.d/puma +61 -0
- data/docs/jungle/rc.d/puma.conf +10 -0
- data/docs/kubernetes.md +73 -0
- data/docs/nginx.md +5 -5
- data/docs/plugins.md +42 -0
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +65 -0
- data/docs/signals.md +98 -0
- data/docs/stats.md +148 -0
- data/docs/systemd.md +253 -0
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -2
- data/ext/puma_http11/extconf.rb +59 -2
- data/ext/puma_http11/http11_parser.c +319 -487
- data/ext/puma_http11/http11_parser.h +15 -13
- data/ext/puma_http11/http11_parser.java.rl +64 -94
- data/ext/puma_http11/http11_parser.rl +27 -24
- data/ext/puma_http11/http11_parser_common.rl +8 -8
- data/ext/puma_http11/mini_ssl.c +696 -38
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +241 -145
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +174 -221
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
- data/ext/puma_http11/puma_http11.c +183 -175
- data/lib/puma/app/status.rb +77 -29
- data/lib/puma/binder.rb +349 -119
- data/lib/puma/cli.rb +163 -801
- data/lib/puma/client.rb +627 -144
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +183 -0
- data/lib/puma/cluster/worker_handle.rb +127 -0
- data/lib/puma/cluster.rb +634 -0
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +115 -0
- data/lib/puma/configuration.rb +420 -198
- data/lib/puma/const.rb +266 -115
- data/lib/puma/control_cli.rb +219 -120
- data/lib/puma/detect.rb +56 -2
- data/lib/puma/dsl.rb +1562 -0
- data/lib/puma/error_logger.rb +115 -0
- data/lib/puma/events.rb +44 -60
- data/lib/puma/io_buffer.rb +48 -5
- data/lib/puma/jruby_restart.rb +2 -51
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +102 -0
- data/lib/puma/launcher.rb +501 -0
- data/lib/puma/log_writer.rb +153 -0
- data/lib/puma/minissl/context_builder.rb +96 -0
- data/lib/puma/minissl.rb +355 -37
- data/lib/puma/null_io.rb +79 -12
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +36 -0
- data/lib/puma/plugin.rb +111 -0
- data/lib/puma/rack/builder.rb +297 -0
- data/lib/puma/rack/urlmap.rb +93 -0
- data/lib/puma/rack_default.rb +21 -4
- data/lib/puma/reactor.rb +102 -133
- data/lib/puma/response.rb +532 -0
- data/lib/puma/runner.rb +211 -0
- data/lib/puma/sd_notify.rb +146 -0
- data/lib/puma/server.rb +582 -445
- data/lib/puma/server_plugin_control.rb +32 -0
- data/lib/puma/single.rb +72 -0
- data/lib/puma/state_file.rb +69 -0
- data/lib/puma/thread_pool.rb +389 -57
- data/lib/puma/util.rb +125 -0
- data/lib/puma.rb +80 -6
- data/lib/rack/handler/puma.rb +125 -47
- data/tools/Dockerfile +26 -0
- data/tools/trickletest.rb +44 -0
- metadata +88 -148
- data/COPYING +0 -55
- data/Gemfile +0 -10
- data/History.txt +0 -302
- data/Manifest.txt +0 -60
- data/Rakefile +0 -165
- data/TODO +0 -5
- data/docs/config.md +0 -0
- data/ext/puma_http11/ext_help.h +0 -15
- data/ext/puma_http11/io_buffer.c +0 -154
- data/lib/puma/accept_nonblock.rb +0 -23
- data/lib/puma/capistrano.rb +0 -34
- data/lib/puma/compat.rb +0 -11
- data/lib/puma/daemon_ext.rb +0 -20
- data/lib/puma/delegation.rb +0 -11
- data/lib/puma/java_io_buffer.rb +0 -45
- data/lib/puma/rack_patch.rb +0 -25
- data/puma.gemspec +0 -45
- data/test/test_app_status.rb +0 -88
- data/test/test_cli.rb +0 -171
- data/test/test_config.rb +0 -16
- data/test/test_http10.rb +0 -27
- data/test/test_http11.rb +0 -126
- data/test/test_integration.rb +0 -154
- data/test/test_iobuffer.rb +0 -38
- data/test/test_minissl.rb +0 -25
- data/test/test_null_io.rb +0 -31
- data/test/test_persistent.rb +0 -238
- data/test/test_puma_server.rb +0 -224
- data/test/test_rack_handler.rb +0 -10
- data/test/test_rack_server.rb +0 -141
- data/test/test_thread_pool.rb +0 -146
- data/test/test_unix_socket.rb +0 -39
- data/test/test_ws.rb +0 -89
- data/tools/jungle/init.d/README.md +0 -54
- data/tools/jungle/init.d/puma +0 -332
- data/tools/jungle/init.d/run-puma +0 -3
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- data/tools/jungle/upstart/puma.conf +0 -52
data/lib/puma/thread_pool.rb
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
15
|
-
@
|
|
16
|
-
@mutex = Mutex.new
|
|
86
|
+
def initialize(name, options = {}, server: nil, &block)
|
|
87
|
+
@server = server
|
|
17
88
|
|
|
18
|
-
@
|
|
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
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
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
|
-
@
|
|
122
|
+
@processors = []
|
|
33
123
|
|
|
34
124
|
@auto_trim = nil
|
|
125
|
+
@reaper = nil
|
|
35
126
|
|
|
36
127
|
@mutex.synchronize do
|
|
37
|
-
min.times
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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.
|
|
242
|
+
work = todo.shift
|
|
88
243
|
end
|
|
89
244
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
312
|
+
with_mutex do
|
|
313
|
+
@out_of_band_condvar.wait(@mutex) while @out_of_band_running
|
|
314
|
+
end
|
|
315
|
+
end
|
|
102
316
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
345
|
+
if @waiting < @todo.size and can_spawn_processor?
|
|
118
346
|
spawn_thread
|
|
119
347
|
end
|
|
120
348
|
|
|
121
|
-
@
|
|
349
|
+
@not_empty.signal
|
|
122
350
|
end
|
|
351
|
+
self
|
|
123
352
|
end
|
|
124
353
|
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
@
|
|
371
|
+
@not_empty.signal
|
|
134
372
|
end
|
|
135
373
|
end
|
|
136
374
|
end
|
|
137
375
|
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
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
|
|
163
|
-
@auto_trim =
|
|
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
|
-
|
|
444
|
+
def shutdown(timeout)
|
|
445
|
+
threads = with_mutex do
|
|
171
446
|
@shutdown = true
|
|
172
|
-
@
|
|
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
|
-
|
|
457
|
+
if @shutdown_debug == true
|
|
458
|
+
shutdown_debug("Shutdown initiated")
|
|
175
459
|
end
|
|
176
460
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
@
|
|
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
|