raptor 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0"
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
@@ -4,14 +4,15 @@ module Raptor
4
4
  # Multi-process web server cluster with advanced concurrency architecture.
5
5
  #
6
6
  # Cluster manages multiple worker processes, each running a complete server
7
- # stack including a reactor thread, server thread, ractor pool for HTTP
8
- # parsing, and thread pool for application processing. It handles process
9
- # forking, signal management, graceful shutdown, and automatic worker
10
- # restart when a worker process unexpectedly exits.
7
+ # stack including a ractor pool for HTTP parsing, a thread pool for
8
+ # application processing, plus dedicated reactor and server threads. It
9
+ # handles process forking, signal management, graceful shutdown, and
10
+ # automatic worker restart when a worker process unexpectedly exits.
11
11
  #
12
12
  # The architecture provides horizontal scaling through processes while
13
- # maintaining efficient I/O and CPU utilization within each process through
14
- # the combination of NIO reactors, ractor-based parsing, and thread pools.
13
+ # maintaining efficient I/O and CPU utilization within each process
14
+ # through the combination of ractor-based parsing and thread pools on
15
+ # top of NIO reactors.
15
16
  #
16
17
  # Flow per worker process:
17
18
  # 1. Server continuously accepts connections but skips acceptance when backlog is high
@@ -22,7 +23,7 @@ module Raptor
22
23
  #
23
24
  # @example Basic usage
24
25
  # options = {
25
- # threads: 8, ractors: 2, workers: 4,
26
+ # workers: 4, ractors: 2, threads: 8,
26
27
  # binds: ["tcp://0.0.0.0:3000"],
27
28
  # rackup: "config.ru",
28
29
  # client: { first_data_timeout: 30, chunk_data_timeout: 10 }
@@ -37,53 +38,66 @@ module Raptor
37
38
  # @rbs (Hash[Symbol, untyped] options) -> void
38
39
  def self.run: (Hash[Symbol, untyped] options) -> void
39
40
 
40
- @phased_restarting: bool
41
+ @thread_count: Integer
41
42
 
42
- @phased_restart_requested: bool
43
+ @client_options: Hash[Symbol, Integer]
43
44
 
44
- @stats: Stats
45
+ @worker_timeout: Integer
45
46
 
46
- @workers: Hash[Integer, Integer]
47
+ @worker_boot_timeout: Integer
47
48
 
48
- @shutdown: bool
49
+ @worker_shutdown_timeout: Integer
49
50
 
50
- @app: untyped
51
+ @stats_file: String?
51
52
 
52
- @server_port: Integer
53
+ @pid_file: String?
54
+
55
+ @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
53
56
 
54
57
  @binder: Binder
55
58
 
56
- @pid_file: String?
59
+ @server_port: Integer
57
60
 
58
- @stats_file: String?
61
+ @app: untyped
59
62
 
60
- @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
63
+ @shutdown: bool
61
64
 
62
- @client_options: Hash[Symbol, Integer]
65
+ @workers: Hash[Integer, Integer]
63
66
 
64
- @worker_count: Integer
67
+ @timed_out: Set[Integer]
68
+
69
+ @stats: Stats
70
+
71
+ @phase: Integer
72
+
73
+ @phased_restart_requested: bool
74
+
75
+ @phased_restarting: bool
65
76
 
66
77
  @ractor_count: Integer
67
78
 
68
- @thread_count: Integer
79
+ @worker_count: Integer
69
80
 
70
81
  # Creates a new Cluster with the specified configuration.
71
82
  #
72
- # Initializes the cluster with thread, ractor, and worker counts,
83
+ # Initializes the cluster with worker, ractor, and thread counts,
73
84
  # sets up network binding, loads the Rack application, and prepares
74
85
  # for multi-process operation.
75
86
  #
76
87
  # @param options [Hash] cluster configuration options
77
- # @option options [Integer] :threads number of threads per worker process
78
- # @option options [Integer] :ractors number of ractors per worker process
79
- # @option options [Integer] :workers number of worker processes
80
88
  # @option options [Array<String>] :binds array of bind URIs
89
+ # @option options [Integer] :workers number of worker processes
90
+ # @option options [Integer] :ractors number of ractors per worker process
91
+ # @option options [Integer] :threads number of threads per worker process
81
92
  # @option options [#call] :app pre-built Rack application
82
93
  # @option options [String] :rackup path to Rack configuration file
83
94
  # @option options [Hash] :client client configuration
