raptor 0.3.0 → 0.4.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.
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,170 @@ module Raptor
68
69
  break if pending.empty?
69
70
 
70
71
  pending.each do |frame|
71
- socket.write(frame) rescue nil
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. State is held in a single `Atom` so updates use CAS.
85
+ #
86
+ class FlowControl
87
+ ACQUIRE_POLL_INTERVAL = 0.001
88
+
89
+ # @rbs @connection_window: Atom
90
+ # @rbs @stream_windows: Atom
91
+ # @rbs @initial_stream_window: Atom
92
+
93
+ # Creates a new FlowControl with the spec-default windows.
94
+ #
95
+ # @rbs () -> void
96
+ def initialize
97
+ @connection_window = Atom.new(DEFAULT_WINDOW_SIZE)
98
+ @stream_windows = Atom.new({})
99
+ @initial_stream_window = Atom.new(DEFAULT_WINDOW_SIZE)
100
+ end
101
+
102
+ # Reserves outbound capacity on the given stream, polling until at
103
+ # least one byte is available on both the connection and stream
104
+ # windows. The returned size is capped at `MAX_FRAME_SIZE`.
105
+ #
106
+ # When `end_stream` is true, `max_bytes` fits within the peer's
107
+ # initial stream window, and no per-stream override has been
108
+ # recorded, only the connection window is consulted. The stream
109
+ # closes on this frame, so its remaining send window will not be
110
+ # consulted again and need not be tracked.
111
+ #
112
+ # @param stream_id [Integer] the HTTP/2 stream identifier
113
+ # @param max_bytes [Integer] the largest size the caller would like to send
114
+ # @param end_stream [Boolean] true when this is the final frame on the stream
115
+ # @return [Integer] the number of bytes the caller may now send
116
+ #
117
+ # @rbs (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
118
+ def acquire(stream_id, max_bytes, end_stream: false)
119
+ initial = @initial_stream_window.value
120
+ capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
121
+
122
+ if end_stream && capped <= initial && !@stream_windows.value.key?(stream_id)
123
+ loop do
124
+ granted = 0
125
+ @connection_window.swap do |window|
126
+ granted = window > capped ? capped : window
127
+ granted > 0 ? window - granted : window
128
+ end
129
+ return granted if granted > 0
130
+
131
+ sleep ACQUIRE_POLL_INTERVAL
132
+ end
133
+ end
134
+
135
+ loop do
136
+ stream_window = @stream_windows.value[stream_id] || initial
137
+ capped_full = capped < stream_window ? capped : stream_window
138
+
139
+ granted = 0
140
+ if capped_full > 0
141
+ @connection_window.swap do |window|
142
+ granted = window > capped_full ? capped_full : window
143
+ granted > 0 ? window - granted : window
144
+ end
145
+ end
146
+
147
+ if granted > 0
148
+ @stream_windows.swap do |s|
149
+ current = s[stream_id] || initial
150
+ s.merge(stream_id => current - granted)
151
+ end
152
+ return granted
153
+ end
154
+
155
+ sleep ACQUIRE_POLL_INTERVAL
156
+ end
157
+ end
158
+
159
+ # Increments the connection-level send window. Called when the peer
160
+ # sends a WINDOW_UPDATE on stream 0.
161
+ #
162
+ # @param increment [Integer] the byte count to add
163
+ # @return [void]
164
+ #
165
+ # @rbs (Integer increment) -> void
166
+ def add_connection_window(increment)
167
+ @connection_window.swap { |window| window + increment }
168
+ end
169
+
170
+ # Increments the per-stream send window. Called when the peer sends
171
+ # a WINDOW_UPDATE on a specific stream.
172
+ #
173
+ # @param stream_id [Integer] the HTTP/2 stream identifier
174
+ # @param increment [Integer] the byte count to add
175
+ # @return [void]
176
+ #
177
+ # @rbs (Integer stream_id, Integer increment) -> void
178
+ def add_stream_window(stream_id, increment)
179
+ initial = @initial_stream_window.value
180
+ @stream_windows.swap do |s|
181
+ current = s[stream_id] || initial
182
+ s.merge(stream_id => current + increment)
183
+ end
184
+ end
185
+
186
+ # Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
187
+ # existing stream window by the delta as required by RFC 7540 §6.9.2.
188
+ #
189
+ # @param new_size [Integer] the peer's new initial window size
190
+ # @return [void]
191
+ #
192
+ # @rbs (Integer new_size) -> void
193
+ def set_initial_stream_window(new_size)
194
+ old = @initial_stream_window.value
195
+ @initial_stream_window.swap { new_size }
196
+ delta = new_size - old
197
+ return if delta.zero?
198
+
199
+ @stream_windows.swap do |s|
200
+ s.transform_values { |size| size + delta }
201
+ end
202
+ end
203
+
204
+ # Discards any per-stream tracking for the given stream. Called
205
+ # after a stream closes so `@stream_windows` does not grow without
206
+ # bound across the lifetime of a connection.
207
+ #
208
+ # @param stream_id [Integer] the HTTP/2 stream identifier
209
+ # @return [void]
210
+ #
211
+ # @rbs (Integer stream_id) -> void
212
+ def discard_stream(stream_id)
213
+ return unless @stream_windows.value.key?(stream_id)
214
+
215
+ @stream_windows.swap do |s|
216
+ next s unless s.key?(stream_id)
217
+
218
+ new = s.dup
219
+ new.delete(stream_id)
220
+ new
221
+ end
222
+ end
223
+ end
224
+
77
225
  FLAG_END_STREAM = 0x1
