raptor 0.2.0 → 0.4.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 +23 -0
- data/README.md +16 -16
- data/ext/raptor_http2/raptor_http2.c +1 -0
- data/lib/rackup/handler/raptor.rb +24 -11
- data/lib/raptor/binder.rb +1 -0
- data/lib/raptor/cli.rb +100 -1
- data/lib/raptor/cluster.rb +96 -26
- data/lib/raptor/http2.rb +333 -44
- data/lib/raptor/reactor.rb +67 -27
- data/lib/raptor/request.rb +196 -69
- data/lib/raptor/server.rb +112 -36
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +3 -3
- data/sig/generated/raptor/cli.rbs +51 -0
- data/sig/generated/raptor/cluster.rbs +31 -3
- data/sig/generated/raptor/http2.rbs +130 -8
- data/sig/generated/raptor/reactor.rbs +22 -0
- data/sig/generated/raptor/request.rbs +75 -22
- data/sig/generated/raptor/server.rbs +51 -12
- data/sig/generated/raptor.rbs +3 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9d82dc0869e270278088d312d03a6ed914e494da5fb6b7bf6e41c03257ddf41
|
|
4
|
+
data.tar.gz: c113ab336b5ad9e5e72095e79af4702b5adb0efb46117ba43b6879fe2e4a728c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 365e09dbfcc7cbb265b8ac6569a66e66a0629ecfb4fd176164cb4c55b6f0223ef05f3e17f3318f7ec6cd58be94396996885b232c496e46620f42049ca9a1fe84
|
|
7
|
+
data.tar.gz: d6d2b9b18cd0da790c2f13fce8994f799bac41e94270f9c69f4b57241f6931d98a8ef81573e8257d1766dfde0a63807d2f06cf57fa4a44ef8f754533b733aeff
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-29
|
|
4
|
+
|
|
5
|
+
- Load `raptor.rb` or `config/raptor.rb` by default when no config path is supplied
|
|
6
|
+
- Honour the peer's HTTP/2 flow-control windows when sending `DATA` frames
|
|
7
|
+
- Assemble HEADERS across `CONTINUATION` frames
|
|
8
|
+
- Validate HTTP/2 stream IDs and emit `GOAWAY` on protocol errors
|
|
9
|
+
- Offload TLS handshakes to the thread pool to keep the server thread responsive
|
|
10
|
+
- Exit eager keep-alive loops on cluster shutdown
|
|
11
|
+
- Apply the write timeout to HTTP/2 frame writes
|
|
12
|
+
- Reject HPACK dynamic table size updates larger than 4096 bytes
|
|
13
|
+
- Reject malformed HTTP/1.1 requests with a 400 response
|
|
14
|
+
- Rescue unexpected errors in the reactor and pipeline collector
|
|
15
|
+
|
|
16
|
+
## [0.3.0] - 2026-05-25
|
|
17
|
+
|
|
18
|
+
- Load cluster options from a Ruby config file via `--config`
|
|
19
|
+
- Replace workers one at a time on `SIGUSR2` (phased restart)
|
|
20
|
+
- Invoke `:on_error` callback with `(env, exception)` when the Rack app raises
|
|
21
|
+
- Spool request bodies larger than `--body-spool-threshold` to a tempfile
|
|
22
|
+
- Reject HTTP/1.1 requests larger than `--max-body-size` with a 413 response
|
|
23
|
+
- Write pidfile via `--pidfile`
|
|
24
|
+
- Set `SO_REUSEPORT` on TCP listeners where supported
|
|
25
|
+
|
|
3
26
|
## [0.2.0] - 2026-05-25
|
|
4
27
|
|
|
5
28
|
- Add Rack handler for booting via `rackup` or `rails server`
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Raptor
|
|
2
2
|
|
|
3
|
-
Raptor is a high-performance, multi-
|
|
4
|
-
HTTP/1.1 and HTTP/2 request processing, native C extensions for HTTP parsing and HPACK
|
|
5
|
-
non-blocking I/O.
|
|
3
|
+
Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server implementing Rack 3.2+,
|
|
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.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -31,9 +31,9 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
31
31
|
```
|
|
32
32
|
> bundle exec raptor -t 3 -w 4 hello_world.ru
|
|
33
33
|
Raptor Cluster initializing:
|
|
34
|
-
├─ Version: 0.
|
|
35
|
-
├─ Ruby Version: ruby 4.0.
|
|
36
|
-
├─ Master PID:
|
|
34
|
+
├─ Version: 0.4.0
|
|
35
|
+
├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
36
|
+
├─ Master PID: 26456
|
|
37
37
|
│ └─ 4 worker processes
|
|
38
38
|
│ ├─ 1 server thread
|
|
39
39
|
│ ├─ 1 reactor thread
|
|
@@ -42,10 +42,10 @@ Raptor Cluster initializing:
|
|
|
42
42
|
│ ├─ 3 worker threads
|
|
43
43
|
│ └─ 1 stats thread
|
|
44
44
|
└─ Listening on 0.0.0.0:9292
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
[
|
|
48
|
-
[
|
|
45
|
+
[26459] Worker 0 booted
|
|
46
|
+
[26460] Worker 1 booted
|
|
47
|
+
[26461] Worker 2 booted
|
|
48
|
+
[26462] Worker 3 booted
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
```
|
|
@@ -60,17 +60,17 @@ Also works with `rackup` and `rails server`:
|
|
|
60
60
|
> bundle exec rails server -u raptor
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
## Benchmarks
|
|
63
|
+
## (Micro) Benchmarks
|
|
64
64
|
|
|
65
|
-
Raptor 0.
|
|
65
|
+
Raptor 0.4.0 vs Puma 8.0.1:
|
|
66
66
|
|
|
67
67
|
| Protocol | Raptor | Puma |
|
|
68
68
|
| --------------------- | ------------ | ------------ |
|
|
69
|
-
| HTTP/1.1 |
|
|
70
|
-
| HTTP/1.1 (keep-alive) |
|
|
71
|
-
| HTTP/2 |
|
|
69
|
+
| HTTP/1.1 | ~20k req/s | ~21k req/s |
|
|
70
|
+
| HTTP/1.1 (keep-alive) | ~60k req/s | ~45k req/s |
|
|
71
|
+
| HTTP/2 | ~23k req/s | N/A |
|
|
72
72
|
|
|
73
|
-
> Ruby 4.0.
|
|
73
|
+
> Ruby 4.0.5 +YJIT, macOS Apple Silicon. 4 workers, 3 threads, 12 concurrent connections.
|
|
74
74
|
|
|
75
75
|
## Development
|
|
76
76
|
|
|
@@ -457,6 +457,7 @@ static int hpack_decode_header_block(const uint8_t *buf, size_t len,
|
|
|
457
457
|
/* dynamic table size update (RFC 7541 6.3) */
|
|
458
458
|
uint64_t new_size;
|
|
459
459
|
if (hpack_decode_int(buf, len, &pos, 5, &new_size) < 0) return -1;
|
|
460
|
+
if (new_size > HTTP2_DEFAULT_HEADER_TABLE_SIZE) return -1;
|
|
460
461
|
*max_table_size = (long)new_size;
|
|
461
462
|
*dyn_table = dynamic_table_evict(*dyn_table, *max_table_size);
|
|
462
463
|
|
|
@@ -44,7 +44,8 @@ module Rackup
|
|
|
44
44
|
"Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
|
|
45
45
|
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
46
46
|
"Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
|
|
47
|
-
"Workers=NUM" => "Number of worker processes (default: nprocessors)"
|
|
47
|
+
"Workers=NUM" => "Number of worker processes (default: nprocessors)",
|
|
48
|
+
"Config=PATH" => "Load additional configuration from PATH"
|
|
48
49
|
}
|
|
49
50
|
end
|
|
50
51
|
|
|
@@ -67,20 +68,32 @@ module Rackup
|
|
|
67
68
|
end
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
host = options[:Host] || defaults[:Host]
|
|
71
|
-
port = options[:Port] || defaults[:Port]
|
|
72
|
-
|
|
73
71
|
cli_defaults = ::Raptor::CLI::DEFAULT_OPTIONS
|
|
72
|
+
config_path = options[:Config] || ::Raptor::CLI.default_config_path
|
|
73
|
+
config = config_path ? ::Raptor::CLI.load_config_file(config_path) : {}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
binds = if options[:Host] || options[:Port]
|
|
76
|
+
host = options[:Host] || defaults[:Host]
|
|
77
|
+
port = options[:Port] || defaults[:Port]
|
|
78
|
+
["tcp://#{host}:#{port}"]
|
|
79
|
+
else
|
|
80
|
+
config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result = {
|
|
76
84
|
app: app,
|
|
77
|
-
binds:
|
|
78
|
-
threads: (options[:Threads] || cli_defaults[:threads]).to_i,
|
|
79
|
-
ractors: (options[:Ractors] || cli_defaults[:ractors]).to_i,
|
|
80
|
-
workers: (options[:Workers] || Etc.nprocessors).to_i,
|
|
81
|
-
client: cli_defaults[:client]
|
|
82
|
-
stats_file: nil
|
|
85
|
+
binds: binds,
|
|
86
|
+
threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
|
|
87
|
+
ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
|
|
88
|
+
workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
|
|
89
|
+
client: cli_defaults[:client].merge(config[:client] || {})
|
|
83
90
|
}
|
|
91
|
+
|
|
92
|
+
[:rackup, :on_error, :stats_file, :pid_file].each do |key|
|
|
93
|
+
result[key] = config[key] if config.key?(key)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result
|
|
84
97
|
end
|
|
85
98
|
private_class_method :build_cluster_options
|
|
86
99
|
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,51 @@ 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
|
+
pid_file: nil,
|
|
41
44
|
}.freeze
|
|
42
45
|
|
|
46
|
+
DEFAULT_CONFIG_PATHS = ["raptor.rb", "config/raptor.rb"].freeze
|
|
47
|
+
|
|
48
|
+
# Loads a configuration file and returns the hash it evaluates to.
|
|
49
|
+
#
|
|
50
|
+
# The file is evaluated at the top level so constants like `Raptor::*` resolve
|
|
51
|
+
# the same as in a regular Ruby script. The final expression must be a Hash
|
|
52
|
+
# of cluster options (the same keys accepted by {Raptor::Cluster#initialize}).
|
|
53
|
+
#
|
|
54
|
+
# @param path [String] path to a Ruby file that evaluates to a Hash
|
|
55
|
+
# @return [Hash{Symbol => untyped}] cluster options
|
|
56
|
+
# @raise [ArgumentError] if the file does not evaluate to a Hash
|
|
57
|
+
#
|
|
58
|
+
# @rbs (String path) -> Hash[Symbol, untyped]
|
|
59
|
+
def self.load_config_file(path)
|
|
60
|
+
config = eval(File.read(path), TOPLEVEL_BINDING, path, 1)
|
|
61
|
+
raise ArgumentError, "Config file at #{path.inspect} must return a Hash, got #{config.class}" unless config.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
config
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the first existing path in {DEFAULT_CONFIG_PATHS} resolved
|
|
67
|
+
# against `root`, or nil if none exist.
|
|
68
|
+
#
|
|
69
|
+
# Used to pick up a project-local config file when no `-c`/`--config`
|
|
70
|
+
# flag was supplied.
|
|
71
|
+
#
|
|
72
|
+
# @param root [String] directory to resolve the default paths against
|
|
73
|
+
# @return [String, nil] the config path, or nil if no default file exists
|
|
74
|
+
#
|
|
75
|
+
# @rbs (?String root) -> String?
|
|
76
|
+
def self.default_config_path(root = Dir.pwd)
|
|
77
|
+
DEFAULT_CONFIG_PATHS.find { |path| File.exist?(File.join(root, path)) }
|
|
78
|
+
end
|
|
79
|
+
|
|
43
80
|
# @rbs @command: Symbol
|
|
44
81
|
# @rbs @options: Hash[Symbol, untyped]
|
|
45
82
|
# @rbs @parser: OptionParser
|
|
@@ -70,6 +107,9 @@ module Raptor
|
|
|
70
107
|
end
|
|
71
108
|
@options = DEFAULT_OPTIONS.dup
|
|
72
109
|
@options[:client] = @options[:client].dup
|
|
110
|
+
|
|
111
|
+
apply_config_file(extract_config_path(argv) || self.class.default_config_path)
|
|
112
|
+
|
|
73
113
|
@parser = create_parser
|
|
74
114
|
@parser.parse!(argv)
|
|
75
115
|
|
|
@@ -111,6 +151,49 @@ module Raptor
|
|
|
111
151
|
end
|
|
112
152
|
end
|
|
113
153
|
|
|
154
|
+
# Scans argv for a `-c`/`--config` flag and returns the configured path.
|
|
155
|
+
#
|
|
156
|
+
# The pre-scan runs before the main OptionParser pass so the config file
|
|
157
|
+
# can be applied as a base layer that CLI args then override. All four
|
|
158
|
+
# OptionParser-accepted forms (`-c PATH`, `-cPATH`, `--config PATH`,
|
|
159
|
+
# `--config=PATH`) are recognized.
|
|
160
|
+
#
|
|
161
|
+
# @param argv [Array<String>] command-line arguments to scan
|
|
162
|
+
# @return [String, nil] the config path, or nil if no flag was supplied
|
|
163
|
+
#
|
|
164
|
+
# @rbs (Array[String] argv) -> String?
|
|
165
|
+
def extract_config_path(argv)
|
|
166
|
+
argv.each_with_index do |arg, i|
|
|
167
|
+
case arg
|
|
168
|
+
when "-c", "--config" then return argv[i + 1]
|
|
169
|
+
when /\A--config=(.*)\z/, /\A-c(.+)\z/ then return Regexp.last_match(1)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Loads a config file and merges it into `@options` over the defaults.
|
|
176
|
+
#
|
|
177
|
+
# Top-level keys replace defaults; the nested `:client` hash is merged
|
|
178
|
+
# key-by-key so a config file does not need to restate every client option.
|
|
179
|
+
#
|
|
180
|
+
# @param path [String, nil] path to the config file, or nil to no-op
|
|
181
|
+
# @return [void]
|
|
182
|
+
#
|
|
183
|
+
# @rbs (String? path) -> void
|
|
184
|
+
def apply_config_file(path)
|
|
185
|
+
return unless path
|
|
186
|
+
|
|
187
|
+
config = self.class.load_config_file(path)
|
|
188
|
+
config.each do |key, value|
|
|
189
|
+
if key == :client && value.is_a?(Hash)
|
|
190
|
+
@options[:client] = @options[:client].merge(value)
|
|
191
|
+
else
|
|
192
|
+
@options[key] = value
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
114
197
|
# Creates the OptionParser instance with all supported command-line options.
|
|
115
198
|
#
|
|
116
199
|
# @return [OptionParser] configured option parser
|
|
@@ -120,6 +203,10 @@ module Raptor
|
|
|
120
203
|
OptionParser.new do |opts|
|
|
121
204
|
opts.banner = "Usage: raptor [options] [rackup file]"
|
|
122
205
|
|
|
206
|
+
opts.on("-c", "--config PATH", String, "Load configuration from PATH") do
|
|
207
|
+
# Loaded in #initialize before parsing so CLI args can override config values
|
|
208
|
+
end
|
|
209
|
+
|
|
123
210
|
opts.on("-b", "--bind URI", String, "Bind address (default: tcp://0.0.0.0:9292)") do |bind|
|
|
124
211
|
if @options[:binds] == DEFAULT_OPTIONS[:binds]
|
|
125
212
|
@options[:binds] = [bind]
|
|
@@ -152,10 +239,22 @@ module Raptor
|
|
|
152
239
|
@options[:client][:persistent_data_timeout] = timeout
|
|
153
240
|
end
|
|
154
241
|
|
|
242
|
+
opts.on("--max-body-size BYTES", Integer, "Maximum request body size in bytes (default: unlimited)") do |bytes|
|
|
243
|
+
@options[:client][:max_body_size] = bytes
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
opts.on("--body-spool-threshold BYTES", Integer, "Spool request bodies larger than this to a tempfile (default: #{1024 * 1024})") do |bytes|
|
|
247
|
+
@options[:client][:body_spool_threshold] = bytes
|
|
248
|
+
end
|
|
249
|
+
|
|
155
250
|
opts.on("--stats-file PATH", String, "Stats file path (default: tmp/raptor.json)") do |path|
|
|
156
251
|
@options[:stats_file] = path
|
|
157
252
|
end
|
|
158
253
|
|
|
254
|
+
opts.on("--pid-file PATH", String, "PID file path (default: none)") do |path|
|
|
255
|
+
@options[:pid_file] = path
|
|
256
|
+
end
|
|
257
|
+
|
|
159
258
|
opts.on("--help", "Show this help") do
|
|
160
259
|
puts opts
|
|
161
260
|
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 @pid_file: 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
|
#
|
|
@@ -79,7 +83,10 @@ module Raptor
|
|
|
79
83
|
# @option options [Array<String>] :binds array of bind URIs
|
|
80
84
|
# @option options [#call] :app pre-built Rack application
|
|
81
85
|
# @option options [String] :rackup path to Rack configuration file
|
|
82
|
-
# @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] :pid_file path to write the master PID to, or nil to disable
|
|
83
90
|
# @return [void]
|
|
84
91
|
#
|
|
85
92
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
@@ -88,6 +95,9 @@ module Raptor
|
|
|
88
95
|
@ractor_count = options[:ractors]
|
|
89
96
|
@worker_count = options[:workers]
|
|
90
97
|
@client_options = options[:client]
|
|
98
|
+
@on_error = options[:on_error]
|
|
99
|
+
@stats_file = options[:stats_file]
|
|
100
|
+
@pid_file = options[:pid_file]
|
|
91
101
|
|
|
92
102
|
@binder = Binder.new(options[:binds])
|
|
93
103
|
@server_port = @binder.server_port
|
|
@@ -97,14 +107,16 @@ module Raptor
|
|
|
97
107
|
@shutdown = false
|
|
98
108
|
@workers = {}
|
|
99
109
|
@stats = Stats.new(@worker_count)
|
|
100
|
-
@
|
|
110
|
+
@phased_restart_requested = false
|
|
111
|
+
@phased_restarting = false
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
# Starts the multi-process cluster and manages worker processes.
|
|
104
115
|
#
|
|
105
116
|
# Forks the configured number of worker processes and monitors them,
|
|
106
117
|
# automatically restarting any that exit unexpectedly. Handles graceful
|
|
107
|
-
# shutdown via INT or TERM signals,
|
|
118
|
+
# shutdown via INT or TERM signals, stats logging via USR1, and phased
|
|
119
|
+
# restart via USR2.
|
|
108
120
|
#
|
|
109
121
|
# Each worker process includes:
|
|
110
122
|
# - 1 server thread (continuously accepts connections with backpressure control)
|
|
@@ -121,6 +133,9 @@ module Raptor
|
|
|
121
133
|
trap("INT") { shutdown }
|
|
122
134
|
trap("TERM") { shutdown }
|
|
123
135
|
trap("USR1") { log_stats }
|
|
136
|
+
trap("USR2") { @phased_restart_requested = true }
|
|
137
|
+
|
|
138
|
+
File.open(@pid_file, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pid_file
|
|
124
139
|
|
|
125
140
|
@worker_count.times { |index| spawn_worker(index) }
|
|
126
141
|
|
|
@@ -133,29 +148,18 @@ module Raptor
|
|
|
133
148
|
end
|
|
134
149
|
|
|
135
150
|
until @shutdown
|
|
136
|
-
|
|
137
|
-
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
138
|
-
rescue Errno::ECHILD
|
|
139
|
-
break
|
|
140
|
-
end
|
|
151
|
+
break if reap_workers == :no_children
|
|
141
152
|
|
|
142
|
-
if
|
|
143
|
-
index = @workers.key(pid)
|
|
144
|
-
@workers.delete(index)
|
|
153
|
+
perform_phased_restart if @phased_restart_requested && !@phased_restarting
|
|
145
154
|
|
|
146
|
-
|
|
147
|
-
warn "[#{Process.pid}] Restarting worker #{index} (#{pid}), #{exit_description(status)}"
|
|
148
|
-
spawn_worker(index)
|
|
149
|
-
end
|
|
150
|
-
else
|
|
151
|
-
sleep 0.1
|
|
152
|
-
end
|
|
155
|
+
sleep 0.1
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
@workers.values.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
156
159
|
@workers.values.each { |pid| Process.wait(pid) rescue nil }
|
|
157
160
|
stats_file_thread&.join
|
|
158
161
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
162
|
+
File.delete(@pid_file) rescue nil if @pid_file
|
|
159
163
|
@stats.unmap
|
|
160
164
|
end
|
|
161
165
|
|
|
@@ -182,6 +186,66 @@ module Raptor
|
|
|
182
186
|
@workers[index] = pid
|
|
183
187
|
end
|
|
184
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
|
+
|
|
185
249
|
# Runs the full server stack inside a worker process.
|
|
186
250
|
#
|
|
187
251
|
# Sets up and coordinates the reactor, server, ractor pool, thread pool,
|
|
@@ -217,24 +281,29 @@ module Raptor
|
|
|
217
281
|
@app.call(env)
|
|
218
282
|
}
|
|
219
283
|
thread_pool = AtomicThreadPool.new(name: "Raptor Workers", size: @thread_count)
|
|
220
|
-
request = Request.new(counting_app, @server_port)
|
|
221
|
-
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)
|
|
222
286
|
ractor_pool = RactorPool.new(
|
|
223
287
|
name: "Raptor Pipeline Workers",
|
|
224
288
|
size: @ractor_count,
|
|
225
289
|
worker: request.http_parser_worker
|
|
226
290
|
) do |parsed_result|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
291
|
+
begin
|
|
292
|
+
if parsed_result[:protocol] == :http2
|
|
293
|
+
http2.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
294
|
+
else
|
|
295
|
+
request.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
296
|
+
end
|
|
297
|
+
rescue => error
|
|
298
|
+
warn "#{Thread.current.name} rescued:"
|
|
299
|
+
warn error.full_message
|
|
231
300
|
end
|
|
232
301
|
end
|
|
233
302
|
|
|
234
303
|
reactor = Reactor.new(thread_pool, ractor_pool, client_options: @client_options)
|
|
235
304
|
reactor_thread = reactor.run
|
|
236
305
|
|
|
237
|
-
server = Server.new(@binder, reactor, thread_pool, request)
|
|
306
|
+
server = Server.new(@binder, reactor, thread_pool, request, client_options: @client_options)
|
|
238
307
|
server_thread = server.run
|
|
239
308
|
|
|
240
309
|
puts "[#{Process.pid}] Worker #{index} booted"
|
|
@@ -269,6 +338,7 @@ module Raptor
|
|
|
269
338
|
reactor.shutdown
|
|
270
339
|
reactor_thread.join
|
|
271
340
|
ractor_pool.shutdown
|
|
341
|
+
request.shutdown
|
|
272
342
|
thread_pool.shutdown
|
|
273
343
|
stats_thread.join
|
|
274
344
|
end
|