raptor 0.2.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/server.rb CHANGED
@@ -14,8 +14,8 @@ module Raptor
14
14
  # providing natural backpressure based on system capacity.
15
15
  #
16
16
  # Supports TCP, Unix domain, and SSL listeners transparently. TCP_NODELAY is
17
- # applied only to TCP sockets, and SSL handshakes are performed synchronously
18
- # before the connection is dispatched.
17
+ # applied only to TCP sockets, and SSL handshakes are offloaded to the thread
18
+ # pool so a slow client cannot block the server thread.
19
19
  #
20
20
  # For HTTP/1.1 connections the first request is parsed inline on the server
21
21
  # thread and dispatched directly to the thread pool, falling back to the
@@ -27,7 +27,7 @@ module Raptor
27
27
  # binder = Binder.new(["tcp://0.0.0.0:3000"])
28
28
  # reactor = Reactor.new(thread_pool, ractor_pool, client_options: {})
29
29
  # request = Request.new(app, 3000)
30
- # server = Server.new(binder, reactor, thread_pool, request)
30
+ # server = Server.new(binder, reactor, thread_pool, request, client_options: { first_data_timeout: 30 })
31
31
  # server.run
32
32
  # # ... later
33
33
  # server.shutdown
@@ -41,6 +41,7 @@ module Raptor
41
41
  # @rbs @reactor: Reactor
42
42
  # @rbs @thread_pool: AtomicThreadPool
43
43
  # @rbs @request: Request
44
+ # @rbs @client_options: Hash[Symbol, untyped]
44
45
  # @rbs @running: AtomicBoolean
45
46
 
46
47
  # Creates a new Server instance.
@@ -49,14 +50,16 @@ module Raptor
49
50
  # @param reactor [Reactor] the reactor for handling client connections
50
51
  # @param thread_pool [AtomicThreadPool] thread pool for application processing
51
52
  # @param request [Request] the HTTP/1.1 request handler
53
+ # @param client_options [Hash] client timeout configuration, used to bound TLS handshakes
52
54
  # @return [void]
53
55
  #
54
- # @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request) -> void
55
- def initialize(binder, reactor, thread_pool, request)
56
+ # @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request, client_options: Hash[Symbol, untyped]) -> void
57
+ def initialize(binder, reactor, thread_pool, request, client_options:)
56
58
  @binder = binder
57
59
  @reactor = reactor
58
60
  @thread_pool = thread_pool
59
61
  @request = request
62
+ @client_options = client_options
60
63
  @running = AtomicBoolean.new(true)
61
64
  end
62
65
 
@@ -108,9 +111,11 @@ module Raptor
108
111
 
109
112
  # Accepts a connection from the given listener and dispatches it.
110
113
  #
111
- # For SSL connections with h2 negotiated via ALPN, the server sends
112
- # initial SETTINGS and adds the connection to the reactor as an HTTP/2
113
- # connection. All other connections follow the HTTP/1.1 path.
114
+ # For SSL listeners the TLS handshake is offloaded to the thread pool so
115
+ # a slow client cannot block the server thread. For SSL connections with
116
+ # h2 negotiated via ALPN, the server sends initial SETTINGS and adds the
117
+ # connection to the reactor as an HTTP/2 connection. All other connections
118
+ # follow the HTTP/1.1 path.
114
119
  #
115
120
  # @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
116
121
  # @param reactor [Reactor] the reactor to dispatch connections to
@@ -131,46 +136,117 @@ module Raptor
131
136
  remote_addr = "127.0.0.1"
132
137
  end
133
138
 
134
- url_scheme = HTTP_SCHEME
135
- client = tcp_client
136
-
137
139
  if listener.is_a?(Binder::SslListener)
138
- url_scheme = HTTPS_SCHEME
139
- begin
140
- ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
141
- ssl_socket.sync_close = true
142
- ssl_socket.accept
143
- client = ssl_socket
144
- rescue OpenSSL::SSL::SSLError => error
145
- warn "SSL handshake failed: #{error.message}"
146
- tcp_client.close rescue nil
147
- return
140
+ @thread_pool << proc do
141
+ dispatch_ssl_connection(listener, tcp_client, remote_addr, reactor)
148
142
  end
143
+ return
144
+ end
149
145
 