78
226
  FLAG_END_HEADERS = 0x4
79
227
  FLAG_ACK = 0x1
80
228
  FLAG_PRIORITY = 0x20
81
229
 
230
+ ERROR_NO_ERROR = 0x0
231
+ ERROR_PROTOCOL_ERROR = 0x1
232
+
233
+ DEFAULT_WINDOW_SIZE = 65_535
234
+ MAX_FRAME_SIZE = 16_384
235
+
82
236
  SERVER_PROTOCOL = "HTTP/2"
83
237
  RACK_HEADER_PREFIX = "rack."
84
238
  HOP_BY_HOP_HEADERS = Set.new(%w[connection transfer-encoding keep-alive upgrade proxy-connection]).freeze
@@ -110,7 +264,7 @@ module Raptor
110
264
  parser = Http2Parser.new
111
265
  settings_payload = parser.build_settings(
112
266
  max_concurrent_streams: 100,
113
- initial_window_size: 65_535
267
+ initial_window_size: DEFAULT_WINDOW_SIZE
114
268
  )
115
269
  parser.build_frame(:settings, 0, 0, settings_payload)
116
270
  end
@@ -132,15 +286,20 @@ module Raptor
132
286
  streams = data[:http2_streams] ? data[:http2_streams].dup : {}
133
287
  outgoing_frames = []
134
288
  completed_requests = []
135
- connection_window = data[:http2_window] || 65_535
289
+ window_updates = []
290
+ peer_initial_window_size = nil
291
+ connection_window = data[:http2_window] || DEFAULT_WINDOW_SIZE
136
292
  preface_received = data[:http2_preface_received] || false
293
+ last_client_stream_id = data[:http2_last_client_stream_id] || 0
294
+ pending_headers = data[:http2_pending_headers]
295
+ goaway_error = nil
137
296
 
138
297
  unless preface_received
139
298
  if buffer.bytesize >= 24 && buffer.byteslice(0, 24) == Http2Parser.connection_preface
140
299
  buffer = buffer.byteslice(24..-1) || ""
141
300
  preface_received = true
142
301
  else
143
- return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
302
+ 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
303
  end
145
304
  end
146
305
 
@@ -151,9 +310,16 @@ module Raptor
151
310
  frame, consumed = parsed
152
311
  buffer = buffer.byteslice(consumed..-1) || ""
153
312
 
313
+ if pending_headers && frame[:type] != :continuation
314
+ goaway_error = ERROR_PROTOCOL_ERROR
315
+ break
316
+ end
317
+
154
318
  case frame[:type]
155
319
  when :settings
156
320
  if (frame[:flags] & FLAG_ACK).zero?
321
+ parsed_settings = parser.parse_settings(frame[:payload])
322
+ peer_initial_window_size = parsed_settings[:initial_window_size] if parsed_settings.key?(:initial_window_size)
157
323
  outgoing_frames << parser.build_frame(:settings, FLAG_ACK, 0, nil)
158
324
  end
159
325
 
