raptor 0.12.0 → 0.13.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
@@ -12,16 +12,9 @@ require_relative "raptor_http2"
12
12
  module Raptor
13
13
  # Handles HTTP/2 request processing and Rack application integration.
14
14
  #
15
- # Http2 manages the HTTP/2 protocol lifecycle including frame processing,
16
- # HPACK header compression, stream management, and response writing.
17
- # It integrates with the same reactor, ractor pool, and thread pool
18
- # pipeline used by HTTP/1.1 connections.
19
- #
20
15
  class Http2
21
- # Lock-free per-connection frame writer.
22
- #
23
- # Serializes concurrent socket writes from multiple stream workers
24
- # without blocking any of them.
16
+ # Serialises concurrent frame writes on a single HTTP/2 connection so
17
+ # exactly one thread is writing at any moment.
25
18
  #
26
19
  class Writer
27
20
  IDLE = :idle
@@ -49,7 +42,7 @@ module Raptor
49
42
  #
50
43
  # @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
51
44
  def write_frames(socket, frames)
52
- return if frames.nil? || frames.empty?
45
+ return if !frames || frames.empty?
53
46
 
54
47
  claimed = false
55
48
  @state.swap do |current|
@@ -80,14 +73,8 @@ module Raptor
80
73
  end
81
74
  end
82
75
 
83
- # Per-connection outbound flow-control accounting.
84
- #
85
76
  # Tracks the peer's connection-level and per-stream receive windows so
86
- # outbound DATA frames respect RFC 7540 section 5.2. Threads dispatching stream
87
- # responses call `acquire` to reserve send capacity; threads applying
88
- # inbound WINDOW_UPDATE or SETTINGS frames call the mutating methods to
89
- # replenish it. The connection window and per-stream windows live in
90
- # separate `Atom`s so the common fast path skips per-stream tracking.
77
+ # outbound `DATA` frames respect RFC 7540 section 5.2.
91
78
  #
92
79
  class FlowControl
93
80
  ACQUIRE_POLL_INTERVAL = 0.001
@@ -98,6 +85,8 @@ module Raptor
98
85
 
99
86
  # Creates a new FlowControl with the spec-default windows.
100
87
  #
88
+ # @return [void]
89
+ #
101
90
  # @rbs () -> void
102
91
  def initialize
103
92
  @connection_window = Atom.new(DEFAULT_WINDOW_SIZE)
@@ -109,12 +98,6 @@ module Raptor
109
98
  # least one byte is available on both the connection and stream
110
99
  # windows. The returned size is capped at `MAX_FRAME_SIZE`.
111
100
  #
112
- # When `end_stream` is true, `max_bytes` fits within the peer's
113
- # initial stream window, and no per-stream override has been
114
- # recorded, only the connection window is consulted. The stream
115
- # closes on this frame, so its remaining send window will not be
116
- # consulted again and need not be tracked.
117
- #
118
101
  # @param stream_id [Integer] the HTTP/2 stream identifier
119
102
  # @param max_bytes [Integer] the largest size the caller would like to send
120
103
  # @param end_stream [Boolean] true when this is the final frame on the stream
@@ -127,11 +110,7 @@ module Raptor
127
110
 
128
111
  if end_stream && capped <= initial && !@stream_windows.value.key?(stream_id)
129
112
  loop do
130
- granted = 0
131
- @connection_window.swap do |window|
132
- granted = window > capped ? capped : window
133
- granted > 0 ? window - granted : window
134
- end
113
+ granted = reserve_connection(capped)
135
114
  return granted if granted > 0
136
115
 
137
116
  sleep ACQUIRE_POLL_INTERVAL
@@ -141,14 +120,7 @@ module Raptor
141
120
  loop do
142
121
  stream_window = @stream_windows.value[stream_id] || initial
143
122
  capped_full = capped < stream_window ? capped : stream_window
144
-
145
- granted = 0
146
- if capped_full > 0
147
- @connection_window.swap do |window|
148
- granted = window > capped_full ? capped_full : window
149
- granted > 0 ? window - granted : window
150
- end
151
- end
123
+ granted = capped_full > 0 ? reserve_connection(capped_full) : 0
152
124
 