150
- if ssl_socket.alpn_protocol == H2_PROTOCOL
151
- ssl_socket.write(Http2.build_server_settings_frame) rescue nil
146
+ @request.eager_accept(
147
+ tcp_client,
148
+ tcp_client.object_id,
149
+ reactor,
150
+ @thread_pool,
151
+ remote_addr,
152
+ HTTP_SCHEME
153
+ )
154
+ end
152
155
 
153
- reactor.add(
154
- id: ssl_socket.object_id,
155
- socket: ssl_socket,
156
- remote_addr: remote_addr,
157
- url_scheme: HTTPS_SCHEME,
158
- protocol: :http2,
159
- writer: Http2::Writer.new
160
- )
156
+ # Performs the TLS handshake for an accepted SSL connection and dispatches
157
+ # it through the HTTP/2 or HTTP/1.1 path. The handshake is bounded by
158
+ # `:first_data_timeout` so a slow client cannot pin a worker thread.
159
+ #
160
+ # @param listener [Binder::SslListener] the SSL listener that accepted the connection
161
+ # @param tcp_client [TCPSocket] the accepted TCP socket
162
+ # @param remote_addr [String] the client's IP address
163
+ # @param reactor [Reactor] the reactor to dispatch the connection to
164
+ # @return [void]
165
+ #
166
+ # @rbs (Binder::SslListener listener, TCPSocket tcp_client, String remote_addr, Reactor reactor) -> void
167
+ def dispatch_ssl_connection(listener, tcp_client, remote_addr, reactor)
168
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
169
+ ssl_socket.sync_close = true
170
+ return unless perform_ssl_handshake(ssl_socket)
171
+
172
+ if ssl_socket.alpn_protocol == H2_PROTOCOL
173
+ ssl_socket.write(Http2.build_server_settings_frame) rescue nil
174
+
175
+ reactor.add(
176
+ id: ssl_socket.object_id,
177
+ socket: ssl_socket,
178
+ remote_addr: remote_addr,
179
+ url_scheme: HTTPS_SCHEME,
180
+ protocol: :http2,
181
+ writer: Http2::Writer.new,
182
+ flow_control: Http2::FlowControl.new
183
+ )
161
184
 
162
- return
163
- end
185
+ return
164
186
  end
165
187
 
166
188
  @request.eager_accept(
167
- client,
168
- client.object_id,
189
+ ssl_socket,
190
+ ssl_socket.object_id,
169
191
  reactor,
170
192
  @thread_pool,
171
193
  remote_addr,
172
- url_scheme
194
+ HTTPS_SCHEME
173
195
  )
174
196
  end
197
+
198
+ # Drives a non-blocking SSL handshake to completion, bounded by the
199
+ # configured first-data timeout. Returns true on success, false on
200
+ # timeout or SSL error.
201
+ #
202
+ # @param ssl_socket [OpenSSL::SSL::SSLSocket] the SSL socket to hand-shake
203
+ # @return [Boolean] true if the handshake completed
204
+ #
205
+ # @rbs (OpenSSL::SSL::SSLSocket ssl_socket) -> bool
206
+ def perform_ssl_handshake(ssl_socket)
207
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @client_options[:first_data_timeout]
208
+
209
+ begin
210
+ ssl_socket.accept_nonblock
211
+ true
212
+ rescue IO::WaitReadable
213
+ return false unless wait_for_handshake(ssl_socket, deadline, :read)
214
+
215
+ retry
216
+ rescue IO::WaitWritable
217
+ return false unless wait_for_handshake(ssl_socket, deadline, :write)
218
+
219
+ retry
220
+ rescue OpenSSL::SSL::SSLError => error
221
+ warn "SSL handshake failed: #{error.message}"
222
+ ssl_socket.close rescue nil
223
+ false
224
+ end
225
+ end
226
+
227
+ # Waits up to `deadline` for the socket to become ready for the next step
228
+ # of the SSL handshake. Closes the socket and returns false on timeout.
229
+ #
230
+ # @param ssl_socket [OpenSSL::SSL::SSLSocket] the SSL socket
231
+ # @param deadline [Float] absolute monotonic deadline
232
+ # @param direction [Symbol] either `:read` or `:write`
233
+ # @return [Boolean] true if the socket became ready before the deadline
234
+ #
235
+ # @rbs (OpenSSL::SSL::SSLSocket ssl_socket, Float deadline, Symbol direction) -> bool
236
+ def wait_for_handshake(ssl_socket, deadline, direction)
237
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
238
+ ready = if remaining <= 0
239
+ false
240
+ elsif direction == :read
241
+ ssl_socket.wait_readable(remaining)
242
+ else
243
+ ssl_socket.wait_writable(remaining)
244
+ end
245
+ return true if ready
246
+
247
+ warn "SSL handshake timed out"
248
+ ssl_socket.close rescue nil
249
+ false
250
+ end
175
251
  end