@@ -161,37 +327,58 @@ module Raptor
161
327
  stream_id = frame[:stream_id]
162
328
  header_payload = frame[:payload]
163
329
 
330
+ unless streams.key?(stream_id)
331
+ if stream_id.even? || stream_id <= last_client_stream_id
332
+ goaway_error = ERROR_PROTOCOL_ERROR
333
+ break
334
+ end
335
+ last_client_stream_id = stream_id
336
+ end
337
+
164
338
  if (frame[:flags] & FLAG_PRIORITY) != 0
165
339
  header_payload = header_payload.byteslice(5..-1) || ""
166
340
  end
167
341
 
168
- decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
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
- }
342
+ end_stream = (frame[:flags] & FLAG_END_STREAM) != 0
179
343
 
180
- streams.delete(stream_id)
344
+ if (frame[:flags] & FLAG_END_HEADERS) != 0
345
+ decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
346
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
181
347
  else
182
- streams[stream_id] = stream
348
+ pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
349
+ end
350
+
351
+ when :continuation
352
+ if pending_headers.nil? || frame[:stream_id] != pending_headers[:stream_id]
353
+ goaway_error = ERROR_PROTOCOL_ERROR
354
+ break
355
+ end
356
+
357
+ pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload])
358
+
359
+ if (frame[:flags] & FLAG_END_HEADERS) != 0
360
+ stream_id = pending_headers[:stream_id]
361
+ decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table)
362
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
363
+ pending_headers = nil
183
364
  end
184
365
 
185
366
  when :data
186
367
  stream_id = frame[:stream_id]
187
- stream = streams[stream_id] || {}
368
+
369
+ unless streams.key?(stream_id)
370
+ goaway_error = ERROR_PROTOCOL_ERROR
371
+ break
372
+ end
373
+
374
+ stream = streams[stream_id]
188
375
  existing_body = stream[:body] || ""
189
376
  stream = stream.merge(body: existing_body + frame[:payload])
190
377
 
191
378
  if frame[:payload].bytesize.positive?
192
379
  connection_window -= frame[:payload].bytesize
193
- if connection_window < 32_768
194
- increment = 65_535 - connection_window
380
+ if connection_window < DEFAULT_WINDOW_SIZE / 2
381
+ increment = DEFAULT_WINDOW_SIZE - connection_window
195
382
  wu_payload = [increment].pack("N")
196
383
  outgoing_frames << parser.build_frame(:window_update, 0, 0, wu_payload)
197
384
  outgoing_frames << parser.build_frame(:window_update, 0, stream_id, wu_payload)
@@ -213,7 +400,8 @@ module Raptor
213
400
  end
214
401
 
215
402
  when :window_update
216
- parser.parse_window_update(frame[:payload])
403
+ increment = parser.parse_window_update(frame[:payload])
404
+ window_updates << [frame[:stream_id], increment]
217
405
 
218
406
  when :ping
219
407
  if (frame[:flags] & FLAG_ACK).zero?
@@ -228,9 +416,45 @@ module Raptor
228
416
  end
229
417
  end
230
418
 
231
- build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, connection_window, preface_received)
419
+ if goaway_error
420
+ goaway_payload = [last_client_stream_id, goaway_error].pack("NN")
421
+ outgoing_frames << parser.build_frame(:goaway, 0, 0, goaway_payload)
422
+ end
423
+
424
+ 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
425
  end
233
426
 
427
+ # Merges a decoded header block into the stream's accumulated state,
428
+ # promoting the stream to `completed_requests` when END_STREAM is set.
429
+ #
430
+ # @param streams [Hash] current open-stream map
431
+ # @param completed_requests [Array<Hash>] accumulator of completed stream requests
432
+ # @param stream_id [Integer] the stream identifier
433
+ # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
434
+ # @param end_stream [Boolean] whether the source frame had END_STREAM set
435
+ # @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
436
+ #
437
+ # @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]]]
438
+ def self.finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
439
+ stream = streams[stream_id] || {}
440
+ stream = stream.merge(headers: decoded_headers)
441
+
442
+ if end_stream
443
+ completed_requests << {
444
+ stream_id: stream_id,
445
+ headers: decoded_headers,
446
+ body: stream[:body] || ""
447
+ }
448
+
449
+ streams.delete(stream_id)
450
+ else
451
+ streams[stream_id] = stream
452
+ end
453
+
454
+ [streams, completed_requests]
455
+ end
456
+ private_class_method :finalize_headers
457
+
234
458
  # Builds a frozen result hash from the current processing state.
