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.
@@ -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