raptor 0.1.0 → 0.3.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/CHANGELOG.md +16 -0
- data/README.md +21 -13
- data/lib/rackup/handler/raptor.rb +102 -0
- data/lib/raptor/binder.rb +1 -0
- data/lib/raptor/cli.rb +84 -1
- data/lib/raptor/cluster.rb +88 -23
- data/lib/raptor/http2.rb +99 -39
- data/lib/raptor/reactor.rb +12 -12
- data/lib/raptor/request.rb +197 -37
- data/lib/raptor/server.rb +22 -13
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +3 -3
- data/sig/generated/rackup/handler/raptor.rbs +40 -0
- data/sig/generated/raptor/cli.rbs +37 -0
- data/sig/generated/raptor/cluster.rbs +32 -3
- data/sig/generated/raptor/http2.rbs +39 -11
- data/sig/generated/raptor/reactor.rbs +7 -5
- data/sig/generated/raptor/request.rbs +72 -8
- data/sig/generated/raptor/server.rbs +13 -7
- data/sig/generated/raptor.rbs +3 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c247b4c221d0f19ff1383ca0a4092a54bc12cd2f945655fa24e1ab49745eb70
|
|
4
|
+
data.tar.gz: cb932a7a048d87eb0913f8c0dbe52750ea4e525e4c4dc32e5fcc531af0daaf41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8e1543054862e8a87c7261884348456da13d4432c3424687b74f506b884283d2773068b6beb10cbe9e209e969b98a918720861b3d0e05bc04d930ba38ec1921
|
|
7
|
+
data.tar.gz: f215cc4a92af86542d49f2c4772f0a65af09988779bbb1c44512ab075d4fb5c0758cb51d9765a0ee9d08fe340d31b42cea1610b021c443818fe40fbdcea63b93
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-25
|
|
4
|
+
|
|
5
|
+
- Load cluster options from a Ruby config file via `--config`
|
|
6
|
+
- Replace workers one at a time on `SIGUSR2` (phased restart)
|
|
7
|
+
- Invoke `:on_error` callback with `(env, exception)` when the Rack app raises
|
|
8
|
+
- Spool request bodies larger than `--body-spool-threshold` to a tempfile
|
|
9
|
+
- Reject HTTP/1.1 requests larger than `--max-body-size` with a 413 response
|
|
10
|
+
- Write pidfile via `--pidfile`
|
|
11
|
+
- Set `SO_REUSEPORT` on TCP listeners where supported
|
|
12
|
+
|
|
13
|
+
## [0.2.0] - 2026-05-25
|
|
14
|
+
|
|
15
|
+
- Add Rack handler for booting via `rackup` or `rails server`
|
|
16
|
+
- Replace the HTTP/2 per-connection write mutex with a lock-free writer
|
|
17
|
+
- Parse the first HTTP/1.1 request inline on the server thread
|
|
18
|
+
|
|
3
19
|
## [0.1.0] - 2026-05-22
|
|
4
20
|
|
|
5
21
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Raptor
|
|
2
2
|
|
|
3
|
-
Raptor is a high-performance, multi-
|
|
4
|
-
and HTTP/2 request processing, native C extensions for HTTP parsing and HPACK
|
|
3
|
+
Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server implementing Rack 3+,
|
|
4
|
+
leveraging Ractors for parallel HTTP/1.1 and HTTP/2 request processing, native C extensions for HTTP parsing and HPACK
|
|
5
|
+
compression, and NIO for non-blocking I/O.
|
|
5
6
|
|
|
6
7
|
## Installation
|
|
7
8
|
|
|
@@ -30,9 +31,9 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
30
31
|
```
|
|
31
32
|
> bundle exec raptor -t 3 -w 4 hello_world.ru
|
|
32
33
|
Raptor Cluster initializing:
|
|
33
|
-
├─ Version: 0.
|
|
34
|
+
├─ Version: 0.3.0
|
|
34
35
|
├─ Ruby Version: ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +YJIT +PRISM [arm64-darwin23]
|
|
35
|
-
├─ Master PID:
|
|
36
|
+
├─ Master PID: 31504
|
|
36
37
|
│ └─ 4 worker processes
|
|
37
38
|
│ ├─ 1 server thread
|
|
38
39
|
│ ├─ 1 reactor thread
|
|
@@ -41,10 +42,10 @@ Raptor Cluster initializing:
|
|
|
41
42
|
│ ├─ 3 worker threads
|
|
42
43
|
│ └─ 1 stats thread
|
|
43
44
|
└─ Listening on 0.0.0.0:9292
|
|
44
|
-
[
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
[
|
|
45
|
+
[31506] Worker 0 booted
|
|
46
|
+
[31507] Worker 1 booted
|
|
47
|
+
[31508] Worker 2 booted
|
|
48
|
+
[31509] Worker 3 booted
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
```
|
|
@@ -52,15 +53,22 @@ Raptor Cluster initializing:
|
|
|
52
53
|
Hello, World!%
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
Also works with `rackup` and `rails server`:
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
```
|
|
59
|
+
> bundle exec rackup -s raptor hello_world.ru
|
|
60
|
+
> bundle exec rails server -u raptor
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## (Micro) Benchmarks
|
|
64
|
+
|
|
65
|
+
Raptor 0.3.0 vs Puma 8.0.1:
|
|
58
66
|
|
|
59
67
|
| Protocol | Raptor | Puma |
|
|
60
68
|
| --------------------- | ------------ | ------------ |
|
|
61
|
-
| HTTP/1.1 |
|
|
62
|
-
| HTTP/1.1 (keep-alive) |
|
|
63
|
-
| HTTP/2 |
|
|
69
|
+
| HTTP/1.1 | 20.3k req/s | 20.8k req/s |
|
|
70
|
+
| HTTP/1.1 (keep-alive) | 60.9k req/s | 45.4k req/s |
|
|
71
|
+
| HTTP/2 | 22.9k req/s | N/A |
|
|
64
72
|
|
|
65
73
|
> Ruby 4.0.4 +YJIT, macOS Apple Silicon. 4 workers, 3 threads, 12 concurrent connections.
|
|
66
74
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "etc"
|
|
5
|
+
|
|
6
|
+
module Rackup
|
|
7
|
+
module Handler
|
|
8
|
+
# Rack handler for booting Raptor through Rackup, `rails server`, or any
|
|
9
|
+
# other host that follows the Rack handler protocol.
|
|
10
|
+
#
|
|
11
|
+
module Raptor
|
|
12
|
+
DEFAULT_OPTIONS = {
|
|
13
|
+
Host: "0.0.0.0",
|
|
14
|
+
Port: 9292
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Boots a Raptor cluster serving the given Rack application.
|
|
18
|
+
#
|
|
19
|
+
# @param app [#call] the Rack application to serve
|
|
20
|
+
# @param options [Hash] handler options provided by Rackup or the host
|
|
21
|
+
# @yield [cluster] the cluster instance, before it starts running
|
|
22
|
+
# @return [void]
|
|
23
|
+
#
|
|
24
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, **untyped options) { (::Raptor::Cluster) -> void } -> void
|
|
25
|
+
def self.run(app, **options)
|
|
26
|
+
require_relative "../../raptor/cli"
|
|
27
|
+
require_relative "../../raptor/cluster"
|
|
28
|
+
|
|
29
|
+
cluster = ::Raptor::Cluster.new(build_cluster_options(app, options))
|
|
30
|
+
|
|
31
|
+
yield cluster if block_given?
|
|
32
|
+
|
|
33
|
+
cluster.run
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the handler-specific options surfaced by `rackup --help`.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash{String => String}] option spec to description mapping
|
|
39
|
+
#
|
|
40
|
+
# @rbs () -> Hash[String, String]
|
|
41
|
+
def self.valid_options
|
|
42
|
+
{
|
|
43
|
+
"Host=HOST" => "Hostname to listen on (default: #{DEFAULT_OPTIONS[:Host]})",
|
|
44
|
+
"Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
|
|
45
|
+
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
46
|
+
"Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
|
|
47
|
+
"Workers=NUM" => "Number of worker processes (default: nprocessors)",
|
|
48
|
+
"Config=PATH" => "Load additional configuration from PATH"
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Builds a Raptor cluster options hash from Rack handler options.
|
|
53
|
+
#
|
|
54
|
+
# Options not explicitly supplied by the user (per the `:user_supplied_options`
|
|
55
|
+
# key) are treated as host-provided defaults.
|
|
56
|
+
#
|
|
57
|
+
# @param app [#call] the Rack application to serve
|
|
58
|
+
# @param options [Hash] handler options provided by Rackup or the host
|
|
59
|
+
# @return [Hash{Symbol => untyped}] cluster configuration
|
|
60
|
+
#
|
|
61
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
62
|
+
def self.build_cluster_options(app, options)
|
|
63
|
+
defaults = DEFAULT_OPTIONS.dup
|
|
64
|
+
|
|
65
|
+
if user_supplied_options = options.delete(:user_supplied_options)
|
|
66
|
+
(options.keys - user_supplied_options).each do |key|
|
|
67
|
+
defaults[key] = options.delete(key)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
cli_defaults = ::Raptor::CLI::DEFAULT_OPTIONS
|
|
72
|
+
config = options[:Config] ? ::Raptor::CLI.load_config_file(options[:Config]) : {}
|
|
73
|
+
|
|
74
|
+
binds = if options[:Host] || options[:Port]
|
|
75
|
+
host = options[:Host] || defaults[:Host]
|
|
76
|
+
port = options[:Port] || defaults[:Port]
|
|
77
|
+
["tcp://#{host}:#{port}"]
|
|
78
|
+
else
|
|
79
|
+
config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
result = {
|
|
83
|
+
app: app,
|
|
84
|
+
binds: binds,
|
|
85
|
+
threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
|
|
86
|
+
ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
|
|
87
|
+
workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
|
|
88
|
+
client: cli_defaults[:client].merge(config[:client] || {})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[:rackup, :on_error, :stats_file, :pidfile].each do |key|
|
|
92
|
+
result[key] = config[key] if config.key?(key)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
private_class_method :build_cluster_options
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
register :raptor, Raptor
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/raptor/binder.rb
CHANGED
|
@@ -173,6 +173,7 @@ module Raptor
|
|
|
173
173
|
|
|
174
174
|
tcp_server = TCPServer.new(host, port)
|
|
175
175
|
tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
176
|
+
tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
|
|
176
177
|
tcp_server.listen SOCKET_BACKLOG
|
|
177
178
|
|
|
178
179
|
[tcp_server]
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -32,14 +32,35 @@ module Raptor
|
|
|
32
32
|
ractors: 1,
|
|
33
33
|
workers: DEFAULT_WORKER_COUNT,
|
|
34
34
|
rackup: "config.ru",
|
|
35
|
-
stats_file: "tmp/raptor.json",
|
|
36
35
|
client: {
|
|
37
36
|
first_data_timeout: 30,
|
|
38
37
|
chunk_data_timeout: 10,
|
|
39
38
|
persistent_data_timeout: 65,
|
|
39
|
+
max_body_size: nil,
|
|
40
|
+
body_spool_threshold: 1024 * 1024,
|
|
40
41
|
},
|
|
42
|
+
stats_file: "tmp/raptor.json",
|
|
43
|
+
pidfile: nil,
|
|
41
44
|
}.freeze
|
|
42
45
|
|
|
46
|
+
# Loads a configuration file and returns the hash it evaluates to.
|
|
47
|
+
#
|
|
48
|
+
# The file is evaluated at the top level so constants like `Raptor::*` resolve
|
|
49
|
+
# the same as in a regular Ruby script. The final expression must be a Hash
|
|
50
|
+
# of cluster options (the same keys accepted by {Raptor::Cluster#initialize}).
|
|
51
|
+
#
|
|
52
|
+
# @param path [String] path to a Ruby file that evaluates to a Hash
|
|
53
|
+
# @return [Hash{Symbol => untyped}] cluster options
|
|
54
|
+
# @raise [ArgumentError] if the file does not evaluate to a Hash
|
|
55
|
+
#
|
|
56
|
+
# @rbs (String path) -> Hash[Symbol, untyped]
|
|
57
|
+
def self.load_config_file(path)
|
|
58
|
+
config = eval(File.read(path), TOPLEVEL_BINDING, path, 1)
|
|
59
|
+
raise ArgumentError, "Config file at #{path.inspect} must return a Hash, got #{config.class}" unless config.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
config
|
|
62
|
+
end
|
|
63
|
+
|
|
43
64
|
# @rbs @command: Symbol
|
|
44
65
|
# @rbs @options: Hash[Symbol, untyped]
|
|
45
66
|
# @rbs @parser: OptionParser
|
|
@@ -70,6 +91,9 @@ module Raptor
|
|
|
70
91
|
end
|
|
71
92
|
@options = DEFAULT_OPTIONS.dup
|
|
72
93
|
@options[:client] = @options[:client].dup
|
|
94
|
+
|
|
95
|
+
apply_config_file(extract_config_path(argv))
|
|
96
|
+
|
|
73
97
|
@parser = create_parser
|
|
74
98
|
@parser.parse!(argv)
|
|
75
99
|
|
|
@@ -111,6 +135,49 @@ module Raptor
|
|
|
111
135
|
end
|
|
112
136
|
end
|
|
113
137
|
|
|
138
|
+
# Scans argv for a `-c`/`--config` flag and returns the configured path.
|
|
139
|
+
#
|
|
140
|
+
# The pre-scan runs before the main OptionParser pass so the config file
|
|
141
|
+
# can be applied as a base layer that CLI args then override. All four
|
|
142
|
+
# OptionParser-accepted forms (`-c PATH`, `-cPATH`, `--config PATH`,
|
|
143
|
+
# `--config=PATH`) are recognized.
|
|
144
|
+
#
|
|
145
|
+
# @param argv [Array<String>] command-line arguments to scan
|
|
146
|
+
# @return [String, nil] the config path, or nil if no flag was supplied
|
|
147
|
+
#
|
|
148
|
+
# @rbs (Array[String] argv) -> String?
|
|
149
|
+
def extract_config_path(argv)
|
|
150
|
+
argv.each_with_index do |arg, i|
|
|
151
|
+
case arg
|
|
152
|
+
when "-c", "--config" then return argv[i + 1]
|
|
153
|
+
when /\A--config=(.*)\z/, /\A-c(.+)\z/ then return Regexp.last_match(1)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Loads a config file and merges it into `@options` over the defaults.
|
|
160
|
+
#
|
|
161
|
+
# Top-level keys replace defaults; the nested `:client` hash is merged
|
|
162
|
+
# key-by-key so a config file does not need to restate every client option.
|
|
163
|
+
#
|
|
164
|
+
# @param path [String, nil] path to the config file, or nil to no-op
|
|
165
|
+
# @return [void]
|
|
166
|
+
#
|
|
167
|
+
# @rbs (String? path) -> void
|
|
168
|
+
def apply_config_file(path)
|
|
169
|
+
return unless path
|
|
170
|
+
|
|
171
|
+
config = self.class.load_config_file(path)
|
|
172
|
+
config.each do |key, value|
|
|
173
|
+
if key == :client && value.is_a?(Hash)
|
|
174
|
+
@options[:client] = @options[:client].merge(value)
|
|
175
|
+
else
|
|
176
|
+
@options[key] = value
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
114
181
|
# Creates the OptionParser instance with all supported command-line options.
|
|
115
182
|
#
|
|
116
183
|
# @return [OptionParser] configured option parser
|
|
@@ -120,6 +187,10 @@ module Raptor
|
|
|
120
187
|
OptionParser.new do |opts|
|
|
121
188
|
opts.banner = "Usage: raptor [options] [rackup file]"
|
|
122
189
|
|
|
190
|
+
opts.on("-c", "--config PATH", String, "Load configuration from PATH") do
|
|
191
|
+
# Loaded in #initialize before parsing so CLI args can override config values
|
|
192
|
+
end
|
|
193
|
+
|
|
123
194
|
opts.on("-b", "--bind URI", String, "Bind address (default: tcp://0.0.0.0:9292)") do |bind|
|
|
124
195
|
if @options[:binds] == DEFAULT_OPTIONS[:binds]
|
|
125
196
|
@options[:binds] = [bind]
|
|
@@ -152,10 +223,22 @@ module Raptor
|
|
|
152
223
|
@options[:client][:persistent_data_timeout] = timeout
|
|
153
224
|
end
|
|
154
225
|
|
|
226
|
+
opts.on("--max-body-size BYTES", Integer, "Maximum request body size in bytes (default: unlimited)") do |bytes|
|
|
227
|
+
@options[:client][:max_body_size] = bytes
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
opts.on("--body-spool-threshold BYTES", Integer, "Spool request bodies larger than this to a tempfile (default: #{1024 * 1024})") do |bytes|
|
|
231
|
+
@options[:client][:body_spool_threshold] = bytes
|
|
232
|
+
end
|
|
233
|
+
|
|
155
234
|
opts.on("--stats-file PATH", String, "Stats file path (default: tmp/raptor.json)") do |path|
|
|
156
235
|
@options[:stats_file] = path
|
|
157
236
|
end
|
|
158
237
|
|
|
238
|
+
opts.on("--pidfile PATH", String, "Pidfile path (default: none)") do |path|
|
|
239
|
+
@options[:pidfile] = path
|
|
240
|
+
end
|
|
241
|
+
|
|
159
242
|
opts.on("--help", "Show this help") do
|
|
160
243
|
puts opts
|
|
161
244
|
exit
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -58,13 +58,17 @@ module Raptor
|
|
|
58
58
|
# @rbs @ractor_count: Integer
|
|
59
59
|
# @rbs @worker_count: Integer
|
|
60
60
|
# @rbs @client_options: Hash[Symbol, Integer]
|
|
61
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
62
|
+
# @rbs @stats_file: String?
|
|
63
|
+
# @rbs @pidfile: String?
|
|
61
64
|
# @rbs @binder: Binder
|
|
62
65
|
# @rbs @server_port: Integer
|
|
63
66
|
# @rbs @app: untyped
|
|
64
67
|
# @rbs @shutdown: bool
|
|
65
68
|
# @rbs @workers: Hash[Integer, Integer]
|
|
66
69
|
# @rbs @stats: Stats
|
|
67
|
-
# @rbs @
|
|
70
|
+
# @rbs @phased_restart_requested: bool
|
|
71
|
+
# @rbs @phased_restarting: bool
|
|
68
72
|
|
|
69
73
|
# Creates a new Cluster with the specified configuration.
|
|
70
74
|
#
|
|
@@ -77,8 +81,12 @@ module Raptor
|
|
|
77
81
|
# @option options [Integer] :ractors number of ractors per worker process
|
|
78
82
|
# @option options [Integer] :workers number of worker processes
|
|
79
83
|
# @option options [Array<String>] :binds array of bind URIs
|
|
84
|
+
# @option options [#call] :app pre-built Rack application
|
|
80
85
|
# @option options [String] :rackup path to Rack configuration file
|
|
81
|
-
# @option options [Hash] :client client
|
|
86
|
+
# @option options [Hash] :client client configuration
|
|
87
|
+
# @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
|
|
88
|
+
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
89
|
+
# @option options [String, nil] :pidfile path to write the master PID to, or nil to disable
|
|
82
90
|
# @return [void]
|
|
83
91
|
#
|
|
84
92
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
@@ -87,23 +95,28 @@ module Raptor
|
|
|
87
95
|
@ractor_count = options[:ractors]
|
|
88
96
|
@worker_count = options[:workers]
|
|
89
97
|
@client_options = options[:client]
|
|
98
|
+
@on_error = options[:on_error]
|
|
99
|
+
@stats_file = options[:stats_file]
|
|
100
|
+
@pidfile = options[:pidfile]
|
|
90
101
|
|
|
91
102
|
@binder = Binder.new(options[:binds])
|
|
92
103
|
@server_port = @binder.server_port
|
|
93
|
-
@app = Rack::Builder.parse_file(options[:rackup])
|
|
104
|
+
@app = options[:app] || Rack::Builder.parse_file(options[:rackup])
|
|
94
105
|
log_initialization
|
|
95
106
|
|
|
96
107
|
@shutdown = false
|
|
97
108
|
@workers = {}
|
|
98
109
|
@stats = Stats.new(@worker_count)
|
|
99
|
-
@
|
|
110
|
+
@phased_restart_requested = false
|
|
111
|
+
@phased_restarting = false
|
|
100
112
|
end
|
|
101
113
|
|
|
102
114
|
# Starts the multi-process cluster and manages worker processes.
|
|
103
115
|
#
|
|
104
116
|
# Forks the configured number of worker processes and monitors them,
|
|
105
117
|
# automatically restarting any that exit unexpectedly. Handles graceful
|
|
106
|
-
# shutdown via INT or TERM signals,
|
|
118
|
+
# shutdown via INT or TERM signals, stats logging via USR1, and phased
|
|
119
|
+
# restart via USR2.
|
|
107
120
|
#
|
|
108
121
|
# Each worker process includes:
|
|
109
122
|
# - 1 server thread (continuously accepts connections with backpressure control)
|
|
@@ -120,6 +133,9 @@ module Raptor
|
|
|
120
133
|
trap("INT") { shutdown }
|
|
121
134
|
trap("TERM") { shutdown }
|
|
122
135
|
trap("USR1") { log_stats }
|
|
136
|
+
trap("USR2") { @phased_restart_requested = true }
|
|
137
|
+
|
|
138
|
+
File.open(@pidfile, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pidfile
|
|
123
139
|
|
|
124
140
|
@worker_count.times { |index| spawn_worker(index) }
|
|
125
141
|
|
|
@@ -132,29 +148,18 @@ module Raptor
|
|
|
132
148
|
end
|
|
133
149
|
|
|
134
150
|
until @shutdown
|
|
135
|
-
|
|
136
|
-
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
137
|
-
rescue Errno::ECHILD
|
|
138
|
-
break
|
|
139
|
-
end
|
|
151
|
+
break if reap_workers == :no_children
|
|
140
152
|
|
|
141
|
-
if
|
|
142
|
-
index = @workers.key(pid)
|
|
143
|
-
@workers.delete(index)
|
|
153
|
+
perform_phased_restart if @phased_restart_requested && !@phased_restarting
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
warn "[#{Process.pid}] Restarting worker #{index} (#{pid}), #{exit_description(status)}"
|
|
147
|
-
spawn_worker(index)
|
|
148
|
-
end
|
|
149
|
-
else
|
|
150
|
-
sleep 0.1
|
|
151
|
-
end
|
|
155
|
+
sleep 0.1
|
|
152
156
|
end
|
|
153
157
|
|
|
154
158
|
@workers.values.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
155
159
|
@workers.values.each { |pid| Process.wait(pid) rescue nil }
|
|
156
160
|
stats_file_thread&.join
|
|
157
161
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
162
|
+
File.delete(@pidfile) rescue nil if @pidfile
|
|
158
163
|
@stats.unmap
|
|
159
164
|
end
|
|
160
165
|
|
|
@@ -181,6 +186,66 @@ module Raptor
|
|
|
181
186
|
@workers[index] = pid
|
|
182
187
|
end
|
|
183
188
|
|
|
189
|
+
# Reaps any worker processes that have exited, respawning each one
|
|
190
|
+
# unless the cluster is shutting down.
|
|
191
|
+
#
|
|
192
|
+
# @return [Symbol] :no_children when there are no remaining children, otherwise :reaped
|
|
193
|
+
#
|
|
194
|
+
# @rbs () -> Symbol
|
|
195
|
+
def reap_workers
|
|
196
|
+
loop do
|
|
197
|
+
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
198
|
+
return :reaped unless pid
|
|
199
|
+
|
|
200
|
+
index = @workers.key(pid)
|
|
201
|
+
@workers.delete(index)
|
|
202
|
+
|
|
203
|
+
unless @shutdown
|
|
204
|
+
warn "[#{Process.pid}] Restarting worker #{index} (#{pid}), #{exit_description(status)}"
|
|
205
|
+
spawn_worker(index)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
rescue Errno::ECHILD
|
|
209
|
+
:no_children
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Replaces each worker process one at a time, waiting for the new
|
|
213
|
+
# worker to boot before moving on to the next. Triggered by SIGUSR2.
|
|
214
|
+
#
|
|
215
|
+
# @return [void]
|
|
216
|
+
#
|
|
217
|
+
# @rbs () -> void
|
|
218
|
+
def perform_phased_restart
|
|
219
|
+
@phased_restart_requested = false
|
|
220
|
+
@phased_restarting = true
|
|
221
|
+
puts "[#{Process.pid}] Phased restart starting"
|
|
222
|
+
|
|
223
|
+
begin
|
|
224
|
+
@workers.keys.sort.each do |index|
|
|
225
|
+
return if @shutdown
|
|
226
|
+
|
|
227
|
+
target_pid = @workers[index]
|
|
228
|
+
next unless target_pid
|
|
229
|
+
|
|
230
|
+
Process.kill("TERM", target_pid) rescue nil
|
|
231
|
+
|
|
232
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 60
|
|
233
|
+
until @shutdown
|
|
234
|
+
reap_workers
|
|
235
|
+
current = @workers[index]
|
|
236
|
+
break if current && current != target_pid && @stats.all[index][:booted]
|
|
237
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
238
|
+
|
|
239
|
+
sleep 0.1
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
puts "[#{Process.pid}] Phased restart complete"
|
|
244
|
+
ensure
|
|
245
|
+
@phased_restarting = false
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
184
249
|
# Runs the full server stack inside a worker process.
|
|
185
250
|
#
|
|
186
251
|
# Sets up and coordinates the reactor, server, ractor pool, thread pool,
|
|
@@ -216,8 +281,8 @@ module Raptor
|
|
|
216
281
|
@app.call(env)
|
|
217
282
|
}
|
|
218
283
|
thread_pool = AtomicThreadPool.new(name: "Raptor Workers", size: @thread_count)
|
|
219
|
-
request = Request.new(counting_app, @server_port)
|
|
220
|
-
http2 = Http2.new(counting_app, @server_port)
|
|
284
|
+
request = Request.new(counting_app, @server_port, client_options: @client_options, on_error: @on_error)
|
|
285
|
+
http2 = Http2.new(counting_app, @server_port, on_error: @on_error)
|
|
221
286
|
ractor_pool = RactorPool.new(
|
|
222
287
|
name: "Raptor Pipeline Workers",
|
|
223
288
|
size: @ractor_count,
|
|
@@ -233,7 +298,7 @@ module Raptor
|
|
|
233
298
|
reactor = Reactor.new(thread_pool, ractor_pool, client_options: @client_options)
|
|
234
299
|
reactor_thread = reactor.run
|
|
235
300
|
|
|
236
|
-
server = Server.new(@binder, reactor, thread_pool)
|
|
301
|
+
server = Server.new(@binder, reactor, thread_pool, request)
|
|
237
302
|
server_thread = server.run
|
|
238
303
|
|
|
239
304
|
puts "[#{Process.pid}] Worker #{index} booted"
|