raptor 0.20.2 → 0.22.0
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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +41 -64
- data/docs/raptor-vs-puma.md +22 -20
- data/ext/raptor_http2/raptor_http2.c +12 -0
- data/lib/raptor/binder.rb +39 -13
- data/lib/raptor/cli.rb +10 -0
- data/lib/raptor/cluster.rb +6 -3
- data/lib/raptor/detached_body.rb +293 -0
- data/lib/raptor/http.rb +18 -0
- data/lib/raptor/http1.rb +87 -35
- data/lib/raptor/http2.rb +501 -112
- data/lib/raptor/reactor.rb +992 -50
- data/lib/raptor/server.rb +12 -0
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +1 -0
- data/sig/generated/raptor/binder.rbs +26 -13
- data/sig/generated/raptor/detached_body.rbs +127 -0
- data/sig/generated/raptor/http.rbs +12 -0
- data/sig/generated/raptor/http1.rbs +22 -16
- data/sig/generated/raptor/http2.rbs +240 -25
- data/sig/generated/raptor/reactor.rbs +388 -19
- metadata +4 -2
data/lib/raptor/reactor.rb
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "nio"
|
|
5
|
+
require "openssl"
|
|
5
6
|
require "red-black-tree"
|
|
7
|
+
require "timeout"
|
|
8
|
+
|
|
9
|
+
require "atomic-ruby/atomic_queue"
|
|
10
|
+
require "atomic-ruby/atom"
|
|
11
|
+
require "atomic-ruby/atomic_condition_variable"
|
|
12
|
+
require "atomic-ruby/atomic_count_down_latch"
|
|
13
|
+
|
|
14
|
+
require_relative "detached_body"
|
|
6
15
|
|
|
7
16
|
module Raptor
|
|
8
|
-
# Multiplexes client connections
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# completed requests back to the caller-provided handlers.
|
|
17
|
+
# Multiplexes client connections, manages connection deadlines, feeds
|
|
18
|
+
# ready bytes to the parser pipeline, and hands completed requests back
|
|
19
|
+
# to the caller-provided handlers.
|
|
12
20
|
#
|
|
13
21
|
class Reactor
|
|
14
22
|
# A client connection node ordered by absolute expiry time so the
|
|
@@ -50,7 +58,117 @@ module Raptor
|
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
|
|
61
|
+
# Tracks reactor-owned output state for a connection.
|
|
62
|
+
#
|
|
63
|
+
class ConnectionIO
|
|
64
|
+
# @rbs attr_accessor offset: Integer
|
|
65
|
+
attr_accessor :offset
|
|
66
|
+
|
|
67
|
+
# @rbs attr_accessor wait: Symbol?
|
|
68
|
+
attr_accessor :wait
|
|
69
|
+
|
|
70
|
+
# @rbs attr_accessor reading: bool
|
|
71
|
+
attr_accessor :reading
|
|
72
|
+
|
|
73
|
+
# @rbs attr_accessor closing: bool
|
|
74
|
+
attr_accessor :closing
|
|
75
|
+
|
|
76
|
+
# @rbs attr_accessor monitor: NIO::Monitor?
|
|
77
|
+
attr_accessor :monitor
|
|
78
|
+
|
|
79
|
+
# @rbs attr_accessor timeout: TimeoutClient?
|
|
80
|
+
attr_accessor :timeout
|
|
81
|
+
|
|
82
|
+
# @rbs attr_reader output: Array[String]
|
|
83
|
+
attr_reader :output
|
|
84
|
+
|
|
85
|
+
# @rbs () -> void
|
|
86
|
+
def initialize
|
|
87
|
+
@output = []
|
|
88
|
+
@offset = 0
|
|
89
|
+
@wait = nil
|
|
90
|
+
@reading = false
|
|
91
|
+
@closing = false
|
|
92
|
+
@monitor = nil
|
|
93
|
+
@timeout = nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs () -> bool
|
|
97
|
+
def empty?
|
|
98
|
+
output.empty?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Tracks reactor-owned I/O state for an HTTP/1.x detached response.
|
|
103
|
+
#
|
|
104
|
+
class Http1IO < ConnectionIO
|
|
105
|
+
# @rbs attr_accessor body: DetachedBody?
|
|
106
|
+
attr_accessor :body
|
|
107
|
+
|
|
108
|
+
# @rbs attr_accessor finishing: bool
|
|
109
|
+
attr_accessor :finishing
|
|
110
|
+
|
|
111
|
+
# @rbs attr_reader budget: DetachedBody::Budget
|
|
112
|
+
attr_reader :budget
|
|
113
|
+
|
|
114
|
+
# @rbs attr_reader input: String
|
|
115
|
+
attr_reader :input
|
|
116
|
+
|
|
117
|
+
# @rbs attr_reader state: Hash[Symbol, untyped]
|
|
118
|
+
attr_reader :state
|
|
119
|
+
|
|
120
|
+
# @rbs (DetachedBody body, Hash[Symbol, untyped] state) -> void
|
|
121
|
+
def initialize(body, state)
|
|
122
|
+
super()
|
|
123
|
+
@body = body
|
|
124
|
+
@state = state
|
|
125
|
+
@budget = DetachedBody::Budget.new(DETACHED_CONNECTION_BUFFER_SIZE)
|
|
126
|
+
@input = String.new(encoding: Encoding::ASCII_8BIT)
|
|
127
|
+
@reading = true
|
|
128
|
+
@finishing = false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @rbs () -> bool
|
|
132
|
+
def empty?
|
|
133
|
+
super && !body
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Tracks reactor-owned I/O state for an HTTP/2 connection.
|
|
138
|
+
#
|
|
139
|
+
class Http2IO < ConnectionIO
|
|
140
|
+
# @rbs attr_reader detached: Hash[Integer, DetachedBody]
|
|
141
|
+
attr_reader :detached
|
|
142
|
+
|
|
143
|
+
# @rbs attr_reader ready: Array[Integer]
|
|
144
|
+
attr_reader :ready
|
|
145
|
+
|
|
146
|
+
# @rbs attr_reader ready_streams: Hash[Integer, bool]
|
|
147
|
+
attr_reader :ready_streams
|
|
148
|
+
|
|
149
|
+
# @rbs attr_reader budget: DetachedBody::Budget
|
|
150
|
+
attr_reader :budget
|
|
151
|
+
|
|
152
|
+
# @rbs () -> void
|
|
153
|
+
def initialize
|
|
154
|
+
super
|
|
155
|
+
@detached = {}
|
|
156
|
+
@ready = []
|
|
157
|
+
@ready_streams = {}
|
|
158
|
+
@budget = DetachedBody::Budget.new(DETACHED_CONNECTION_BUFFER_SIZE)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @rbs () -> bool
|
|
162
|
+
def empty?
|
|
163
|
+
super && detached.empty?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
53
167
|
CHUNK_SIZE = 64 * 1024
|
|
168
|
+
DETACHED_CONNECTION_BUFFER_SIZE = 1024 * 1024
|
|
169
|
+
DETACHED_MAX_CHUNKS = 8
|
|
170
|
+
DETACHED_MAX_FRAMES = 8
|
|
171
|
+
DETACHED_WORKER_BUFFER_SIZE = 16 * 1024 * 1024
|
|
54
172
|
TIMEOUT_RESPONSE = "HTTP/1.1 408 Request Timeout\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
55
173
|
|
|
56
174
|
# @rbs @thread_pool: untyped
|
|
@@ -59,14 +177,26 @@ module Raptor
|
|
|
59
177
|
# @rbs @first_data_timeout: Integer
|
|
60
178
|
# @rbs @chunk_data_timeout: Integer
|
|
61
179
|
# @rbs @persistent_data_timeout: Integer
|
|
180
|
+
# @rbs @write_timeout: Integer
|
|
181
|
+
# @rbs @http2_keepalive_interval: Integer
|
|
182
|
+
# @rbs @http2_keepalive_timeout: Integer
|
|
62
183
|
# @rbs @selector: NIO::Selector
|
|
63
184
|
# @rbs @queue: Queue[TCPSocket]
|
|
185
|
+
# @rbs @io_queue: AtomicQueue
|
|
64
186
|
# @rbs @timeouts: RedBlackTree[TimeoutClient]
|
|
65
187
|
# @rbs @id_to_socket: Hash[Integer, TCPSocket]
|
|
66
188
|
# @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
|
|
67
189
|
# @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
|
|
68
190
|
# @rbs @id_to_writer: Hash[Integer, untyped]
|
|
69
191
|
# @rbs @id_to_flow_control: Hash[Integer, untyped]
|
|
192
|
+
# @rbs @id_to_http2_last_stream: Hash[Integer, Integer]
|
|
193
|
+
# @rbs @id_to_http2_drain_stream: Hash[Integer, Integer]
|
|
194
|
+
# @rbs @id_to_http2_keepalive: Hash[Integer, Hash[Symbol, untyped]]
|
|
195
|
+
# @rbs @id_to_io: Hash[Integer, ConnectionIO]
|
|
196
|
+
# @rbs @detached_budget: DetachedBody::Budget
|
|
197
|
+
# @rbs @detached_count: Atom
|
|
198
|
+
# @rbs @detached_drained: AtomicConditionVariable
|
|
199
|
+
# @rbs @thread: Thread?
|
|
70
200
|
|
|
71
201
|
# Creates a new Reactor instance.
|
|
72
202
|
#
|
|
@@ -78,19 +208,26 @@ module Raptor
|
|
|
78
208
|
# @option connection_options [Integer] :chunk_data_timeout timeout for subsequent chunks
|
|
79
209
|
# @param http1_options [Hash] HTTP/1.1-specific configuration
|
|
80
210
|
# @option http1_options [Integer] :persistent_data_timeout timeout for keep-alive idle connections
|
|
211
|
+
# @param http2_options [Hash] HTTP/2-specific configuration
|
|
212
|
+
# @option http2_options [Integer] :keepalive_interval idle time before sending a PING
|
|
213
|
+
# @option http2_options [Integer] :keepalive_timeout time to await a PING acknowledgement
|
|
81
214
|
# @return [void]
|
|
82
215
|
#
|
|
83
|
-
# @rbs (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
|
|
84
|
-
def initialize(http1_ractor_pool, http2_ractor_pool, thread_pool, connection_options:, http1_options:)
|
|
216
|
+
# @rbs (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped], http2_options: Hash[Symbol, untyped]) -> void
|
|
217
|
+
def initialize(http1_ractor_pool, http2_ractor_pool, thread_pool, connection_options:, http1_options:, http2_options:)
|
|
85
218
|
@http1_ractor_pool = http1_ractor_pool
|
|
86
219
|
@http2_ractor_pool = http2_ractor_pool
|
|
87
220
|
@thread_pool = thread_pool
|
|
88
221
|
@first_data_timeout = connection_options[:first_data_timeout]
|
|
89
222
|
@chunk_data_timeout = connection_options[:chunk_data_timeout]
|
|
223
|
+
@write_timeout = connection_options[:write_timeout]
|
|
90
224
|
@persistent_data_timeout = http1_options[:persistent_data_timeout]
|
|
225
|
+
@http2_keepalive_interval = http2_options[:keepalive_interval]
|
|
226
|
+
@http2_keepalive_timeout = http2_options[:keepalive_timeout]
|
|
91
227
|
|
|
92
228
|
@selector = NIO::Selector.new
|
|
93
229
|
@queue = Queue.new
|
|
230
|
+
@io_queue = AtomicQueue.new
|
|
94
231
|
@timeouts = RedBlackTree.new
|
|
95
232
|
|
|
96
233
|
@id_to_socket = {}
|
|
@@ -98,10 +235,18 @@ module Raptor
|
|
|
98
235
|
@id_to_timeout = {}
|
|
99
236
|
@id_to_writer = {}
|
|
100
237
|
@id_to_flow_control = {}
|
|
238
|
+
@id_to_http2_last_stream = {}
|
|
239
|
+
@id_to_http2_drain_stream = {}
|
|
240
|
+
@id_to_http2_keepalive = {}
|
|
241
|
+
@id_to_io = {}
|
|
242
|
+
@detached_budget = DetachedBody::Budget.new(DETACHED_WORKER_BUFFER_SIZE)
|
|
243
|
+
@detached_count = Atom.new(0)
|
|
244
|
+
@detached_drained = AtomicConditionVariable.new
|
|
245
|
+
@thread = nil
|
|
101
246
|
end
|
|
102
247
|
|
|
103
248
|
# Starts the reactor's main event loop in a new thread. Runs until
|
|
104
|
-
#
|
|
249
|
+
# shutdown work and pending writes are drained.
|
|
105
250
|
#
|
|
106
251
|
# @return [Thread] the thread running the reactor event loop
|
|
107
252
|
#
|
|
@@ -109,33 +254,19 @@ module Raptor
|
|
|
109
254
|
def run
|
|
110
255
|
Thread.new do
|
|
111
256
|
Thread.current.name = "Reactor"
|
|
257
|
+
@thread = Thread.current
|
|
112
258
|
|
|
113
|
-
until
|
|
259
|
+
until stopped?
|
|
114
260
|
begin
|
|
115
|
-
|
|
261
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
262
|
+
timeout = @timeouts.min&.timeout(now)
|
|
116
263
|
@selector.select(timeout) do |monitor|
|
|
117
|
-
|
|
264
|
+
handle_monitor(monitor)
|
|
118
265
|
end
|
|
119
266
|
|
|
120
267
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
break unless to_client.timeout(now).zero?
|
|
124
|
-
|
|
125
|
-
expired << to_client
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
expired.each do |to_client|
|
|
129
|
-
@timeouts.delete!(to_client)
|
|
130
|
-
id = to_client.client_data[:id]
|
|
131
|
-
@id_to_timeout.delete(id)
|
|
132
|
-
socket = @id_to_socket[id]
|
|
133
|
-
next unless socket
|
|
134
|
-
|
|
135
|
-
@selector.deregister(socket)
|
|
136
|
-
socket.write(TIMEOUT_RESPONSE) rescue nil
|
|
137
|
-
cleanup(socket)
|
|
138
|
-
end
|
|
268
|
+
expire_timeouts(now)
|
|
269
|
+
drain_io_queue
|
|
139
270
|
|
|
140
271
|
until @queue.empty?
|
|
141
272
|
register(@queue.pop)
|
|
@@ -145,13 +276,19 @@ module Raptor
|
|
|
145
276
|
end
|
|
146
277
|
end
|
|
147
278
|
|
|
279
|
+
@id_to_flow_control.each_value(&:close)
|
|
148
280
|
@id_to_socket.each_value { |socket| socket.close rescue nil }
|
|
149
281
|
@id_to_socket.clear
|
|
150
282
|
@socket_to_state.clear
|
|
151
283
|
@id_to_timeout.clear
|
|
152
284
|
@id_to_writer.clear
|
|
153
285
|
@id_to_flow_control.clear
|
|
286
|
+
@id_to_http2_last_stream.clear
|
|
287
|
+
@id_to_http2_drain_stream.clear
|
|
288
|
+
@id_to_http2_keepalive.clear
|
|
289
|
+
@id_to_io.clear
|
|
154
290
|
@timeouts.clear!
|
|
291
|
+
@thread = nil
|
|
155
292
|
@selector.close
|
|
156
293
|
end
|
|
157
294
|
end
|
|
@@ -270,6 +407,137 @@ module Raptor
|
|
|
270
407
|
@id_to_flow_control[id]
|
|
271
408
|
end
|
|
272
409
|
|
|
410
|
+
# Attaches a detached response body to an HTTP/1.x connection.
|
|
411
|
+
#
|
|
412
|
+
# @rbs (TCPSocket socket, Integer id, DetachedBody body, Hash[Symbol, untyped] state, ^(Symbol) -> void finished) -> bool
|
|
413
|
+
def attach_http1_body(socket, id, body, state, finished)
|
|
414
|
+
attached = AtomicCountDownLatch.new(1)
|
|
415
|
+
@io_queue << [:attach_http1, id, socket, body, attached, finished, state]
|
|
416
|
+
@selector.wakeup rescue nil
|
|
417
|
+
attached.wait
|
|
418
|
+
return false if body.closed?
|
|
419
|
+
|
|
420
|
+
body.open
|
|
421
|
+
true
|
|
422
|
+
rescue
|
|
423
|
+
remove_connection(id)
|
|
424
|
+
raise
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Records an HTTP/2 stream before application dispatch, rejecting streams
|
|
428
|
+
# that arrived after graceful draining began.
|
|
429
|
+
#
|
|
430
|
+
# @param id [Integer] unique connection identifier
|
|
431
|
+
# @param stream_id [Integer] HTTP/2 stream identifier
|
|
432
|
+
# @return [Boolean] whether the stream may be dispatched
|
|
433
|
+
#
|
|
434
|
+
# @rbs (Integer id, Integer stream_id) -> bool
|
|
435
|
+
def dispatch_http2_stream(id, stream_id)
|
|
436
|
+
if (last_stream = @id_to_http2_drain_stream[id])
|
|
437
|
+
return stream_id <= last_stream
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
current = @id_to_http2_last_stream[id] || 0
|
|
441
|
+
@id_to_http2_last_stream[id] = stream_id if stream_id > current
|
|
442
|
+
true
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Sends GOAWAY on every HTTP/2 connection and fixes the last stream each
|
|
446
|
+
# connection may dispatch while existing application work drains.
|
|
447
|
+
#
|
|
448
|
+
# @yieldparam stream_id [Integer] last dispatched stream identifier
|
|
449
|
+
# @yieldreturn [String] serialized GOAWAY frame
|
|
450
|
+
# @return [void]
|
|
451
|
+
#
|
|
452
|
+
# @rbs () { (Integer stream_id) -> String } -> void
|
|
453
|
+
def drain_http2
|
|
454
|
+
@id_to_flow_control.each_key do |id|
|
|
455
|
+
last_stream = @id_to_http2_last_stream[id] || 0
|
|
456
|
+
@id_to_http2_drain_stream[id] = last_stream
|
|
457
|
+
writer = @id_to_writer[id]
|
|
458
|
+
socket = @id_to_socket[id]
|
|
459
|
+
writer.write_frames(socket, [yield(last_stream)]) if writer && socket
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Queues serialized HTTP/2 frames for a connection. Producers never
|
|
464
|
+
# write to the socket or wait for it to become writable.
|
|
465
|
+
#
|
|
466
|
+
# @param id [Integer] unique connection identifier
|
|
467
|
+
# @param frames [Array<String>] frame bytes to write in order
|
|
468
|
+
# @return [void]
|
|
469
|
+
#
|
|
470
|
+
# @rbs (Integer id, Array[String] frames) -> void
|
|
471
|
+
def write_http2_frames(id, frames)
|
|
472
|
+
data = frames.join
|
|
473
|
+
return if data.empty?
|
|
474
|
+
|
|
475
|
+
if Thread.current.equal?(@thread)
|
|
476
|
+
io = @id_to_io[id]
|
|
477
|
+
queue_output(io, data)
|
|
478
|
+
flush_output(id, io) if io && !io.wait
|
|
479
|
+
else
|
|
480
|
+
@io_queue << [:write, id, data]
|
|
481
|
+
@selector.wakeup rescue nil
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Attaches a detached response body to an HTTP/2 stream.
|
|
486
|
+
#
|
|
487
|
+
# @rbs (Integer id, Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> bool
|
|
488
|
+
def attach_http2_body(id, stream_id, body, finished)
|
|
489
|
+
attached = AtomicCountDownLatch.new(1)
|
|
490
|
+
@io_queue << [:attach_http2, id, stream_id, body, attached, finished]
|
|
491
|
+
@selector.wakeup rescue nil
|
|
492
|
+
attached.wait
|
|
493
|
+
return false if body.closed?
|
|
494
|
+
|
|
495
|
+
body.open
|
|
496
|
+
true
|
|
497
|
+
rescue
|
|
498
|
+
cancel_http2_body(id, stream_id)
|
|
499
|
+
raise
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Reconsiders detached streams after an outbound window update.
|
|
503
|
+
#
|
|
504
|
+
# @rbs (Integer id) -> void
|
|
505
|
+
def resume_http2_bodies(id)
|
|
506
|
+
return if @detached_count.value.zero?
|
|
507
|
+
|
|
508
|
+
@io_queue << [:resume_http2, id]
|
|
509
|
+
@selector.wakeup rescue nil
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Cancels a detached response stream.
|
|
513
|
+
#
|
|
514
|
+
# @rbs (Integer id, Integer stream_id) -> void
|
|
515
|
+
def cancel_http2_body(id, stream_id)
|
|
516
|
+
return if @detached_count.value.zero?
|
|
517
|
+
|
|
518
|
+
@io_queue << [:cancel_http2, id, stream_id]
|
|
519
|
+
@selector.wakeup rescue nil
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Waits for detached responses to finish, then cancels any that outlive
|
|
523
|
+
# the worker drain period.
|
|
524
|
+
#
|
|
525
|
+
# @rbs (Numeric timeout) -> void
|
|
526
|
+
def drain_detached_bodies(timeout)
|
|
527
|
+
barrier = AtomicCountDownLatch.new(1)
|
|
528
|
+
@io_queue << [:barrier, nil, barrier]
|
|
529
|
+
@selector.wakeup rescue nil
|
|
530
|
+
barrier.wait
|
|
531
|
+
|
|
532
|
+
Timeout.timeout(timeout) do
|
|
533
|
+
@detached_drained.wait { @detached_count.value.zero? }
|
|
534
|
+
end
|
|
535
|
+
rescue Timeout::Error
|
|
536
|
+
@io_queue << [:cancel_all]
|
|
537
|
+
@selector.wakeup rescue nil
|
|
538
|
+
@detached_drained.wait { @detached_count.value.zero? }
|
|
539
|
+
end
|
|
540
|
+
|
|
273
541
|
# Stores an HTTP/2 connection's socket, state, writer, and flow
|
|
274
542
|
# controller in the reactor's per-connection maps.
|
|
275
543
|
#
|
|
@@ -278,14 +546,36 @@ module Raptor
|
|
|
278
546
|
# @param state [Hash] initial connection state
|
|
279
547
|
# @param writer [Http2::Writer] per-connection frame writer
|
|
280
548
|
# @param flow_control [Http2::FlowControl] per-connection outbound flow controller
|
|
549
|
+
# @param ping_frame [String] serialized PING frame for keepalive probes
|
|
550
|
+
# @param ping_payload [String] payload expected in the corresponding acknowledgement
|
|
281
551
|
# @return [void]
|
|
282
552
|
#
|
|
283
|
-
# @rbs (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped) -> void
|
|
284
|
-
def attach_http2(id:, socket:, state:, writer:, flow_control:)
|
|
553
|
+
# @rbs (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped, ping_frame: String, ping_payload: String) -> void
|
|
554
|
+
def attach_http2(id:, socket:, state:, writer:, flow_control:, ping_frame:, ping_payload:)
|
|
285
555
|
@id_to_socket[id] = socket
|
|
286
556
|
@socket_to_state[socket] = state
|
|
287
557
|
@id_to_writer[id] = writer
|
|
288
558
|
@id_to_flow_control[id] = flow_control
|
|
559
|
+
@id_to_http2_last_stream[id] = 0
|
|
560
|
+
@id_to_io[id] = Http2IO.new
|
|
561
|
+
writer.attach(self, id)
|
|
562
|
+
if @http2_keepalive_interval.positive?
|
|
563
|
+
@id_to_http2_keepalive[id] = {frame: ping_frame, payload: ping_payload}
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Records acknowledgements for server keepalive probes.
|
|
568
|
+
#
|
|
569
|
+
# @param id [Integer] unique connection identifier
|
|
570
|
+
# @param payloads [Array<String>, nil] acknowledged PING payloads
|
|
571
|
+
# @return [void]
|
|
572
|
+
#
|
|
573
|
+
# @rbs (Integer id, Array[String]? payloads) -> void
|
|
574
|
+
def acknowledge_http2_ping(id, payloads)
|
|
575
|
+
keepalive = @id_to_http2_keepalive[id]
|
|
576
|
+
return unless keepalive && payloads&.include?(keepalive[:payload])
|
|
577
|
+
|
|
578
|
+
keepalive.delete(:deadline)
|
|
289
579
|
end
|
|
290
580
|
|
|
291
581
|
# Registers an attached socket for future reactor-driven reads.
|
|
@@ -322,21 +612,15 @@ module Raptor
|
|
|
322
612
|
socket.close
|
|
323
613
|
end
|
|
324
614
|
|
|
325
|
-
# Closes
|
|
326
|
-
# state associated with it.
|
|
615
|
+
# Closes a connection after writing any frames already queued for it.
|
|
327
616
|
#
|
|
328
617
|
# @param id [Integer] unique client identifier
|
|
329
618
|
# @return [void]
|
|
330
619
|
#
|
|
331
620
|
# @rbs (Integer id) -> void
|
|
332
621
|
def close_connection(id)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
@socket_to_state.delete(socket)
|
|
337
|
-
@id_to_writer.delete(id)
|
|
338
|
-
@id_to_flow_control.delete(id)
|
|
339
|
-
socket.close rescue nil
|
|
622
|
+
@io_queue << [:close, id]
|
|
623
|
+
@selector.wakeup rescue nil
|
|
340
624
|
end
|
|
341
625
|
|
|
342
626
|
# Closes the registration queue and wakes the selector so the
|
|
@@ -362,6 +646,520 @@ module Raptor
|
|
|
362
646
|
|
|
363
647
|
private
|
|
364
648
|
|
|
649
|
+
# Returns whether shutdown has no registrations or writes left to drain.
|
|
650
|
+
#
|
|
651
|
+
# @return [Boolean]
|
|
652
|
+
#
|
|
653
|
+
# @rbs () -> bool
|
|
654
|
+
def stopped?
|
|
655
|
+
@queue.closed? && @queue.empty? && @io_queue.empty? &&
|
|
656
|
+
@id_to_io.each_value.all?(&:empty?)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Handles every expired connection deadline in order.
|
|
660
|
+
#
|
|
661
|
+
# @param now [Float] current monotonic timestamp
|
|
662
|
+
# @return [void]
|
|
663
|
+
#
|
|
664
|
+
# @rbs (Float now) -> void
|
|
665
|
+
def expire_timeouts(now)
|
|
666
|
+
while (client = @timeouts.min) && client.timeout(now).zero?
|
|
667
|
+
@timeouts.delete!(client)
|
|
668
|
+
id = client.client_data[:id]
|
|
669
|
+
if client.client_data[:write]
|
|
670
|
+
@id_to_io[id]&.timeout = nil
|
|
671
|
+
remove_connection(id)
|
|
672
|
+
else
|
|
673
|
+
@id_to_timeout.delete(id)
|
|
674
|
+
handle_timeout(client, now)
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Applies queued reactor-owned I/O operations.
|
|
680
|
+
#
|
|
681
|
+
# @return [void]
|
|
682
|
+
#
|
|
683
|
+
# @rbs () -> void
|
|
684
|
+
def drain_io_queue
|
|
685
|
+
connections = {}
|
|
686
|
+
while (operation = @io_queue.pop)
|
|
687
|
+
type, id, value, body, attached, finished, state = operation
|
|
688
|
+
case type
|
|
689
|
+
when :write
|
|
690
|
+
data = value
|
|
691
|
+
queue_output(@id_to_io[id], data)
|
|
692
|
+
connections[id] = true
|
|
693
|
+
when :close
|
|
694
|
+
close_after_writes(id)
|
|
695
|
+
when :attach_http2
|
|
696
|
+
begin
|
|
697
|
+
attach_http2_detached_body(id, value, body, finished)
|
|
698
|
+
ensure
|
|
699
|
+
attached.count_down
|
|
700
|
+
end
|
|
701
|
+
connections[id] = true
|
|
702
|
+
when :attach_http1
|
|
703
|
+
begin
|
|
704
|
+
attach_http1_detached_body(value, id, body, state, finished)
|
|
705
|
+
ensure
|
|
706
|
+
attached.count_down
|
|
707
|
+
end
|
|
708
|
+
when :ready_http1
|
|
709
|
+
flush_http1_detached_body(id)
|
|
710
|
+
when :ready_http2
|
|
711
|
+
mark_http2_detached_body_ready(id, value)
|
|
712
|
+
connections[id] = true
|
|
713
|
+
when :flush_http2
|
|
714
|
+
connections[id] = true
|
|
715
|
+
when :resume_http2
|
|
716
|
+
resume_http2_detached_bodies(id)
|
|
717
|
+
connections[id] = true
|
|
718
|
+
when :cancel_http2
|
|
719
|
+
remove_http2_detached_body(id, value, :cancelled)
|
|
720
|
+
when :cancel_all
|
|
721
|
+
cancel_detached_bodies
|
|
722
|
+
when :barrier
|
|
723
|
+
value.count_down
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
connections.each_key do |id|
|
|
728
|
+
io = @id_to_io[id]
|
|
729
|
+
flush_output(id, io) if io && !io.wait
|
|
730
|
+
flush_http2_detached_bodies(id) if io.is_a?(Http2IO) && !io.wait && !io.detached.empty?
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Adds a detached body to an HTTP/1.x connection.
|
|
735
|
+
#
|
|
736
|
+
# @rbs (TCPSocket socket, Integer id, DetachedBody body, Hash[Symbol, untyped] state, ^(Symbol) -> void finished) -> void
|
|
737
|
+
def attach_http1_detached_body(socket, id, body, state, finished)
|
|
738
|
+
io = Http1IO.new(body, state)
|
|
739
|
+
dispatch = proc { |callback| @thread_pool << callback }
|
|
740
|
+
wake = proc do
|
|
741
|
+
@io_queue << [:ready_http1, id]
|
|
742
|
+
@selector.wakeup rescue nil
|
|
743
|
+
end
|
|
744
|
+
unless body.attach([io.budget, @detached_budget], wake, dispatch, finished)
|
|
745
|
+
socket.close rescue nil
|
|
746
|
+
return
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
@id_to_socket[id] = socket
|
|
750
|
+
@socket_to_state[socket] = {id: id, protocol: :http1, detached: true}
|
|
751
|
+
@id_to_io[id] = io
|
|
752
|
+
@detached_count.swap { |count| count + 1 }
|
|
753
|
+
update_monitor(id, io)
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Writes buffered HTTP/1.x body chunks without blocking.
|
|
757
|
+
#
|
|
758
|
+
# @rbs (Integer id) -> void
|
|
759
|
+
def flush_http1_detached_body(id)
|
|
760
|
+
io = @id_to_io[id]
|
|
761
|
+
return unless io.is_a?(Http1IO) && !io.wait && io.output.empty?
|
|
762
|
+
|
|
763
|
+
DETACHED_MAX_CHUNKS.times do
|
|
764
|
+
size = io.body&.next_size
|
|
765
|
+
break unless size
|
|
766
|
+
|
|
767
|
+
if size.zero?
|
|
768
|
+
queue_output(io, Http1.encode_trailers(io.body.trailers)) if io.state[:chunked]
|
|
769
|
+
io.finishing = true
|
|
770
|
+
flush_output(id, io)
|
|
771
|
+
break
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
chunk = io.body.shift(size)
|
|
775
|
+
queue_output(io, io.state[:chunked] ? Http1.encode_chunk(chunk) : chunk)
|
|
776
|
+
flush_output(id, io)
|
|
777
|
+
break if io.wait
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
if !io.finishing && !io.wait && io.body&.next_size
|
|
781
|
+
@io_queue << [:ready_http1, id]
|
|
782
|
+
@selector.wakeup rescue nil
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Finishes an HTTP/1.x detached response and either reuses or closes its connection.
|
|
787
|
+
#
|
|
788
|
+
# @rbs (Integer id, Http1IO io) -> void
|
|
789
|
+
def finish_http1_detached_body(id, io)
|
|
790
|
+
socket = @id_to_socket[id]
|
|
791
|
+
return unless socket
|
|
792
|
+
|
|
793
|
+
body = io.body
|
|
794
|
+
io.body = nil
|
|
795
|
+
body.finish(:closed)
|
|
796
|
+
detached_body_removed
|
|
797
|
+
@id_to_io.delete(id)
|
|
798
|
+
deregister(socket, io)
|
|
799
|
+
clear_write_timeout(io)
|
|
800
|
+
|
|
801
|
+
if io.state[:keep_alive]
|
|
802
|
+
state = {
|
|
803
|
+
id: id,
|
|
804
|
+
request_count: io.state[:request_count],
|
|
805
|
+
remote_addr: io.state[:remote_addr],
|
|
806
|
+
url_scheme: io.state[:url_scheme],
|
|
807
|
+
persisted: true,
|
|
808
|
+
}
|
|
809
|
+
unless io.input.empty?
|
|
810
|
+
state[:buffer] = io.input
|
|
811
|
+
@socket_to_state[socket] = state
|
|
812
|
+
@http1_ractor_pool << Ractor.make_shareable(state)
|
|
813
|
+
return
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
persist(socket, id, io.state[:request_count], remote_addr: io.state[:remote_addr], url_scheme: io.state[:url_scheme])
|
|
817
|
+
else
|
|
818
|
+
@socket_to_state.delete(socket)
|
|
819
|
+
@id_to_socket.delete(id)
|
|
820
|
+
socket.close rescue nil
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Cancels an HTTP/1.x detached response and closes its connection.
|
|
825
|
+
#
|
|
826
|
+
# @rbs (Integer id, Symbol reason) -> void
|
|
827
|
+
def remove_http1_detached_body(id, reason)
|
|
828
|
+
io = @id_to_io[id]
|
|
829
|
+
return unless io.is_a?(Http1IO) && io.body
|
|
830
|
+
|
|
831
|
+
body = io.body
|
|
832
|
+
io.body = nil
|
|
833
|
+
body.finish(reason)
|
|
834
|
+
detached_body_removed
|
|
835
|
+
remove_connection(id)
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Adds a detached body to an HTTP/2 stream.
|
|
839
|
+
#
|
|
840
|
+
# @rbs (Integer id, Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> void
|
|
841
|
+
def attach_http2_detached_body(id, stream_id, body, finished)
|
|
842
|
+
io = @id_to_io[id]
|
|
843
|
+
dispatch = proc { |callback| @thread_pool << callback }
|
|
844
|
+
unless io
|
|
845
|
+
body.attach([], proc {}, dispatch, finished)
|
|
846
|
+
body.finish(:connection_closed)
|
|
847
|
+
return
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
wake = proc do
|
|
851
|
+
@io_queue << [:ready_http2, id, stream_id]
|
|
852
|
+
@selector.wakeup rescue nil
|
|
853
|
+
end
|
|
854
|
+
return unless body.attach([io.budget, @detached_budget], wake, dispatch, finished)
|
|
855
|
+
|
|
856
|
+
io.detached[stream_id] = body
|
|
857
|
+
@detached_count.swap { |count| count + 1 }
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Marks an HTTP/2 detached stream eligible for fair scheduling.
|
|
861
|
+
#
|
|
862
|
+
# @rbs (Integer id, Integer stream_id) -> void
|
|
863
|
+
def mark_http2_detached_body_ready(id, stream_id)
|
|
864
|
+
io = @id_to_io[id]
|
|
865
|
+
return unless io&.detached&.key?(stream_id) && !io.ready_streams.key?(stream_id)
|
|
866
|
+
|
|
867
|
+
io.ready << stream_id
|
|
868
|
+
io.ready_streams[stream_id] = true
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
# Marks every HTTP/2 detached stream eligible after flow-control capacity changes.
|
|
872
|
+
#
|
|
873
|
+
# @rbs (Integer id) -> void
|
|
874
|
+
def resume_http2_detached_bodies(id)
|
|
875
|
+
io = @id_to_io[id]
|
|
876
|
+
return unless io
|
|
877
|
+
|
|
878
|
+
io.detached.each_key { |stream_id| mark_http2_detached_body_ready(id, stream_id) }
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Writes HTTP/2 detached streams in round-robin order without waiting for flow control.
|
|
882
|
+
#
|
|
883
|
+
# @rbs (Integer id) -> void
|
|
884
|
+
def flush_http2_detached_bodies(id)
|
|
885
|
+
io = @id_to_io[id]
|
|
886
|
+
flow_control = @id_to_flow_control[id]
|
|
887
|
+
socket = @id_to_socket[id]
|
|
888
|
+
return unless io && flow_control && socket
|
|
889
|
+
|
|
890
|
+
DETACHED_MAX_FRAMES.times do
|
|
891
|
+
break if io.wait || !io.output.empty?
|
|
892
|
+
|
|
893
|
+
stream_id = io.ready.shift
|
|
894
|
+
break unless stream_id
|
|
895
|
+
|
|
896
|
+
io.ready_streams.delete(stream_id)
|
|
897
|
+
body = io.detached[stream_id]
|
|
898
|
+
next unless body
|
|
899
|
+
|
|
900
|
+
size = body.next_size
|
|
901
|
+
next unless size
|
|
902
|
+
|
|
903
|
+
if size.zero?
|
|
904
|
+
finish_http2_detached_body(id, stream_id, body)
|
|
905
|
+
next
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
granted = flow_control.try_acquire(stream_id, size)
|
|
909
|
+
next if granted.zero?
|
|
910
|
+
|
|
911
|
+
chunk = body.shift(granted)
|
|
912
|
+
parser = Http2Parser.new
|
|
913
|
+
queue_output(io, parser.build_frame(:data, 0, stream_id, chunk))
|
|
914
|
+
flush_output(id, io)
|
|
915
|
+
mark_http2_detached_body_ready(id, stream_id) if body.next_size
|
|
916
|
+
rescue Http2::StreamClosedError
|
|
917
|
+
remove_http2_detached_body(id, stream_id, :cancelled)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
if !io.wait && io.output.empty? && !io.ready.empty?
|
|
921
|
+
@io_queue << [:flush_http2, id]
|
|
922
|
+
@selector.wakeup rescue nil
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# Finishes an HTTP/2 detached stream with DATA or trailing HEADERS.
|
|
927
|
+
#
|
|
928
|
+
# @rbs (Integer id, Integer stream_id, DetachedBody body) -> void
|
|
929
|
+
def finish_http2_detached_body(id, stream_id, body)
|
|
930
|
+
parser = Http2Parser.new
|
|
931
|
+
frame = if body.trailers.empty?
|
|
932
|
+
parser.build_frame(:data, Http2::FLAG_END_STREAM, stream_id, nil)
|
|
933
|
+
else
|
|
934
|
+
encoded = parser.encode_response_trailers(body.trailers)
|
|
935
|
+
parser.build_frame(:headers, Http2::FLAG_END_STREAM | Http2::FLAG_END_HEADERS, stream_id, encoded)
|
|
936
|
+
end
|
|
937
|
+
io = @id_to_io[id]
|
|
938
|
+
queue_output(io, frame)
|
|
939
|
+
flush_output(id, io)
|
|
940
|
+
remove_http2_detached_body(id, stream_id, :closed)
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# Removes an HTTP/2 detached stream and schedules its close callback.
|
|
944
|
+
#
|
|
945
|
+
# @rbs (Integer id, Integer stream_id, Symbol reason) -> void
|
|
946
|
+
def remove_http2_detached_body(id, stream_id, reason)
|
|
947
|
+
io = @id_to_io[id]
|
|
948
|
+
body = io&.detached&.delete(stream_id)
|
|
949
|
+
return unless body
|
|
950
|
+
|
|
951
|
+
io.ready_streams.delete(stream_id)
|
|
952
|
+
body.finish(reason)
|
|
953
|
+
detached_body_removed
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
# Cancels every detached response during worker shutdown.
|
|
957
|
+
#
|
|
958
|
+
# @rbs () -> void
|
|
959
|
+
def cancel_detached_bodies
|
|
960
|
+
@id_to_io.to_a.each do |id, io|
|
|
961
|
+
if io.is_a?(Http1IO)
|
|
962
|
+
remove_http1_detached_body(id, :shutdown)
|
|
963
|
+
else
|
|
964
|
+
io.detached.each_key.to_a.each do |stream_id|
|
|
965
|
+
remove_http2_detached_body(id, stream_id, :shutdown)
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
# Records one detached stream finishing.
|
|
972
|
+
#
|
|
973
|
+
# @rbs () -> void
|
|
974
|
+
def detached_body_removed
|
|
975
|
+
remaining = nil
|
|
976
|
+
@detached_count.swap do |count|
|
|
977
|
+
remaining = count - 1
|
|
978
|
+
remaining
|
|
979
|
+
end
|
|
980
|
+
@detached_drained.broadcast if remaining.zero?
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
# Closes a connection once its queued frames are written.
|
|
984
|
+
#
|
|
985
|
+
# @param id [Integer] unique client identifier
|
|
986
|
+
# @return [void]
|
|
987
|
+
#
|
|
988
|
+
# @rbs (Integer id) -> void
|
|
989
|
+
def close_after_writes(id)
|
|
990
|
+
io = @id_to_io[id]
|
|
991
|
+
return unless io
|
|
992
|
+
|
|
993
|
+
if io.empty?
|
|
994
|
+
remove_connection(id)
|
|
995
|
+
else
|
|
996
|
+
io.closing = true
|
|
997
|
+
end
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
# Immediately closes a connection and drops its reactor state.
|
|
1001
|
+
#
|
|
1002
|
+
# @param id [Integer] unique client identifier
|
|
1003
|
+
# @return [void]
|
|
1004
|
+
#
|
|
1005
|
+
# @rbs (Integer id) -> void
|
|
1006
|
+
def remove_connection(id)
|
|
1007
|
+
socket = @id_to_socket[id]
|
|
1008
|
+
cleanup(socket) if socket
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
# Adds bytes to one connection and writes as much as the socket accepts.
|
|
1012
|
+
#
|
|
1013
|
+
# @param io [ConnectionIO] connection I/O state
|
|
1014
|
+
# @param data [String] serialized response bytes
|
|
1015
|
+
# @return [void]
|
|
1016
|
+
#
|
|
1017
|
+
# @rbs (ConnectionIO? io, String data) -> void
|
|
1018
|
+
def queue_output(io, data)
|
|
1019
|
+
return unless io && !io.closing
|
|
1020
|
+
|
|
1021
|
+
if io.output.empty?
|
|
1022
|
+
io.output << data
|
|
1023
|
+
else
|
|
1024
|
+
io.output[-1] << data
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
# Drains one connection's queued response bytes without blocking.
|
|
1029
|
+
#
|
|
1030
|
+
# @param id [Integer] unique connection identifier
|
|
1031
|
+
# @param io [ConnectionIO] connection I/O state
|
|
1032
|
+
# @return [void]
|
|
1033
|
+
#
|
|
1034
|
+
# @rbs (Integer id, ConnectionIO? io) -> void
|
|
1035
|
+
def flush_output(id, io)
|
|
1036
|
+
socket = @id_to_socket[id]
|
|
1037
|
+
return unless socket && io
|
|
1038
|
+
|
|
1039
|
+
while (data = io.output.first)
|
|
1040
|
+
chunk = io.offset.zero? ? data : data.byteslice(io.offset..-1)
|
|
1041
|
+
|
|
1042
|
+
begin
|
|
1043
|
+
written = socket.write_nonblock(chunk)
|
|
1044
|
+
rescue IO::WaitReadable
|
|
1045
|
+
wait_for_write(id, io, :read)
|
|
1046
|
+
return
|
|
1047
|
+
rescue IO::WaitWritable
|
|
1048
|
+
wait_for_write(id, io, :write)
|
|
1049
|
+
return
|
|
1050
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
1051
|
+
remove_connection(id)
|
|
1052
|
+
return
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
if written.zero?
|
|
1056
|
+
wait_for_write(id, io, :write)
|
|
1057
|
+
return
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
clear_write_timeout(io)
|
|
1061
|
+
io.offset += written
|
|
1062
|
+
if io.offset == data.bytesize
|
|
1063
|
+
io.output.shift
|
|
1064
|
+
io.offset = 0
|
|
1065
|
+
end
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
io.wait = nil
|
|
1069
|
+
if io.is_a?(Http1IO) && io.finishing
|
|
1070
|
+
finish_http1_detached_body(id, io)
|
|
1071
|
+
elsif io.closing
|
|
1072
|
+
remove_connection(id)
|
|
1073
|
+
else
|
|
1074
|
+
update_monitor(id, io)
|
|
1075
|
+
end
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
# Registers the readiness needed to continue a partial socket write.
|
|
1079
|
+
#
|
|
1080
|
+
# @param id [Integer] unique connection identifier
|
|
1081
|
+
# @param io [ConnectionIO] connection I/O state
|
|
1082
|
+
# @param readiness [Symbol] `:read` or `:write`
|
|
1083
|
+
# @return [void]
|
|
1084
|
+
#
|
|
1085
|
+
# @rbs (Integer id, ConnectionIO io, Symbol readiness) -> void
|
|
1086
|
+
def wait_for_write(id, io, readiness)
|
|
1087
|
+
io.wait = readiness
|
|
1088
|
+
track_write_timeout(id, io)
|
|
1089
|
+
update_monitor(id, io)
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# Updates selector interest for one reactor-owned connection.
|
|
1093
|
+
#
|
|
1094
|
+
# @param id [Integer] unique connection identifier
|
|
1095
|
+
# @param io [ConnectionIO] connection I/O state
|
|
1096
|
+
# @return [void]
|
|
1097
|
+
#
|
|
1098
|
+
# @rbs (Integer id, ConnectionIO io) -> void
|
|
1099
|
+
def update_monitor(id, io)
|
|
1100
|
+
socket = @id_to_socket[id]
|
|
1101
|
+
return unless socket && io
|
|
1102
|
+
|
|
1103
|
+
read = io.reading || io.wait == :read
|
|
1104
|
+
write = io.wait == :write
|
|
1105
|
+
interests = if read && write
|
|
1106
|
+
:rw
|
|
1107
|
+
elsif read
|
|
1108
|
+
:r
|
|
1109
|
+
elsif write
|
|
1110
|
+
:w
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
if io.monitor
|
|
1114
|
+
io.monitor.interests = interests
|
|
1115
|
+
elsif interests
|
|
1116
|
+
io.monitor = @selector.register(socket, interests)
|
|
1117
|
+
io.monitor.value = socket
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
# Starts or refreshes one connection's non-blocking write deadline.
|
|
1122
|
+
#
|
|
1123
|
+
# @param id [Integer] unique connection identifier
|
|
1124
|
+
# @param io [ConnectionIO] connection I/O state
|
|
1125
|
+
# @return [void]
|
|
1126
|
+
#
|
|
1127
|
+
# @rbs (Integer id, ConnectionIO io) -> void
|
|
1128
|
+
def track_write_timeout(id, io)
|
|
1129
|
+
clear_write_timeout(io)
|
|
1130
|
+
client = TimeoutClient.new({id: id, write: true})
|
|
1131
|
+
client.timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @write_timeout
|
|
1132
|
+
@timeouts << client
|
|
1133
|
+
io.timeout = client
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Removes one connection's non-blocking write deadline.
|
|
1137
|
+
#
|
|
1138
|
+
# @param io [ConnectionIO, nil] connection I/O state
|
|
1139
|
+
# @return [void]
|
|
1140
|
+
#
|
|
1141
|
+
# @rbs (ConnectionIO? io) -> void
|
|
1142
|
+
def clear_write_timeout(io)
|
|
1143
|
+
client = io&.timeout
|
|
1144
|
+
@timeouts.delete!(client) if client
|
|
1145
|
+
io.timeout = nil if io
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# Removes a socket from selector tracking.
|
|
1149
|
+
#
|
|
1150
|
+
# @param socket [TCPSocket] socket to deregister
|
|
1151
|
+
# @param io [ConnectionIO, nil] connection I/O state
|
|
1152
|
+
# @return [void]
|
|
1153
|
+
#
|
|
1154
|
+
# @rbs (TCPSocket socket, ConnectionIO? io) -> void
|
|
1155
|
+
def deregister(socket, io)
|
|
1156
|
+
return unless io&.monitor || @selector.registered?(socket)
|
|
1157
|
+
|
|
1158
|
+
@selector.deregister(socket)
|
|
1159
|
+
io.monitor = nil if io
|
|
1160
|
+
rescue IOError
|
|
1161
|
+
end
|
|
1162
|
+
|
|
365
1163
|
# Registers a socket with the NIO selector and sets up timeout tracking.
|
|
366
1164
|
#
|
|
367
1165
|
# @param socket [TCPSocket] the socket to register
|
|
@@ -369,22 +1167,146 @@ module Raptor
|
|
|
369
1167
|
#
|
|
370
1168
|
# @rbs (TCPSocket socket) -> void
|
|
371
1169
|
def register(socket)
|
|
372
|
-
@selector.register(socket, :r).value = socket
|
|
373
|
-
|
|
374
1170
|
state = @socket_to_state[socket]
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1171
|
+
return unless state
|
|
1172
|
+
|
|
1173
|
+
if state[:protocol] == :http2
|
|
1174
|
+
io = @id_to_io[state[:id]]
|
|
1175
|
+
io.reading = true
|
|
1176
|
+
update_monitor(state[:id], io)
|
|
1177
|
+
else
|
|
1178
|
+
@selector.register(socket, :r).value = socket
|
|
1179
|
+
end
|
|
1180
|
+
|
|
1181
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1182
|
+
timeout_at = if state[:protocol] == :http2
|
|
1183
|
+
if state[:http2_preface_received]
|
|
1184
|
+
keepalive = @id_to_http2_keepalive[state[:id]]
|
|
1185
|
+
keepalive && (keepalive[:deadline] || now + @http2_keepalive_interval)
|
|
1186
|
+
else
|
|
1187
|
+
now + @first_data_timeout
|
|
1188
|
+
end
|
|
1189
|
+
elsif state[:persisted]
|
|
1190
|
+
now + @persistent_data_timeout
|
|
378
1191
|
elsif first_data_received?(state)
|
|
379
|
-
@chunk_data_timeout
|
|
1192
|
+
now + @chunk_data_timeout
|
|
380
1193
|
else
|
|
381
|
-
@first_data_timeout
|
|
1194
|
+
now + @first_data_timeout
|
|
382
1195
|
end
|
|
383
|
-
|
|
1196
|
+
track_timeout(state, timeout_at) if timeout_at
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
# Adds a connection deadline to the timeout tree.
|
|
1200
|
+
#
|
|
1201
|
+
# @param state [Hash] client connection state
|
|
1202
|
+
# @param timeout_at [Float] absolute monotonic expiry time
|
|
1203
|
+
# @return [void]
|
|
1204
|
+
#
|
|
1205
|
+
# @rbs (Hash[Symbol, untyped] state, Float timeout_at) -> void
|
|
1206
|
+
def track_timeout(state, timeout_at)
|
|
1207
|
+
client = TimeoutClient.new(state)
|
|
1208
|
+
client.timeout_at = timeout_at
|
|
384
1209
|
@timeouts << client
|
|
385
1210
|
@id_to_timeout[state[:id]] = client
|
|
386
1211
|
end
|
|
387
1212
|
|
|
1213
|
+
# Handles an expired connection deadline, sending an HTTP/2 keepalive
|
|
1214
|
+
# probe before closing the connection when its acknowledgement is late.
|
|
1215
|
+
#
|
|
1216
|
+
# @param client [TimeoutClient] expired client
|
|
1217
|
+
# @param now [Float] current monotonic timestamp
|
|
1218
|
+
# @return [void]
|
|
1219
|
+
#
|
|
1220
|
+
# @rbs (TimeoutClient client, Float now) -> void
|
|
1221
|
+
def handle_timeout(client, now)
|
|
1222
|
+
state = client.client_data
|
|
1223
|
+
id = state[:id]
|
|
1224
|
+
socket = @id_to_socket[id]
|
|
1225
|
+
return unless socket
|
|
1226
|
+
|
|
1227
|
+
keepalive = @id_to_http2_keepalive[id] if state[:http2_preface_received]
|
|
1228
|
+
if keepalive && !keepalive[:deadline]
|
|
1229
|
+
keepalive[:deadline] = now + @http2_keepalive_timeout
|
|
1230
|
+
@id_to_writer[id].write_frames(socket, [keepalive[:frame]])
|
|
1231
|
+
track_timeout(state, keepalive[:deadline])
|
|
1232
|
+
return
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
socket.write(TIMEOUT_RESPONSE) rescue nil unless state[:protocol] == :http2
|
|
1236
|
+
cleanup(socket)
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
# Dispatches readable and writable events without allowing application
|
|
1240
|
+
# threads to take ownership of HTTP/2 sockets.
|
|
1241
|
+
#
|
|
1242
|
+
# @param monitor [NIO::Monitor] ready selector monitor
|
|
1243
|
+
# @return [void]
|
|
1244
|
+
#
|
|
1245
|
+
# @rbs (NIO::Monitor monitor) -> void
|
|
1246
|
+
def handle_monitor(monitor)
|
|
1247
|
+
socket = monitor.value
|
|
1248
|
+
state = @socket_to_state[socket]
|
|
1249
|
+
return unless state
|
|
1250
|
+
|
|
1251
|
+
if state[:detached]
|
|
1252
|
+
handle_http1_detached_monitor(monitor, state[:id])
|
|
1253
|
+
return
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
unless state[:protocol] == :http2
|
|
1257
|
+
wakeup!(socket)
|
|
1258
|
+
return
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
id = state[:id]
|
|
1262
|
+
io = @id_to_io[id]
|
|
1263
|
+
return unless io
|
|
1264
|
+
|
|
1265
|
+
wait = io.wait
|
|
1266
|
+
if (wait == :read && monitor.readable?) || (wait == :write && monitor.writable?)
|
|
1267
|
+
flush_output(id, io)
|
|
1268
|
+
return unless @id_to_socket.key?(id)
|
|
1269
|
+
return if io.wait == :read
|
|
1270
|
+
flush_http2_detached_bodies(id) unless io.detached.empty?
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
return unless io.reading && monitor.readable?
|
|
1274
|
+
|
|
1275
|
+
io.reading = false
|
|
1276
|
+
update_monitor(id, io)
|
|
1277
|
+
to_client = @id_to_timeout.delete(id)
|
|
1278
|
+
@timeouts.delete!(to_client) if to_client
|
|
1279
|
+
read_and_queue_for_parse(socket, state)
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
# Handles readiness for an HTTP/1.x detached response.
|
|
1283
|
+
#
|
|
1284
|
+
# @rbs (NIO::Monitor monitor, Integer id) -> void
|
|
1285
|
+
def handle_http1_detached_monitor(monitor, id)
|
|
1286
|
+
socket = monitor.value
|
|
1287
|
+
io = @id_to_io[id]
|
|
1288
|
+
return unless io.is_a?(Http1IO)
|
|
1289
|
+
|
|
1290
|
+
wait = io.wait
|
|
1291
|
+
if (wait == :read && monitor.readable?) || (wait == :write && monitor.writable?)
|
|
1292
|
+
flush_output(id, io)
|
|
1293
|
+
return unless @id_to_io.key?(id)
|
|
1294
|
+
|
|
1295
|
+
flush_http1_detached_body(id) unless io.wait
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
return unless io.reading && monitor.readable?
|
|
1299
|
+
|
|
1300
|
+
io.input << socket.read_nonblock(CHUNK_SIZE)
|
|
1301
|
+
io.reading = false
|
|
1302
|
+
update_monitor(id, io)
|
|
1303
|
+
rescue IO::WaitReadable
|
|
1304
|
+
rescue IO::WaitWritable
|
|
1305
|
+
wait_for_write(id, io, :write)
|
|
1306
|
+
rescue EOFError, IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
1307
|
+
remove_connection(id)
|
|
1308
|
+
end
|
|
1309
|
+
|
|
388
1310
|
# Handles socket wakeup by deregistering and queuing for processing.
|
|
389
1311
|
#
|
|
390
1312
|
# @param socket [TCPSocket] the socket that became ready
|
|
@@ -395,7 +1317,7 @@ module Raptor
|
|
|
395
1317
|
@selector.deregister(socket)
|
|
396
1318
|
state = @socket_to_state[socket]
|
|
397
1319
|
to_client = @id_to_timeout.delete(state[:id])
|
|
398
|
-
@timeouts.delete!(to_client)
|
|
1320
|
+
@timeouts.delete!(to_client) if to_client
|
|
399
1321
|
read_and_queue_for_parse(socket, state)
|
|
400
1322
|
end
|
|
401
1323
|
|
|
@@ -439,10 +1361,30 @@ module Raptor
|
|
|
439
1361
|
# @rbs (TCPSocket socket) -> void
|
|
440
1362
|
def cleanup(socket)
|
|
441
1363
|
state = @socket_to_state.delete(socket)
|
|
1364
|
+
io = @id_to_io.delete(state[:id])
|
|
1365
|
+
deregister(socket, io)
|
|
442
1366
|
@id_to_socket.delete(state[:id])
|
|
443
1367
|
@id_to_writer.delete(state[:id])
|
|
444
|
-
@id_to_flow_control.delete(state[:id])
|
|
445
|
-
|
|
1368
|
+
@id_to_flow_control.delete(state[:id])&.close
|
|
1369
|
+
@id_to_http2_last_stream.delete(state[:id])
|
|
1370
|
+
@id_to_http2_drain_stream.delete(state[:id])
|
|
1371
|
+
@id_to_http2_keepalive.delete(state[:id])
|
|
1372
|
+
if io.is_a?(Http1IO)
|
|
1373
|
+
if io.body
|
|
1374
|
+
io.body.finish(:connection_closed)
|
|
1375
|
+
io.body = nil
|
|
1376
|
+
detached_body_removed
|
|
1377
|
+
end
|
|
1378
|
+
elsif io
|
|
1379
|
+
io.detached.each_value do |body|
|
|
1380
|
+
body.finish(:connection_closed)
|
|
1381
|
+
detached_body_removed
|
|
1382
|
+
end
|
|
1383
|
+
end
|
|
1384
|
+
client = @id_to_timeout.delete(state[:id])
|
|
1385
|
+
@timeouts.delete!(client) if client
|
|
1386
|
+
clear_write_timeout(io)
|
|
1387
|
+
socket.close rescue nil
|
|
446
1388
|
end
|
|
447
1389
|
|
|
448
1390
|
# Returns true when the request has been fully parsed.
|