spinel-spinner 0.1.0.pre.1
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/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/lib/spinner/body_storage.rb +106 -0
- data/lib/spinner/chunk_decoder.rb +72 -0
- data/lib/spinner/host_authority.rb +86 -0
- data/lib/spinner/input.rb +58 -0
- data/lib/spinner/proxy_metadata.rb +93 -0
- data/lib/spinner/reactor.rb +575 -0
- data/lib/spinner/request_parser.rb +183 -0
- data/lib/spinner/response_writer.rb +123 -0
- data/lib/spinner/server.rb +384 -0
- data/lib/spinner/task_pool.rb +62 -0
- data/lib/spinner/thread_pool.rb +229 -0
- data/lib/spinner/tls.rb +116 -0
- data/lib/spinner/transport_socket.rb +121 -0
- data/lib/spinner/upgrade.rb +20 -0
- data/lib/spinner/version.rb +5 -0
- data/lib/spinner/websocket.rb +179 -0
- data/lib/spinner.rb +21 -0
- data/sig/spinner.rbs +4 -0
- metadata +63 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
module Spinner
|
|
2
|
+
class Transport
|
|
3
|
+
def initialize(socket)
|
|
4
|
+
@socket = socket
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def track_activity(client, timeout)
|
|
8
|
+
@activity_client = client
|
|
9
|
+
@activity_timeout = timeout
|
|
10
|
+
progress!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def progress!
|
|
14
|
+
unless @activity_client.nil?
|
|
15
|
+
@activity_deadline = Transport.now + @activity_timeout
|
|
16
|
+
@activity_client.stream_deadline = @activity_deadline
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.now
|
|
21
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read_chunk(deadline, max_bytes = 4096, phase = "header")
|
|
25
|
+
loop do
|
|
26
|
+
deadline = @activity_deadline unless @activity_client.nil?
|
|
27
|
+
remaining = deadline - Transport.now
|
|
28
|
+
raise RequestError.new(408, "#{phase} read deadline exceeded") if remaining <= 0
|
|
29
|
+
begin
|
|
30
|
+
bytes = @socket.read_nonblock(max_bytes)
|
|
31
|
+
progress!
|
|
32
|
+
return bytes
|
|
33
|
+
rescue IO::WaitReadable
|
|
34
|
+
if IO.select([@socket.to_io], nil, nil, remaining).nil?
|
|
35
|
+
raise RequestError.new(408, "#{phase} read deadline exceeded")
|
|
36
|
+
end
|
|
37
|
+
rescue IO::WaitWritable
|
|
38
|
+
if IO.select(nil, [@socket.to_io], nil, remaining).nil?
|
|
39
|
+
raise RequestError.new(408, "#{phase} read deadline exceeded")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write_some(bytes, deadline)
|
|
46
|
+
binary = bytes.to_s.b
|
|
47
|
+
loop do
|
|
48
|
+
deadline = @activity_deadline unless @activity_client.nil?
|
|
49
|
+
remaining = deadline - Transport.now
|
|
50
|
+
raise IOError, "response write deadline exceeded" if remaining <= 0
|
|
51
|
+
begin
|
|
52
|
+
# Explicit binary encoding selects Spinel's length-aware socket API.
|
|
53
|
+
count = @socket.write_nonblock(binary)
|
|
54
|
+
progress!
|
|
55
|
+
return count
|
|
56
|
+
rescue IO::WaitWritable
|
|
57
|
+
raise IOError, "response write deadline exceeded" if IO.select(nil, [@socket.to_io], nil, remaining).nil?
|
|
58
|
+
rescue IO::WaitReadable
|
|
59
|
+
raise IOError, "response write deadline exceeded" if IO.select([@socket.to_io], nil, nil, remaining).nil?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A single bounded write for protocols that own their framing and deadline.
|
|
65
|
+
def write_stream_chunk(bytes, deadline)
|
|
66
|
+
loop do
|
|
67
|
+
remaining = deadline - Transport.now
|
|
68
|
+
raise IOError, "stream write deadline exceeded" if remaining <= 0
|
|
69
|
+
begin
|
|
70
|
+
count = @socket.write_stream_nonblock(bytes)
|
|
71
|
+
progress!
|
|
72
|
+
return count
|
|
73
|
+
rescue IO::WaitWritable
|
|
74
|
+
raise IOError, "stream write deadline exceeded" if IO.select(nil, [@socket.to_io], nil, remaining).nil?
|
|
75
|
+
rescue IO::WaitReadable
|
|
76
|
+
raise IOError, "stream write deadline exceeded" if IO.select([@socket.to_io], nil, nil, remaining).nil?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Connection
|
|
83
|
+
def initialize(application, header_limit, read_timeout, write_timeout, body_timeout, server_port)
|
|
84
|
+
@application = application
|
|
85
|
+
@header_limit = header_limit
|
|
86
|
+
@read_timeout = read_timeout
|
|
87
|
+
@write_timeout = write_timeout
|
|
88
|
+
@body_timeout = body_timeout
|
|
89
|
+
@server_port = server_port
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def serve_buffered(client, executor, stream_timeout)
|
|
93
|
+
transport = Transport.new(client.socket)
|
|
94
|
+
writer = ResponseWriter.new
|
|
95
|
+
response = nil
|
|
96
|
+
handed_off = false
|
|
97
|
+
app_started = Transport.now
|
|
98
|
+
begin
|
|
99
|
+
response = @application.call(client.environment)
|
|
100
|
+
unless response.is_a?(Array) && response.length == 3
|
|
101
|
+
raise ArgumentError, "expected [status, headers, body]"
|
|
102
|
+
end
|
|
103
|
+
body = response[2]
|
|
104
|
+
keep_alive = client.keep_alive?
|
|
105
|
+
if body.is_a?(Upgrade)
|
|
106
|
+
connection_tokens = client.parser.headers.fetch("connection", "").downcase.split(",").map(&:strip)
|
|
107
|
+
unless client.parser.protocol == "HTTP/1.1" && connection_tokens.include?("upgrade") && !client.parser.headers.fetch("upgrade", "").empty?
|
|
108
|
+
raise ArgumentError, "upgrade requires HTTP/1.1 upgrade request"
|
|
109
|
+
end
|
|
110
|
+
head = writer.upgrade_header(response)
|
|
111
|
+
accepted = executor.call(:upgrade, client, body) do
|
|
112
|
+
transport.track_activity(client, stream_timeout)
|
|
113
|
+
begin
|
|
114
|
+
writer.write(transport, [head], Transport.now + @write_timeout)
|
|
115
|
+
body.call(transport, client.remainder)
|
|
116
|
+
false
|
|
117
|
+
ensure
|
|
118
|
+
body.close
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
if accepted
|
|
122
|
+
handed_off = true
|
|
123
|
+
return :async
|
|
124
|
+
end
|
|
125
|
+
body.close
|
|
126
|
+
response = [503, {}, ["Service Unavailable"]]
|
|
127
|
+
elsif !body.is_a?(Array)
|
|
128
|
+
stream = writer.prepare_stream(response, keep_alive, client.parser.protocol)
|
|
129
|
+
accepted = executor.call(:stream, client, body) do
|
|
130
|
+
transport.track_activity(client, stream_timeout)
|
|
131
|
+
writer.write_stream(transport, response, client.stream_deadline,
|
|
132
|
+
stream[0], stream[1], client.parser.method == "HEAD", stream_timeout, client)
|
|
133
|
+
keep_alive && stream[1]
|
|
134
|
+
end
|
|
135
|
+
if accepted
|
|
136
|
+
handed_off = true
|
|
137
|
+
return :async
|
|
138
|
+
end
|
|
139
|
+
body.close if body.respond_to?(:close)
|
|
140
|
+
response = [503, {}, ["Service Unavailable"]]
|
|
141
|
+
end
|
|
142
|
+
chunks = writer.prepare(response, keep_alive, client.parser.protocol, client.parser.method == "HEAD")
|
|
143
|
+
rescue StandardError => error
|
|
144
|
+
warn "SPINNER_METRIC event=application_failure message=#{error.message}"
|
|
145
|
+
response[2].close if response.is_a?(Array) && response.length == 3 && response[2].respond_to?(:close)
|
|
146
|
+
response = [500, {}, ["Internal server error"]]
|
|
147
|
+
chunks = writer.prepare(response, false, client.parser.protocol, client.parser.method == "HEAD")
|
|
148
|
+
keep_alive = false
|
|
149
|
+
end
|
|
150
|
+
writer.write(transport, chunks, Transport.now + @write_timeout)
|
|
151
|
+
[((Transport.now - app_started) * 1000).round, keep_alive]
|
|
152
|
+
rescue IOError, SystemCallError => error
|
|
153
|
+
warn "SPINNER_METRIC event=transport_failure message=#{error.message}"
|
|
154
|
+
nil
|
|
155
|
+
ensure
|
|
156
|
+
unless handed_off
|
|
157
|
+
begin
|
|
158
|
+
response[2].close if response.is_a?(Array) && response.length == 3 && response[2].respond_to?(:close)
|
|
159
|
+
ensure
|
|
160
|
+
client.cleanup_body
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class Server
|
|
167
|
+
def initialize(application, host: "127.0.0.1", port: 4000, threads: 4,
|
|
168
|
+
queue_capacity: 64, max_connections: 128, queue_timeout: 5.0,
|
|
169
|
+
drain_timeout: 10.0, unix_path: nil, listener: nil,
|
|
170
|
+
keepalive_timeout: 5.0, max_requests_per_connection: 100,
|
|
171
|
+
worker_processes: 1, tls_cert: nil, tls_key: nil,
|
|
172
|
+
max_body_bytes: 1048576, body_memory_bytes: 65536,
|
|
173
|
+
spool_directory: nil, spool_budget_bytes: 134217728, storage_workers: 2,
|
|
174
|
+
header_timeout: 5.0, body_timeout: 5.0, write_timeout: 5.0,
|
|
175
|
+
stream_idle_timeout: 30.0, max_streams: 32, max_upgrades: 32,
|
|
176
|
+
trusted_proxies: [], max_header_bytes: 32768, reactor_heartbeat: nil,
|
|
177
|
+
request_metrics: nil)
|
|
178
|
+
{"threads" => threads, "queue_capacity" => queue_capacity,
|
|
179
|
+
"max_connections" => max_connections, "max_requests_per_connection" => max_requests_per_connection,
|
|
180
|
+
"worker_processes" => worker_processes}.each do |name, value|
|
|
181
|
+
raise ArgumentError, "#{name} must be a positive integer" unless value.is_a?(Integer) && value > 0
|
|
182
|
+
end
|
|
183
|
+
{"queue_timeout" => queue_timeout, "drain_timeout" => drain_timeout,
|
|
184
|
+
"keepalive_timeout" => keepalive_timeout}.each do |name, value|
|
|
185
|
+
raise ArgumentError, "#{name} must be positive and finite" unless value.is_a?(Numeric) && value > 0 && value.finite?
|
|
186
|
+
end
|
|
187
|
+
raise ArgumentError, "port must be between zero and 65535" unless port.is_a?(Integer) && port >= 0 && port <= 65535
|
|
188
|
+
{"max_body_bytes" => max_body_bytes, "spool_budget_bytes" => spool_budget_bytes,
|
|
189
|
+
"storage_workers" => storage_workers, "max_streams" => max_streams,
|
|
190
|
+
"max_upgrades" => max_upgrades, "max_header_bytes" => max_header_bytes}.each do |name, value|
|
|
191
|
+
raise ArgumentError, "#{name} must be a positive integer" unless value.is_a?(Integer) && value > 0
|
|
192
|
+
end
|
|
193
|
+
unless body_memory_bytes.is_a?(Integer) && body_memory_bytes >= 0 && body_memory_bytes <= max_body_bytes
|
|
194
|
+
raise ArgumentError, "body_memory_bytes must be between zero and max_body_bytes"
|
|
195
|
+
end
|
|
196
|
+
{"header_timeout" => header_timeout, "body_timeout" => body_timeout,
|
|
197
|
+
"write_timeout" => write_timeout, "stream_idle_timeout" => stream_idle_timeout}.each do |name, value|
|
|
198
|
+
raise ArgumentError, "#{name} must be positive and finite" unless value.is_a?(Numeric) && value > 0 && value.finite?
|
|
199
|
+
end
|
|
200
|
+
@options = {max_body_bytes: max_body_bytes, body_memory_bytes: body_memory_bytes,
|
|
201
|
+
spool_directory: spool_directory || ENV["TMPDIR"] || "/tmp",
|
|
202
|
+
spool_budget_bytes: spool_budget_bytes, storage_workers: storage_workers,
|
|
203
|
+
stream_idle_timeout: stream_idle_timeout, max_streams: max_streams,
|
|
204
|
+
max_upgrades: max_upgrades, proxy_policy: ProxyMetadata.new(trusted_proxies),
|
|
205
|
+
reactor_heartbeat: reactor_heartbeat.nil? ? ENV.fetch("SPINNER_REACTOR_HEARTBEAT", "0") == "1" : reactor_heartbeat,
|
|
206
|
+
request_metrics: request_metrics.nil? ? ENV.fetch("SPINNER_REQUEST_METRICS", "0") == "1" : request_metrics}
|
|
207
|
+
@header_limit = max_header_bytes
|
|
208
|
+
@header_timeout = header_timeout
|
|
209
|
+
@body_timeout = body_timeout
|
|
210
|
+
@write_timeout = write_timeout
|
|
211
|
+
@application = application
|
|
212
|
+
@host = host
|
|
213
|
+
@port = port
|
|
214
|
+
@threads = threads
|
|
215
|
+
@queue_capacity = queue_capacity
|
|
216
|
+
@max_connections = max_connections
|
|
217
|
+
@queue_timeout = queue_timeout
|
|
218
|
+
@drain_timeout = drain_timeout
|
|
219
|
+
@unix_path = unix_path
|
|
220
|
+
@listener = listener
|
|
221
|
+
@keepalive_timeout = keepalive_timeout
|
|
222
|
+
@max_requests_per_connection = max_requests_per_connection
|
|
223
|
+
@worker_processes = worker_processes
|
|
224
|
+
@options[:spool_budget_bytes] = spool_budget_bytes / worker_processes
|
|
225
|
+
@tls_cert = tls_cert
|
|
226
|
+
@tls_key = tls_key
|
|
227
|
+
raise ArgumentError, "tls_cert and tls_key must be supplied together" if @tls_cert.nil? != @tls_key.nil?
|
|
228
|
+
raise ArgumentError, "TLS requires a TCP listener" if !@tls_cert.nil? && !@unix_path.nil?
|
|
229
|
+
raise ArgumentError, "unix_path must be nonempty" if !@unix_path.nil? && @unix_path.empty?
|
|
230
|
+
raise ArgumentError, "choose listener or unix_path" if !@listener.nil? && !@unix_path.nil?
|
|
231
|
+
raise ArgumentError, "multi-process mode needs a TCP listener" if @worker_processes > 1 && (!@unix_path.nil? || !@listener.nil?)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def run
|
|
235
|
+
if @worker_processes > 1 && ENV["SPINNER_CHILD_FD"].nil?
|
|
236
|
+
run_supervisor
|
|
237
|
+
else
|
|
238
|
+
run_worker
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def run_worker
|
|
243
|
+
require_relative "tls" unless @tls_cert.nil?
|
|
244
|
+
inherited = ENV["SPINNER_CHILD_FD"]
|
|
245
|
+
listener = if inherited.nil?
|
|
246
|
+
@listener || (@unix_path.nil? ? TCPServer.new(@host, @port) : UNIXServer.new(@unix_path))
|
|
247
|
+
else
|
|
248
|
+
TCPServer.for_fd(inherited.to_i)
|
|
249
|
+
end
|
|
250
|
+
begin
|
|
251
|
+
tls_context = @tls_cert.nil? ? nil : TLS::Context.new(@tls_cert, @tls_key)
|
|
252
|
+
port = @unix_path.nil? ? listener.addr[1].to_s : "0"
|
|
253
|
+
reactor = Reactor.new(listener, @application, port, @header_limit, @header_timeout, @body_timeout, @write_timeout,
|
|
254
|
+
@threads, @queue_capacity, @max_connections,
|
|
255
|
+
@queue_timeout, @drain_timeout,
|
|
256
|
+
@keepalive_timeout, @max_requests_per_connection,
|
|
257
|
+
tls_context, @options)
|
|
258
|
+
previous_term = Signal.trap("TERM") { reactor.shutdown! }
|
|
259
|
+
previous_int = Signal.trap("INT") { reactor.shutdown! }
|
|
260
|
+
previous_usr1 = Signal.trap("USR1") { reactor.shutdown!(true) }
|
|
261
|
+
# Emitted only after bind and pool startup.
|
|
262
|
+
if inherited.nil?
|
|
263
|
+
puts "SPINNER_READY #{Process.pid} #{port}"
|
|
264
|
+
$stdout.flush
|
|
265
|
+
end
|
|
266
|
+
reactor.run
|
|
267
|
+
ensure
|
|
268
|
+
Signal.trap("TERM", previous_term) unless previous_term.nil?
|
|
269
|
+
Signal.trap("INT", previous_int) unless previous_int.nil?
|
|
270
|
+
Signal.trap("USR1", previous_usr1) unless previous_usr1.nil?
|
|
271
|
+
listener.close unless listener.closed?
|
|
272
|
+
tls_context.close unless tls_context.nil?
|
|
273
|
+
File.unlink(@unix_path) if !@unix_path.nil? && File.socket?(@unix_path)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def run_supervisor
|
|
278
|
+
unless @tls_cert.nil?
|
|
279
|
+
require_relative "tls"
|
|
280
|
+
validation = TLS::Context.new(@tls_cert, @tls_key)
|
|
281
|
+
validation.close
|
|
282
|
+
end
|
|
283
|
+
listener = TCPServer.new(@host, @port)
|
|
284
|
+
listener.close_on_exec = false
|
|
285
|
+
port = listener.addr[1]
|
|
286
|
+
ENV["SPINNER_CHILD_FD"] = listener.fileno.to_s
|
|
287
|
+
ENV["SPINNER_PARENT_PID"] = Process.pid.to_s
|
|
288
|
+
children = []
|
|
289
|
+
stopping = false
|
|
290
|
+
failed = false
|
|
291
|
+
previous_term = nil
|
|
292
|
+
previous_int = nil
|
|
293
|
+
previous_hup = nil
|
|
294
|
+
restart_pending = []
|
|
295
|
+
restarting_pid = nil
|
|
296
|
+
begin
|
|
297
|
+
@worker_processes.times { children << spawn_child }
|
|
298
|
+
previous_term = Signal.trap("TERM") { stopping = true; signal_children(children, "TERM") }
|
|
299
|
+
previous_int = Signal.trap("INT") { stopping = true; signal_children(children, "TERM") }
|
|
300
|
+
previous_hup = Signal.trap("HUP") do
|
|
301
|
+
if !stopping && restarting_pid.nil?
|
|
302
|
+
restart_pending = children.dup
|
|
303
|
+
restarting_pid = restart_pending.shift
|
|
304
|
+
warn "SPINNER_METRIC event=roll_start pid=#{restarting_pid} pending=#{restart_pending.length}"
|
|
305
|
+
signal_children([restarting_pid], "USR1") unless restarting_pid.nil?
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
puts "SPINNER_READY #{Process.pid} #{port}"
|
|
309
|
+
$stdout.flush
|
|
310
|
+
restarts = []
|
|
311
|
+
until children.empty?
|
|
312
|
+
begin
|
|
313
|
+
pid, _status = Process.waitpid2(-1)
|
|
314
|
+
rescue Errno::EINTR
|
|
315
|
+
next
|
|
316
|
+
end
|
|
317
|
+
children.delete(pid)
|
|
318
|
+
Dir.glob(File.join(@options[:spool_directory], "spinner-#{pid}-*.body")).each do |path|
|
|
319
|
+
begin
|
|
320
|
+
File.unlink(path)
|
|
321
|
+
rescue SystemCallError => error
|
|
322
|
+
warn "Spinner spool cleanup: #{error.message}"
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
restart_pending.delete(pid)
|
|
326
|
+
warn "SPINNER_METRIC event=worker_exit pid=#{pid} remaining=#{children.length}"
|
|
327
|
+
next if stopping
|
|
328
|
+
if pid == restarting_pid
|
|
329
|
+
warn "SPINNER_METRIC event=roll_exit pid=#{pid}"
|
|
330
|
+
children << spawn_child
|
|
331
|
+
restarting_pid = restart_pending.shift
|
|
332
|
+
warn "SPINNER_METRIC event=roll_start pid=#{restarting_pid} pending=#{restart_pending.length}" unless restarting_pid.nil?
|
|
333
|
+
signal_children([restarting_pid], "USR1") unless restarting_pid.nil?
|
|
334
|
+
next
|
|
335
|
+
end
|
|
336
|
+
now = Transport.now
|
|
337
|
+
restarts.reject! { |time| now - time > 10.0 }
|
|
338
|
+
restarts << now
|
|
339
|
+
if restarts.length > 5
|
|
340
|
+
warn "Spinner supervisor: worker restart limit reached"
|
|
341
|
+
failed = true
|
|
342
|
+
stopping = true
|
|
343
|
+
signal_children(children, "TERM")
|
|
344
|
+
else
|
|
345
|
+
warn "SPINNER_METRIC event=worker_replacement exited_pid=#{pid}"
|
|
346
|
+
children << spawn_child
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
ensure
|
|
350
|
+
signal_children(children, "TERM")
|
|
351
|
+
children.each do |pid|
|
|
352
|
+
begin
|
|
353
|
+
Process.waitpid(pid)
|
|
354
|
+
rescue Errno::ECHILD
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
Signal.trap("TERM", previous_term) unless previous_term.nil?
|
|
359
|
+
Signal.trap("INT", previous_int) unless previous_int.nil?
|
|
360
|
+
Signal.trap("HUP", previous_hup) unless previous_hup.nil?
|
|
361
|
+
ENV.delete("SPINNER_CHILD_FD")
|
|
362
|
+
ENV.delete("SPINNER_PARENT_PID")
|
|
363
|
+
listener.close unless listener.closed?
|
|
364
|
+
end
|
|
365
|
+
raise "worker restart limit reached" if failed
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def spawn_child
|
|
369
|
+
pid = Process.spawn($PROGRAM_NAME)
|
|
370
|
+
warn "SPINNER_METRIC event=worker_spawn pid=#{pid}"
|
|
371
|
+
pid
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def signal_children(children, signal)
|
|
375
|
+
children.each do |pid|
|
|
376
|
+
begin
|
|
377
|
+
Process.kill(signal, pid)
|
|
378
|
+
rescue Errno::ESRCH
|
|
379
|
+
nil
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Spinner
|
|
2
|
+
# Bounded admission; no unbounded task queue or per-request thread creation.
|
|
3
|
+
class TaskPool
|
|
4
|
+
def initialize(size, capacity = nil)
|
|
5
|
+
@size = size
|
|
6
|
+
@capacity = capacity || size
|
|
7
|
+
@tasks = []
|
|
8
|
+
@workers = []
|
|
9
|
+
@count = 0
|
|
10
|
+
@lock = Mutex.new
|
|
11
|
+
@ready = ConditionVariable.new
|
|
12
|
+
@stopping = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def submit(on_cancel = nil, &task)
|
|
16
|
+
@lock.synchronize do
|
|
17
|
+
return false if @stopping || @count >= @capacity
|
|
18
|
+
@count += 1
|
|
19
|
+
@tasks << [task, on_cancel]
|
|
20
|
+
if @workers.length < @size
|
|
21
|
+
@workers << Thread.new { run }
|
|
22
|
+
end
|
|
23
|
+
@ready.signal
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run
|
|
29
|
+
loop do
|
|
30
|
+
task = @lock.synchronize do
|
|
31
|
+
@ready.wait(@lock) while @tasks.empty? && !@stopping
|
|
32
|
+
@tasks.empty? ? nil : @tasks.shift
|
|
33
|
+
end
|
|
34
|
+
break if task.nil?
|
|
35
|
+
begin
|
|
36
|
+
task[0].call
|
|
37
|
+
rescue StandardError => error
|
|
38
|
+
warn "Spinner task: #{error.message}"
|
|
39
|
+
ensure
|
|
40
|
+
@lock.synchronize { @count -= 1 }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def count
|
|
46
|
+
@lock.synchronize { @count }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop
|
|
50
|
+
cancelled = @lock.synchronize do
|
|
51
|
+
@stopping = true
|
|
52
|
+
tasks = @tasks
|
|
53
|
+
@tasks = []
|
|
54
|
+
@ready.broadcast
|
|
55
|
+
tasks
|
|
56
|
+
end
|
|
57
|
+
@workers.each(&:kill)
|
|
58
|
+
@workers.each(&:join)
|
|
59
|
+
cancelled.each { |task| task[1].call unless task[1].nil? }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
module Spinner
|
|
2
|
+
class Capacity
|
|
3
|
+
def initialize(maximum)
|
|
4
|
+
@maximum = maximum
|
|
5
|
+
@count = 0
|
|
6
|
+
@lock = Mutex.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def acquire
|
|
10
|
+
@lock.synchronize do
|
|
11
|
+
return false if @count >= @maximum
|
|
12
|
+
@count += 1
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def release
|
|
18
|
+
@lock.synchronize { @count -= 1 }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def count
|
|
22
|
+
@lock.synchronize { @count }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def full?
|
|
26
|
+
count >= @maximum
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ThreadPool
|
|
31
|
+
def initialize(application, size, capacity, header_limit, read_timeout, write_timeout, body_timeout, port, connections, options)
|
|
32
|
+
@options = options
|
|
33
|
+
@streams = TaskPool.new(options[:max_streams])
|
|
34
|
+
@upgrades = TaskPool.new(options[:max_upgrades])
|
|
35
|
+
@async_clients = []
|
|
36
|
+
@capacity = capacity
|
|
37
|
+
@connections = connections
|
|
38
|
+
@queue = []
|
|
39
|
+
@completed = []
|
|
40
|
+
@active = {}
|
|
41
|
+
@lock = Mutex.new
|
|
42
|
+
@ready = ConditionVariable.new
|
|
43
|
+
@stopping = false
|
|
44
|
+
@stop_deadline = nil
|
|
45
|
+
@workers = []
|
|
46
|
+
size.times do
|
|
47
|
+
@workers << spawn_worker(application, header_limit, read_timeout,
|
|
48
|
+
write_timeout, body_timeout, port)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def spawn_worker(application, header_limit, read_timeout, write_timeout, body_timeout, port)
|
|
53
|
+
Thread.new do
|
|
54
|
+
connection = Connection.new(application, header_limit, read_timeout,
|
|
55
|
+
write_timeout, body_timeout, port)
|
|
56
|
+
loop do
|
|
57
|
+
client = take
|
|
58
|
+
break if client.nil?
|
|
59
|
+
if @options[:request_metrics]
|
|
60
|
+
warn "SPINNER_METRIC event=start id=#{client.id} queue_wait_ms=#{client.queue_wait_ms} active=#{active_count} queued=#{queued_count} connections=#{@connections.count}"
|
|
61
|
+
end
|
|
62
|
+
app_ms = nil
|
|
63
|
+
reuse = false
|
|
64
|
+
begin
|
|
65
|
+
reuse = client.keep_alive?
|
|
66
|
+
result = connection.serve_buffered(client, method(:execute_async), @options[:stream_idle_timeout])
|
|
67
|
+
unless result.nil? || result == :async
|
|
68
|
+
app_ms = result[0]
|
|
69
|
+
reuse = result[1]
|
|
70
|
+
end
|
|
71
|
+
warn "SPINNER_METRIC event=finish id=#{client.id} app_ms=#{app_ms}" if @options[:request_metrics]
|
|
72
|
+
ensure
|
|
73
|
+
if result == :async
|
|
74
|
+
@lock.synchronize { @active.delete(Thread.current) }
|
|
75
|
+
elsif !app_ms.nil? && reuse && !client.socket.closed?
|
|
76
|
+
@lock.synchronize do
|
|
77
|
+
@completed << client
|
|
78
|
+
@active.delete(Thread.current)
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
client.close
|
|
82
|
+
@lock.synchronize { @active.delete(Thread.current) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def execute_async(kind, client, body, &work)
|
|
90
|
+
pool = kind == :upgrade ? @upgrades : @streams
|
|
91
|
+
@lock.synchronize do
|
|
92
|
+
client.stream_deadline = Transport.now + @options[:stream_idle_timeout]
|
|
93
|
+
@async_clients << client
|
|
94
|
+
end
|
|
95
|
+
cancelled = proc do
|
|
96
|
+
begin
|
|
97
|
+
body.close if body.respond_to?(:close)
|
|
98
|
+
ensure
|
|
99
|
+
client.cleanup_body
|
|
100
|
+
client.close
|
|
101
|
+
@lock.synchronize { @async_clients.delete(client) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
accepted = pool.submit(cancelled) do
|
|
105
|
+
reuse = false
|
|
106
|
+
begin
|
|
107
|
+
warn "SPINNER_METRIC event=#{kind}_start id=#{client.id}" if @options[:request_metrics]
|
|
108
|
+
reuse = work.call
|
|
109
|
+
rescue IOError, SystemCallError => error
|
|
110
|
+
warn "SPINNER_METRIC event=transport_failure message=#{error.message}"
|
|
111
|
+
rescue StandardError => error
|
|
112
|
+
warn "SPINNER_METRIC event=stream_failure message=#{error.message}"
|
|
113
|
+
ensure
|
|
114
|
+
client.cleanup_body
|
|
115
|
+
@lock.synchronize do
|
|
116
|
+
@async_clients.delete(client)
|
|
117
|
+
client.stream_deadline = nil
|
|
118
|
+
if reuse && !client.closed? && !@stopping
|
|
119
|
+
@completed << client
|
|
120
|
+
else
|
|
121
|
+
client.close
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
warn "SPINNER_METRIC event=#{kind}_finish id=#{client.id}" if @options[:request_metrics]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
unless accepted
|
|
128
|
+
@lock.synchronize { @async_clients.delete(client) }
|
|
129
|
+
warn "SPINNER_METRIC event=rejected kind=#{kind} id=#{client.id}"
|
|
130
|
+
end
|
|
131
|
+
accepted
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def enqueue(client)
|
|
135
|
+
@lock.synchronize do
|
|
136
|
+
return false if @stopping || @queue.length >= @capacity
|
|
137
|
+
client.queued!
|
|
138
|
+
@queue << client
|
|
139
|
+
@ready.signal
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def take
|
|
145
|
+
@lock.synchronize do
|
|
146
|
+
@ready.wait(@lock) while @queue.empty? && !@stopping
|
|
147
|
+
return nil if @queue.empty?
|
|
148
|
+
return nil if !@stop_deadline.nil? && Transport.now >= @stop_deadline
|
|
149
|
+
client = @queue.shift
|
|
150
|
+
client.claim!
|
|
151
|
+
@active[Thread.current] = client
|
|
152
|
+
client
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def queued?(client)
|
|
157
|
+
@lock.synchronize { @queue.include?(client) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def cancel(client)
|
|
161
|
+
@lock.synchronize { !@queue.delete(client).nil? }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def stream_count
|
|
165
|
+
@streams.count
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def upgrade_count
|
|
169
|
+
@upgrades.count
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def active_count
|
|
173
|
+
@lock.synchronize { @active.length }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def queued_count
|
|
177
|
+
@lock.synchronize { @queue.length }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def completed
|
|
181
|
+
@lock.synchronize do
|
|
182
|
+
result = @completed
|
|
183
|
+
@completed = []
|
|
184
|
+
result
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def request_stop(deadline = nil)
|
|
189
|
+
@lock.synchronize do
|
|
190
|
+
@stopping = true
|
|
191
|
+
@stop_deadline = deadline unless deadline.nil?
|
|
192
|
+
@ready.broadcast
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def expire_async
|
|
197
|
+
@lock.synchronize { @async_clients.each { |client| client.expire_stream(Transport.now) } }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def maintain(application, header_limit, read_timeout, write_timeout, body_timeout, port)
|
|
201
|
+
return if @stopping
|
|
202
|
+
@workers.each_with_index do |worker, index|
|
|
203
|
+
next if worker.alive?
|
|
204
|
+
begin
|
|
205
|
+
worker.join
|
|
206
|
+
rescue Exception => error
|
|
207
|
+
warn "Spinner worker: #{error.message}"
|
|
208
|
+
end
|
|
209
|
+
@workers[index] = spawn_worker(application, header_limit, read_timeout,
|
|
210
|
+
write_timeout, body_timeout, port)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def stop
|
|
215
|
+
request_stop
|
|
216
|
+
@lock.synchronize { @async_clients.each(&:close) }
|
|
217
|
+
@streams.stop
|
|
218
|
+
@upgrades.stop
|
|
219
|
+
@workers.each(&:kill)
|
|
220
|
+
@workers.each do |worker|
|
|
221
|
+
begin
|
|
222
|
+
worker.join
|
|
223
|
+
rescue Exception => error
|
|
224
|
+
warn "Spinner worker: #{error.message}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|