153
125
  if granted > 0
154
126
  @stream_windows.swap do |windows|
@@ -162,8 +134,7 @@ module Raptor
162
134
  end
163
135
  end
164
136
 
165
- # Increments the connection-level send window. Called when the peer
166
- # sends a WINDOW_UPDATE on stream 0.
137
+ # Increments the connection-level send window by `increment` bytes.
167
138
  #
168
139
  # @param increment [Integer] the byte count to add
169
140
  # @return [void]
@@ -173,8 +144,7 @@ module Raptor
173
144
  @connection_window.swap { |window| window + increment }
174
145
  end
175
146
 
176
- # Increments the per-stream send window. Called when the peer sends
177
- # a WINDOW_UPDATE on a specific stream.
147
+ # Increments the send window for the given stream by `increment` bytes.
178
148
  #
179
149
  # @param stream_id [Integer] the HTTP/2 stream identifier
180
150
  # @param increment [Integer] the byte count to add
@@ -207,9 +177,7 @@ module Raptor
207
177
  end
208
178
  end
209
179
 
210
- # Discards any per-stream tracking for the given stream. Called
211
- # after a stream closes so `@stream_windows` does not grow without
212
- # bound across the lifetime of a connection.
180
+ # Discards any per-stream tracking for the given stream.
213
181
  #
214
182
  # @param stream_id [Integer] the HTTP/2 stream identifier
215
183
  # @return [void]
@@ -226,6 +194,25 @@ module Raptor
226
194
  pruned
227
195
  end
228
196
  end
197
+
198
+ private
199
+
200
+ # Reserves up to `capped` bytes from the connection window,
201
+ # returning the number of bytes granted (0 when the window is
202
+ # exhausted).
203
+ #
204
+ # @param capped [Integer] the largest reservation the caller will accept
205
+ # @return [Integer]
206
+ #
207
+ # @rbs (Integer capped) -> Integer
208
+ def reserve_connection(capped)
209
+ granted = 0
210
+ @connection_window.swap do |window|
211
+ granted = window > capped ? capped : window
212
+ granted > 0 ? window - granted : window
213
+ end
214
+ granted
215
+ end
229
216
  end
230
217
 
231
218
  EAGER_READ_TIMEOUT = 0.001
@@ -287,9 +274,10 @@ module Raptor
287
274
  # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
288
275
  # @rbs @initial_settings_frame: String
289
276
 
290
- # The initial server SETTINGS frame sent on every new HTTP/2 connection.
277
+ # Returns the initial server SETTINGS frame to send on every new
278
+ # HTTP/2 connection.
291
279
  #
292
- # @return [String] the encoded SETTINGS frame
280
+ # @return [String]
293
281
  attr_reader :initial_settings_frame
294
282
 
295
283
  # Creates a new Http2 handler.
@@ -329,11 +317,9 @@ module Raptor
329
317
  Writer.new(write_timeout: @write_timeout)
330
318
  end
331
319
 
332
- # Processes HTTP/2 frames from the connection buffer.
333
- #
334
- # Parses frames, handles HPACK decoding, tracks stream state, and returns
320
+ # Advances HTTP/2 frame parsing from the connection buffer, returning
335
321
  # updated connection state along with any outgoing protocol frames and
336
- # completed stream requests. Ractor-safe.
322
+ # completed stream requests.
337
323
  #
338
324
  # @param data [Hash] the connection state including buffer and HPACK table
339
325
  # @return [Hash] updated state with outgoing_frames and completed_requests
@@ -414,7 +400,7 @@ module Raptor
414
400
  end
415
401
 
416
402
  when :continuation
417
- if pending_headers.nil? || frame[:stream_id] != pending_headers[:stream_id]
403
+ if !pending_headers || frame[:stream_id] != pending_headers[:stream_id]
418
404
  goaway_error = ERROR_PROTOCOL_ERROR
419
405
  break
420
406
  end
@@ -566,12 +552,10 @@ module Raptor
566
552
  end