84
- # @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
95
+ # @option options [Integer] :worker_timeout seconds to wait for a booted worker to check in before killing it
96
+ # @option options [Integer] :worker_boot_timeout seconds to wait for a worker to finish booting before killing it
97
+ # @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
85
98
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
86
99
  # @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
100
+ # @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
87
101
  # @return [void]
88
102
  #
89
103
  # @rbs (Hash[Symbol, untyped] options) -> void
@@ -92,15 +106,15 @@ module Raptor
92
106
  # Starts the multi-process cluster and manages worker processes.
93
107
  #
94
108
  # Forks the configured number of worker processes and monitors them,
95
- # automatically restarting any that exit unexpectedly. Handles graceful
96
- # shutdown via INT or TERM signals, stats logging via USR1, and phased
97
- # restart via USR2.
109
+ # restarting any that exit unexpectedly or stop checking in. Handles
110
+ # graceful shutdown via INT or TERM signals, stats logging via USR1,
111
+ # and phased restart via USR2.
98
112
  #
99
113
  # Each worker process includes:
100
114
  # - 1 server thread (continuously accepts connections with backpressure control)
101
115
  # - 1 reactor thread (I/O multiplexing, timeout handling, backlog monitoring)
102
- # - N ractor workers (parallel HTTP parsing)
103
- # - 1 ractor collector thread (coordinates parsing results)
116
+ # - N pipeline ractors (parallel HTTP parsing)
117
+ # - 1 pipeline collector thread (coordinates parsing results)
104
118
  # - M worker threads (Rack application processing and response writing)
105
119
  # - 1 stats thread (writes per-worker metrics to shared memory every second)
106
120
  #
@@ -120,6 +134,7 @@ module Raptor
120
134
  private
121
135
 
122
136
  # Forks a new worker process and registers it at the given index.
137
+ # The worker inherits the cluster's current phase.
123
138
  #
124
139
  # @param index [Integer] slot index for this worker in the stats region
125
140
  # @return [void]
@@ -135,6 +150,25 @@ module Raptor
135
150
  # @rbs () -> Symbol
136
151
  def reap_workers: () -> Symbol
137
152
 
153
+ # Stops every worker, escalating from TERM to KILL if any fail to
154
+ # exit within `worker_shutdown_timeout`.
155
+ #
156
+ # @return [void]
157
+ #
158
+ # @rbs () -> void
159
+ def stop_workers: () -> void
160
+
161
+ # Kills workers that have stopped checking in. A booted worker that
162
+ # fails to update its stats slot within `worker_timeout` seconds is
163
+ # assumed to be hung (deadlocked app, runaway loop, blocked syscall);
164
+ # a worker still in startup is held to `worker_boot_timeout`. Killed
165
+ # workers are then restarted by `reap_workers`.
166
+ #
167
+ # @return [void]
168
+ #
169
+ # @rbs () -> void
170
+ def timeout_hung_workers: () -> void
171
+
138
172
  # Replaces each worker process one at a time, waiting for the new
139
173
  # worker to boot before moving on to the next. Triggered by SIGUSR2.
140
174
  #
@@ -150,10 +184,11 @@ module Raptor
150
184
  # critical component fails.
151
185
  #
152
186
  # @param index [Integer] slot index for this worker in the stats region
187
+ # @param phase [Integer] the cluster phase this worker was forked at
153
188
  # @return [void]
154
189
  #
155
- # @rbs (Integer index) -> void
156
- def run_worker: (Integer index) -> void
190
+ # @rbs (Integer index, Integer phase) -> void
191
+ def run_worker: (Integer index, Integer phase) -> void
157
192
 
158
193
  # Returns a human-readable description of how a process exited.
159
194
  #
@@ -170,11 +205,8 @@ module Raptor
170
205
  # @rbs () -> void
171
206
  def shutdown: () -> void
172
207
 
173
- # Logs cluster initialization details including architecture and bind addresses.
174
- #
175
- # Outputs a hierarchical view of the cluster configuration showing
176
- # the master process, worker processes, and per-process thread/ractor
177
- # allocation along with listening addresses.
208
+ # Prints the cluster's startup banner showing process structure
209
+ # and bind addresses.
178
210
  #
179
211
  # @return [void]
180
212
  #
@@ -39,7 +39,8 @@ module Raptor
39
39
  # outbound DATA frames respect RFC 7540 §5.2. Threads dispatching stream
40
40
  # responses call `acquire` to reserve send capacity; threads applying
41
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.
42
+ # replenish it. The connection window and per-stream windows live in
43
+ # separate `Atom`s so the common fast path skips per-stream tracking.
43
44
  class FlowControl
44
45
  ACQUIRE_POLL_INTERVAL: ::Float
45
46