spider-gazelle 2.0.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/spider-gazelle.rb +12 -10
- data/lib/spider-gazelle/gazelle.rb +12 -21
- data/lib/spider-gazelle/gazelle/app_store.rb +7 -4
- data/lib/spider-gazelle/gazelle/http1.rb +64 -87
- data/lib/spider-gazelle/gazelle/request.rb +55 -62
- data/lib/spider-gazelle/logger.rb +9 -12
- data/lib/spider-gazelle/options.rb +8 -7
- data/lib/spider-gazelle/reactor.rb +11 -11
- data/lib/spider-gazelle/signaller.rb +10 -6
- data/lib/spider-gazelle/signaller/signal_parser.rb +2 -0
- data/lib/spider-gazelle/spider.rb +23 -12
- data/lib/spider-gazelle/spider/binding.rb +3 -1
- data/lib/spider-gazelle/upgrades/websocket.rb +11 -9
- data/lib/spider-gazelle/version.rb +3 -2
- data/spec/http1_spec.rb +22 -17
- data/spider-gazelle.gemspec +9 -9
- metadata +47 -51
- data/lib/rack/lock_patch.rb +0 -39
- data/spec/rack_lock_spec.rb +0 -126
@@ -1,22 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'stringio'
|
2
4
|
require 'rack' # Ruby webserver abstraction
|
3
5
|
|
4
6
|
module SpiderGazelle
|
5
7
|
class Request < ::Libuv::Q::DeferredPromise
|
6
|
-
RACK_VERSION = 'rack.version'
|
7
|
-
RACK_ERRORS = 'rack.errors'
|
8
|
-
RACK_MULTITHREAD = "rack.multithread"
|
9
|
-
RACK_MULTIPROCESS = "rack.multiprocess"
|
10
|
-
RACK_RUN_ONCE = "rack.run_once"
|
11
|
-
SCRIPT_NAME = "SCRIPT_NAME"
|
12
|
-
EMPTY = ''
|
13
|
-
SERVER_PROTOCOL = "SERVER_PROTOCOL"
|
14
|
-
HTTP_11 = "HTTP/1.1"
|
15
|
-
SERVER_SOFTWARE = "SERVER_SOFTWARE"
|
16
|
-
GATEWAY_INTERFACE = "GATEWAY_INTERFACE"
|
17
|
-
CGI_VER = "CGI/1.2"
|
18
|
-
SERVER = "SpiderGazelle"
|
19
|
-
LOCALHOST = 'localhost'
|
8
|
+
RACK_VERSION = 'rack.version'
|
9
|
+
RACK_ERRORS = 'rack.errors'
|
10
|
+
RACK_MULTITHREAD = "rack.multithread"
|
11
|
+
RACK_MULTIPROCESS = "rack.multiprocess"
|
12
|
+
RACK_RUN_ONCE = "rack.run_once"
|
13
|
+
SCRIPT_NAME = "SCRIPT_NAME"
|
14
|
+
EMPTY = ''
|
15
|
+
SERVER_PROTOCOL = "SERVER_PROTOCOL"
|
16
|
+
HTTP_11 = "HTTP/1.1"
|
17
|
+
SERVER_SOFTWARE = "SERVER_SOFTWARE"
|
18
|
+
GATEWAY_INTERFACE = "GATEWAY_INTERFACE"
|
19
|
+
CGI_VER = "CGI/1.2"
|
20
|
+
SERVER = "SpiderGazelle"
|
21
|
+
LOCALHOST = 'localhost'
|
20
22
|
|
21
23
|
|
22
24
|
# TODO:: Add HTTP headers to the env and capitalise them and prefix them with HTTP_
|
@@ -28,7 +30,7 @@ module SpiderGazelle
|
|
28
30
|
RACK_MULTIPROCESS => true, # will the app be simultaneously be invoked in a separate process?
|
29
31
|
RACK_RUN_ONCE => false, # this isn't CGI so will always be false
|
30
32
|
|
31
|
-
SCRIPT_NAME => ENV['SCRIPT_NAME'
|
33
|
+
SCRIPT_NAME => ENV['SCRIPT_NAME'] || EMPTY, # The virtual path of the app base (empty if root)
|
32
34
|
SERVER_PROTOCOL => HTTP_11,
|
33
35
|
|
34
36
|
GATEWAY_INTERFACE => CGI_VER,
|
@@ -39,17 +41,18 @@ module SpiderGazelle
|
|
39
41
|
attr_reader :hijacked, :defer, :is_async
|
40
42
|
|
41
43
|
|
42
|
-
SERVER_PORT = "SERVER_PORT"
|
43
|
-
REMOTE_ADDR = "REMOTE_ADDR"
|
44
|
-
RACK_URL_SCHEME = "rack.url_scheme"
|
44
|
+
SERVER_PORT = "SERVER_PORT"
|
45
|
+
REMOTE_ADDR = "REMOTE_ADDR"
|
46
|
+
RACK_URL_SCHEME = "rack.url_scheme"
|
45
47
|
|
46
|
-
def initialize(thread, app, port, remote_ip, scheme)
|
48
|
+
def initialize(thread, app, port, remote_ip, scheme, socket)
|
47
49
|
super(thread, thread.defer)
|
48
50
|
|
51
|
+
@socket = socket
|
49
52
|
@app = app
|
50
|
-
@body =
|
51
|
-
@header =
|
52
|
-
@url =
|
53
|
+
@body = String.new
|
54
|
+
@header = String.new
|
55
|
+
@url = String.new
|
53
56
|
@env = PROTO_ENV.dup
|
54
57
|
@env[SERVER_PORT] = port
|
55
58
|
@env[REMOTE_ADDR] = remote_ip
|
@@ -57,41 +60,41 @@ module SpiderGazelle
|
|
57
60
|
end
|
58
61
|
|
59
62
|
|
60
|
-
CONTENT_LENGTH = "CONTENT_LENGTH"
|
61
|
-
HTTP_CONTENT_LENGTH = "HTTP_CONTENT_LENGTH"
|
62
|
-
CONTENT_TYPE = "CONTENT_TYPE"
|
63
|
-
HTTP_CONTENT_TYPE = "HTTP_CONTENT_TYPE"
|
64
|
-
DEFAULT_TYPE = "text/plain"
|
65
|
-
REQUEST_URI= "REQUEST_URI"
|
66
|
-
ASCII_8BIT = "ASCII-8BIT"
|
67
|
-
RACK_INPUT = "rack.input"
|
68
|
-
PATH_INFO = "PATH_INFO"
|
69
|
-
REQUEST_PATH = "REQUEST_PATH"
|
70
|
-
QUERY_STRING = "QUERY_STRING"
|
71
|
-
HTTP_HOST = "HTTP_HOST"
|
72
|
-
COLON = ":"
|
73
|
-
SERVER_NAME = "SERVER_NAME"
|
63
|
+
CONTENT_LENGTH = "CONTENT_LENGTH"
|
64
|
+
HTTP_CONTENT_LENGTH = "HTTP_CONTENT_LENGTH"
|
65
|
+
CONTENT_TYPE = "CONTENT_TYPE"
|
66
|
+
HTTP_CONTENT_TYPE = "HTTP_CONTENT_TYPE"
|
67
|
+
DEFAULT_TYPE = "text/plain"
|
68
|
+
REQUEST_URI= "REQUEST_URI"
|
69
|
+
ASCII_8BIT = "ASCII-8BIT"
|
70
|
+
RACK_INPUT = "rack.input"
|
71
|
+
PATH_INFO = "PATH_INFO"
|
72
|
+
REQUEST_PATH = "REQUEST_PATH"
|
73
|
+
QUERY_STRING = "QUERY_STRING"
|
74
|
+
HTTP_HOST = "HTTP_HOST"
|
75
|
+
COLON = ":"
|
76
|
+
SERVER_NAME = "SERVER_NAME"
|
74
77
|
# Hijacking IO is supported
|
75
|
-
HIJACK_P = "rack.hijack?"
|
78
|
+
HIJACK_P = "rack.hijack?"
|
76
79
|
# Callback for indicating that this socket will be hijacked
|
77
|
-
HIJACK = "rack.hijack"
|
80
|
+
HIJACK = "rack.hijack"
|
78
81
|
# The object for performing IO on after hijack is called
|
79
|
-
HIJACK_IO = "rack.hijack_io"
|
80
|
-
QUESTION_MARK = "?"
|
82
|
+
HIJACK_IO = "rack.hijack_io"
|
83
|
+
QUESTION_MARK = "?"
|
81
84
|
|
82
|
-
HTTP_UPGRADE = 'HTTP_UPGRADE'
|
83
|
-
USE_HTTP2 = 'h2c'
|
85
|
+
HTTP_UPGRADE = 'HTTP_UPGRADE'
|
86
|
+
USE_HTTP2 = 'h2c'
|
84
87
|
|
85
88
|
|
86
89
|
|
87
90
|
|
88
91
|
def execute!
|
89
|
-
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.
|
92
|
+
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.bytesize.to_s
|
90
93
|
@env[CONTENT_TYPE] = @env.delete(HTTP_CONTENT_TYPE) || DEFAULT_TYPE
|
91
94
|
@env[REQUEST_URI] = @url.freeze
|
92
95
|
|
93
96
|
# For Rack::Lint on 1.9, ensure that the encoding is always for spec
|
94
|
-
@body.force_encoding(ASCII_8BIT)
|
97
|
+
@body.force_encoding(ASCII_8BIT)
|
95
98
|
@env[RACK_INPUT] = StringIO.new @body
|
96
99
|
|
97
100
|
# Break the request into its components
|
@@ -111,7 +114,7 @@ module SpiderGazelle
|
|
111
114
|
if host = @env[HTTP_HOST]
|
112
115
|
if colon = host.index(COLON)
|
113
116
|
@env[SERVER_NAME] = host[0, colon]
|
114
|
-
@env[SERVER_PORT] = host[colon + 1, host.bytesize]
|
117
|
+
@env[SERVER_PORT] = host[colon + 1, host.bytesize]
|
115
118
|
else
|
116
119
|
@env[SERVER_NAME] = host
|
117
120
|
end
|
@@ -119,18 +122,16 @@ module SpiderGazelle
|
|
119
122
|
@env[SERVER_NAME] = LOCALHOST
|
120
123
|
end
|
121
124
|
|
122
|
-
|
123
|
-
|
124
|
-
if @env[HTTP_UPGRADE] == USE_HTTP2
|
125
|
-
# TODO:: implement the upgrade process here
|
126
|
-
else
|
127
|
-
@env[HIJACK_P] = true
|
128
|
-
@env[HIJACK] = method :hijack
|
129
|
-
end
|
125
|
+
if @upgrade == true && @env[HTTP_UPGRADE] == USE_HTTP2
|
126
|
+
# TODO:: implement the upgrade process here
|
130
127
|
end
|
131
128
|
|
129
|
+
# Provide hijack options
|
130
|
+
@env[HIJACK_P] = true
|
131
|
+
@env[HIJACK] = proc { @env[HIJACK_IO] = @socket }
|
132
|
+
|
132
133
|
# Execute the request
|
133
|
-
# NOTE:: Catch was overloaded by Promise so this does the
|
134
|
+
# NOTE:: Catch was overloaded by Promise so this does the trick now
|
134
135
|
resp = ruby_catch(:async) { @app.call @env }
|
135
136
|
if resp.nil? || resp[0] == -1
|
136
137
|
@is_async = true
|
@@ -143,13 +144,5 @@ module SpiderGazelle
|
|
143
144
|
end
|
144
145
|
resp
|
145
146
|
end
|
146
|
-
|
147
|
-
protected
|
148
|
-
|
149
|
-
def hijack
|
150
|
-
@hijacked = @loop.defer
|
151
|
-
@env.delete HIJACK # don't want to hold a reference to this request object
|
152
|
-
@env[HIJACK_IO] = @hijacked.promise
|
153
|
-
end
|
154
147
|
end
|
155
148
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'libuv'
|
2
4
|
|
3
5
|
module SpiderGazelle
|
@@ -19,7 +21,7 @@ module SpiderGazelle
|
|
19
21
|
|
20
22
|
|
21
23
|
def initialize
|
22
|
-
@thread = ::Libuv::
|
24
|
+
@thread = ::Libuv::Reactor.default
|
23
25
|
@level = DEFAULT_LEVEL
|
24
26
|
@write = method(:server_write)
|
25
27
|
end
|
@@ -85,17 +87,12 @@ module SpiderGazelle
|
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
88
|
-
def print_error(e, msg =
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
if
|
93
|
-
|
94
|
-
elsif trace.nil?
|
95
|
-
trace = caller
|
96
|
-
end
|
97
|
-
msg << "Caller backtrace:\n#{trace.join("\n")}\n" if trace
|
98
|
-
error(msg)
|
90
|
+
def print_error(e, msg = nil, trace = nil)
|
91
|
+
message = String.new(msg || e.message)
|
92
|
+
message << "\n#{e.message}" if msg
|
93
|
+
message << "\n#{e.backtrace.join("\n")}" if e.respond_to?(:backtrace) && e.backtrace
|
94
|
+
message << "\nCaller backtrace:\n#{trace.join("\n")}" if trace
|
95
|
+
error(message)
|
99
96
|
end
|
100
97
|
|
101
98
|
# NOTE:: should only be called on reactor thread
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'set'
|
2
4
|
require 'optparse'
|
3
5
|
require 'spider-gazelle/logger'
|
@@ -10,10 +12,9 @@ module SpiderGazelle
|
|
10
12
|
port: 3000,
|
11
13
|
verbose: false,
|
12
14
|
tls: false,
|
13
|
-
backlog:
|
15
|
+
backlog: 4096,
|
14
16
|
rackup: "#{Dir.pwd}/config.ru",
|
15
17
|
mode: :thread,
|
16
|
-
app_mode: :thread_pool,
|
17
18
|
isolate: true
|
18
19
|
}.freeze
|
19
20
|
|
@@ -59,12 +60,12 @@ module SpiderGazelle
|
|
59
60
|
options[:rackup] = arg
|
60
61
|
end
|
61
62
|
|
62
|
-
opts.on "-
|
63
|
-
options[:
|
63
|
+
opts.on "-rl", "--rack-lint", "enable rack lint on all requests" do
|
64
|
+
options[:lint] = true
|
64
65
|
end
|
65
66
|
|
66
|
-
opts.on "-
|
67
|
-
options[:
|
67
|
+
opts.on "-m", "--mode MODE", MODES, "Either process, thread or no_ipc (default: thread)" do |arg|
|
68
|
+
options[:mode] = arg
|
68
69
|
end
|
69
70
|
|
70
71
|
opts.on "-b", "--backlog BACKLOG", Integer, "Number of pending connections allowed (default: 5000)" do |arg|
|
@@ -175,7 +176,7 @@ module SpiderGazelle
|
|
175
176
|
|
176
177
|
# Ensure there is at least one component
|
177
178
|
# (This will occur when no options are provided)
|
178
|
-
components <<
|
179
|
+
components << String.new if components.empty?
|
179
180
|
|
180
181
|
# Parse the commandline options
|
181
182
|
options = []
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'libuv'
|
2
4
|
require 'spider-gazelle/logger'
|
3
5
|
|
@@ -9,7 +11,7 @@ module SpiderGazelle
|
|
9
11
|
|
10
12
|
|
11
13
|
def initialize
|
12
|
-
@thread = ::Libuv::
|
14
|
+
@thread = ::Libuv::Reactor.default
|
13
15
|
@logger = Logger.instance
|
14
16
|
@running = false
|
15
17
|
@shutdown = method(:shutdown)
|
@@ -21,18 +23,15 @@ module SpiderGazelle
|
|
21
23
|
@thread.schedule block
|
22
24
|
else
|
23
25
|
@running = true
|
24
|
-
@thread.
|
25
|
-
|
26
|
-
|
27
|
-
# Listen for the signal to shutdown
|
28
|
-
@thread.signal :INT, @shutdown
|
29
|
-
|
26
|
+
@thread.notifier method(:log)
|
27
|
+
@thread.on_program_interrupt @shutdown
|
28
|
+
@thread.run { |thread|
|
30
29
|
block.call
|
31
30
|
}
|
32
31
|
end
|
33
32
|
end
|
34
33
|
|
35
|
-
def shutdown
|
34
|
+
def shutdown
|
36
35
|
if @shutdown_called
|
37
36
|
@logger.warn "Shutdown called twice! Callstack:\n#{caller.join("\n")}"
|
38
37
|
return
|
@@ -54,12 +53,13 @@ module SpiderGazelle
|
|
54
53
|
end
|
55
54
|
|
56
55
|
# This is an unhandled error on the Libuv Event loop
|
57
|
-
def log(
|
58
|
-
msg =
|
56
|
+
def log(error, context, trace = nil)
|
57
|
+
msg = String.new
|
59
58
|
if error.respond_to?(:backtrace)
|
60
|
-
msg << "unhandled exception: #{error.message} (#{
|
59
|
+
msg << "unhandled exception: #{error.message} (#{context})"
|
61
60
|
backtrace = error.backtrace
|
62
61
|
msg << "\n#{backtrace.join("\n")}" if backtrace
|
62
|
+
msg << "\n#{trace.join("\n")}" if trace
|
63
63
|
else
|
64
64
|
msg << "unhandled exception: #{args}"
|
65
65
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'libuv'
|
2
4
|
|
3
5
|
|
@@ -11,7 +13,7 @@ module SpiderGazelle
|
|
11
13
|
|
12
14
|
|
13
15
|
def initialize
|
14
|
-
@thread = ::Libuv::
|
16
|
+
@thread = ::Libuv::Reactor.default
|
15
17
|
@logger = Logger.instance
|
16
18
|
|
17
19
|
# This is used to check if an instance of spider-gazelle is already running
|
@@ -69,7 +71,7 @@ module SpiderGazelle
|
|
69
71
|
end
|
70
72
|
|
71
73
|
def general_failure
|
72
|
-
@pipe.write "\x02Signaller general_failure\x03"
|
74
|
+
@pipe.write "\x02Signaller general_failure\x03"
|
73
75
|
rescue
|
74
76
|
ensure
|
75
77
|
Reactor.instance.shutdown
|
@@ -91,7 +93,7 @@ module SpiderGazelle
|
|
91
93
|
@is_client = true
|
92
94
|
@is_connected = true
|
93
95
|
|
94
|
-
@logger.verbose "Client connected to SG Master"
|
96
|
+
@logger.verbose "Client connected to SG Master"
|
95
97
|
|
96
98
|
require 'uv-rays/buffered_tokenizer'
|
97
99
|
@parser = ::UV::BufferedTokenizer.new({
|
@@ -172,6 +174,8 @@ module SpiderGazelle
|
|
172
174
|
#@logger.error "Master pipe went missing: #{reason}"
|
173
175
|
# Server most likely exited
|
174
176
|
# We'll shutdown here
|
177
|
+
STDERR.puts "\n\npanic! #{reason.inspect}\n\n\n"
|
178
|
+
STDERR.flush
|
175
179
|
Reactor.instance.shutdown
|
176
180
|
end
|
177
181
|
|
@@ -188,13 +192,13 @@ module SpiderGazelle
|
|
188
192
|
case result
|
189
193
|
when :validated
|
190
194
|
@validated.each do |old|
|
191
|
-
old.write "\x02update\x03"
|
195
|
+
old.write "\x02update\x03"
|
192
196
|
end
|
193
197
|
@validated << client
|
194
198
|
if @validated.length > 1
|
195
|
-
client.write "\x02wait\x03"
|
199
|
+
client.write "\x02wait\x03"
|
196
200
|
else
|
197
|
-
client.write "\x02ready\x03"
|
201
|
+
client.write "\x02ready\x03"
|
198
202
|
end
|
199
203
|
@logger.verbose { "Client <0x#{client.object_id.to_s(16)}> connection was validated" }
|
200
204
|
when :close_connection
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spider-gazelle/spider/binding' # Holds a reference to a bound port
|
2
4
|
require 'securerandom'
|
3
5
|
|
@@ -67,13 +69,13 @@ module SpiderGazelle
|
|
67
69
|
load_promise.then do
|
68
70
|
# Check a shutdown request didn't occur as we were loading
|
69
71
|
if @running
|
70
|
-
@logger.verbose "All gazelles running"
|
72
|
+
@logger.verbose "All gazelles running"
|
71
73
|
|
72
74
|
# This happends on the master thread so we don't need to check
|
73
75
|
# for the shutdown events here
|
74
76
|
bind_application_ports
|
75
77
|
else
|
76
|
-
@logger.warn "A shutdown event occured while loading"
|
78
|
+
@logger.warn "A shutdown event occured while loading"
|
77
79
|
perform_shutdown
|
78
80
|
end
|
79
81
|
end
|
@@ -125,7 +127,7 @@ module SpiderGazelle
|
|
125
127
|
rescue
|
126
128
|
end
|
127
129
|
|
128
|
-
shutdown = false
|
130
|
+
@shutdown = false
|
129
131
|
check = method(:check_credentials)
|
130
132
|
@pipe.bind(@pipe_file) do |client|
|
131
133
|
@logger.verbose { "Gazelle <0x#{client.object_id.to_s(16)}> connection made" }
|
@@ -145,8 +147,8 @@ module SpiderGazelle
|
|
145
147
|
rescue
|
146
148
|
ensure
|
147
149
|
@gazelles.delete client
|
148
|
-
if
|
149
|
-
shutdown = true
|
150
|
+
if !@shutdown
|
151
|
+
@shutdown = true
|
150
152
|
@signaller.general_failure
|
151
153
|
end
|
152
154
|
end
|
@@ -243,7 +245,7 @@ module SpiderGazelle
|
|
243
245
|
|
244
246
|
@threads = []
|
245
247
|
count.times do
|
246
|
-
thread = ::Libuv::
|
248
|
+
thread = ::Libuv::Reactor.new
|
247
249
|
@threads << thread
|
248
250
|
|
249
251
|
Thread.new { load_gazelle_thread(reactor, thread, mode, options) }
|
@@ -266,10 +268,18 @@ module SpiderGazelle
|
|
266
268
|
end
|
267
269
|
|
268
270
|
def load_gazelle_thread(reactor, thread, mode, options)
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
271
|
+
# Log any unhandled errors
|
272
|
+
thread.notifier reactor.method(:log)
|
273
|
+
# Give current requests 5 seconds to complete
|
274
|
+
thread.on_program_interrupt do
|
275
|
+
timer = thread.timer {
|
276
|
+
puts "Forcing gazelle exit"
|
277
|
+
thread.stop
|
278
|
+
}
|
279
|
+
timer.unref
|
280
|
+
timer.start(5000)
|
281
|
+
end
|
282
|
+
thread.run do |thread|
|
273
283
|
# Start the gazelle
|
274
284
|
::SpiderGazelle::Gazelle.new(thread, :thread).run!(options)
|
275
285
|
end
|
@@ -283,9 +293,9 @@ module SpiderGazelle
|
|
283
293
|
if @running
|
284
294
|
@thread.schedule do
|
285
295
|
if result
|
286
|
-
@logger.verbose "Gazelle process exited with exit status 0"
|
296
|
+
@logger.verbose "Gazelle process exited with exit status 0"
|
287
297
|
else
|
288
|
-
@logger.error "Gazelle process exited unexpectedly"
|
298
|
+
@logger.error "Gazelle process exited unexpectedly"
|
289
299
|
end
|
290
300
|
|
291
301
|
@signaller.general_failure
|
@@ -330,6 +340,7 @@ module SpiderGazelle
|
|
330
340
|
|
331
341
|
def shutdown_gazelles
|
332
342
|
@bound = false
|
343
|
+
@shutdown = true
|
333
344
|
promises = []
|
334
345
|
|
335
346
|
@iterator_source.each do |mode, gazelles|
|