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,575 @@
1
+ module Spinner
2
+ # A socket belongs to this state machine until a complete request is handed
3
+ # to a worker. It also writes protocol errors and queue rejection responses.
4
+ class PendingClient
5
+ attr_reader :socket, :parser, :state, :id, :remainder
6
+
7
+ def initialize(socket, port, header_limit, read_timeout, body_timeout, write_timeout,
8
+ capacity, queue_timeout, id, keepalive_timeout, max_requests,
9
+ tls_context = nil, options = {})
10
+ @options = options
11
+ @storage_pool = options[:storage_pool]
12
+ @storage_pending = false
13
+ @storage_finished = false
14
+ @storage_error = nil
15
+ @storage_status = 503
16
+ @storage = nil
17
+ @close_lock = Mutex.new
18
+ @peer = socket.respond_to?(:peeraddr) ? socket.peeraddr[3] : ""
19
+ @peer = "" if @peer.nil?
20
+ @socket = TLS::Socket.new(socket, tls_context)
21
+ @port = port
22
+ @scheme = tls_context.nil? ? "http" : "https"
23
+ @header_limit = header_limit
24
+ @parser = RequestParser.new(header_limit, port, @scheme, @options[:max_body_bytes])
25
+ @state = tls_context.nil? ? :headers : :handshake
26
+ @read_timeout = read_timeout
27
+ @body_timeout = body_timeout
28
+ @write_timeout = write_timeout
29
+ @capacity = capacity
30
+ @queue_timeout = queue_timeout
31
+ @id = id
32
+ @deadline = Transport.now + read_timeout
33
+ @body_size = 0
34
+ @body_complete = false
35
+ @remainder = "".b
36
+ @chunk_decoder = nil
37
+ @requests_served = 0
38
+ @keepalive_timeout = keepalive_timeout
39
+ @max_requests = max_requests
40
+ @idle = false
41
+ @chunks = []
42
+ @chunk_index = 0
43
+ @chunk_offset = 0
44
+ @wait_direction = :read
45
+ end
46
+
47
+ def storage_pending?
48
+ @storage_pending
49
+ end
50
+
51
+ def reading?
52
+ !@storage_pending && (@state == :handshake || @state == :headers || @state == :body)
53
+ end
54
+
55
+ def writing?
56
+ @state == :writing || @state == :interim
57
+ end
58
+
59
+ def wait_readable?
60
+ (reading? || writing?) && @wait_direction == :read
61
+ end
62
+
63
+ def wait_writable?
64
+ (reading? || writing?) && @wait_direction == :write
65
+ end
66
+
67
+ def buffered_tls_data?
68
+ @state != :handshake && reading? && @socket.respond_to?(:pending) && @socket.pending > 0
69
+ end
70
+
71
+ def io_step
72
+ if @state == :handshake
73
+ handshake_step
74
+ elsif reading?
75
+ read_step
76
+ elsif writing?
77
+ write_step
78
+ end
79
+ end
80
+
81
+ def handshake_step
82
+ @socket.accept_nonblock
83
+ @state = :headers
84
+ @wait_direction = :read
85
+ @deadline = Transport.now + @read_timeout
86
+ rescue IO::WaitReadable
87
+ @wait_direction = :read
88
+ rescue IO::WaitWritable
89
+ @wait_direction = :write
90
+ rescue EOFError, IOError, SystemCallError => error
91
+ warn "SPINNER_METRIC event=tls_failure message=#{error.message}"
92
+ close
93
+ end
94
+
95
+ def ready?
96
+ @state == :ready
97
+ end
98
+
99
+ def closed?
100
+ @state == :closed
101
+ end
102
+
103
+ def idle?
104
+ @idle
105
+ end
106
+
107
+ def deadline
108
+ @deadline
109
+ end
110
+
111
+ def queued!
112
+ @state = :queued
113
+ @queued_at = Transport.now
114
+ @deadline = @queued_at + @queue_timeout
115
+ end
116
+
117
+ def claim!
118
+ @state = :worker
119
+ @queue_wait_ms = ((Transport.now - @queued_at) * 1000).round
120
+ end
121
+
122
+ def queue_wait_ms
123
+ @queue_wait_ms
124
+ end
125
+
126
+ def queued?
127
+ @state == :queued
128
+ end
129
+
130
+ def body
131
+ @storage.input
132
+ end
133
+
134
+ def environment
135
+ @options[:proxy_policy].apply(@parser.environment(body), @peer)
136
+ end
137
+
138
+ def cleanup_body
139
+ @storage.close unless @storage.nil? || (@storage_pending && !@storage_finished)
140
+ end
141
+
142
+ def storage_step
143
+ return unless @storage_pending && @storage_finished
144
+ @storage_pending = false
145
+ if closed?
146
+ cleanup_body
147
+ elsif !@storage_error.nil?
148
+ reject(@storage_status)
149
+ else
150
+ @state = :ready if @body_complete
151
+ finish_continue
152
+ end
153
+ end
154
+
155
+ def store_chunk(chunk)
156
+ if @body_size + chunk.bytesize <= @options[:body_memory_bytes] && @chunk_decoder.nil?
157
+ append_body(chunk)
158
+ @state = :ready if @body_complete
159
+ finish_continue
160
+ return
161
+ end
162
+ @storage_pending = true
163
+ @storage_finished = false
164
+ @storage_error = nil
165
+ @storage_status = 503
166
+ accepted = @storage_pool.submit(proc { @storage.close }) do
167
+ begin
168
+ append_body(chunk)
169
+ rescue RequestError => error
170
+ @storage_status = error.status
171
+ @storage_error = error
172
+ rescue StandardError => error
173
+ @storage_error = error
174
+ ensure
175
+ @storage_finished = true
176
+ @storage.close if closed?
177
+ end
178
+ end
179
+ unless accepted
180
+ @storage_pending = false
181
+ reject(503)
182
+ end
183
+ end
184
+
185
+ def finish_continue
186
+ if @parser.expect_continue? && @state == :body && !@continued
187
+ @continued = true
188
+ @chunks = ["HTTP/1.1 100 Continue\r\n\r\n"]
189
+ @chunk_index = 0
190
+ @chunk_offset = 0
191
+ @state = :interim
192
+ @wait_direction = :write
193
+ @deadline = Transport.now + @write_timeout
194
+ end
195
+ end
196
+
197
+ attr_accessor :stream_deadline
198
+
199
+ def expire_stream(now)
200
+ close if !@stream_deadline.nil? && now >= @stream_deadline
201
+ end
202
+
203
+ def keep_alive?
204
+ @parser.keep_alive? && @requests_served + 1 < @max_requests
205
+ end
206
+
207
+ def next_request!
208
+ @requests_served += 1
209
+ bytes = @remainder
210
+ @remainder = "".b
211
+ cleanup_body
212
+ @storage = nil
213
+ @continued = false
214
+ @parser = RequestParser.new(@header_limit, @port, @scheme, @options[:max_body_bytes])
215
+ @body_size = 0
216
+ @body_complete = false
217
+ @chunk_decoder = nil
218
+ @state = :headers
219
+ @wait_direction = :read
220
+ @idle = true
221
+ @deadline = Transport.now + @keepalive_timeout
222
+ consume(bytes) unless bytes.empty?
223
+ end
224
+
225
+ def read_step
226
+ chunk = @socket.read_nonblock(65536)
227
+ @wait_direction = :read
228
+ consume(chunk)
229
+ rescue IO::WaitReadable
230
+ @wait_direction = :read
231
+ rescue IO::WaitWritable
232
+ @wait_direction = :write
233
+ rescue EOFError
234
+ if @state == :headers
235
+ close
236
+ else
237
+ reject(400)
238
+ end
239
+ end
240
+
241
+ def consume(chunk)
242
+ if @idle && !chunk.empty?
243
+ @idle = false
244
+ @deadline = Transport.now + @read_timeout
245
+ end
246
+ if @state == :headers
247
+ if @parser.feed(chunk)
248
+ @state = :body
249
+ @body_deadline = Transport.now + @body_timeout
250
+ @deadline = @body_deadline
251
+ @storage = BodyStorage.new(@options[:max_body_bytes], @options[:body_memory_bytes],
252
+ @options[:spool_directory], @options[:storage_budget])
253
+ @chunk_decoder = ChunkDecoder.new(@storage, @options[:max_body_bytes]) if @parser.chunked?
254
+ store_chunk(@parser.remainder)
255
+ end
256
+ else
257
+ store_chunk(chunk)
258
+ end
259
+ end
260
+
261
+ def append_body(chunk)
262
+ unless @chunk_decoder.nil?
263
+ if @chunk_decoder.feed(chunk)
264
+ @remainder = @chunk_decoder.remainder
265
+ @body_complete = true
266
+ end
267
+ return
268
+ end
269
+ remaining = @parser.content_length - @body_size
270
+ count = [remaining, chunk.bytesize].min
271
+ if count > 0
272
+ @storage.append(chunk.byteslice(0, count).b)
273
+ @body_size += count
274
+ end
275
+ if @body_size == @parser.content_length
276
+ @remainder = chunk.byteslice(count, chunk.bytesize - count).to_s.b
277
+ @body_complete = true
278
+ end
279
+ end
280
+
281
+ def reject(status)
282
+ cleanup_body
283
+ warn "SPINNER_METRIC event=rejected status=#{status} id=#{@id}"
284
+ writer = ResponseWriter.new
285
+ respond(writer.prepare([status, {"content-type" => "text/plain"}, [ResponseWriter.reason(status)]]))
286
+ end
287
+
288
+ def respond(chunks)
289
+ @chunks = chunks
290
+ @chunk_index = 0
291
+ @chunk_offset = 0
292
+ @state = :writing
293
+ @wait_direction = :write
294
+ @deadline = Transport.now + @write_timeout
295
+ end
296
+
297
+ def write_step
298
+ while @chunk_index < @chunks.length
299
+ chunk = @chunks[@chunk_index]
300
+ if @chunk_offset == chunk.bytesize
301
+ @chunk_index += 1
302
+ @chunk_offset = 0
303
+ next
304
+ end
305
+ # The binary local selects Spinel's length-aware socket write path.
306
+ binary = chunk.byteslice(@chunk_offset, chunk.bytesize - @chunk_offset).to_s.b
307
+ count = @socket.write_nonblock(binary)
308
+ @wait_direction = :write
309
+ raise IOError, "invalid write progress" if count <= 0 || count > binary.bytesize
310
+ @chunk_offset += count
311
+ return
312
+ end
313
+ if @state == :interim
314
+ @chunks = []
315
+ @state = :body
316
+ @wait_direction = :read
317
+ @deadline = @body_deadline
318
+ else
319
+ close
320
+ end
321
+ rescue IO::WaitWritable
322
+ @wait_direction = :write
323
+ rescue IO::WaitReadable
324
+ @wait_direction = :read
325
+ rescue EOFError, IOError, SystemCallError => error
326
+ warn "Spinner transport: #{error.message}"
327
+ close
328
+ end
329
+
330
+ def expire(now)
331
+ return unless reading? || writing? || @storage_pending
332
+ return if now < @deadline
333
+ if @storage_pending
334
+ close
335
+ elsif reading?
336
+ @state == :handshake || @idle ? close : reject(408)
337
+ else
338
+ close
339
+ end
340
+ end
341
+
342
+ def close
343
+ @close_lock.synchronize do
344
+ return if @state == :closed
345
+ @state = :closed
346
+ begin
347
+ @socket.close unless @socket.closed?
348
+ ensure
349
+ cleanup_body
350
+ @capacity.release
351
+ end
352
+ end
353
+ end
354
+
355
+ end
356
+
357
+ class Reactor
358
+ def initialize(listener, application, port, header_limit, read_timeout, body_timeout, write_timeout,
359
+ threads, queue_capacity, max_connections, queue_timeout, drain_timeout,
360
+ keepalive_timeout, max_requests, tls_context = nil, options = {})
361
+ @options = options
362
+ @options[:storage_pool] = TaskPool.new(options[:storage_workers], max_connections)
363
+ @options[:storage_budget] = StorageBudget.new(options[:spool_budget_bytes])
364
+ @listener = listener
365
+ @application = application
366
+ @port = port
367
+ @header_limit = header_limit
368
+ @read_timeout = read_timeout
369
+ @body_timeout = body_timeout
370
+ @write_timeout = write_timeout
371
+ @clients = []
372
+ @queued = []
373
+ @capacity = Capacity.new(max_connections)
374
+ @queue_timeout = queue_timeout
375
+ @drain_timeout = drain_timeout
376
+ @keepalive_timeout = keepalive_timeout
377
+ @max_requests = max_requests
378
+ @tls_context = tls_context
379
+ @reactor_heartbeat = options[:reactor_heartbeat]
380
+ @shutdown_requested = false
381
+ @graceful_shutdown = false
382
+ @force_shutdown_requested = false
383
+ @draining = false
384
+ @next_id = 0
385
+ @accept_backoff_until = 0.0
386
+ @reserve_fd = File.open("/dev/null", "r")
387
+ @reserve_fd.close # Initialize Spinel’s shared closed-file sentinel before descriptors can exhaust.
388
+ @reserve_fd = File.open("/dev/null", "r")
389
+ @reserve_reopen_count = nil
390
+ @last_heartbeat = Transport.now
391
+ @pool = ThreadPool.new(application, threads, queue_capacity, header_limit,
392
+ read_timeout, write_timeout, body_timeout, port, @capacity, @options)
393
+ end
394
+
395
+ def shutdown!(graceful = false)
396
+ @graceful_shutdown = true if graceful
397
+ @force_shutdown_requested = true unless graceful
398
+ @shutdown_requested = true
399
+ end
400
+
401
+ def run
402
+ loop do
403
+ now = Transport.now
404
+ if @reactor_heartbeat && now - @last_heartbeat >= 1.0
405
+ warn "SPINNER_METRIC event=reactor pid=#{Process.pid} connections=#{@capacity.count} clients=#{@clients.length} queued=#{@queued.length} app_active=#{@pool.active_count}"
406
+ @last_heartbeat = now
407
+ end
408
+ if @reserve_fd.nil? && !@reserve_reopen_count.nil? && @capacity.count < @reserve_reopen_count
409
+ begin
410
+ @reserve_fd = File.open("/dev/null", "r")
411
+ @reserve_reopen_count = nil
412
+ rescue SystemCallError, IOError
413
+ nil
414
+ end
415
+ end
416
+ parent_pid = ENV["SPINNER_PARENT_PID"]
417
+ @shutdown_requested = true if !parent_pid.nil? && Process.ppid != parent_pid.to_i
418
+ if @shutdown_requested && !@draining
419
+ @draining = true
420
+ @drain_deadline = now + @drain_timeout
421
+ @listener.close unless @listener.closed?
422
+ if @graceful_shutdown
423
+ @clients.each { |client| client.close if client.idle? }
424
+ @clients.reject!(&:closed?)
425
+ else
426
+ @clients.each(&:close)
427
+ @clients.clear
428
+ @pool.request_stop(@drain_deadline)
429
+ end
430
+ warn "SPINNER_METRIC event=shutdown connections=#{@capacity.count} queued=#{@pool.queued_count} active=#{@pool.active_count} streams=#{@pool.stream_count} upgrades=#{@pool.upgrade_count} pid=#{Process.pid}"
431
+ end
432
+ if @draining && @graceful_shutdown && @force_shutdown_requested
433
+ @graceful_shutdown = false
434
+ @clients.each(&:close)
435
+ @clients.clear
436
+ @pool.request_stop(@drain_deadline)
437
+ end
438
+ break if @draining && (@capacity.count == 0 || now >= @drain_deadline)
439
+ @clients.each do |client|
440
+ client.storage_step
441
+ client.expire(now)
442
+ queue_client(client) if client.ready? && !client.storage_pending?
443
+ end
444
+ @clients.reject!(&:closed?)
445
+ @queued.reject! do |client|
446
+ if @pool.queued?(client)
447
+ if !@draining && now >= client.deadline && @pool.cancel(client)
448
+ client.reject(503)
449
+ @clients << client
450
+ true
451
+ else
452
+ false
453
+ end
454
+ else
455
+ true
456
+ end
457
+ end
458
+ @pool.expire_async
459
+ @pool.maintain(@application, @header_limit, @read_timeout,
460
+ @write_timeout, @body_timeout, @port) unless @draining
461
+ @pool.completed.each do |client|
462
+ if @draining || client.closed?
463
+ client.close
464
+ else
465
+ begin
466
+ client.next_request!
467
+ @clients << client
468
+ queue_client(client) if client.ready?
469
+ rescue RequestError => error
470
+ client.reject(error.status)
471
+ @clients << client
472
+ end
473
+ end
474
+ end
475
+ @clients.each do |client|
476
+ read_client(client) if !client.closed? && client.buffered_tls_data?
477
+ end
478
+ reads = []
479
+ reads << @listener unless @draining || @capacity.full? || now < @accept_backoff_until
480
+ writes = []
481
+ deadline = nil
482
+ @clients.each do |client|
483
+ reads << client.socket.to_io if client.wait_readable?
484
+ writes << client.socket.to_io if client.wait_writable?
485
+ if client.reading? || client.writing?
486
+ deadline = client.deadline if deadline.nil? || client.deadline < deadline
487
+ end
488
+ end
489
+ @queued.each do |client|
490
+ next if @draining
491
+ deadline = client.deadline if deadline.nil? || client.deadline < deadline
492
+ end
493
+ timeout = deadline.nil? ? 0.05 : [[deadline - Transport.now, 0.0].max, 0.05].min
494
+ timeout = [timeout, 0.001].min if @clients.any?(&:storage_pending?)
495
+ selected = IO.select(reads, writes, nil, timeout)
496
+ next if selected.nil?
497
+ selected[0].each do |socket|
498
+ if socket == @listener
499
+ accept_ready
500
+ else
501
+ client = @clients.find { |candidate| candidate.socket.to_io == socket }
502
+ read_client(client) unless client.nil?
503
+ end
504
+ end
505
+ selected[1].each do |socket|
506
+ client = @clients.find { |candidate| candidate.socket.to_io == socket }
507
+ read_client(client) unless client.nil? || client.closed?
508
+ end
509
+ end
510
+ ensure
511
+ warn "SPINNER_METRIC event=shutdown_complete pid=#{Process.pid} remaining=#{@capacity.count} outcome=#{@capacity.count == 0 ? 'drained' : 'forced'}"
512
+ @clients.each(&:close)
513
+ @queued.each do |client|
514
+ client.close if @pool.cancel(client)
515
+ end
516
+ @pool.stop
517
+ @options[:storage_pool].stop
518
+ @reserve_fd.close unless @reserve_fd.nil? || @reserve_fd.closed?
519
+ @pool.completed.each(&:close)
520
+ end
521
+
522
+ def accept_ready
523
+ return if @capacity.full?
524
+ socket = @listener.accept_nonblock
525
+ @capacity.acquire
526
+ @next_id += 1
527
+ begin
528
+ @clients << PendingClient.new(socket, @port, @header_limit, @read_timeout,
529
+ @body_timeout, @write_timeout,
530
+ @capacity, @queue_timeout, @next_id,
531
+ @keepalive_timeout, @max_requests,
532
+ @tls_context, @options)
533
+ rescue StandardError
534
+ socket.close unless socket.closed?
535
+ @capacity.release
536
+ raise
537
+ end
538
+ warn "SPINNER_METRIC event=accept id=#{@next_id} connections=#{@capacity.count} pid=#{Process.pid}"
539
+ rescue IO::WaitReadable
540
+ nil
541
+ rescue Errno::EMFILE, Errno::ENFILE, Errno::ECONNABORTED => error
542
+ warn "Spinner accept: #{error.message}"
543
+ unless @reserve_fd.nil?
544
+ @reserve_fd.close
545
+ @reserve_fd = nil
546
+ @reserve_reopen_count = @capacity.count
547
+ end
548
+ @accept_backoff_until = Transport.now + 0.1
549
+ end
550
+
551
+ def read_client(client)
552
+ return if client.closed?
553
+ client.io_step
554
+ return unless client.ready? && !client.storage_pending?
555
+ queue_client(client)
556
+ rescue RequestError => error
557
+ client.reject(error.status)
558
+ rescue IOError, SystemCallError => error
559
+ warn "Spinner transport: #{error.message}"
560
+ client.close
561
+ end
562
+
563
+ def queue_client(client)
564
+ if @pool.enqueue(client)
565
+ @clients.delete(client)
566
+ @queued << client
567
+ if @options[:request_metrics]
568
+ warn "SPINNER_METRIC event=queued id=#{client.id} queued=#{@pool.queued_count} connections=#{@capacity.count}"
569
+ end
570
+ else
571
+ client.reject(503)
572
+ end
573
+ end
574
+ end
575
+ end