235
459
  #
236
460
  # @param data [Hash] original connection state
@@ -239,13 +463,18 @@ module Raptor
239
463
  # @param streams [Hash] updated stream states
240
464
  # @param outgoing_frames [Array<String>] frames to write to the socket
241
465
  # @param completed_requests [Array<Hash>] fully received stream requests
466
+ # @param window_updates [Array<Array(Integer, Integer)>] inbound WINDOW_UPDATE pairs as [stream_id, increment]
467
+ # @param peer_initial_window_size [Integer, nil] new SETTINGS_INITIAL_WINDOW_SIZE announced by the peer
242
468
  # @param connection_window [Integer] current connection flow control window
243
469
  # @param preface_received [Boolean] whether the connection preface has been received
470
+ # @param last_client_stream_id [Integer] highest client-initiated stream ID seen
471
+ # @param pending_headers [Hash, nil] in-progress HEADERS+CONTINUATION assembly
472
+ # @param close_connection [Boolean] whether the connection should be closed after writing outgoing frames
244
473
  # @return [Hash] frozen result hash
245
474
  #
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
- Ractor.make_shareable({
475
+ # @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]
476
+ 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)
477
+ result = {
249
478
  id: data[:id],
250
479
  protocol: :http2,
251
480
  buffer: buffer || "",
@@ -253,11 +482,17 @@ module Raptor
253
482
  http2_streams: streams,
254
483
  http2_window: connection_window,
255
484
  http2_preface_received: preface_received,
485
+ http2_last_client_stream_id: last_client_stream_id,
486
+ http2_pending_headers: pending_headers,
256
487
  outgoing_frames: outgoing_frames,
257
488
  completed_requests: completed_requests,
489
+ close_connection: close_connection,
258
490
  remote_addr: data[:remote_addr],
259
491
  url_scheme: data[:url_scheme]
260
- })
492
+ }
493
+ result[:window_updates] = window_updates unless window_updates.empty?
494
+ result[:peer_initial_window_size] = peer_initial_window_size if peer_initial_window_size
495
+ Ractor.make_shareable(result)
261
496
  end
262
497
  private_class_method :build_result
263
498
 
@@ -277,9 +512,19 @@ module Raptor
277
512
  return unless socket
278
513
 
279
514
  writer = reactor.writer_for(result[:id])
515
+ flow_control = reactor.flow_control_for(result[:id])
516
+
517
+ if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
518
+ apply_flow_control_updates(flow_control, result)
519
+ end
280
520
 
281
521
  writer.write_frames(socket, result[:outgoing_frames])
282
522
 
523
+ if result[:close_connection]
524
+ reactor.close_connection(result[:id])
525
+ return
526
+ end
527
+
283
528
  reactor.update_http2_state(result)
284
529
 
285
530
  result[:completed_requests]&.each do |request|
@@ -288,7 +533,7 @@ module Raptor
288
533
 
289
534
  thread_pool << proc do
290
535
  dispatch_stream_request(
291
- socket, writer, stream_id,
536
+ socket, writer, flow_control, stream_id,
292
537
  request[:headers], request[:body],
293
538
  remote_addr: remote_addr
294
539
  )
@@ -298,23 +543,46 @@ module Raptor
298
543
 
299
544
  private
300
545
 
546
+ # Applies inbound flow-control updates from a parsed result to the
547
+ # connection's `FlowControl`.
548
+ #
549
+ # @param flow_control [FlowControl] the per-connection flow controller
550
+ # @param result [Hash] the parsed result from `process_frames`
551
+ # @return [void]
552
+ #
553
+ # @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
554
+ def apply_flow_control_updates(flow_control, result)
555
+ result[:window_updates]&.each do |stream_id, increment|
556
+ if stream_id.zero?
557
+ flow_control.add_connection_window(increment)
558
+ else
559
+ flow_control.add_stream_window(stream_id, increment)
560
+ end
561
+ end
562
+
563
+ if (new_size = result[:peer_initial_window_size])
564
+ flow_control.set_initial_stream_window(new_size)
565
+ end
566
+ end
567
+
301
568
  # Dispatches a completed stream request to the Rack app and writes