176
252
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
data/lib/raptor.rb CHANGED
@@ -5,9 +5,9 @@ require_relative "raptor/version"
5
5
 
6
6
  # Main module for the Raptor web server.
7
7
  #
8
- # Raptor is a high-performance, multi-threaded, multi-process Ruby web server that
9
- # leverages Ractors for parallel HTTP/1.1 and HTTP/2 request processing, native C
10
- # extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
8
+ # Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server
9
+ # implementing Rack 3+, leveraging Ractors for parallel HTTP/1.1 and HTTP/2 request processing,
10
+ # native C extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
11
11
  #
12
12
  module Raptor
13
13
  end
@@ -20,6 +20,33 @@ module Raptor
20
20
 
21
21
  DEFAULT_OPTIONS: untyped
22
22
 
23
+ DEFAULT_CONFIG_PATHS: untyped
24
+
25
+ # Loads a configuration file and returns the hash it evaluates to.
26
+ #
27
+ # The file is evaluated at the top level so constants like `Raptor::*` resolve
28
+ # the same as in a regular Ruby script. The final expression must be a Hash
29
+ # of cluster options (the same keys accepted by {Raptor::Cluster#initialize}).
30
+ #
31
+ # @param path [String] path to a Ruby file that evaluates to a Hash
32
+ # @return [Hash{Symbol => untyped}] cluster options
33
+ # @raise [ArgumentError] if the file does not evaluate to a Hash
34
+ #
35
+ # @rbs (String path) -> Hash[Symbol, untyped]
36
+ def self.load_config_file: (String path) -> Hash[Symbol, untyped]
37
+
38
+ # Returns the first existing path in {DEFAULT_CONFIG_PATHS} resolved
39
+ # against `root`, or nil if none exist.
40
+ #
41
+ # Used to pick up a project-local config file when no `-c`/`--config`
42
+ # flag was supplied.
43
+ #
44
+ # @param root [String] directory to resolve the default paths against
45
+ # @return [String, nil] the config path, or nil if no default file exists
46
+ #
47
+ # @rbs (?String root) -> String?
48
+ def self.default_config_path: (?String root) -> String?
49
+
23
50
  @command: Symbol
24
51
 
25
52
  @options: Hash[Symbol, untyped]
@@ -61,6 +88,30 @@ module Raptor
61
88
  # @rbs () -> void
62
89
  def run_stats: () -> void
63
90
 
91
+ # Scans argv for a `-c`/`--config` flag and returns the configured path.
92
+ #
93
+ # The pre-scan runs before the main OptionParser pass so the config file
94
+ # can be applied as a base layer that CLI args then override. All four
95
+ # OptionParser-accepted forms (`-c PATH`, `-cPATH`, `--config PATH`,
96
+ # `--config=PATH`) are recognized.
97
+ #
98
+ # @param argv [Array<String>] command-line arguments to scan
99
+ # @return [String, nil] the config path, or nil if no flag was supplied
100
+ #
101
+ # @rbs (Array[String] argv) -> String?
102
+ def extract_config_path: (Array[String] argv) -> String?
103
+
104
+ # Loads a config file and merges it into `@options` over the defaults.
105
+ #
106
+ # Top-level keys replace defaults; the nested `:client` hash is merged
107
+ # key-by-key so a config file does not need to restate every client option.
108
+ #
109
+ # @param path [String, nil] path to the config file, or nil to no-op
110
+ # @return [void]
111
+ #
112
+ # @rbs (String? path) -> void
113
+ def apply_config_file: (String? path) -> void
114
+
64
115
  # Creates the OptionParser instance with all supported command-line options.
65
116
  #
66
117
  # @return [OptionParser] configured option parser
@@ -37,7 +37,9 @@ module Raptor
37
37
  # @rbs (Hash[Symbol, untyped] options) -> void
38
38
  def self.run: (Hash[Symbol, untyped] options) -> void
39
39
 
40
- @stats_file: String?
40
+ @phased_restarting: bool
41
+
42
+ @phased_restart_requested: bool
41
43
 
42
44
  @stats: Stats
43
45
 
@@ -51,6 +53,12 @@ module Raptor
51
53
 
52
54
  @binder: Binder