567
553
  private_class_method :build_result
568
554
 
569
- # Handles a parsed HTTP/2 request from the ractor pool.
570
- #
571
- # Writes outgoing protocol frames to the socket, updates reactor state,
572
- # and dispatches completed stream requests to the thread pool. Eagerly
573
- # consumes subsequent frame batches that are already buffered, skipping
574
- # the reactor and ractor pool hops while the connection is hot.
555
+ # Handles a parsed HTTP/2 result. Writes outgoing frames, dispatches
556
+ # completed stream requests to the thread pool, and eagerly consumes
557
+ # further buffered frame batches before returning control to the
558
+ # reactor.
575
559
  #
576
560
  # @param result [Hash] the parsed result from the ractor pool
577
561
  # @param reactor [Reactor] the reactor managing the connection
@@ -706,10 +690,8 @@ module Raptor
706
690
  flow_control.discard_stream(stream_id) if flow_control
707
691
  end
708
692
 
709
- # Writes a Rack response as HTTP/2 frames to the socket.
710
- #
711
- # DATA frames are partitioned through `flow_control` so each write fits
712
- # within the peer's per-stream and connection windows.
693
+ # Writes a Rack response as HTTP/2 frames to the socket, partitioning
694
+ # DATA frames through `flow_control` to fit within the peer's windows.
713
695
  #
714
696
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
715
697
  # @param writer [Writer] lock-free frame writer for the connection
@@ -805,9 +787,6 @@ module Raptor
805
787
 
806
788
  # Builds a Rack environment hash from HTTP/2 headers and body.
807
789
  #
808
- # Translates HTTP/2 pseudo-headers into Rack-compatible environment keys
809
- # and populates all required Rack env entries.
810
- #
811
790
  # @param headers [Array<Array(String, String)>] HTTP/2 header pairs
812
791
  # @param body [String] the request body
813
792
  # @param remote_addr [String] the client IP address
@@ -5,25 +5,10 @@ require "nio"
5
5
  require "red-black-tree"
6
6
 
7
7
  module Raptor
8
- # High-performance I/O reactor for managing client connections and timeouts.
9
- #
10
- # Reactor uses NIO selectors for efficient I/O multiplexing and implements
11
- # client timeouts using a red-black tree for O(log n) timeout management.
12
- # It coordinates between ractor pools for CPU-intensive HTTP parsing and
13
- # thread pools for blocking operations, and provides backlog metrics that
14
- # the server uses for backpressure control to prevent overload.
15
- #
16
- # @example
17
- # reactor = Reactor.new(
18
- # ractor_pool,
19
- # thread_pool,
20
- # connection_options: { first_data_timeout: 30, chunk_data_timeout: 10 },
21
- # http1_options: { persistent_data_timeout: 65 }
22
- # )
23
- # reactor.run
24
- # reactor.add(id: client.object_id, socket: client)
25
- # # ... later
26
- # reactor.shutdown
8
+ # Multiplexes client connections and closes them when they overrun
9
+ # their per-phase timeouts (first data, subsequent chunks, and
10
+ # keep-alive idle). Feeds ready bytes to the parser pipeline and hands
11
+ # completed requests back to the caller-provided handlers.
27
12
  #
28
13
  class Reactor
29
14
  # A client connection node ordered by absolute expiry time so the
@@ -33,7 +18,7 @@ module Raptor
33
18
  # @rbs attr_accessor timeout_at: Float
34
19
  attr_accessor :timeout_at
35
20
 
36
- # Semantic alias for the inherited `data` slot.
21
+ # Returns the client connection state.
37
22
  #
38
23
  # @return [Hash] the client connection state
39
24
  #
@@ -112,11 +97,8 @@ module Raptor
112
97
  @id_to_flow_control = {}
113
98
  end
114
99
 
115
- # Starts the reactor's main event loop in a new thread.
116
- #
117
- # The event loop handles I/O events, processes timeouts, manages
118
- # the registration queue, and controls server connection acceptance.
119
- # It continues until the queue is closed and emptied.
100
+ # Starts the reactor's main event loop in a new thread. Runs until
101
+ # the registration queue is closed and drained.
120
102
  #