302
569
  # the response back as HTTP/2 frames.
303
570
  #
304
571
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
305
572
  # @param writer [Writer] lock-free frame writer for the connection
573
+ # @param flow_control [FlowControl] per-connection outbound flow controller
306
574
  # @param stream_id [Integer] the HTTP/2 stream identifier
307
575
  # @param headers [Array<Array(String, String)>] request headers
308
576
  # @param body [String] request body
309
577
  # @param remote_addr [String] the client IP address
310
578
  # @return [void]
311
579
  #
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:)
580
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
581
+ def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
314
582
  env = build_rack_env(headers, body, remote_addr: remote_addr)
315
583
  status, response_headers, response_body = @app.call(env)
316
584
 
317
- write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
585
+ write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
318
586
  rescue => error
319
587
  write_http2_error_response(socket, writer, stream_id)
320
588
 
@@ -325,20 +593,25 @@ module Raptor
325
593
  end
326
594
  ensure
327
595
  response_body.close if response_body.respond_to?(:close)
596
+ flow_control.discard_stream(stream_id) if flow_control
328
597
  end
329
598
 
330
599
  # Writes a Rack response as HTTP/2 frames to the socket.
331
600
  #
601
+ # DATA frames are partitioned through `flow_control` so each write fits
602
+ # within the peer's per-stream and connection windows.
603
+ #
332
604
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
333
605
  # @param writer [Writer] lock-free frame writer for the connection
606
+ # @param flow_control [FlowControl] per-connection outbound flow controller
334
607
  # @param stream_id [Integer] the HTTP/2 stream identifier
335
608
  # @param status [Integer] HTTP status code
336
609
  # @param headers [Hash] response headers from the Rack application
337
610
  # @param body [Object] response body responding to each
338
611
  # @return [void]
339
612
  #
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)
613
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
614
+ def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
342
615
  parser = Http2Parser.new
343
616
 
344
617
  header_pairs = [[":status", status.to_s]]
@@ -358,16 +631,24 @@ module Raptor
358
631
  body_chunks = []
359
632
  body.each { |chunk| body_chunks << chunk unless chunk.empty? }
360
633
 
361
- frames = []
362
634
  if body_chunks.empty?
363
- frames << parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)
364
- else
365
- frames << parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)
635
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
636
+ return
637
+ end
366
638
 
367
- last_index = body_chunks.size - 1
368
- body_chunks.each_with_index do |chunk, index|
369
- flags = index == last_index ? FLAG_END_STREAM : 0
370
- frames << parser.build_frame(:data, flags, stream_id, chunk)
639
+ frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
640
+
641
+ last_chunk_index = body_chunks.size - 1
642
+ body_chunks.each_with_index do |chunk, chunk_index|
643
+ offset = 0
644
+ while offset < chunk.bytesize
645
+ remaining = chunk.bytesize - offset
646
+ last_frame = chunk_index == last_chunk_index && remaining <= MAX_FRAME_SIZE
647
+ granted = flow_control.acquire(stream_id, remaining, end_stream: last_frame)
648
+ slice = offset == 0 && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted)
649
+ offset += granted
650
+ end_stream = chunk_index == last_chunk_index && offset == chunk.bytesize
651
+ frames << parser.build_frame(:data, end_stream ? FLAG_END_STREAM : 0, stream_id, slice)
371
652
  end
372
653
  end
373
654
 
@@ -76,6 +76,7 @@ module Raptor
76
76
  # @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
77
77
  # @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
78
78
  # @rbs @id_to_writer: Hash[Integer, untyped]
79
+ # @rbs @id_to_flow_control: Hash[Integer, untyped]
79
80
 
80
81
  # Creates a new Reactor instance.
81
82
  #
@@ -101,6 +102,7 @@ module Raptor
101
102
  @socket_to_state = {}
102
103
  @id_to_timeout = {}
103
104
  @id_to_writer = {}
105
+ @id_to_flow_control = {}
104
106
  end
105
107
 
106
108
  # Starts the reactor's main event loop in a new thread.
@@ -117,33 +119,38 @@ module Raptor
117
119
  Thread.current.name = self.class.name
118
120
 
119
121
  until @queue.closed? && @queue.empty?
