puma 4.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.
- checksums.yaml +7 -0
- data/History.md +1532 -0
- data/LICENSE +26 -0
- data/README.md +291 -0
- data/bin/puma +10 -0
- data/bin/puma-wild +31 -0
- data/bin/pumactl +12 -0
- data/docs/architecture.md +37 -0
- data/docs/deployment.md +111 -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/nginx.md +80 -0
- data/docs/plugins.md +38 -0
- data/docs/restart.md +41 -0
- data/docs/signals.md +96 -0
- data/docs/systemd.md +290 -0
- data/docs/tcp_mode.md +96 -0
- data/ext/puma_http11/PumaHttp11Service.java +19 -0
- data/ext/puma_http11/ext_help.h +15 -0
- data/ext/puma_http11/extconf.rb +28 -0
- data/ext/puma_http11/http11_parser.c +1044 -0
- data/ext/puma_http11/http11_parser.h +65 -0
- data/ext/puma_http11/http11_parser.java.rl +145 -0
- data/ext/puma_http11/http11_parser.rl +147 -0
- data/ext/puma_http11/http11_parser_common.rl +54 -0
- data/ext/puma_http11/io_buffer.c +155 -0
- data/ext/puma_http11/mini_ssl.c +553 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +226 -0
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +455 -0
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +363 -0
- data/ext/puma_http11/puma_http11.c +502 -0
- data/lib/puma.rb +31 -0
- data/lib/puma/accept_nonblock.rb +29 -0
- data/lib/puma/app/status.rb +80 -0
- data/lib/puma/binder.rb +385 -0
- data/lib/puma/cli.rb +239 -0
- data/lib/puma/client.rb +494 -0
- data/lib/puma/cluster.rb +554 -0
- data/lib/puma/commonlogger.rb +108 -0
- data/lib/puma/configuration.rb +362 -0
- data/lib/puma/const.rb +235 -0
- data/lib/puma/control_cli.rb +289 -0
- data/lib/puma/detect.rb +15 -0
- data/lib/puma/dsl.rb +740 -0
- data/lib/puma/events.rb +156 -0
- data/lib/puma/io_buffer.rb +4 -0
- data/lib/puma/jruby_restart.rb +84 -0
- data/lib/puma/launcher.rb +475 -0
- data/lib/puma/minissl.rb +278 -0
- data/lib/puma/minissl/context_builder.rb +76 -0
- data/lib/puma/null_io.rb +44 -0
- data/lib/puma/plugin.rb +120 -0
- data/lib/puma/plugin/tmp_restart.rb +36 -0
- data/lib/puma/rack/builder.rb +301 -0
- data/lib/puma/rack/urlmap.rb +93 -0
- data/lib/puma/rack_default.rb +9 -0
- data/lib/puma/reactor.rb +400 -0
- data/lib/puma/runner.rb +192 -0
- data/lib/puma/server.rb +1030 -0
- data/lib/puma/single.rb +123 -0
- data/lib/puma/state_file.rb +31 -0
- data/lib/puma/tcp_logger.rb +41 -0
- data/lib/puma/thread_pool.rb +328 -0
- data/lib/puma/util.rb +124 -0
- data/lib/rack/handler/puma.rb +115 -0
- data/tools/docker/Dockerfile +16 -0
- data/tools/jungle/README.md +19 -0
- data/tools/jungle/init.d/README.md +61 -0
- data/tools/jungle/init.d/puma +421 -0
- data/tools/jungle/init.d/run-puma +18 -0
- data/tools/jungle/rc.d/README.md +74 -0
- data/tools/jungle/rc.d/puma +61 -0
- data/tools/jungle/rc.d/puma.conf +10 -0
- data/tools/jungle/upstart/README.md +61 -0
- data/tools/jungle/upstart/puma-manager.conf +31 -0
- data/tools/jungle/upstart/puma.conf +69 -0
- data/tools/trickletest.rb +44 -0
- metadata +144 -0
data/lib/puma/single.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puma/runner'
|
4
|
+
require 'puma/detect'
|
5
|
+
require 'puma/plugin'
|
6
|
+
|
7
|
+
module Puma
|
8
|
+
# This class is instantiated by the `Puma::Launcher` and used
|
9
|
+
# to boot and serve a Ruby application when no puma "workers" are needed
|
10
|
+
# i.e. only using "threaded" mode. For example `$ puma -t 1:5`
|
11
|
+
#
|
12
|
+
# At the core of this class is running an instance of `Puma::Server` which
|
13
|
+
# gets created via the `start_server` method from the `Puma::Runner` class
|
14
|
+
# that this inherits from.
|
15
|
+
class Single < Runner
|
16
|
+
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} }!
|
22
|
+
end
|
23
|
+
|
24
|
+
def restart
|
25
|
+
@server.begin_restart
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop
|
29
|
+
@server.stop(false) if @server
|
30
|
+
end
|
31
|
+
|
32
|
+
def halt
|
33
|
+
@server.halt
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop_blocked
|
37
|
+
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)
|
49
|
+
end
|
50
|
+
|
51
|
+
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
|
+
output_header "single"
|
67
|
+
|
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
|
100
|
+
|
101
|
+
Plugins.fire_background
|
102
|
+
|
103
|
+
@launcher.write_state
|
104
|
+
|
105
|
+
start_control
|
106
|
+
|
107
|
+
@server = server = start_server
|
108
|
+
|
109
|
+
unless daemon?
|
110
|
+
log "Use Ctrl-C to stop"
|
111
|
+
redirect_io
|
112
|
+
end
|
113
|
+
|
114
|
+
@launcher.events.fire_on_booted!
|
115
|
+
|
116
|
+
begin
|
117
|
+
server.run.join
|
118
|
+
rescue Interrupt
|
119
|
+
# Swallow it
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Puma
|
6
|
+
class StateFile
|
7
|
+
def initialize
|
8
|
+
@options = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(path)
|
12
|
+
File.write path, YAML.dump(@options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(path)
|
16
|
+
@options = YAML.load File.read(path)
|
17
|
+
end
|
18
|
+
|
19
|
+
FIELDS = %w!control_url control_auth_token pid!
|
20
|
+
|
21
|
+
FIELDS.each do |f|
|
22
|
+
define_method f do
|
23
|
+
@options[f]
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method "#{f}=" do |v|
|
27
|
+
@options[f] = v
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puma
|
4
|
+
class TCPLogger
|
5
|
+
def initialize(logger, app, quiet=false)
|
6
|
+
@logger = logger
|
7
|
+
@app = app
|
8
|
+
@quiet = quiet
|
9
|
+
end
|
10
|
+
|
11
|
+
FORMAT = "%s - %s"
|
12
|
+
|
13
|
+
def log(who, str)
|
14
|
+
now = Time.now.strftime("%d/%b/%Y %H:%M:%S")
|
15
|
+
|
16
|
+
log_str = "#{now} - #{who} - #{str}"
|
17
|
+
|
18
|
+
case @logger
|
19
|
+
when IO
|
20
|
+
@logger.puts log_str
|
21
|
+
when Events
|
22
|
+
@logger.log log_str
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(env, socket)
|
27
|
+
who = env[Const::REMOTE_ADDR]
|
28
|
+
log who, "connected" unless @quiet
|
29
|
+
|
30
|
+
env['log'] = lambda { |str| log(who, str) }
|
31
|
+
|
32
|
+
begin
|
33
|
+
@app.call env, socket
|
34
|
+
rescue Object => e
|
35
|
+
log who, "exception: #{e.message} (#{e.class})"
|
36
|
+
else
|
37
|
+
log who, "disconnected" unless @quiet
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,328 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module Puma
|
6
|
+
# Internal Docs for A simple thread pool management object.
|
7
|
+
#
|
8
|
+
# Each Puma "worker" has a thread pool to process requests.
|
9
|
+
#
|
10
|
+
# First a connection to a client is made in `Puma::Server`. It is wrapped in a
|
11
|
+
# `Puma::Client` instance and then passed to the `Puma::Reactor` to ensure
|
12
|
+
# the whole request is buffered into memory. Once the request is ready, it is passed into
|
13
|
+
# a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
|
14
|
+
#
|
15
|
+
# Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
|
16
|
+
# and proceses it.
|
17
|
+
class ThreadPool
|
18
|
+
class ForceShutdown < RuntimeError
|
19
|
+
end
|
20
|
+
|
21
|
+
# How long, after raising the ForceShutdown of a thread during
|
22
|
+
# forced shutdown mode, to wait for the thread to try and finish
|
23
|
+
# up its work before leaving the thread to die on the vine.
|
24
|
+
SHUTDOWN_GRACE_TIME = 5 # seconds
|
25
|
+
|
26
|
+
# Maintain a minimum of +min+ and maximum of +max+ threads
|
27
|
+
# in the pool.
|
28
|
+
#
|
29
|
+
# The block passed is the work that will be performed in each
|
30
|
+
# thread.
|
31
|
+
#
|
32
|
+
def initialize(min, max, *extra, &block)
|
33
|
+
@not_empty = ConditionVariable.new
|
34
|
+
@not_full = ConditionVariable.new
|
35
|
+
@mutex = Mutex.new
|
36
|
+
|
37
|
+
@todo = []
|
38
|
+
|
39
|
+
@spawned = 0
|
40
|
+
@waiting = 0
|
41
|
+
|
42
|
+
@min = Integer(min)
|
43
|
+
@max = Integer(max)
|
44
|
+
@block = block
|
45
|
+
@extra = extra
|
46
|
+
|
47
|
+
@shutdown = false
|
48
|
+
|
49
|
+
@trim_requested = 0
|
50
|
+
|
51
|
+
@workers = []
|
52
|
+
|
53
|
+
@auto_trim = nil
|
54
|
+
@reaper = nil
|
55
|
+
|
56
|
+
@mutex.synchronize do
|
57
|
+
@min.times { spawn_thread }
|
58
|
+
end
|
59
|
+
|
60
|
+
@clean_thread_locals = false
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :spawned, :trim_requested, :waiting
|
64
|
+
attr_accessor :clean_thread_locals
|
65
|
+
|
66
|
+
def self.clean_thread_locals
|
67
|
+
Thread.current.keys.each do |key| # rubocop: disable Performance/HashEachMethods
|
68
|
+
Thread.current[key] = nil unless key == :__recursive_key__
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# How many objects have yet to be processed by the pool?
|
73
|
+
#
|
74
|
+
def backlog
|
75
|
+
@mutex.synchronize { @todo.size }
|
76
|
+
end
|
77
|
+
|
78
|
+
def pool_capacity
|
79
|
+
waiting + (@max - spawned)
|
80
|
+
end
|
81
|
+
|
82
|
+
# :nodoc:
|
83
|
+
#
|
84
|
+
# Must be called with @mutex held!
|
85
|
+
#
|
86
|
+
def spawn_thread
|
87
|
+
@spawned += 1
|
88
|
+
|
89
|
+
th = Thread.new(@spawned) do |spawned|
|
90
|
+
Puma.set_thread_name 'threadpool %03i' % spawned
|
91
|
+
todo = @todo
|
92
|
+
block = @block
|
93
|
+
mutex = @mutex
|
94
|
+
not_empty = @not_empty
|
95
|
+
not_full = @not_full
|
96
|
+
|
97
|
+
extra = @extra.map { |i| i.new }
|
98
|
+
|
99
|
+
while true
|
100
|
+
work = nil
|
101
|
+
|
102
|
+
continue = true
|
103
|
+
|
104
|
+
mutex.synchronize do
|
105
|
+
while todo.empty?
|
106
|
+
if @trim_requested > 0
|
107
|
+
@trim_requested -= 1
|
108
|
+
continue = false
|
109
|
+
not_full.signal
|
110
|
+
break
|
111
|
+
end
|
112
|
+
|
113
|
+
if @shutdown
|
114
|
+
continue = false
|
115
|
+
break
|
116
|
+
end
|
117
|
+
|
118
|
+
@waiting += 1
|
119
|
+
not_full.signal
|
120
|
+
not_empty.wait mutex
|
121
|
+
@waiting -= 1
|
122
|
+
end
|
123
|
+
|
124
|
+
work = todo.shift if continue
|
125
|
+
end
|
126
|
+
|
127
|
+
break unless continue
|
128
|
+
|
129
|
+
if @clean_thread_locals
|
130
|
+
ThreadPool.clean_thread_locals
|
131
|
+
end
|
132
|
+
|
133
|
+
begin
|
134
|
+
block.call(work, *extra)
|
135
|
+
rescue Exception => e
|
136
|
+
STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
mutex.synchronize do
|
141
|
+
@spawned -= 1
|
142
|
+
@workers.delete th
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
@workers << th
|
147
|
+
|
148
|
+
th
|
149
|
+
end
|
150
|
+
|
151
|
+
private :spawn_thread
|
152
|
+
|
153
|
+
# Add +work+ to the todo list for a Thread to pickup and process.
|
154
|
+
def <<(work)
|
155
|
+
@mutex.synchronize do
|
156
|
+
if @shutdown
|
157
|
+
raise "Unable to add work while shutting down"
|
158
|
+
end
|
159
|
+
|
160
|
+
@todo << work
|
161
|
+
|
162
|
+
if @waiting < @todo.size and @spawned < @max
|
163
|
+
spawn_thread
|
164
|
+
end
|
165
|
+
|
166
|
+
@not_empty.signal
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# This method is used by `Puma::Server` to let the server know when
|
171
|
+
# the thread pool can pull more requests from the socket and
|
172
|
+
# pass to the reactor.
|
173
|
+
#
|
174
|
+
# The general idea is that the thread pool can only work on a fixed
|
175
|
+
# number of requests at the same time. If it is already processing that
|
176
|
+
# number of requests then it is at capacity. If another Puma process has
|
177
|
+
# spare capacity, then the request can be left on the socket so the other
|
178
|
+
# worker can pick it up and process it.
|
179
|
+
#
|
180
|
+
# For example: if there are 5 threads, but only 4 working on
|
181
|
+
# requests, this method will not wait and the `Puma::Server`
|
182
|
+
# can pull a request right away.
|
183
|
+
#
|
184
|
+
# If there are 5 threads and all 5 of them are busy, then it will
|
185
|
+
# pause here, and wait until the `not_full` condition variable is
|
186
|
+
# signaled, usually this indicates that a request has been processed.
|
187
|
+
#
|
188
|
+
# It's important to note that even though the server might accept another
|
189
|
+
# request, it might not be added to the `@todo` array right away.
|
190
|
+
# For example if a slow client has only sent a header, but not a body
|
191
|
+
# then the `@todo` array would stay the same size as the reactor works
|
192
|
+
# to try to buffer the request. In that scenario the next call to this
|
193
|
+
# 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
|
195
|
+
# 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
|
+
def wait_until_not_full
|
200
|
+
@mutex.synchronize do
|
201
|
+
while true
|
202
|
+
return if @shutdown
|
203
|
+
|
204
|
+
# If we can still spin up new threads and there
|
205
|
+
# is work queued that cannot be handled by waiting
|
206
|
+
# threads, then accept more work until we would
|
207
|
+
# spin up the max number of threads.
|
208
|
+
busy_threads = @spawned - @waiting + @todo.size
|
209
|
+
return busy_threads if @max > busy_threads
|
210
|
+
|
211
|
+
@not_full.wait @mutex
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# If too many threads are in the pool, tell one to finish go ahead
|
217
|
+
# and exit. If +force+ is true, then a trim request is requested
|
218
|
+
# even if all threads are being utilized.
|
219
|
+
#
|
220
|
+
def trim(force=false)
|
221
|
+
@mutex.synchronize do
|
222
|
+
if (force or @waiting > 0) and @spawned - @trim_requested > @min
|
223
|
+
@trim_requested += 1
|
224
|
+
@not_empty.signal
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# If there are dead threads in the pool make them go away while decreasing
|
230
|
+
# spawned counter so that new healthy threads could be created again.
|
231
|
+
def reap
|
232
|
+
@mutex.synchronize do
|
233
|
+
dead_workers = @workers.reject(&:alive?)
|
234
|
+
|
235
|
+
dead_workers.each do |worker|
|
236
|
+
worker.kill
|
237
|
+
@spawned -= 1
|
238
|
+
end
|
239
|
+
|
240
|
+
@workers.delete_if do |w|
|
241
|
+
dead_workers.include?(w)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class Automaton
|
247
|
+
def initialize(pool, timeout, thread_name, message)
|
248
|
+
@pool = pool
|
249
|
+
@timeout = timeout
|
250
|
+
@thread_name = thread_name
|
251
|
+
@message = message
|
252
|
+
@running = false
|
253
|
+
end
|
254
|
+
|
255
|
+
def start!
|
256
|
+
@running = true
|
257
|
+
|
258
|
+
@thread = Thread.new do
|
259
|
+
Puma.set_thread_name @thread_name
|
260
|
+
while @running
|
261
|
+
@pool.public_send(@message)
|
262
|
+
sleep @timeout
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def stop
|
268
|
+
@running = false
|
269
|
+
@thread.wakeup
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def auto_trim!(timeout=30)
|
274
|
+
@auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
|
275
|
+
@auto_trim.start!
|
276
|
+
end
|
277
|
+
|
278
|
+
def auto_reap!(timeout=5)
|
279
|
+
@reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
|
280
|
+
@reaper.start!
|
281
|
+
end
|
282
|
+
|
283
|
+
# Tell all threads in the pool to exit and wait for them to finish.
|
284
|
+
#
|
285
|
+
def shutdown(timeout=-1)
|
286
|
+
threads = @mutex.synchronize do
|
287
|
+
@shutdown = true
|
288
|
+
@not_empty.broadcast
|
289
|
+
@not_full.broadcast
|
290
|
+
|
291
|
+
@auto_trim.stop if @auto_trim
|
292
|
+
@reaper.stop if @reaper
|
293
|
+
# dup workers so that we join them all safely
|
294
|
+
@workers.dup
|
295
|
+
end
|
296
|
+
|
297
|
+
if timeout == -1
|
298
|
+
# Wait for threads to finish without force shutdown.
|
299
|
+
threads.each(&:join)
|
300
|
+
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
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
threads.each do |t|
|
316
|
+
t.raise ForceShutdown
|
317
|
+
end
|
318
|
+
|
319
|
+
threads.each do |t|
|
320
|
+
t.join SHUTDOWN_GRACE_TIME
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
@spawned = 0
|
325
|
+
@workers = []
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|