raptor 0.4.0 → 0.5.1

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
@@ -81,7 +81,8 @@ module Raptor
81
81
  # outbound DATA frames respect RFC 7540 §5.2. Threads dispatching stream
82
82
  # responses call `acquire` to reserve send capacity; threads applying
83
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.
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.
85
86
  #
86
87
  class FlowControl
87
88
  ACQUIRE_POLL_INTERVAL = 0.001
@@ -529,7 +530,7 @@ module Raptor
529
530
 
530
531
  result[:completed_requests]&.each do |request|
531
532
  stream_id = request[:stream_id]
532
- remote_addr = result[:remote_addr] || "127.0.0.1"
533
+ remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
533
534
 
534
535
  thread_pool << proc do
535
536
  dispatch_stream_request(
@@ -749,7 +750,7 @@ module Raptor
749
750
  env[Rack::SERVER_NAME] ||= host
750
751
  env[Rack::SERVER_PORT] ||= port || @server_port.to_s
751
752
  else
752
- env[Rack::SERVER_NAME] ||= "localhost"
753
+ env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
753
754
  env[Rack::SERVER_PORT] ||= @server_port.to_s
754
755
  end
755
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
@@ -9,12 +9,12 @@ module Raptor
9
9
  #
10
10
  # Reactor uses NIO selectors for efficient I/O multiplexing and implements
11
11
  # client timeouts using a red-black tree for O(log n) timeout management.
12
- # It coordinates between thread pools for blocking operations and ractor
13
- # pools for CPU-intensive HTTP parsing, and provides backlog metrics
14
- # that the server uses for backpressure control to prevent overload.
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
15
  #
16
16
  # @example
17
- # reactor = Reactor.new(thread_pool, ractor_pool, client_options: {
17
+ # reactor = Reactor.new(ractor_pool, thread_pool, client_options: {
18
18
  # first_data_timeout: 30,
19
19
  # chunk_data_timeout: 10
20
20
  # })
@@ -24,38 +24,38 @@ module Raptor
24
24
  # reactor.shutdown
25
25
  #
26
26
  class Reactor
27
- # Red-black tree node representing a client connection with timeout tracking.
28
- #
29
- # TimeoutClient extends RedBlackTree::Node to enable efficient timeout
30
- # management using the tree's ordering properties.
27
+ # A client connection node ordered by absolute expiry time so the
28
+ # soonest-to-expire is always at the tree's minimum.
31
29
  #
32
30
  class TimeoutClient < RedBlackTree::Node
33
31
  # @rbs attr_accessor timeout_at: Float
34
32
  attr_accessor :timeout_at
35
33
 
36
- # Returns the client data stored in this timeout node.
34
+ # Semantic alias for the inherited `data` slot.
37
35
  #
38
- # @return [Hash] the client connection state data
36
+ # @return [Hash] the client connection state
39
37
  #
40
38
  # @rbs () -> Hash[Symbol, untyped]
41
39
  def client_data
42
40
  data
43
41
  end
44
42
 
45
- # Calculates remaining timeout duration from the current time.
43
+ # Returns seconds until expiry, clamped to 0 so an already-expired
44
+ # client doesn't push the next selector wait into the future.
46
45
  #
47
46
  # @param now [Float] current monotonic timestamp
48
- # @return [Float] remaining timeout in seconds, minimum 0
47
+ # @return [Float] seconds until expiry, never negative
49
48
  #
50
49
  # @rbs (Float now) -> Float
51
50
  def timeout(now)
52
51
  [timeout_at - now, 0].max
53
52
  end
54
53
 
55
- # Compares timeout nodes by their timeout_at values for tree ordering.
54
+ # Orders nodes by `timeout_at` so the tree minimum is the next
55
+ # client to expire.
56
56
  #
57
57
  # @param other [TimeoutClient] another timeout client to compare
58
- # @return [Integer] -1, 0, or 1 for ordering
58
+ # @return [Integer] -1, 0, or 1
59
59
  #
60
60
  # @rbs (TimeoutClient other) -> Integer
61
61
  def <=>(other)
@@ -80,18 +80,18 @@ module Raptor
80
80
 
81
81
  # Creates a new Reactor instance.
82
82
  #
83
- # @param thread_pool [AtomicThreadPool] thread pool for application processing
84
83
  # @param ractor_pool [RactorPool] ractor pool for HTTP parsing
84
+ # @param thread_pool [AtomicThreadPool] thread pool for application processing
85
85
  # @param client_options [Hash] timeout configuration options
86
86
  # @option client_options [Integer] :first_data_timeout timeout for initial data
87
87
  # @option client_options [Integer] :chunk_data_timeout timeout for subsequent chunks
88
88
  # @option client_options [Integer] :persistent_data_timeout timeout for keep-alive connections
89
89
  # @return [void]
90
90
  #
91
- # @rbs (untyped thread_pool, untyped ractor_pool, client_options: Hash[Symbol, Integer]) -> void
92
- def initialize(thread_pool, ractor_pool, client_options:)
93
- @thread_pool = thread_pool
91
+ # @rbs (untyped ractor_pool, untyped thread_pool, client_options: Hash[Symbol, Integer]) -> void
92
+ def initialize(ractor_pool, thread_pool, client_options:)
94
93
  @ractor_pool = ractor_pool
94
+ @thread_pool = thread_pool
95
95
  @client_options = client_options
96
96
 
97
97
  @selector = NIO::Selector.new
@@ -149,8 +149,7 @@ module Raptor
149
149
  register(@queue.pop)
150
150
  end
151
151
  rescue => error
152
- warn "#{Thread.current.name} rescued:"
153
- warn error.full_message
152
+ Log.rescued_error(error)
154
153
  end
155
154
  end
156
155
 
@@ -200,13 +199,12 @@ module Raptor
200
199
  socket.close
201
200
  end
202
201
 
203
- # Removes a client connection from the reactor.
204
- #
205
- # Called when an HTTP request is complete and ready for application
206
- # processing. Triggers server accept re-enabling if system capacity allows.
202
+ # Drops the reactor's references to a client whose parsed request
203
+ # has been handed off to the thread pool. The socket itself is kept
204
+ # open so the worker can write the response.
207
205
  #
208
206
  # @param id [Integer] unique client identifier
209
- # @return [TCPSocket, nil] the removed socket, if found
207
+ # @return [TCPSocket, nil] the socket associated with `id`, if any
210
208
  #
211
209
  # @rbs (Integer id) -> TCPSocket?
212
210
  def remove(id)
@@ -321,10 +319,8 @@ module Raptor
321
319
  socket.close rescue nil
322
320
  end
323
321
 
324
- # Initiates reactor shutdown.
325
- #
326
- # Closes the registration queue and wakes up the selector to begin
327
- # graceful shutdown process.
322
+ # Closes the registration queue and wakes the selector so the
323
+ # event loop drains pending work and exits.
328
324
  #
329
325
  # @return [void]
330
326
  #
@@ -11,13 +11,10 @@ require "rack"
11
11
  require_relative "raptor_http"
12
12
 
13
13
  module Raptor
14
- # Handles HTTP request processing and Rack application integration.
15
- #
16
- # Request manages the HTTP parsing pipeline using Ractors and coordinates
17
- # with the reactor for connection state management. It bridges between the
18
- # low-level HTTP parsing and high-level Rack application interface, handling
19
- # both incomplete requests (that need more data) and complete requests
20
- # (ready for application processing).
14
+ # Parses HTTP/1.x requests and dispatches them to the Rack
15
+ # application. Coordinates with the Ractor pool for parsing and
16
+ # with the reactor for requests that need more data before they
17
+ # can be handled.
21
18
  #
22
19
  class Request
23
20
  BODY_BUFFER_THRESHOLD = 256 * 1024
@@ -27,7 +24,6 @@ module Raptor
27
24
  KEEPALIVE_READ_TIMEOUT = 0.001
28
25
  MAX_KEEPALIVE_REQUESTS = 100
29
26
 
30
- HTTP_SCHEME = "http"
31
27
  HTTP_10 = "HTTP/1.0"
32
28
  HTTP_11 = "HTTP/1.1"
33
29
  STATUS_LINE_CACHE_10 = Hash.new do |h, status|
@@ -94,10 +90,8 @@ module Raptor
94
90
  [decoded, :incomplete]
95
91
  end
96
92
 
97
- # Writes a string to the socket, retrying on partial writes and flow control blocks.
98
- #
99
- # Uses write_nonblock with `WRITE_TIMEOUT` to avoid blocking the thread
100
- # indefinitely on slow clients.
93
+ # Writes `string` in full, retrying on partial writes. Bounded by
94
+ # `WRITE_TIMEOUT` so a slow client can't pin the writing thread.
101
95
  #
102
96
  # @param socket [TCPSocket] the socket to write to
103
97
  # @param string [String] the data to write
@@ -330,8 +324,8 @@ module Raptor
330
324
  else
331
325
  socket = reactor.remove(parsed_request[:id])
332
326
  request_count = (parsed_request[:request_count] || 0) + 1
333
- remote_addr = parsed_request[:remote_addr] || "127.0.0.1"
334
- url_scheme = parsed_request[:url_scheme] || HTTP_SCHEME
327
+ remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
328
+ url_scheme = parsed_request[:url_scheme] || Server::HTTP_SCHEME
335
329
 
336
330
  thread_pool << proc do
337
331
  process_client(
@@ -607,7 +601,7 @@ module Raptor
607
601
  # @return [Hash] fully populated Rack environment hash
608
602
  #
609
603
  # @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
610
- def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
604
+ def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
611
605
  env[Rack::RACK_VERSION] = Rack::VERSION
612
606
  env[Rack::RACK_URL_SCHEME] = url_scheme
613
607
  env[Rack::RACK_INPUT] = build_rack_input(body)
@@ -647,7 +641,7 @@ module Raptor
647
641
  env[Rack::SERVER_NAME] ||= host
648
642
  env[Rack::SERVER_PORT] ||= port || @server_port.to_s
649
643
  else
650
- env[Rack::SERVER_NAME] ||= "localhost"
644
+ env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
651
645
  env[Rack::SERVER_PORT] ||= @server_port.to_s
652
646
  end
653
647
 
data/lib/raptor/server.rb CHANGED
@@ -6,26 +6,20 @@ require "socket"
6
6
  require "atomic-ruby/atomic_boolean"
7
7
 
8
8
  module Raptor
9
- # High-performance HTTP server that accepts connections and dispatches them.
9
+ # Accepts client connections and dispatches them into the request
10
+ # pipeline. Skips acceptance when the reactor backlog is high so an
11
+ # overloaded process leaves connections for peers that can absorb
12
+ # them (via shared `SO_REUSEPORT` listeners).
10
13
  #
11
- # Server manages the main accept loop, handling incoming client connections from
12
- # bound sockets. It uses IO.select for efficient polling and implements automatic
13
- # load balancing by checking reactor backlog before accepting connections,
14
- # providing natural backpressure based on system capacity.
15
- #
16
- # Supports TCP, Unix domain, and SSL listeners transparently. TCP_NODELAY is
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
- #
20
- # For HTTP/1.1 connections the first request is parsed inline on the server
21
- # thread and dispatched directly to the thread pool, falling back to the
22
- # reactor only when more data is needed. For HTTP/2 connections (negotiated
23
- # via ALPN) the server sends initial SETTINGS and registers the connection
24
- # with the reactor for frame processing through the ractor pool.
14
+ # Supports TCP, Unix, and SSL listeners. SSL handshakes are offloaded
15
+ # to the thread pool so a slow client can't pin the server thread.
16
+ # For HTTP/1.1 the first request is parsed inline and dispatched
17
+ # straight to the thread pool; HTTP/2 (negotiated via ALPN) is
18
+ # registered with the reactor for frame processing.
25
19
  #
26
20
  # @example
27
21
  # binder = Binder.new(["tcp://0.0.0.0:3000"])
28
- # reactor = Reactor.new(thread_pool, ractor_pool, client_options: {})
22
+ # reactor = Reactor.new(ractor_pool, thread_pool, client_options: {})
29
23
  # request = Request.new(app, 3000)
30
24
  # server = Server.new(binder, reactor, thread_pool, request, client_options: { first_data_timeout: 30 })
31
25
  # server.run
@@ -35,8 +29,12 @@ module Raptor
35
29
  class Server
36
30
  HTTP_SCHEME = "http"
37
31
  HTTPS_SCHEME = "https"
32
+
38
33
  H2_PROTOCOL = "h2"
39
34
 
35
+ DEFAULT_REMOTE_ADDR = "127.0.0.1"
36
+ DEFAULT_SERVER_NAME = "localhost"
37
+
40
38
  # @rbs @binder: Binder
41
39
  # @rbs @reactor: Reactor
42
40
  # @rbs @thread_pool: AtomicThreadPool
@@ -133,7 +131,7 @@ module Raptor
133
131
  tcp_client.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
134
132
  remote_addr = tcp_client.remote_address.ip_address
135
133
  else
136
- remote_addr = "127.0.0.1"
134
+ remote_addr = DEFAULT_REMOTE_ADDR
137
135
  end
138
136
 
139
137
  if listener.is_a?(Binder::SslListener)
@@ -218,7 +216,7 @@ module Raptor
218
216
 
219
217
  retry
220
218
  rescue OpenSSL::SSL::SSLError => error
221
- warn "SSL handshake failed: #{error.message}"
219
+ Log.rescued_error(error)
222
220
  ssl_socket.close rescue nil
223
221
  false
224
222
  end
@@ -244,7 +242,7 @@ module Raptor
244
242
  end
245
243
  return true if ready
246
244
 
247
- warn "SSL handshake timed out"
245
+ Log.warn "SSL handshake timed out"
248
246
  ssl_socket.close rescue nil
249
247
  false
250
248
  end
data/lib/raptor/stats.rb CHANGED
@@ -12,26 +12,27 @@ module Raptor
12
12
  # assigned a fixed-size slot in the shared region.
13
13
  #
14
14
  # Binary layout per slot (native byte order):
15
- # pid uint32 4 bytes
16
- # requests uint64 8 bytes
17
- # backlog uint32 4 bytes
18
- # started_at float64 8 bytes
19
- # last_checkin float64 8 bytes
20
- # booted uint8 1 byte
21
- # 33 bytes total
15
+ # pid uint32 4 bytes
16
+ # index uint32 4 bytes
17
+ # phase uint32 4 bytes
18
+ # requests uint64 8 bytes
19
+ # backlog uint32 4 bytes
20
+ # busy_threads uint32 4 bytes
21
+ # thread_capacity uint32 4 bytes
22
+ # started_at float64 8 bytes
23
+ # last_checkin float64 8 bytes
24
+ # booted uint8 1 byte
25
+ # 49 bytes total
22
26
  #
23
27
  class Stats
24
- SLOT_FORMAT = "LQLddC"
25
- SLOT_SIZE = [0, 0, 0, 0.0, 0.0, 0].pack(SLOT_FORMAT).bytesize
28
+ SLOT_FORMAT = "LLLQLLLddC"
29
+ SLOT_SIZE = [0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0].pack(SLOT_FORMAT).bytesize
26
30
 
27
31
  # @rbs @num_workers: Integer
28
32
  # @rbs @mmap: untyped
29
33
 
30
- # Creates a new Stats instance backed by anonymous shared memory.
31
- #
32
- # Allocates a MAP_ANON | MAP_SHARED mmap region large enough for
33
- # num_workers slots. Must be called before forking so that all
34
- # worker processes share the same backing memory.
34
+ # Allocates the shared mmap region. Must be called before forking
35
+ # workers so the mapping is inherited by every child process.
35
36
  #
36
37
  # @param num_workers [Integer] number of worker slots to allocate
37
38
  # @return [void]
@@ -44,24 +45,27 @@ module Raptor
44
45
 
45
46
  # Writes stats for a worker slot into shared memory.
46
47
  #
47
- # @param index [Integer] slot index to write into
48
+ # @param index [Integer] slot index to write into; also written into the slot itself
48
49
  # @param pid [Integer] worker process ID
50
+ # @param phase [Integer] cluster phase this worker was forked at
49
51
  # @param requests [Integer] total requests handled by this worker
50
52
  # @param backlog [Integer] current queue depth
53
+ # @param busy_threads [Integer] worker threads currently processing requests
54
+ # @param thread_capacity [Integer] worker threads configured for this worker
51
55
  # @param started_at [Float] process start time as a Unix timestamp
52
56
  # @param last_checkin [Float] time of last stats write as a Unix timestamp
53
57
  # @param booted [Boolean] whether the worker has finished starting
54
58
  # @return [void]
55
59
  #
56
- # @rbs (Integer index, pid: Integer, requests: Integer, backlog: Integer, started_at: Float, last_checkin: Float, booted: bool) -> void
57
- def write(index, pid:, requests:, backlog:, started_at:, last_checkin:, booted:)
58
- data = [pid, requests, backlog, started_at, last_checkin, booted ? 1 : 0].pack(SLOT_FORMAT)
60
+ # @rbs (Integer index, pid: Integer, phase: Integer, requests: Integer, backlog: Integer, busy_threads: Integer, thread_capacity: Integer, started_at: Float, last_checkin: Float, booted: bool) -> void
61
+ def write(index, pid:, phase:, requests:, backlog:, busy_threads:, thread_capacity:, started_at:, last_checkin:, booted:)
62
+ data = [pid, index, phase, requests, backlog, busy_threads, thread_capacity, started_at, last_checkin, booted ? 1 : 0].pack(SLOT_FORMAT)
59
63
  @mmap.semlock { @mmap[index * SLOT_SIZE, SLOT_SIZE] = data }
60
64
  end
61
65
 
62
66
  # Returns stats for all worker slots.
63
67
  #
64
- # @return [Array<Hash>] per-worker stat hashes with :pid, :requests, :backlog, :started_at, :last_checkin, and :booted
68
+ # @return [Array<Hash>] per-worker stat hashes with :pid, :index, :phase, :requests, :backlog, :busy_threads, :thread_capacity, :started_at, :last_checkin, and :booted
65
69
  #
66
70
  # @rbs () -> Array[Hash[Symbol, untyped]]
67
71
  def all
@@ -81,14 +85,14 @@ module Raptor
81
85
 
82
86
  # Reads stats for a worker slot from shared memory.
83
87
  #
84
- # @param index [Integer] slot index to read from
85
- # @return [Hash] stat hash with :pid, :requests, :backlog, :started_at, :last_checkin, and :booted
88
+ # @param slot [Integer] slot offset to read from
89
+ # @return [Hash] stat hash with :pid, :index, :phase, :requests, :backlog, :busy_threads, :thread_capacity, :started_at, :last_checkin, and :booted
86
90
  #
87
- # @rbs (Integer index) -> Hash[Symbol, untyped]
88
- def read(index)
89
- data = @mmap[index * SLOT_SIZE, SLOT_SIZE]
90
- pid, requests, backlog, started_at, last_checkin, booted = data.unpack(SLOT_FORMAT)
91
- { pid:, requests:, backlog:, started_at:, last_checkin:, booted: booted == 1 }
91
+ # @rbs (Integer slot) -> Hash[Symbol, untyped]
92
+ def read(slot)
93
+ data = @mmap[slot * SLOT_SIZE, SLOT_SIZE]
94
+ pid, index, phase, requests, backlog, busy_threads, thread_capacity, started_at, last_checkin, booted = data.unpack(SLOT_FORMAT)
95
+ { pid:, index:, phase:, requests:, backlog:, busy_threads:, thread_capacity:, started_at:, last_checkin:, booted: booted == 1 }
92
96
  end
93
97
  end
94
98
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.1"
6
6
  end
@@ -5,7 +5,7 @@ module Raptor
5
5
  #
6
6
  # CLI parses command-line arguments and starts the server cluster with the
7
7
  # specified configuration options. It supports configuring the number of
8
- # workers, threads, ractors, bind addresses, and various client timeout
8
+ # workers, ractors, threads, bind addresses, and various client timeout
9
9
  # settings.
10
10
  #
11
11
  # @example Basic usage