120
- timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
121
- @selector.select(timeout) do |monitor|
122
- wakeup!(monitor.value)
123
- end
124
-
125
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
- expired = []
127
- @timeouts.traverse do |to_client|
128
- break unless to_client.timeout(now) == 0
129
-
130
- expired << to_client
131
- end
132
-
133
- expired.each do |to_client|
134
- @timeouts.delete!(to_client)
135
- id = to_client.client_data[:id]
136
- @id_to_timeout.delete(id)
137
- socket = @id_to_socket[id]
138
- next unless socket
139
-
140
- @selector.deregister(socket)
141
- socket.write(TIMEOUT_RESPONSE) rescue nil
142
- cleanup(socket)
143
- end
144
-
145
- until @queue.empty?
146
- register(@queue.pop)
122
+ begin
123
+ timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
124
+ @selector.select(timeout) do |monitor|
125
+ wakeup!(monitor.value)
126
+ end
127
+
128
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
+ expired = []
130
+ @timeouts.traverse do |to_client|
131
+ break unless to_client.timeout(now) == 0
132
+
133
+ expired << to_client
134
+ end
135
+
136
+ expired.each do |to_client|
137
+ @timeouts.delete!(to_client)
138
+ id = to_client.client_data[:id]
139
+ @id_to_timeout.delete(id)
140
+ socket = @id_to_socket[id]
141
+ next unless socket
142
+
143
+ @selector.deregister(socket)
144
+ socket.write(TIMEOUT_RESPONSE) rescue nil
145
+ cleanup(socket)
146
+ end
147
+
148
+ until @queue.empty?
149
+ register(@queue.pop)
150
+ end
151
+ rescue => error
152
+ warn "#{Thread.current.name} rescued:"
153
+ warn error.full_message
147
154
  end
148
155
  end
149
156
 
@@ -163,9 +170,11 @@ module Raptor
163
170
  socket = state[:socket]
164
171
  state.delete(:socket)
165
172
  writer = state.delete(:writer)
173
+ flow_control = state.delete(:flow_control)
166
174
  @id_to_socket[state[:id]] = socket
167
175
  @socket_to_state[socket] = state
168
176
  @id_to_writer[state[:id]] = writer if writer
177
+ @id_to_flow_control[state[:id]] = flow_control if flow_control
169
178
 
170
179
  read_and_queue_for_parse(socket, state)
171
180
  end
@@ -262,6 +271,18 @@ module Raptor
262
271
  @id_to_writer[id]
263
272
  end
264
273
 
274
+ # Returns the flow controller associated with a given connection, if one
275
+ # was supplied when the connection was added. Used by HTTP/2 stream
276
+ # dispatchers to honour the peer's flow-control windows.
277
+ #
278
+ # @param id [Integer] unique client identifier
279
+ # @return [Object, nil] the flow controller, if found
280
+ #
281
+ # @rbs (Integer id) -> untyped?
282
+ def flow_control_for(id)
283
+ @id_to_flow_control[id]
284
+ end
285
+
265
286
  # Updates connection state for an HTTP/2 connection after frame processing.
266
287
  #
267
288
  # Re-registers the socket with the selector for further reads and stores
@@ -282,6 +303,24 @@ module Raptor
282
303
  socket.close
283
304
  end
284
305
 
306
+ # Closes the socket for the given connection and drops all reactor state
307
+ # associated with it. Used to terminate HTTP/2 connections after sending
308
+ # a GOAWAY frame.
309
+ #
310
+ # @param id [Integer] unique client identifier
311
+ # @return [void]
312
+ #
313
+ # @rbs (Integer id) -> void
314
+ def close_connection(id)
315
+ socket = @id_to_socket.delete(id)
316
+ return unless socket
317
+
318
+ @socket_to_state.delete(socket)
319
+ @id_to_writer.delete(id)
320
+ @id_to_flow_control.delete(id)
321
+ socket.close rescue nil
322
+ end
323
+
285
324
  # Initiates reactor shutdown.
286
325
  #
287
326
  # Closes the registration queue and wakes up the selector to begin
@@ -385,6 +424,7 @@ module Raptor
385
424
  state = @socket_to_state.delete(socket)
386
425
  @id_to_socket.delete(state[:id])
387
426
  @id_to_writer.delete(state[:id])
427
+ @id_to_flow_control.delete(state[:id])
388
428
  socket.close
389
429
  end
390
430