121
103
  # @return [Thread] the thread running the reactor event loop
122
104
  #
@@ -185,10 +167,8 @@ module Raptor
185
167
  read_and_queue_for_parse(socket, state)
186
168
  end
187
169
 
188
- # Updates the state of an existing client connection.
189
- #
190
- # Called when an incomplete HTTP request needs to be
191
- # re-registered with the reactor for further processing.
170
+ # Updates the state of an existing client connection and re-registers
171
+ # it for further I/O.
192
172
  #
193
173
  # @param state [Hash] updated client connection state
194
174
  # @option state [Integer] :id client identifier
@@ -220,11 +200,8 @@ module Raptor
220
200
  end
221
201
  end
222
202
 
223
- # Re-registers a kept-alive connection for the next request cycle.
224
- #
225
- # Called after successfully writing a response when keep-alive is active.
226
- # Resets the connection state and re-queues the socket in the selector
227
- # using the persistent data timeout.
203
+ # Re-registers a kept-alive connection for the next request cycle
204
+ # under the persistent-data timeout.
228
205
  #
229
206
  # @param socket [TCPSocket] the kept-alive client socket
230
207
  # @param id [Integer] the unique client identifier
@@ -253,9 +230,6 @@ module Raptor
253
230
 
254
231
  # Returns the socket for a given client identifier without removing it.
255
232
  #
256
- # Used by HTTP/2 connections where the socket remains registered across
257
- # multiple stream requests.
258
- #
259
233
  # @param id [Integer] unique client identifier
260
234
  # @return [TCPSocket, nil] the socket, if found
261
235
  #
@@ -265,8 +239,7 @@ module Raptor
265
239
  end
266
240
 
267
241
  # Returns the writer object associated with a given connection, if one
268
- # was supplied when the connection was added. Used by protocol handlers
269
- # that need to coordinate concurrent socket writes.
242
+ # was supplied when the connection was added.
270
243
  #
271
244
  # @param id [Integer] unique client identifier
272
245
  # @return [Object, nil] the writer, if found
@@ -276,9 +249,8 @@ module Raptor
276
249
  @id_to_writer[id]
277
250
  end
278
251
 
279
- # Returns the flow controller associated with a given connection, if one
280
- # was supplied when the connection was added. Used by HTTP/2 stream
281
- # dispatchers to honour the peer's flow-control windows.
252
+ # Returns the flow controller associated with a given connection, if
253
+ # one was supplied when the connection was added.
282
254
  #
283
255
  # @param id [Integer] unique client identifier
284
256
  # @return [Object, nil] the flow controller, if found
@@ -288,10 +260,8 @@ module Raptor
288
260
  @id_to_flow_control[id]
289
261
  end
290
262
 
291
- # Updates connection state for an HTTP/2 connection after frame processing.
292
- #
293
- # Re-registers the socket with the selector for further reads and stores
294
- # the updated HPACK table and stream states.
263
+ # Updates connection state for an HTTP/2 connection and re-registers
264
+ # the socket for further reads.
295
265
  #
296
266
  # @param state [Hash] updated connection state from the ractor pool
297
267
  # @return [void]
@@ -308,9 +278,8 @@ module Raptor
308
278
  socket.close
309
279
  end
310
280
 
311
- # Closes the socket for the given connection and drops all reactor state
312
- # associated with it. Used to terminate HTTP/2 connections after sending
313
- # a GOAWAY frame.
281
+ # Closes the socket for the given connection and drops all reactor
282
+ # state associated with it.
314
283
  #
315
284
  # @param id [Integer] unique client identifier
316
285
  # @return [void]
@@ -386,8 +355,8 @@ module Raptor
386
355
  read_and_queue_for_parse(socket, state)
387
356
  end
388
357
 
389
- # Reads data from a socket and either queues it for parsing,
390
- # or for selector registration.
358
+ # Reads data from a socket and either queues it for parsing or
359
+ # returns it to the selector.
391
360
  #
392
361
  # @param socket [TCPSocket] the socket to read from and queue
