raptor 0.3.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 +21 -0
- data/README.md +28 -25
- data/ext/raptor_http2/raptor_http2.c +1 -0
- data/lib/rackup/handler/raptor.rb +20 -21
- data/lib/raptor/cli.rb +46 -14
- data/lib/raptor/cluster.rb +142 -64
- data/lib/raptor/http2.rb +324 -42
- data/lib/raptor/log.rb +55 -0
- data/lib/raptor/reactor.rb +89 -53
- data/lib/raptor/request.rb +106 -61
- data/lib/raptor/server.rb +125 -51
- data/lib/raptor/stats.rb +30 -26
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cli.rbs +15 -1
- data/sig/generated/raptor/cluster.rbs +70 -38
- data/sig/generated/raptor/http2.rbs +126 -6
- data/sig/generated/raptor/log.rbs +41 -0
- data/sig/generated/raptor/reactor.rbs +44 -25
- data/sig/generated/raptor/request.rbs +36 -22
- data/sig/generated/raptor/server.rbs +63 -26
- data/sig/generated/raptor/stats.rbs +24 -20
- metadata +5 -1
data/lib/raptor/server.rb
CHANGED
|
@@ -6,28 +6,22 @@ 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 performed synchronously
|
|
18
|
-
# before the connection is dispatched.
|
|
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
|
-
# server = Server.new(binder, reactor, thread_pool, request)
|
|
24
|
+
# server = Server.new(binder, reactor, thread_pool, request, client_options: { first_data_timeout: 30 })
|
|
31
25
|
# server.run
|
|
32
26
|
# # ... later
|
|
33
27
|
# server.shutdown
|
|
@@ -35,12 +29,17 @@ 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
|
|
43
41
|
# @rbs @request: Request
|
|
42
|
+
# @rbs @client_options: Hash[Symbol, untyped]
|
|
44
43
|
# @rbs @running: AtomicBoolean
|
|
45
44
|
|
|
46
45
|
# Creates a new Server instance.
|
|
@@ -49,14 +48,16 @@ module Raptor
|
|
|
49
48
|
# @param reactor [Reactor] the reactor for handling client connections
|
|
50
49
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
51
50
|
# @param request [Request] the HTTP/1.1 request handler
|
|
51
|
+
# @param client_options [Hash] client timeout configuration, used to bound TLS handshakes
|
|
52
52
|
# @return [void]
|
|
53
53
|
#
|
|
54
|
-
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request) -> void
|
|
55
|
-
def initialize(binder, reactor, thread_pool, request)
|
|
54
|
+
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request, client_options: Hash[Symbol, untyped]) -> void
|
|
55
|
+
def initialize(binder, reactor, thread_pool, request, client_options:)
|
|
56
56
|
@binder = binder
|
|
57
57
|
@reactor = reactor
|
|
58
58
|
@thread_pool = thread_pool
|
|
59
59
|
@request = request
|
|
60
|
+
@client_options = client_options
|
|
60
61
|
@running = AtomicBoolean.new(true)
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -108,9 +109,11 @@ module Raptor
|
|
|
108
109
|
|
|
109
110
|
# Accepts a connection from the given listener and dispatches it.
|
|
110
111
|
#
|
|
111
|
-
# For SSL
|
|
112
|
-
#
|
|
113
|
-
#
|
|
112
|
+
# For SSL listeners the TLS handshake is offloaded to the thread pool so
|
|
113
|
+
# a slow client cannot block the server thread. For SSL connections with
|
|
114
|
+
# h2 negotiated via ALPN, the server sends initial SETTINGS and adds the
|
|
115
|
+
# connection to the reactor as an HTTP/2 connection. All other connections
|
|
116
|
+
# follow the HTTP/1.1 path.
|
|
114
117
|
#
|
|
115
118
|
# @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
|
|
116
119
|
# @param reactor [Reactor] the reactor to dispatch connections to
|
|
@@ -128,49 +131,120 @@ module Raptor
|
|
|
128
131
|
tcp_client.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
129
132
|
remote_addr = tcp_client.remote_address.ip_address
|
|
130
133
|
else
|
|
131
|
-
remote_addr =
|
|
134
|
+
remote_addr = DEFAULT_REMOTE_ADDR
|
|
132
135
|
end
|
|
133
136
|
|
|
134
|
-
url_scheme = HTTP_SCHEME
|
|
135
|
-
client = tcp_client
|
|
136
|
-
|
|
137
137
|
if listener.is_a?(Binder::SslListener)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
|
|
141
|
-
ssl_socket.sync_close = true
|
|
142
|
-
ssl_socket.accept
|
|
143
|
-
client = ssl_socket
|
|
144
|
-
rescue OpenSSL::SSL::SSLError => error
|
|
145
|
-
warn "SSL handshake failed: #{error.message}"
|
|
146
|
-
tcp_client.close rescue nil
|
|
147
|
-
return
|
|
138
|
+
@thread_pool << proc do
|
|
139
|
+
dispatch_ssl_connection(listener, tcp_client, remote_addr, reactor)
|
|
148
140
|
end
|
|
141
|
+
return
|
|
142
|
+
end
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
@request.eager_accept(
|
|
145
|
+
tcp_client,
|
|
146
|
+
tcp_client.object_id,
|
|
147
|
+
reactor,
|
|
148
|
+
@thread_pool,
|
|
149
|
+
remote_addr,
|
|
150
|
+
HTTP_SCHEME
|
|
151
|
+
)
|
|
152
|
+
end
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
# Performs the TLS handshake for an accepted SSL connection and dispatches
|
|
155
|
+
# it through the HTTP/2 or HTTP/1.1 path. The handshake is bounded by
|
|
156
|
+
# `:first_data_timeout` so a slow client cannot pin a worker thread.
|
|
157
|
+
#
|
|
158
|
+
# @param listener [Binder::SslListener] the SSL listener that accepted the connection
|
|
159
|
+
# @param tcp_client [TCPSocket] the accepted TCP socket
|
|
160
|
+
# @param remote_addr [String] the client's IP address
|
|
161
|
+
# @param reactor [Reactor] the reactor to dispatch the connection to
|
|
162
|
+
# @return [void]
|
|
163
|
+
#
|
|
164
|
+
# @rbs (Binder::SslListener listener, TCPSocket tcp_client, String remote_addr, Reactor reactor) -> void
|
|
165
|
+
def dispatch_ssl_connection(listener, tcp_client, remote_addr, reactor)
|
|
166
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
|
|
167
|
+
ssl_socket.sync_close = true
|
|
168
|
+
return unless perform_ssl_handshake(ssl_socket)
|
|
169
|
+
|
|
170
|
+
if ssl_socket.alpn_protocol == H2_PROTOCOL
|
|
171
|
+
ssl_socket.write(Http2.build_server_settings_frame) rescue nil
|
|
172
|
+
|
|
173
|
+
reactor.add(
|
|
174
|
+
id: ssl_socket.object_id,
|
|
175
|
+
socket: ssl_socket,
|
|
176
|
+
remote_addr: remote_addr,
|
|
177
|
+
url_scheme: HTTPS_SCHEME,
|
|
178
|
+
protocol: :http2,
|
|
179
|
+
writer: Http2::Writer.new,
|
|
180
|
+
flow_control: Http2::FlowControl.new
|
|
181
|
+
)
|
|
161
182
|
|
|
162
|
-
|
|
163
|
-
end
|
|
183
|
+
return
|
|
164
184
|
end
|
|
165
185
|
|
|
166
186
|
@request.eager_accept(
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
ssl_socket,
|
|
188
|
+
ssl_socket.object_id,
|
|
169
189
|
reactor,
|
|
170
190
|
@thread_pool,
|
|
171
191
|
remote_addr,
|
|
172
|
-
|
|
192
|
+
HTTPS_SCHEME
|
|
173
193
|
)
|
|
174
194
|
end
|
|
195
|
+
|
|
196
|
+
# Drives a non-blocking SSL handshake to completion, bounded by the
|
|
197
|
+
# configured first-data timeout. Returns true on success, false on
|
|
198
|
+
# timeout or SSL error.
|
|
199
|
+
#
|
|
200
|
+
# @param ssl_socket [OpenSSL::SSL::SSLSocket] the SSL socket to hand-shake
|
|
201
|
+
# @return [Boolean] true if the handshake completed
|
|
202
|
+
#
|
|
203
|
+
# @rbs (OpenSSL::SSL::SSLSocket ssl_socket) -> bool
|
|
204
|
+
def perform_ssl_handshake(ssl_socket)
|
|
205
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @client_options[:first_data_timeout]
|
|
206
|
+
|
|
207
|
+
begin
|
|
208
|
+
ssl_socket.accept_nonblock
|
|
209
|
+
true
|
|
210
|
+
rescue IO::WaitReadable
|
|
211
|
+
return false unless wait_for_handshake(ssl_socket, deadline, :read)
|
|
212
|
+
|
|
213
|
+
retry
|
|
214
|
+
rescue IO::WaitWritable
|
|
215
|
+
return false unless wait_for_handshake(ssl_socket, deadline, :write)
|
|
216
|
+
|
|
217
|
+
retry
|
|
218
|
+
rescue OpenSSL::SSL::SSLError => error
|
|
219
|
+
Log.rescued_error(error)
|
|
220
|
+
ssl_socket.close rescue nil
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Waits up to `deadline` for the socket to become ready for the next step
|
|
226
|
+
# of the SSL handshake. Closes the socket and returns false on timeout.
|
|
227
|
+
#
|
|
228
|
+
# @param ssl_socket [OpenSSL::SSL::SSLSocket] the SSL socket
|
|
229
|
+
# @param deadline [Float] absolute monotonic deadline
|
|
230
|
+
# @param direction [Symbol] either `:read` or `:write`
|
|
231
|
+
# @return [Boolean] true if the socket became ready before the deadline
|
|
232
|
+
#
|
|
233
|
+
# @rbs (OpenSSL::SSL::SSLSocket ssl_socket, Float deadline, Symbol direction) -> bool
|
|
234
|
+
def wait_for_handshake(ssl_socket, deadline, direction)
|
|
235
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
236
|
+
ready = if remaining <= 0
|
|
237
|
+
false
|
|
238
|
+
elsif direction == :read
|
|
239
|
+
ssl_socket.wait_readable(remaining)
|
|
240
|
+
else
|
|
241
|
+
ssl_socket.wait_writable(remaining)
|
|
242
|
+
end
|
|
243
|
+
return true if ready
|
|
244
|
+
|
|
245
|
+
Log.warn "SSL handshake timed out"
|
|
246
|
+
ssl_socket.close rescue nil
|
|
247
|
+
false
|
|
248
|
+
end
|
|
175
249
|
end
|
|
176
250
|
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
|
|
@@ -20,6 +20,8 @@ module Raptor
|
|
|
20
20
|
|
|
21
21
|
DEFAULT_OPTIONS: untyped
|
|
22
22
|
|
|
23
|
+
DEFAULT_CONFIG_PATHS: untyped
|
|
24
|
+
|
|
23
25
|
# Loads a configuration file and returns the hash it evaluates to.
|
|
24
26
|
#
|
|
25
27
|
# The file is evaluated at the top level so constants like `Raptor::*` resolve
|
|
@@ -33,6 +35,18 @@ module Raptor
|
|
|
33
35
|
# @rbs (String path) -> Hash[Symbol, untyped]
|
|
34
36
|
def self.load_config_file: (String path) -> Hash[Symbol, untyped]
|
|
35
37
|
|
|
38
|
+
# Returns the first existing path in {DEFAULT_CONFIG_PATHS} resolved
|
|
39
|
+
# against `root`, or nil if none exist.
|
|
40
|
+
#
|
|
41
|
+
# Used to pick up a project-local config file when no `-c`/`--config`
|
|
42
|
+
# flag was supplied.
|
|
43
|
+
#
|
|
44
|
+
# @param root [String] directory to resolve the default paths against
|
|
45
|
+
# @return [String, nil] the config path, or nil if no default file exists
|
|
46
|
+
#
|
|
47
|
+
# @rbs (?String root) -> String?
|
|
48
|
+
def self.default_config_path: (?String root) -> String?
|
|
49
|
+
|
|
36
50
|
@command: Symbol
|
|
37
51
|
|
|
38
52
|
@options: Hash[Symbol, untyped]
|
|
@@ -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
|
-
# @option options [String, nil] :
|
|
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
|
#
|