53
55
 
56
+ @pid_file: String?
57
+
58
+ @stats_file: String?
59
+
60
+ @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
61
+
54
62
  @client_options: Hash[Symbol, Integer]
55
63
 
56
64
  @worker_count: Integer
@@ -72,7 +80,10 @@ module Raptor
72
80
  # @option options [Array<String>] :binds array of bind URIs
73
81
  # @option options [#call] :app pre-built Rack application
74
82
  # @option options [String] :rackup path to Rack configuration file
75
- # @option options [Hash] :client client timeout configuration
83
+ # @option options [Hash] :client client configuration
84
+ # @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
85
+ # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
86
+ # @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
76
87
  # @return [void]
77
88
  #
78
89
  # @rbs (Hash[Symbol, untyped] options) -> void
@@ -82,7 +93,8 @@ module Raptor
82
93
  #
83
94
  # Forks the configured number of worker processes and monitors them,
84
95
  # automatically restarting any that exit unexpectedly. Handles graceful
85
- # shutdown via INT or TERM signals, and stats logging via USR1.
96
+ # shutdown via INT or TERM signals, stats logging via USR1, and phased
97
+ # restart via USR2.
86
98
  #
87
99
  # Each worker process includes:
88
100
  # - 1 server thread (continuously accepts connections with backpressure control)
@@ -115,6 +127,22 @@ module Raptor
115
127
  # @rbs (Integer index) -> void
116
128
  def spawn_worker: (Integer index) -> void
117
129
 
130
+ # Reaps any worker processes that have exited, respawning each one
131
+ # unless the cluster is shutting down.
132
+ #
133
+ # @return [Symbol] :no_children when there are no remaining children, otherwise :reaped
134
+ #
135
+ # @rbs () -> Symbol
136
+ def reap_workers: () -> Symbol
137
+
138
+ # Replaces each worker process one at a time, waiting for the new
139
+ # worker to boot before moving on to the next. Triggered by SIGUSR2.
140
+ #
141
+ # @return [void]
142
+ #
143
+ # @rbs () -> void
144
+ def perform_phased_restart: () -> void
145
+
118
146
  # Runs the full server stack inside a worker process.
119
147
  #
120
148
  # Sets up and coordinates the reactor, server, ractor pool, thread pool,