393
362
  # @param state [Hash] current connection state
@@ -431,7 +400,7 @@ module Raptor
431
400
  socket.close
432
401
  end
433
402
 
434
- # Checks if a request is complete i.e., processable.
403
+ # Returns true when the request has been fully parsed.
435
404
  #
436
405
  # @param state [Hash] connection state
437
406
  # @return [Boolean] true if the request is complete
@@ -23,8 +23,8 @@ module Raptor
23
23
  LIBBPF_LOADED = !!defined?(LibBPFRuby)
24
24
  BPF_OBJECT_PATH = File.expand_path("../../ext/raptor_bpf/reuseport_select.bpf.o", __dir__)
25
25
 
26
- # Whether the preconditions for BPF-directed reuseport are met on this
27
- # platform. Does not imply that the kernel will accept the program.
26
+ # Returns true when the preconditions for BPF-directed reuseport are
27
+ # met on this platform.
28
28
  #
29
29
  # @return [Boolean]
30
30
  #
@@ -65,9 +65,6 @@ module Raptor
65
65
  #
66
66
  # @rbs (Array[String] bind_uris, Integer worker_index, Integer socket_backlog) -> Array[TCPServer]
67
67
  def self.create_worker_listeners(bind_uris, worker_index, socket_backlog)
68
- @load_key = [worker_index + 1].pack("L")
69
- @load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
70
-
71
68
  bind_uris.filter_map do |bind_uri|
72
69
  uri = URI(bind_uri)
73
70
  next unless uri.scheme == "tcp"
@@ -92,6 +89,18 @@ module Raptor
92
89
  end
93
90
  end
94
91
 
92
+ # Prepares this worker's load-reporting slot so subsequent
93
+ # {update_load} calls can publish its backlog.
94
+ #
95
+ # @param worker_index [Integer] slot index for this worker
96
+ # @return [void]
97
+ #
98
+ # @rbs (Integer worker_index) -> void
99
+ def self.enable_load_reporting(worker_index)
100
+ @load_key = [worker_index + 1].pack("L")
101
+ @load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
102
+ end
103
+
95
104
  # Publishes this worker's current backlog to the BPF map.
96
105
  #
97
106
  # @param backlog [Integer] the worker's current reactor backlog
data/lib/raptor/server.rb CHANGED
@@ -8,26 +8,10 @@ require "atomic-ruby/atomic_boolean"
8
8
  require_relative "reuseport_bpf"
9
9
 
10
10
  module Raptor
11
- # Accepts client connections and dispatches them into the request
12
- # pipeline. Skips acceptance when the reactor backlog is high so an
13
- # overloaded process leaves connections for peers that can absorb
14
- # them (via shared `SO_REUSEPORT` listeners).
15
- #
16
- # Supports TCP, Unix, and SSL listeners. SSL handshakes are offloaded
17
- # to the thread pool so a slow client can't pin the server thread.
18
- # For HTTP/1.1 the first request is parsed inline and dispatched
19
- # straight to the thread pool; HTTP/2 (negotiated via ALPN) is
20
- # registered with the reactor for frame processing.
21
- #
22
- # @example
23
- # binder = Binder.new(["tcp://0.0.0.0:3000"])
24
- # reactor = Reactor.new(ractor_pool, thread_pool, connection_options: {}, http1_options: {})
25
- # http1 = Http1.new(app, 3000)
26
- # http2 = Http2.new(app, 3000)
27
- # server = Server.new(binder, reactor, thread_pool, http1, http2, connection_options: { first_data_timeout: 30 }, listeners: binder.listeners)
28
- # server.run
29
- # # ... later
30
- # server.shutdown
11
+ # Accepts client connections on TCP, Unix, and SSL listeners and
12
+ # dispatches them into the request pipeline. Backs off from accepting
13
+ # when the reactor backlog is high so an overloaded process leaves
14
+ # connections for peers to absorb.
31
15
  #
32
16
  class Server
33
17
  HTTP_SCHEME = "http"
@@ -80,11 +64,6 @@ module Raptor
80
64
 
