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.
- checksums.yaml +4 -4
- data/.mise.toml +2 -0
- data/Brewfile +2 -0
- data/CHANGELOG.md +8 -0
- data/README.md +27 -24
- data/lib/rackup/handler/raptor.rb +18 -20
- data/lib/raptor/cli.rb +26 -10
- data/lib/raptor/cluster.rb +128 -56
- data/lib/raptor/http2.rb +4 -3
- data/lib/raptor/log.rb +55 -0
- data/lib/raptor/reactor.rb +25 -29
- data/lib/raptor/request.rb +10 -16
- data/lib/raptor/server.rb +17 -19
- data/lib/raptor/stats.rb +30 -26
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cli.rbs +1 -1
- data/sig/generated/raptor/cluster.rbs +69 -37
- data/sig/generated/raptor/http2.rbs +2 -1
- data/sig/generated/raptor/log.rbs +41 -0
- data/sig/generated/raptor/reactor.rbs +23 -26
- data/sig/generated/raptor/request.rbs +6 -13
- data/sig/generated/raptor/server.rbs +14 -16
- data/sig/generated/raptor/stats.rbs +24 -20
- metadata +5 -1
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
|
data/lib/raptor/reactor.rb
CHANGED
|
@@ -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
|
|
13
|
-
# pools for
|
|
14
|
-
#
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
#
|
|
34
|
+
# Semantic alias for the inherited `data` slot.
|
|
37
35
|
#
|
|
38
|
-
# @return [Hash] the client connection state
|
|
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
|
-
#
|
|
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]
|
|
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
|
-
#
|
|
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
|
|
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
|
|
92
|
-
def initialize(
|
|
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
|
-
|
|
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
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
#
|
data/lib/raptor/request.rb
CHANGED
|
@@ -11,13 +11,10 @@ require "rack"
|
|
|
11
11
|
require_relative "raptor_http"
|
|
12
12
|
|
|
13
13
|
module Raptor
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
|
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] ||
|
|
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:
|
|
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] ||=
|
|
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
|
-
#
|
|
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
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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 = "
|
|
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
|
-
#
|
|
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
|
|
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
|
|
88
|
-
def read(
|
|
89
|
-
data = @mmap[
|
|
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
|
data/lib/raptor/version.rb
CHANGED
|
@@ -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,
|
|
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
|
|
8
|
-
#
|
|
9
|
-
# forking, signal management, graceful shutdown, and
|
|
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
|
|
14
|
-
# the combination of
|
|
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
|
-
#
|
|
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
|
-
@
|
|
41
|
+
@thread_count: Integer
|
|
41
42
|
|
|
42
|
-
@
|
|
43
|
+
@client_options: Hash[Symbol, Integer]
|
|
43
44
|
|
|
44
|
-
@
|
|
45
|
+
@worker_timeout: Integer
|
|
45
46
|
|
|
46
|
-
@
|
|
47
|
+
@worker_boot_timeout: Integer
|
|
47
48
|
|
|
48
|
-
@
|
|
49
|
+
@worker_shutdown_timeout: Integer
|
|
49
50
|
|
|
50
|
-
@
|
|
51
|
+
@stats_file: String?
|
|
51
52
|
|
|
52
|
-
@
|
|
53
|
+
@pid_file: String?
|
|
54
|
+
|
|
55
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
53
56
|
|
|
54
57
|
@binder: Binder
|
|
55
58
|
|
|
56
|
-
@
|
|
59
|
+
@server_port: Integer
|
|
57
60
|
|
|
58
|
-
@
|
|
61
|
+
@app: untyped
|
|
59
62
|
|
|
60
|
-
@
|
|
63
|
+
@shutdown: bool
|
|
61
64
|
|
|
62
|
-
@
|
|
65
|
+
@workers: Hash[Integer, Integer]
|
|
63
66
|
|
|
64
|
-
@
|
|
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
|
-
@
|
|
79
|
+
@worker_count: Integer
|
|
69
80
|
|
|
70
81
|
# Creates a new Cluster with the specified configuration.
|
|
71
82
|
#
|
|
72
|
-
# Initializes the cluster with
|
|
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 [
|
|
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
|
-
#
|
|
96
|
-
# shutdown via INT or TERM signals, stats logging via USR1,
|
|
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
|
|
103
|
-
# - 1
|
|
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
|
-
#
|
|
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.
|
|
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
|
|