@@ -33,6 +33,84 @@ module Raptor
33
33
  def write_frames: (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
34
34
  end
35
35
 
36
+ # Per-connection outbound flow-control accounting.
37
+ #
38
+ # Tracks the peer's connection-level and per-stream receive windows so
39
+ # outbound DATA frames respect RFC 7540 §5.2. Threads dispatching stream
40
+ # responses call `acquire` to reserve send capacity; threads applying
41
+ # inbound WINDOW_UPDATE or SETTINGS frames call the mutating methods to
42
+ # replenish it. State is held in a single `Atom` so updates use CAS.
43
+ class FlowControl
44
+ ACQUIRE_POLL_INTERVAL: ::Float
45
+
46
+ @connection_window: Atom
47
+
48
+ @stream_windows: Atom
49
+
50
+ @initial_stream_window: Atom
51
+
52
+ # Creates a new FlowControl with the spec-default windows.
53
+ #
54
+ # @rbs () -> void
55
+ def initialize: () -> void
56
+
57
+ # Reserves outbound capacity on the given stream, polling until at
58
+ # least one byte is available on both the connection and stream
59
+ # windows. The returned size is capped at `MAX_FRAME_SIZE`.
60
+ #
61
+ # When `end_stream` is true, `max_bytes` fits within the peer's
62
+ # initial stream window, and no per-stream override has been
63
+ # recorded, only the connection window is consulted. The stream
64
+ # closes on this frame, so its remaining send window will not be
65
+ # consulted again and need not be tracked.
66
+ #
67
+ # @param stream_id [Integer] the HTTP/2 stream identifier
68
+ # @param max_bytes [Integer] the largest size the caller would like to send
69
+ # @param end_stream [Boolean] true when this is the final frame on the stream
70
+ # @return [Integer] the number of bytes the caller may now send
71
+ #
72
+ # @rbs (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
73
+ def acquire: (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
74
+
75
+ # Increments the connection-level send window. Called when the peer
76
+ # sends a WINDOW_UPDATE on stream 0.
77
+ #
78
+ # @param increment [Integer] the byte count to add
79
+ # @return [void]
80
+ #
81
+ # @rbs (Integer increment) -> void
82
+ def add_connection_window: (Integer increment) -> void
83
+
84
+ # Increments the per-stream send window. Called when the peer sends
85
+ # a WINDOW_UPDATE on a specific stream.
86
+ #
87
+ # @param stream_id [Integer] the HTTP/2 stream identifier
88
+ # @param increment [Integer] the byte count to add
89
+ # @return [void]
90
+ #
91
+ # @rbs (Integer stream_id, Integer increment) -> void
92
+ def add_stream_window: (Integer stream_id, Integer increment) -> void
93
+
94
+ # Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
95
+ # existing stream window by the delta as required by RFC 7540 §6.9.2.
96
+ #
97
+ # @param new_size [Integer] the peer's new initial window size
98
+ # @return [void]
99
+ #
100
+ # @rbs (Integer new_size) -> void
101
+ def set_initial_stream_window: (Integer new_size) -> void
102
+
103
+ # Discards any per-stream tracking for the given stream. Called
104
+ # after a stream closes so `@stream_windows` does not grow without
105
+ # bound across the lifetime of a connection.
106
+ #
107
+ # @param stream_id [Integer] the HTTP/2 stream identifier
108
+ # @return [void]
109
+ #
110
+ # @rbs (Integer stream_id) -> void
111
+ def discard_stream: (Integer stream_id) -> void
112
+ end
113
+
36
114
  FLAG_END_STREAM: ::Integer
37
115
 
38
116
  FLAG_END_HEADERS: ::Integer
@@ -41,12 +119,22 @@ module Raptor
41
119
 
42
120
  FLAG_PRIORITY: ::Integer
43
121
 
122
+ ERROR_NO_ERROR: ::Integer
123
+
124
+ ERROR_PROTOCOL_ERROR: ::Integer
125
+
126
+ DEFAULT_WINDOW_SIZE: ::Integer
127
+
128
+ MAX_FRAME_SIZE: ::Integer
129
+
44
130
  SERVER_PROTOCOL: ::String
45
131
 
46
132
  RACK_HEADER_PREFIX: ::String
47
133
 
48
134
  HOP_BY_HOP_HEADERS: untyped
49
135
 
136
+ @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
137
+
50
138
  @server_port: Integer
51
139
 
52
140
  @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
@@ -55,10 +143,11 @@ module Raptor
55
143
  #
56
144
  # @param app [#call] the Rack application to dispatch requests to
57
145
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
146
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
58
147
  # @return [void]
59
148
  #
60
- # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
61
- def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port) -> void
149
+ # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
150
+ def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
62
151
 
63
152
  # Builds the initial server SETTINGS frame to send on connection establishment.
64
153
  #
@@ -79,6 +168,19 @@ module Raptor
79
168
  # @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
80
169
  def self.process_frames: (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
81
170
 
171
+ # Merges a decoded header block into the stream's accumulated state,
172
+ # promoting the stream to `completed_requests` when END_STREAM is set.
173
+ #
174
+ # @param streams [Hash] current open-stream map
175
+ # @param completed_requests [Array<Hash>] accumulator of completed stream requests
176
+ # @param stream_id [Integer] the stream identifier
177
+ # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
178
+ # @param end_stream [Boolean] whether the source frame had END_STREAM set
179
+ # @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
180
+ #
181
+ # @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]]]
182
+ def self.finalize_headers: (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]] ]
183
+
82
184
  # Builds a frozen result hash from the current processing state.
83
185
  #
84
186
  # @param data [Hash] original connection state
@@ -87,12 +189,17 @@ module Raptor
87
189
  # @param streams [Hash] updated stream states
88
190
  # @param outgoing_frames [Array<String>] frames to write to the socket
89
191
  # @param completed_requests [Array<Hash>] fully received stream requests
192
+ # @param window_updates [Array<Array(Integer, Integer)>] inbound WINDOW_UPDATE pairs as [stream_id, increment]
193
+ # @param peer_initial_window_size [Integer, nil] new SETTINGS_INITIAL_WINDOW_SIZE announced by the peer
90
194
  # @param connection_window [Integer] current connection flow control window
91
195
  # @param preface_received [Boolean] whether the connection preface has been received
196
+ # @param last_client_stream_id [Integer] highest client-initiated stream ID seen
197
+ # @param pending_headers [Hash, nil] in-progress HEADERS+CONTINUATION assembly
198
+ # @param close_connection [Boolean] whether the connection should be closed after writing outgoing frames
92
199
  # @return [Hash] frozen result hash