81
65
  # Starts the server's main accept loop in a new thread.
82
66
  #
83
- # The accept loop polls listening sockets for ready connections and accepts
84
- # them when the reactor backlog is under the backpressure threshold. On
85
- # Linux with BPF-directed dispatch active, a companion thread publishes
86
- # this worker's backlog to the BPF map.
87
- #
88
67
  # @return [Thread] the thread running the accept loop
89
68
  #
90
69
  # @rbs () -> Thread
@@ -148,7 +127,7 @@ module Raptor
148
127
 
149
128
  while @running.true?
150
129
  ReuseportBPF.update_load(@reactor.backlog)
151
- sleep 0.01
130
+ sleep 0.001
152
131
  end
153
132
  end
154
133
  end
@@ -167,13 +146,8 @@ module Raptor
167
146
  end
168
147
  end
169
148
 
170
- # Accepts a connection from the given listener and dispatches it.
171
- #
172
- # For SSL listeners the TLS handshake is offloaded to the thread pool so
173
- # a slow client cannot block the server thread. For SSL connections with
174
- # h2 negotiated via ALPN, the server sends initial SETTINGS and adds the
175
- # connection to the reactor as an HTTP/2 connection. All other connections
176
- # follow the HTTP/1.1 path.
149
+ # Accepts a connection from the given listener and dispatches it into
150
+ # the HTTP/1.1 or SSL/HTTP-2 pipeline.
177
151
  #
178
152
  # @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
179
153
  # @return [Boolean] true if a connection was accepted, false if the listener had nothing to dispatch
@@ -210,9 +184,9 @@ module Raptor
210
184
  true
211
185
  end
212
186
 
213
- # Performs the TLS handshake for an accepted SSL connection and dispatches
214
- # it through the HTTP/2 or HTTP/1.1 path. The handshake is bounded by
215
- # `:first_data_timeout` so a slow client cannot pin a worker thread.
187
+ # Performs the TLS handshake for an accepted SSL connection, bounded
188
+ # by `:first_data_timeout`, and dispatches it through the HTTP/2 or
189
+ # HTTP/1.1 path.
216
190
  #
217
191
  # @param listener [Binder::SslListener] the SSL listener that accepted the connection
218
192
  # @param tcp_client [TCPSocket] the accepted TCP socket
data/lib/raptor/stats.rb CHANGED
@@ -4,12 +4,8 @@
4
4
  require "mmap-ruby"
5
5
 
6
6
  module Raptor
7
- # Shared memory store for per-worker process statistics.
8
- #
9
- # Stats uses an anonymous mmap (MAP_ANON | MAP_SHARED) created before
10
- # forking so that worker processes can write their stats and the master
11
- # process can read them without any pipes or signals. Each worker is
12
- # assigned a fixed-size slot in the shared region.
7
+ # Shared-memory store for per-worker process statistics. Each worker
8
+ # holds a fixed-size slot in an anonymous mmap region.
13
9
  #
14
10
  # Binary layout per slot (native byte order):
15
11
  # pid uint32 4 bytes
@@ -31,8 +27,7 @@ module Raptor
31
27
  # @rbs @num_workers: Integer
32
28
  # @rbs @mmap: untyped
33
29
 
34
- # Allocates the shared mmap region. Must be called before forking
35
- # workers so the mapping is inherited by every child process.
30
+ # Allocates the shared mmap region for `num_workers` slots.
36
31
  #
37
32
  # @param num_workers [Integer] number of worker slots to allocate
38
33
  # @return [void]
@@ -24,7 +24,7 @@ module Raptor
24
24
  # @rbs (String message) -> bool
25
25
  def self.notify(message)
26
26
  socket_path = ENV[NOTIFY_SOCKET_ENV]
27
- return false if socket_path.nil? || socket_path.empty?
27
+ return false if !socket_path || socket_path.empty?
28
28
 
29
29
  address = if socket_path.start_with?("@")
30
30
  Socket.pack_sockaddr_un("\0#{socket_path[1..]}")
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.12.0"
5
+ VERSION = "0.13.0"
6
6
  end