raptor 0.3.0 → 0.5.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/.mise.toml +2 -0
- data/Brewfile +2 -0
- data/CHANGELOG.md +21 -0
- data/README.md +28 -25
- data/ext/raptor_http2/raptor_http2.c +1 -0
- data/lib/rackup/handler/raptor.rb +20 -21
- data/lib/raptor/cli.rb +46 -14
- data/lib/raptor/cluster.rb +142 -64
- data/lib/raptor/http2.rb +324 -42
- data/lib/raptor/log.rb +55 -0
- data/lib/raptor/reactor.rb +89 -53
- data/lib/raptor/request.rb +106 -61
- data/lib/raptor/server.rb +125 -51
- data/lib/raptor/stats.rb +30 -26
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cli.rbs +15 -1
- data/sig/generated/raptor/cluster.rbs +70 -38
- data/sig/generated/raptor/http2.rbs +126 -6
- data/sig/generated/raptor/log.rbs +41 -0
- data/sig/generated/raptor/reactor.rbs +44 -25
- data/sig/generated/raptor/request.rbs +36 -22
- data/sig/generated/raptor/server.rbs +63 -26
- data/sig/generated/raptor/stats.rbs +24 -20
- metadata +5 -1
data/lib/raptor/http2.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "stringio"
|
|
|
6
6
|
require "atomic-ruby/atom"
|
|
7
7
|
require "rack"
|
|
8
8
|
|
|
9
|
+
require_relative "request"
|
|
9
10
|
require_relative "raptor_http2"
|
|
10
11
|
|
|
11
12
|
module Raptor
|
|
@@ -68,17 +69,171 @@ module Raptor
|
|
|
68
69
|
break if pending.empty?
|
|
69
70
|
|
|
70
71
|
pending.each do |frame|
|
|
71
|
-
|
|
72
|
+
Request.socket_write(socket, frame) rescue nil
|
|
72
73
|
end
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
# Per-connection outbound flow-control accounting.
|
|
79
|
+
#
|
|
80
|
+
# Tracks the peer's connection-level and per-stream receive windows so
|
|
81
|
+
# outbound DATA frames respect RFC 7540 §5.2. Threads dispatching stream
|
|
82
|
+
# responses call `acquire` to reserve send capacity; threads applying
|
|
83
|
+
# inbound WINDOW_UPDATE or SETTINGS frames call the mutating methods to
|
|
84
|
+
# replenish it. The connection window and per-stream windows live in
|
|
85
|
+
# separate `Atom`s so the common fast path skips per-stream tracking.
|
|
86
|
+
#
|
|
87
|
+
class FlowControl
|
|
88
|
+
ACQUIRE_POLL_INTERVAL = 0.001
|
|
89
|
+
|
|
90
|
+
# @rbs @connection_window: Atom
|
|
91
|
+
# @rbs @stream_windows: Atom
|
|
92
|
+
# @rbs @initial_stream_window: Atom
|
|
93
|
+
|
|
94
|
+
# Creates a new FlowControl with the spec-default windows.
|
|
95
|
+
#
|
|
96
|
+
# @rbs () -> void
|
|
97
|
+
def initialize
|
|
98
|
+
@connection_window = Atom.new(DEFAULT_WINDOW_SIZE)
|
|
99
|
+
@stream_windows = Atom.new({})
|
|
100
|
+
@initial_stream_window = Atom.new(DEFAULT_WINDOW_SIZE)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Reserves outbound capacity on the given stream, polling until at
|
|
104
|
+
# least one byte is available on both the connection and stream
|
|
105
|
+
# windows. The returned size is capped at `MAX_FRAME_SIZE`.
|
|
106
|
+
#
|
|
107
|
+
# When `end_stream` is true, `max_bytes` fits within the peer's
|
|
108
|
+
# initial stream window, and no per-stream override has been
|
|
109
|
+
# recorded, only the connection window is consulted. The stream
|
|
110
|
+
# closes on this frame, so its remaining send window will not be
|
|
111
|
+
# consulted again and need not be tracked.
|
|
112
|
+
#
|
|
113
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
114
|
+
# @param max_bytes [Integer] the largest size the caller would like to send
|
|
115
|
+
# @param end_stream [Boolean] true when this is the final frame on the stream
|
|
116
|
+
# @return [Integer] the number of bytes the caller may now send
|
|
117
|
+
#
|
|
118
|
+
# @rbs (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
|
|
119
|
+
def acquire(stream_id, max_bytes, end_stream: false)
|
|
120
|
+
initial = @initial_stream_window.value
|
|
121
|
+
capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
|
|
122
|
+
|
|
123
|
+
if end_stream && capped <= initial && !@stream_windows.value.key?(stream_id)
|
|
124
|
+
loop do
|
|
125
|
+
granted = 0
|
|
126
|
+
@connection_window.swap do |window|
|
|
127
|
+
granted = window > capped ? capped : window
|
|
128
|
+
granted > 0 ? window - granted : window
|
|
129
|
+
end
|
|
130
|
+
return granted if granted > 0
|
|
131
|
+
|
|
132
|
+
sleep ACQUIRE_POLL_INTERVAL
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
loop do
|
|
137
|
+
stream_window = @stream_windows.value[stream_id] || initial
|
|
138
|
+
capped_full = capped < stream_window ? capped : stream_window
|
|
139
|
+
|
|
140
|
+
granted = 0
|
|
141
|
+
if capped_full > 0
|
|
142
|
+
@connection_window.swap do |window|
|
|
143
|
+
granted = window > capped_full ? capped_full : window
|
|
144
|
+
granted > 0 ? window - granted : window
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if granted > 0
|
|
149
|
+
@stream_windows.swap do |s|
|
|
150
|
+
current = s[stream_id] || initial
|
|
151
|
+
s.merge(stream_id => current - granted)
|
|
152
|
+
end
|
|
153
|
+
return granted
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sleep ACQUIRE_POLL_INTERVAL
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Increments the connection-level send window. Called when the peer
|
|
161
|
+
# sends a WINDOW_UPDATE on stream 0.
|
|
162
|
+
#
|
|
163
|
+
# @param increment [Integer] the byte count to add
|
|
164
|
+
# @return [void]
|
|
165
|
+
#
|
|
166
|
+
# @rbs (Integer increment) -> void
|
|
167
|
+
def add_connection_window(increment)
|
|
168
|
+
@connection_window.swap { |window| window + increment }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Increments the per-stream send window. Called when the peer sends
|
|
172
|
+
# a WINDOW_UPDATE on a specific stream.
|
|
173
|
+
#
|
|
174
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
175
|
+
# @param increment [Integer] the byte count to add
|
|
176
|
+
# @return [void]
|
|
177
|
+
#
|
|
178
|
+
# @rbs (Integer stream_id, Integer increment) -> void
|
|
179
|
+
def add_stream_window(stream_id, increment)
|
|
180
|
+
initial = @initial_stream_window.value
|
|
181
|
+
@stream_windows.swap do |s|
|
|
182
|
+
current = s[stream_id] || initial
|
|
183
|
+
s.merge(stream_id => current + increment)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
|
|
188
|
+
# existing stream window by the delta as required by RFC 7540 §6.9.2.
|
|
189
|
+
#
|
|
190
|
+
# @param new_size [Integer] the peer's new initial window size
|
|
191
|
+
# @return [void]
|
|
192
|
+
#
|
|
193
|
+
# @rbs (Integer new_size) -> void
|
|
194
|
+
def set_initial_stream_window(new_size)
|
|
195
|
+
old = @initial_stream_window.value
|
|
196
|
+
@initial_stream_window.swap { new_size }
|
|
197
|
+
delta = new_size - old
|
|
198
|
+
return if delta.zero?
|
|
199
|
+
|
|
200
|
+
@stream_windows.swap do |s|
|
|
201
|
+
s.transform_values { |size| size + delta }
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Discards any per-stream tracking for the given stream. Called
|
|
206
|
+
# after a stream closes so `@stream_windows` does not grow without
|
|
207
|
+
# bound across the lifetime of a connection.
|
|
208
|
+
#
|
|
209
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
210
|
+
# @return [void]
|
|
211
|
+
#
|
|
212
|
+
# @rbs (Integer stream_id) -> void
|
|
213
|
+
def discard_stream(stream_id)
|
|
214
|
+
return unless @stream_windows.value.key?(stream_id)
|
|
215
|
+
|
|
216
|
+
@stream_windows.swap do |s|
|
|
217
|
+
next s unless s.key?(stream_id)
|
|
218
|
+
|
|
219
|
+
new = s.dup
|
|
220
|
+
new.delete(stream_id)
|
|
221
|
+
new
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
77
226
|
FLAG_END_STREAM = 0x1
|
|
78
227
|
FLAG_END_HEADERS = 0x4
|
|
79
228
|
FLAG_ACK = 0x1
|
|
80
229
|
FLAG_PRIORITY = 0x20
|
|
81
230
|
|
|
231
|
+
ERROR_NO_ERROR = 0x0
|
|
232
|
+
ERROR_PROTOCOL_ERROR = 0x1
|
|
233
|
+
|
|
234
|
+
DEFAULT_WINDOW_SIZE = 65_535
|
|
235
|
+
MAX_FRAME_SIZE = 16_384
|
|
236
|
+
|
|
82
237
|
SERVER_PROTOCOL = "HTTP/2"
|
|
83
238
|
RACK_HEADER_PREFIX = "rack."
|
|
84
239
|
HOP_BY_HOP_HEADERS = Set.new(%w[connection transfer-encoding keep-alive upgrade proxy-connection]).freeze
|
|
@@ -110,7 +265,7 @@ module Raptor
|
|
|
110
265
|
parser = Http2Parser.new
|
|
111
266
|
settings_payload = parser.build_settings(
|
|
112
267
|
max_concurrent_streams: 100,
|
|
113
|
-
initial_window_size:
|
|
268
|
+
initial_window_size: DEFAULT_WINDOW_SIZE
|
|
114
269
|
)
|
|
115
270
|
parser.build_frame(:settings, 0, 0, settings_payload)
|
|
116
271
|
end
|
|
@@ -132,15 +287,20 @@ module Raptor
|
|
|
132
287
|
streams = data[:http2_streams] ? data[:http2_streams].dup : {}
|
|
133
288
|
outgoing_frames = []
|
|
134
289
|
completed_requests = []
|
|
135
|
-
|
|
290
|
+
window_updates = []
|
|
291
|
+
peer_initial_window_size = nil
|
|
292
|
+
connection_window = data[:http2_window] || DEFAULT_WINDOW_SIZE
|
|
136
293
|
preface_received = data[:http2_preface_received] || false
|
|
294
|
+
last_client_stream_id = data[:http2_last_client_stream_id] || 0
|
|
295
|
+
pending_headers = data[:http2_pending_headers]
|
|
296
|
+
goaway_error = nil
|
|
137
297
|
|
|
138
298
|
unless preface_received
|
|
139
299
|
if buffer.bytesize >= 24 && buffer.byteslice(0, 24) == Http2Parser.connection_preface
|
|
140
300
|
buffer = buffer.byteslice(24..-1) || ""
|
|
141
301
|
preface_received = true
|
|
142
302
|
else
|
|
143
|
-
return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
|
|
303
|
+
return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, false)
|
|
144
304
|
end
|
|
145
305
|
end
|
|
146
306
|
|
|
@@ -151,9 +311,16 @@ module Raptor
|
|
|
151
311
|
frame, consumed = parsed
|
|
152
312
|
buffer = buffer.byteslice(consumed..-1) || ""
|
|
153
313
|
|
|
314
|
+
if pending_headers && frame[:type] != :continuation
|
|
315
|
+
goaway_error = ERROR_PROTOCOL_ERROR
|
|
316
|
+
break
|
|
317
|
+
end
|
|
318
|
+
|
|
154
319
|
case frame[:type]
|
|
155
320
|
when :settings
|
|
156
321
|
if (frame[:flags] & FLAG_ACK).zero?
|
|
322
|
+
parsed_settings = parser.parse_settings(frame[:payload])
|
|
323
|
+
peer_initial_window_size = parsed_settings[:initial_window_size] if parsed_settings.key?(:initial_window_size)
|
|
157
324
|
outgoing_frames << parser.build_frame(:settings, FLAG_ACK, 0, nil)
|
|
158
325
|
end
|
|
159
326
|
|
|
@@ -161,37 +328,58 @@ module Raptor
|
|
|
161
328
|
stream_id = frame[:stream_id]
|
|
162
329
|
header_payload = frame[:payload]
|
|
163
330
|
|
|
331
|
+
unless streams.key?(stream_id)
|
|
332
|
+
if stream_id.even? || stream_id <= last_client_stream_id
|
|
333
|
+
goaway_error = ERROR_PROTOCOL_ERROR
|
|
334
|
+
break
|
|
335
|
+
end
|
|
336
|
+
last_client_stream_id = stream_id
|
|
337
|
+
end
|
|
338
|
+
|
|
164
339
|
if (frame[:flags] & FLAG_PRIORITY) != 0
|
|
165
340
|
header_payload = header_payload.byteslice(5..-1) || ""
|
|
166
341
|
end
|
|
167
342
|
|
|
168
|
-
|
|
169
|
-
stream = streams[stream_id] || {}
|
|
170
|
-
stream = stream.merge(headers: decoded_headers)
|
|
171
|
-
|
|
172
|
-
if (frame[:flags] & FLAG_END_STREAM) != 0
|
|
173
|
-
stream = stream.merge(end_stream: true)
|
|
174
|
-
completed_requests << {
|
|
175
|
-
stream_id: stream_id,
|
|
176
|
-
headers: decoded_headers,
|
|
177
|
-
body: stream[:body] || ""
|
|
178
|
-
}
|
|
343
|
+
end_stream = (frame[:flags] & FLAG_END_STREAM) != 0
|
|
179
344
|
|
|
180
|
-
|
|
345
|
+
if (frame[:flags] & FLAG_END_HEADERS) != 0
|
|
346
|
+
decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
|
|
347
|
+
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
|
|
181
348
|
else
|
|
182
|
-
|
|
349
|
+
pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
when :continuation
|
|
353
|
+
if pending_headers.nil? || frame[:stream_id] != pending_headers[:stream_id]
|
|
354
|
+
goaway_error = ERROR_PROTOCOL_ERROR
|
|
355
|
+
break
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload])
|
|
359
|
+
|
|
360
|
+
if (frame[:flags] & FLAG_END_HEADERS) != 0
|
|
361
|
+
stream_id = pending_headers[:stream_id]
|
|
362
|
+
decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table)
|
|
363
|
+
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
|
|
364
|
+
pending_headers = nil
|
|
183
365
|
end
|
|
184
366
|
|
|
185
367
|
when :data
|
|
186
368
|
stream_id = frame[:stream_id]
|
|
187
|
-
|
|
369
|
+
|
|
370
|
+
unless streams.key?(stream_id)
|
|
371
|
+
goaway_error = ERROR_PROTOCOL_ERROR
|
|
372
|
+
break
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
stream = streams[stream_id]
|
|
188
376
|
existing_body = stream[:body] || ""
|
|
189
377
|
stream = stream.merge(body: existing_body + frame[:payload])
|
|
190
378
|
|
|
191
379
|
if frame[:payload].bytesize.positive?
|
|
192
380
|
connection_window -= frame[:payload].bytesize
|
|
193
|
-
if connection_window <
|
|
194
|
-
increment =
|
|
381
|
+
if connection_window < DEFAULT_WINDOW_SIZE / 2
|
|
382
|
+
increment = DEFAULT_WINDOW_SIZE - connection_window
|
|
195
383
|
wu_payload = [increment].pack("N")
|
|
196
384
|
outgoing_frames << parser.build_frame(:window_update, 0, 0, wu_payload)
|
|
197
385
|
outgoing_frames << parser.build_frame(:window_update, 0, stream_id, wu_payload)
|
|
@@ -213,7 +401,8 @@ module Raptor
|
|
|
213
401
|
end
|
|
214
402
|
|
|
215
403
|
when :window_update
|
|
216
|
-
parser.parse_window_update(frame[:payload])
|
|
404
|
+
increment = parser.parse_window_update(frame[:payload])
|
|
405
|
+
window_updates << [frame[:stream_id], increment]
|
|
217
406
|
|
|
218
407
|
when :ping
|
|
219
408
|
if (frame[:flags] & FLAG_ACK).zero?
|
|
@@ -228,9 +417,45 @@ module Raptor
|
|
|
228
417
|
end
|
|
229
418
|
end
|
|
230
419
|
|
|
231
|
-
|
|
420
|
+
if goaway_error
|
|
421
|
+
goaway_payload = [last_client_stream_id, goaway_error].pack("NN")
|
|
422
|
+
outgoing_frames << parser.build_frame(:goaway, 0, 0, goaway_payload)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, !goaway_error.nil?)
|
|
232
426
|
end
|
|
233
427
|
|
|
428
|
+
# Merges a decoded header block into the stream's accumulated state,
|
|
429
|
+
# promoting the stream to `completed_requests` when END_STREAM is set.
|
|
430
|
+
#
|
|
431
|
+
# @param streams [Hash] current open-stream map
|
|
432
|
+
# @param completed_requests [Array<Hash>] accumulator of completed stream requests
|
|
433
|
+
# @param stream_id [Integer] the stream identifier
|
|
434
|
+
# @param decoded_headers [Array<Array(String, String)>] decoded header pairs
|
|
435
|
+
# @param end_stream [Boolean] whether the source frame had END_STREAM set
|
|
436
|
+
# @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
|
|
437
|
+
#
|
|
438
|
+
# @rbs (Hash[Integer, Hash[Symbol, untyped]] streams, Array[Hash[Symbol, untyped]] completed_requests, Integer stream_id, Array[[String, String]] decoded_headers, bool end_stream) -> [Hash[Integer, Hash[Symbol, untyped]], Array[Hash[Symbol, untyped]]]
|
|
439
|
+
def self.finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
|
|
440
|
+
stream = streams[stream_id] || {}
|
|
441
|
+
stream = stream.merge(headers: decoded_headers)
|
|
442
|
+
|
|
443
|
+
if end_stream
|
|
444
|
+
completed_requests << {
|
|
445
|
+
stream_id: stream_id,
|
|
446
|
+
headers: decoded_headers,
|
|
447
|
+
body: stream[:body] || ""
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
streams.delete(stream_id)
|
|
451
|
+
else
|
|
452
|
+
streams[stream_id] = stream
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
[streams, completed_requests]
|
|
456
|
+
end
|
|
457
|
+
private_class_method :finalize_headers
|
|
458
|
+
|
|
234
459
|
# Builds a frozen result hash from the current processing state.
|
|
235
460
|
#
|
|
236
461
|
# @param data [Hash] original connection state
|
|
@@ -239,13 +464,18 @@ module Raptor
|
|
|
239
464
|
# @param streams [Hash] updated stream states
|
|
240
465
|
# @param outgoing_frames [Array<String>] frames to write to the socket
|
|
241
466
|
# @param completed_requests [Array<Hash>] fully received stream requests
|
|
467
|
+
# @param window_updates [Array<Array(Integer, Integer)>] inbound WINDOW_UPDATE pairs as [stream_id, increment]
|
|
468
|
+
# @param peer_initial_window_size [Integer, nil] new SETTINGS_INITIAL_WINDOW_SIZE announced by the peer
|
|
242
469
|
# @param connection_window [Integer] current connection flow control window
|
|
243
470
|
# @param preface_received [Boolean] whether the connection preface has been received
|
|
471
|
+
# @param last_client_stream_id [Integer] highest client-initiated stream ID seen
|
|
472
|
+
# @param pending_headers [Hash, nil] in-progress HEADERS+CONTINUATION assembly
|
|
473
|
+
# @param close_connection [Boolean] whether the connection should be closed after writing outgoing frames
|
|
244
474
|
# @return [Hash] frozen result hash
|
|
245
475
|
#
|
|
246
|
-
# @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Integer connection_window, bool preface_received) -> Hash[Symbol, untyped]
|
|
247
|
-
def self.build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
|
|
248
|
-
|
|
476
|
+
# @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[[Integer, Integer]] window_updates, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
|
|
477
|
+
def self.build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, close_connection)
|
|
478
|
+
result = {
|
|
249
479
|
id: data[:id],
|
|
250
480
|
protocol: :http2,
|
|
251
481
|
buffer: buffer || "",
|
|
@@ -253,11 +483,17 @@ module Raptor
|
|
|
253
483
|
http2_streams: streams,
|
|
254
484
|
http2_window: connection_window,
|
|
255
485
|
http2_preface_received: preface_received,
|
|
486
|
+
http2_last_client_stream_id: last_client_stream_id,
|
|
487
|
+
http2_pending_headers: pending_headers,
|
|
256
488
|
outgoing_frames: outgoing_frames,
|
|
257
489
|
completed_requests: completed_requests,
|
|
490
|
+
close_connection: close_connection,
|
|
258
491
|
remote_addr: data[:remote_addr],
|
|
259
492
|
url_scheme: data[:url_scheme]
|
|
260
|
-
}
|
|
493
|
+
}
|
|
494
|
+
result[:window_updates] = window_updates unless window_updates.empty?
|
|
495
|
+
result[:peer_initial_window_size] = peer_initial_window_size if peer_initial_window_size
|
|
496
|
+
Ractor.make_shareable(result)
|
|
261
497
|
end
|
|
262
498
|
private_class_method :build_result
|
|
263
499
|
|
|
@@ -277,18 +513,28 @@ module Raptor
|
|
|
277
513
|
return unless socket
|
|
278
514
|
|
|
279
515
|
writer = reactor.writer_for(result[:id])
|
|
516
|
+
flow_control = reactor.flow_control_for(result[:id])
|
|
517
|
+
|
|
518
|
+
if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
|
|
519
|
+
apply_flow_control_updates(flow_control, result)
|
|
520
|
+
end
|
|
280
521
|
|
|
281
522
|
writer.write_frames(socket, result[:outgoing_frames])
|
|
282
523
|
|
|
524
|
+
if result[:close_connection]
|
|
525
|
+
reactor.close_connection(result[:id])
|
|
526
|
+
return
|
|
527
|
+
end
|
|
528
|
+
|
|
283
529
|
reactor.update_http2_state(result)
|
|
284
530
|
|
|
285
531
|
result[:completed_requests]&.each do |request|
|
|
286
532
|
stream_id = request[:stream_id]
|
|
287
|
-
remote_addr = result[:remote_addr] ||
|
|
533
|
+
remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
|
|
288
534
|
|
|
289
535
|
thread_pool << proc do
|
|
290
536
|
dispatch_stream_request(
|
|
291
|
-
socket, writer, stream_id,
|
|
537
|
+
socket, writer, flow_control, stream_id,
|
|
292
538
|
request[:headers], request[:body],
|
|
293
539
|
remote_addr: remote_addr
|
|
294
540
|
)
|
|
@@ -298,23 +544,46 @@ module Raptor
|
|
|
298
544
|
|
|
299
545
|
private
|
|
300
546
|
|
|
547
|
+
# Applies inbound flow-control updates from a parsed result to the
|
|
548
|
+
# connection's `FlowControl`.
|
|
549
|
+
#
|
|
550
|
+
# @param flow_control [FlowControl] the per-connection flow controller
|
|
551
|
+
# @param result [Hash] the parsed result from `process_frames`
|
|
552
|
+
# @return [void]
|
|
553
|
+
#
|
|
554
|
+
# @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
|
|
555
|
+
def apply_flow_control_updates(flow_control, result)
|
|
556
|
+
result[:window_updates]&.each do |stream_id, increment|
|
|
557
|
+
if stream_id.zero?
|
|
558
|
+
flow_control.add_connection_window(increment)
|
|
559
|
+
else
|
|
560
|
+
flow_control.add_stream_window(stream_id, increment)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
if (new_size = result[:peer_initial_window_size])
|
|
565
|
+
flow_control.set_initial_stream_window(new_size)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
301
569
|
# Dispatches a completed stream request to the Rack app and writes
|
|
302
570
|
# the response back as HTTP/2 frames.
|
|
303
571
|
#
|
|
304
572
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
305
573
|
# @param writer [Writer] lock-free frame writer for the connection
|
|
574
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
306
575
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
307
576
|
# @param headers [Array<Array(String, String)>] request headers
|
|
308
577
|
# @param body [String] request body
|
|
309
578
|
# @param remote_addr [String] the client IP address
|
|
310
579
|
# @return [void]
|
|
311
580
|
#
|
|
312
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
313
|
-
def dispatch_stream_request(socket, writer, stream_id, headers, body, remote_addr:)
|
|
581
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
582
|
+
def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
314
583
|
env = build_rack_env(headers, body, remote_addr: remote_addr)
|
|
315
584
|
status, response_headers, response_body = @app.call(env)
|
|
316
585
|
|
|
317
|
-
write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
|
|
586
|
+
write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
|
|
318
587
|
rescue => error
|
|
319
588
|
write_http2_error_response(socket, writer, stream_id)
|
|
320
589
|
|
|
@@ -325,20 +594,25 @@ module Raptor
|
|
|
325
594
|
end
|
|
326
595
|
ensure
|
|
327
596
|
response_body.close if response_body.respond_to?(:close)
|
|
597
|
+
flow_control.discard_stream(stream_id) if flow_control
|
|
328
598
|
end
|
|
329
599
|
|
|
330
600
|
# Writes a Rack response as HTTP/2 frames to the socket.
|
|
331
601
|
#
|
|
602
|
+
# DATA frames are partitioned through `flow_control` so each write fits
|
|
603
|
+
# within the peer's per-stream and connection windows.
|
|
604
|
+
#
|
|
332
605
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
333
606
|
# @param writer [Writer] lock-free frame writer for the connection
|
|
607
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
334
608
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
335
609
|
# @param status [Integer] HTTP status code
|
|
336
610
|
# @param headers [Hash] response headers from the Rack application
|
|
337
611
|
# @param body [Object] response body responding to each
|
|
338
612
|
# @return [void]
|
|
339
613
|
#
|
|
340
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
|
|
341
|
-
def write_http2_response(socket, writer, stream_id, status, headers, body)
|
|
614
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
|
|
615
|
+
def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
|
|
342
616
|
parser = Http2Parser.new
|
|
343
617
|
|
|
344
618
|
header_pairs = [[":status", status.to_s]]
|
|
@@ -358,16 +632,24 @@ module Raptor
|
|
|
358
632
|
body_chunks = []
|
|
359
633
|
body.each { |chunk| body_chunks << chunk unless chunk.empty? }
|
|
360
634
|
|
|
361
|
-
frames = []
|
|
362
635
|
if body_chunks.empty?
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
636
|
+
writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
|
|
637
|
+
return
|
|
638
|
+
end
|
|
366
639
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
640
|
+
frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
|
|
641
|
+
|
|
642
|
+
last_chunk_index = body_chunks.size - 1
|
|
643
|
+
body_chunks.each_with_index do |chunk, chunk_index|
|
|
644
|
+
offset = 0
|
|
645
|
+
while offset < chunk.bytesize
|
|
646
|
+
remaining = chunk.bytesize - offset
|
|
647
|
+
last_frame = chunk_index == last_chunk_index && remaining <= MAX_FRAME_SIZE
|
|
648
|
+
granted = flow_control.acquire(stream_id, remaining, end_stream: last_frame)
|
|
649
|
+
slice = offset == 0 && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted)
|
|
650
|
+
offset += granted
|
|
651
|
+
end_stream = chunk_index == last_chunk_index && offset == chunk.bytesize
|
|
652
|
+
frames << parser.build_frame(:data, end_stream ? FLAG_END_STREAM : 0, stream_id, slice)
|
|
371
653
|
end
|
|
372
654
|
end
|
|
373
655
|
|
|
@@ -468,7 +750,7 @@ module Raptor
|
|
|
468
750
|
env[Rack::SERVER_NAME] ||= host
|
|
469
751
|
env[Rack::SERVER_PORT] ||= port || @server_port.to_s
|
|
470
752
|
else
|
|
471
|
-
env[Rack::SERVER_NAME] ||=
|
|
753
|
+
env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
|
|
472
754
|
env[Rack::SERVER_PORT] ||= @server_port.to_s
|
|
473
755
|
end
|
|
474
756
|
end
|
data/lib/raptor/log.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Raptor
|
|
5
|
+
# Shared logging helpers. Every line is prefixed with
|
|
6
|
+
# `[Raptor <pid>|<ractor>|<thread>]` so output is identifiable
|
|
7
|
+
# and traceable to its source in a mixed log stream.
|
|
8
|
+
#
|
|
9
|
+
module Log
|
|
10
|
+
# Writes an informational message to stdout.
|
|
11
|
+
#
|
|
12
|
+
# @param message [String] the message to log
|
|
13
|
+
# @return [void]
|
|
14
|
+
#
|
|
15
|
+
# @rbs (String message) -> void
|
|
16
|
+
def self.info(message)
|
|
17
|
+
Kernel.puts "#{prefix} #{message}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Writes a warning to stderr.
|
|
21
|
+
#
|
|
22
|
+
# @param message [String] the message to log
|
|
23
|
+
# @return [void]
|
|
24
|
+
#
|
|
25
|
+
# @rbs (String message) -> void
|
|
26
|
+
def self.warn(message)
|
|
27
|
+
Kernel.warn "#{prefix} #{message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Logs a rescued exception to stderr. The full message (class,
|
|
31
|
+
# message, backtrace) is written on subsequent unprefixed lines.
|
|
32
|
+
#
|
|
33
|
+
# @param error [Exception] the rescued exception
|
|
34
|
+
# @return [void]
|
|
35
|
+
#
|
|
36
|
+
# @rbs (Exception error) -> void
|
|
37
|
+
def self.rescued_error(error)
|
|
38
|
+
Kernel.warn "#{prefix} rescued:"
|
|
39
|
+
Kernel.warn error.full_message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Builds the log line prefix from the current process, ractor,
|
|
43
|
+
# and thread. Unnamed ractors and threads are reported as `main`.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] the prefix
|
|
46
|
+
#
|
|
47
|
+
# @rbs () -> String
|
|
48
|
+
def self.prefix
|
|
49
|
+
ractor = Ractor.current.name || "main"
|
|
50
|
+
thread = Thread.current.name || "main"
|
|
51
|
+
"[Raptor #{Process.pid}|#{ractor}|#{thread}]"
|
|
52
|
+
end
|
|
53
|
+
private_class_method :prefix
|
|
54
|
+
end
|
|
55
|
+
end
|