93
200
  #
94
- # @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]
95
- def self.build_result: (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]
201
+ # @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]
202
+ def self.build_result: (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]
96
203
 
97
204
  # Handles a parsed HTTP/2 request from the ractor pool.
98
205
  #
@@ -109,32 +216,47 @@ module Raptor
109
216
 
110
217
  private
111
218
 
219
+ # Applies inbound flow-control updates from a parsed result to the
220
+ # connection's `FlowControl`.
221
+ #
222
+ # @param flow_control [FlowControl] the per-connection flow controller
223
+ # @param result [Hash] the parsed result from `process_frames`
224
+ # @return [void]
225
+ #
226
+ # @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
227
+ def apply_flow_control_updates: (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
228
+
112
229
  # Dispatches a completed stream request to the Rack app and writes
113
230
  # the response back as HTTP/2 frames.
114
231
  #
115
232
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
116
233
  # @param writer [Writer] lock-free frame writer for the connection
234
+ # @param flow_control [FlowControl] per-connection outbound flow controller
117
235
  # @param stream_id [Integer] the HTTP/2 stream identifier
118
236
  # @param headers [Array<Array(String, String)>] request headers
119
237
  # @param body [String] request body
120
238
  # @param remote_addr [String] the client IP address
121
239
  # @return [void]
122
240
  #
123
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
124
- def dispatch_stream_request: (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[ String, String ]] headers, String body, remote_addr: String) -> void
241
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
242
+ def dispatch_stream_request: (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[ String, String ]] headers, String body, remote_addr: String) -> void
125
243
 
126
244
  # Writes a Rack response as HTTP/2 frames to the socket.
127
245
  #
246
+ # DATA frames are partitioned through `flow_control` so each write fits
247
+ # within the peer's per-stream and connection windows.
248
+ #
128
249
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
129
250
  # @param writer [Writer] lock-free frame writer for the connection
251
+ # @param flow_control [FlowControl] per-connection outbound flow controller
130
252
  # @param stream_id [Integer] the HTTP/2 stream identifier
131
253
  # @param status [Integer] HTTP status code
132
254
  # @param headers [Hash] response headers from the Rack application
133
255
  # @param body [Object] response body responding to each
134
256
  # @return [void]
135
257
  #
136
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
137
- def write_http2_response: (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
258
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
259
+ def write_http2_response: (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
138
260
 
139
261
  # Writes a 500 error response as HTTP/2 frames.
140
262
  #
@@ -55,6 +55,8 @@ module Raptor
55
55
 
56
56
  TIMEOUT_RESPONSE: ::String
57
57
 
58
+ @id_to_flow_control: Hash[Integer, untyped]
59
+
58
60
  @id_to_writer: Hash[Integer, untyped]
59
61
 
60
62
  @id_to_timeout: Hash[Integer, TimeoutClient]
@@ -169,6 +171,16 @@ module Raptor
169
171
  # @rbs (Integer id) -> untyped?
170
172
  def writer_for: (Integer id) -> untyped?
171
173
 
174
+ # Returns the flow controller associated with a given connection, if one
175
+ # was supplied when the connection was added. Used by HTTP/2 stream
176
+ # dispatchers to honour the peer's flow-control windows.
177
+ #
178
+ # @param id [Integer] unique client identifier
179
+ # @return [Object, nil] the flow controller, if found
180
+ #
181
+ # @rbs (Integer id) -> untyped?
182
+ def flow_control_for: (Integer id) -> untyped?
183
+
172
184
  # Updates connection state for an HTTP/2 connection after frame processing.
173
185
  #
174
186
  # Re-registers the socket with the selector for further reads and stores
@@ -180,6 +192,16 @@ module Raptor
180
192
  # @rbs (Hash[Symbol, untyped] state) -> void
181
193
  def update_http2_state: (Hash[Symbol, untyped] state) -> void
182
194
 
195
+ # Closes the socket for the given connection and drops all reactor state
196
+ # associated with it. Used to terminate HTTP/2 connections after sending
197
+ # a GOAWAY frame.
198
+ #
199
+ # @param id [Integer] unique client identifier
200
+ # @return [void]
201
+ #
202
+ # @rbs (Integer id) -> void
203
+ def close_connection: (Integer id) -> void
204
+
183
205
  # Initiates reactor shutdown.
184
206
  #
185
207
  # Closes the registration queue and wakes up the selector to begin