raptor 0.2.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 +10 -0
- data/README.md +11 -11
- data/lib/rackup/handler/raptor.rb +23 -11
- data/lib/raptor/binder.rb +1 -0
- data/lib/raptor/cli.rb +84 -1
- data/lib/raptor/cluster.rb +85 -21
- data/lib/raptor/http2.rb +12 -4
- data/lib/raptor/request.rb +96 -20
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +3 -3
- data/sig/generated/raptor/cli.rbs +37 -0
- data/sig/generated/raptor/cluster.rbs +31 -3
- data/sig/generated/raptor/http2.rbs +5 -2
- data/sig/generated/raptor/request.rbs +41 -9
- 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: 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,15 @@
|
|
|
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
|
+
|
|
3
13
|
## [0.2.0] - 2026-05-25
|
|
4
14
|
|
|
5
15
|
- 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+,
|
|
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.
|
|
34
|
+
├─ Version: 0.3.0
|
|
35
35
|
├─ Ruby Version: ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +YJIT +PRISM [arm64-darwin23]
|
|
36
|
-
├─ Master PID:
|
|
36
|
+
├─ Master PID: 31504
|
|
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
|
+
[31506] Worker 0 booted
|
|
46
|
+
[31507] Worker 1 booted
|
|
47
|
+
[31508] Worker 2 booted
|
|
48
|
+
[31509] Worker 3 booted
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
```
|
|
@@ -60,9 +60,9 @@ 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.3.0 vs Puma 8.0.1:
|
|
66
66
|
|
|
67
67
|
| Protocol | Raptor | Puma |
|
|
68
68
|
| --------------------- | ------------ | ------------ |
|
|
@@ -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,31 @@ 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 = options[:Config] ? ::Raptor::CLI.load_config_file(options[:Config]) : {}
|
|
74
73
|
|
|
75
|
-
|
|
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 = {
|
|
76
83
|
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
|
|
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] || {})
|
|
83
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
|
|
84
96
|
end
|
|
85
97
|
private_class_method :build_cluster_options
|
|
86
98
|
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
|
#
|
|
@@ -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] :pidfile 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
|
+
@pidfile = options[:pidfile]
|
|
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(@pidfile, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pidfile
|
|
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(@pidfile) rescue nil if @pidfile
|
|
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,8 +281,8 @@ 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,
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -85,17 +85,20 @@ module Raptor
|
|
|
85
85
|
|
|
86
86
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
87
87
|
# @rbs @server_port: Integer
|
|
88
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
88
89
|
|
|
89
90
|
# Creates a new Http2 handler.
|
|
90
91
|
#
|
|
91
92
|
# @param app [#call] the Rack application to dispatch requests to
|
|
92
93
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
94
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
93
95
|
# @return [void]
|
|
94
96
|
#
|
|
95
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
96
|
-
def initialize(app, server_port)
|
|
97
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
98
|
+
def initialize(app, server_port, on_error: nil)
|
|
97
99
|
@app = app
|
|
98
100
|
@server_port = server_port
|
|
101
|
+
@on_error = on_error
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
# Builds the initial server SETTINGS frame to send on connection establishment.
|
|
@@ -312,9 +315,14 @@ module Raptor
|
|
|
312
315
|
status, response_headers, response_body = @app.call(env)
|
|
313
316
|
|
|
314
317
|
write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
|
|
315
|
-
rescue
|
|
318
|
+
rescue => error
|
|
316
319
|
write_http2_error_response(socket, writer, stream_id)
|
|
317
|
-
|
|
320
|
+
|
|
321
|
+
if @on_error
|
|
322
|
+
@on_error.call(env, error) rescue nil
|
|
323
|
+
else
|
|
324
|
+
raise
|
|
325
|
+
end
|
|
318
326
|
ensure
|
|
319
327
|
response_body.close if response_body.respond_to?(:close)
|
|
320
328
|
end
|
data/lib/raptor/request.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "socket"
|
|
5
5
|
require "stringio"
|
|
6
|
+
require "tempfile"
|
|
6
7
|
|
|
7
8
|
require "rack"
|
|
8
9
|
|
|
@@ -38,7 +39,8 @@ module Raptor
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
STATUS_WITH_NO_ENTITY_BODY = Set.new([204, 304, *100..199]).freeze
|
|
41
|
-
|
|
42
|
+
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
43
|
+
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
42
44
|
|
|
43
45
|
CONNECTION_CLOSE = "close"
|
|
44
46
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
@@ -61,46 +63,58 @@ module Raptor
|
|
|
61
63
|
|
|
62
64
|
# Decodes a chunked transfer-encoded body buffer.
|
|
63
65
|
#
|
|
64
|
-
# Returns the decoded bytes and a
|
|
65
|
-
# zero-length chunk was found
|
|
66
|
-
#
|
|
66
|
+
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
67
|
+
# terminating zero-length chunk was found, `:too_large` when the decoded
|
|
68
|
+
# size would exceed `max_size`, or `:incomplete` otherwise.
|
|
67
69
|
#
|
|
68
70
|
# @param buffer [String] the raw body buffer to decode
|
|
69
|
-
# @
|
|
71
|
+
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
72
|
+
# @return [Array(String, Symbol)] decoded body and completion state
|
|
70
73
|
#
|
|
71
|
-
# @rbs (String buffer) -> [String,
|
|
72
|
-
def self.decode_chunked(buffer)
|
|
74
|
+
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
75
|
+
def self.decode_chunked(buffer, max_size = nil)
|
|
73
76
|
decoded = String.new
|
|
74
77
|
offset = 0
|
|
75
78
|
|
|
76
79
|
while offset < buffer.bytesize
|
|
77
80
|
crlf = buffer.index("\r\n", offset)
|
|
78
|
-
return [decoded,
|
|
81
|
+
return [decoded, :incomplete] unless crlf
|
|
79
82
|
|
|
80
83
|
chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
81
|
-
return [decoded,
|
|
84
|
+
return [decoded, :complete] if chunk_size == 0
|
|
85
|
+
return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
|
|
82
86
|
|
|
83
87
|
offset = crlf + 2
|
|
84
88
|
decoded << buffer.byteslice(offset, chunk_size)
|
|
85
89
|
offset += chunk_size + 2
|
|
86
90
|
end
|
|
87
91
|
|
|
88
|
-
[decoded,
|
|
92
|
+
[decoded, :incomplete]
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
92
96
|
# @rbs @server_port: Integer
|
|
97
|
+
# @rbs @max_body_size: Integer?
|
|
98
|
+
# @rbs @body_spool_threshold: Integer?
|
|
99
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
93
100
|
|
|
94
101
|
# Creates a new Request handler.
|
|
95
102
|
#
|
|
96
103
|
# @param app [#call] the Rack application to dispatch complete requests to
|
|
97
104
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
105
|
+
# @param client_options [Hash] client limits configuration
|
|
106
|
+
# @option client_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
107
|
+
# @option client_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
|
|
108
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
98
109
|
# @return [void]
|
|
99
110
|
#
|
|
100
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
101
|
-
def initialize(app, server_port)
|
|
111
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?client_options: Hash[Symbol, untyped], ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
112
|
+
def initialize(app, server_port, client_options: {}, on_error: nil)
|
|
102
113
|
@app = app
|
|
103
114
|
@server_port = server_port
|
|
115
|
+
@max_body_size = client_options[:max_body_size]
|
|
116
|
+
@body_spool_threshold = client_options[:body_spool_threshold]
|
|
117
|
+
@on_error = on_error
|
|
104
118
|
end
|
|
105
119
|
|
|
106
120
|
# Eagerly reads and parses the first request on a freshly accepted
|
|
@@ -149,12 +163,21 @@ module Raptor
|
|
|
149
163
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
150
164
|
return
|
|
151
165
|
elsif parser.has_body?
|
|
166
|
+
if @max_body_size && parser.content_length > @max_body_size
|
|
167
|
+
reject_oversized(socket)
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
|
|
152
171
|
body = buffer.byteslice(nread..-1) || ""
|
|
153
172
|
|
|
154
173
|
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
155
|
-
body,
|
|
156
|
-
|
|
174
|
+
body, chunked_state = Request.decode_chunked(body, @max_body_size)
|
|
175
|
+
case chunked_state
|
|
176
|
+
when :complete
|
|
157
177
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
178
|
+
when :too_large
|
|
179
|
+
reject_oversized(socket)
|
|
180
|
+
return
|
|
158
181
|
else
|
|
159
182
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
160
183
|
return
|
|
@@ -180,6 +203,8 @@ module Raptor
|
|
|
180
203
|
#
|
|
181
204
|
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
182
205
|
def http_parser_worker
|
|
206
|
+
max_body_size = @max_body_size
|
|
207
|
+
|
|
183
208
|
proc do |data|
|
|
184
209
|
next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
|
|
185
210
|
|
|
@@ -197,12 +222,17 @@ module Raptor
|
|
|
197
222
|
if parser.has_body?
|
|
198
223
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
199
224
|
|
|
200
|
-
if
|
|
201
|
-
|
|
225
|
+
if max_body_size && parser.content_length > max_body_size
|
|
226
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
227
|
+
elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
228
|
+
decoded_body, chunked_state = Raptor::Request.decode_chunked(body_buffer, max_body_size)
|
|
202
229
|
|
|
203
|
-
|
|
230
|
+
case chunked_state
|
|
231
|
+
when :complete
|
|
204
232
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
205
233
|
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
234
|
+
when :too_large
|
|
235
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
206
236
|
else
|
|
207
237
|
data.merge(env: env, parse_data: parse_data)
|
|
208
238
|
end
|
|
@@ -233,6 +263,12 @@ module Raptor
|
|
|
233
263
|
#
|
|
234
264
|
# @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
235
265
|
def handle_parsed_request(parsed_request, reactor, thread_pool)
|
|
266
|
+
if parsed_request[:too_large]
|
|
267
|
+
socket = reactor.remove(parsed_request[:id])
|
|
268
|
+
reject_oversized(socket) if socket
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
|
|
236
272
|
unless parsed_request[:complete]
|
|
237
273
|
reactor.update_state(parsed_request)
|
|
238
274
|
else
|
|
@@ -322,10 +358,18 @@ module Raptor
|
|
|
322
358
|
keep_alive && !hijacked
|
|
323
359
|
rescue => error
|
|
324
360
|
call_response_finished(rack_env, status, headers, error) if rack_env
|
|
325
|
-
socket.write(
|
|
361
|
+
socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
|
|
326
362
|
keep_alive = false
|
|
327
|
-
|
|
363
|
+
|
|
364
|
+
if @on_error
|
|
365
|
+
@on_error.call(rack_env, error) rescue nil
|
|
366
|
+
else
|
|
367
|
+
raise
|
|
368
|
+
end
|
|
328
369
|
ensure
|
|
370
|
+
rack_input = rack_env && rack_env[Rack::RACK_INPUT]
|
|
371
|
+
rack_input.close! rescue nil if rack_input.respond_to?(:close!)
|
|
372
|
+
|
|
329
373
|
unless hijacked || keep_alive
|
|
330
374
|
socket.close rescue nil
|
|
331
375
|
end
|
|
@@ -459,6 +503,18 @@ module Raptor
|
|
|
459
503
|
reactor.update_state(Ractor.make_shareable(state))
|
|
460
504
|
end
|
|
461
505
|
|
|
506
|
+
# Writes a 413 response and closes the socket. Used when a request body
|
|
507
|
+
# exceeds the configured maximum size.
|
|
508
|
+
#
|
|
509
|
+
# @param socket [TCPSocket] the client socket
|
|
510
|
+
# @return [void]
|
|
511
|
+
#
|
|
512
|
+
# @rbs (TCPSocket socket) -> void
|
|
513
|
+
def reject_oversized(socket)
|
|
514
|
+
socket.write(CONTENT_TOO_LARGE_RESPONSE) rescue nil
|
|
515
|
+
socket.close rescue nil
|
|
516
|
+
end
|
|
517
|
+
|
|
462
518
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
463
519
|
#
|
|
464
520
|
# Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
|
|
@@ -476,7 +532,7 @@ module Raptor
|
|
|
476
532
|
def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
|
|
477
533
|
env[Rack::RACK_VERSION] = Rack::VERSION
|
|
478
534
|
env[Rack::RACK_URL_SCHEME] = url_scheme
|
|
479
|
-
env[Rack::RACK_INPUT] = (body
|
|
535
|
+
env[Rack::RACK_INPUT] = build_rack_input(body)
|
|
480
536
|
env[Rack::RACK_ERRORS] = $stderr
|
|
481
537
|
env[Rack::RACK_RESPONSE_FINISHED] = []
|
|
482
538
|
|
|
@@ -520,6 +576,26 @@ module Raptor
|
|
|
520
576
|
env
|
|
521
577
|
end
|
|
522
578
|
|
|
579
|
+
# Builds the `rack.input` IO object for the request body. Returns an
|
|
580
|
+
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
581
|
+
# for larger bodies to bound per-worker memory.
|
|
582
|
+
#
|
|
583
|
+
# @param body [String, nil] decoded request body
|
|
584
|
+
# @return [IO] an IO-like object positioned at the start of the body
|
|
585
|
+
#
|
|
586
|
+
# @rbs (String? body) -> IO
|
|
587
|
+
def build_rack_input(body)
|
|
588
|
+
if body && @body_spool_threshold && body.bytesize > @body_spool_threshold
|
|
589
|
+
tempfile = Tempfile.new("raptor-body")
|
|
590
|
+
tempfile.binmode
|
|
591
|
+
tempfile.write(body)
|
|
592
|
+
tempfile.rewind
|
|
593
|
+
tempfile
|
|
594
|
+
else
|
|
595
|
+
(body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
523
599
|
# Determines whether the connection should be kept alive after the response.
|
|
524
600
|
#
|
|
525
601
|
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|
data/lib/raptor/version.rb
CHANGED
data/lib/raptor.rb
CHANGED
|
@@ -5,9 +5,9 @@ require_relative "raptor/version"
|
|
|
5
5
|
|
|
6
6
|
# Main module for the Raptor web server.
|
|
7
7
|
#
|
|
8
|
-
# Raptor is a high-performance, multi-
|
|
9
|
-
#
|
|
10
|
-
# extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
|
|
8
|
+
# Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server
|
|
9
|
+
# implementing Rack 3+, leveraging Ractors for parallel HTTP/1.1 and HTTP/2 request processing,
|
|
10
|
+
# native C extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
|
|
11
11
|
#
|
|
12
12
|
module Raptor
|
|
13
13
|
end
|
|
@@ -20,6 +20,19 @@ module Raptor
|
|
|
20
20
|
|
|
21
21
|
DEFAULT_OPTIONS: untyped
|
|
22
22
|
|
|
23
|
+
# Loads a configuration file and returns the hash it evaluates to.
|
|
24
|
+
#
|
|
25
|
+
# The file is evaluated at the top level so constants like `Raptor::*` resolve
|
|
26
|
+
# the same as in a regular Ruby script. The final expression must be a Hash
|
|
27
|
+
# of cluster options (the same keys accepted by {Raptor::Cluster#initialize}).
|
|
28
|
+
#
|
|
29
|
+
# @param path [String] path to a Ruby file that evaluates to a Hash
|
|
30
|
+
# @return [Hash{Symbol => untyped}] cluster options
|
|
31
|
+
# @raise [ArgumentError] if the file does not evaluate to a Hash
|
|
32
|
+
#
|
|
33
|
+
# @rbs (String path) -> Hash[Symbol, untyped]
|
|
34
|
+
def self.load_config_file: (String path) -> Hash[Symbol, untyped]
|
|
35
|
+
|
|
23
36
|
@command: Symbol
|
|
24
37
|
|
|
25
38
|
@options: Hash[Symbol, untyped]
|
|
@@ -61,6 +74,30 @@ module Raptor
|
|
|
61
74
|
# @rbs () -> void
|
|
62
75
|
def run_stats: () -> void
|
|
63
76
|
|
|
77
|
+
# Scans argv for a `-c`/`--config` flag and returns the configured path.
|
|
78
|
+
#
|
|
79
|
+
# The pre-scan runs before the main OptionParser pass so the config file
|
|
80
|
+
# can be applied as a base layer that CLI args then override. All four
|
|
81
|
+
# OptionParser-accepted forms (`-c PATH`, `-cPATH`, `--config PATH`,
|
|
82
|
+
# `--config=PATH`) are recognized.
|
|
83
|
+
#
|
|
84
|
+
# @param argv [Array<String>] command-line arguments to scan
|
|
85
|
+
# @return [String, nil] the config path, or nil if no flag was supplied
|
|
86
|
+
#
|
|
87
|
+
# @rbs (Array[String] argv) -> String?
|
|
88
|
+
def extract_config_path: (Array[String] argv) -> String?
|
|
89
|
+
|
|
90
|
+
# Loads a config file and merges it into `@options` over the defaults.
|
|
91
|
+
#
|
|
92
|
+
# Top-level keys replace defaults; the nested `:client` hash is merged
|
|
93
|
+
# key-by-key so a config file does not need to restate every client option.
|
|
94
|
+
#
|
|
95
|
+
# @param path [String, nil] path to the config file, or nil to no-op
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
# @rbs (String? path) -> void
|
|
99
|
+
def apply_config_file: (String? path) -> void
|
|
100
|
+
|
|
64
101
|
# Creates the OptionParser instance with all supported command-line options.
|
|
65
102
|
#
|
|
66
103
|
# @return [OptionParser] configured option parser
|
|
@@ -37,7 +37,9 @@ module Raptor
|
|
|
37
37
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
38
38
|
def self.run: (Hash[Symbol, untyped] options) -> void
|
|
39
39
|
|
|
40
|
-
@
|
|
40
|
+
@phased_restarting: bool
|
|
41
|
+
|
|
42
|
+
@phased_restart_requested: bool
|
|
41
43
|
|
|
42
44
|
@stats: Stats
|
|
43
45
|
|
|
@@ -51,6 +53,12 @@ module Raptor
|
|
|
51
53
|
|
|
52
54
|
@binder: Binder
|
|
53
55
|
|
|
56
|
+
@pidfile: String?
|
|
57
|
+
|
|
58
|
+
@stats_file: String?
|
|
59
|
+
|
|
60
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
61
|
+
|
|
54
62
|
@client_options: Hash[Symbol, Integer]
|
|
55
63
|
|
|
56
64
|
@worker_count: Integer
|
|
@@ -72,7 +80,10 @@ module Raptor
|
|
|
72
80
|
# @option options [Array<String>] :binds array of bind URIs
|
|
73
81
|
# @option options [#call] :app pre-built Rack application
|
|
74
82
|
# @option options [String] :rackup path to Rack configuration file
|
|
75
|
-
# @option options [Hash] :client client
|
|
83
|
+
# @option options [Hash] :client client configuration
|
|
84
|
+
# @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
|
|
85
|
+
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
86
|
+
# @option options [String, nil] :pidfile path to write the master PID to, or nil to disable
|
|
76
87
|
# @return [void]
|
|
77
88
|
#
|
|
78
89
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
@@ -82,7 +93,8 @@ module Raptor
|
|
|
82
93
|
#
|
|
83
94
|
# Forks the configured number of worker processes and monitors them,
|
|
84
95
|
# automatically restarting any that exit unexpectedly. Handles graceful
|
|
85
|
-
# shutdown via INT or TERM signals,
|
|
96
|
+
# shutdown via INT or TERM signals, stats logging via USR1, and phased
|
|
97
|
+
# restart via USR2.
|
|
86
98
|
#
|
|
87
99
|
# Each worker process includes:
|
|
88
100
|
# - 1 server thread (continuously accepts connections with backpressure control)
|
|
@@ -115,6 +127,22 @@ module Raptor
|
|
|
115
127
|
# @rbs (Integer index) -> void
|
|
116
128
|
def spawn_worker: (Integer index) -> void
|
|
117
129
|
|
|
130
|
+
# Reaps any worker processes that have exited, respawning each one
|
|
131
|
+
# unless the cluster is shutting down.
|
|
132
|
+
#
|
|
133
|
+
# @return [Symbol] :no_children when there are no remaining children, otherwise :reaped
|
|
134
|
+
#
|
|
135
|
+
# @rbs () -> Symbol
|
|
136
|
+
def reap_workers: () -> Symbol
|
|
137
|
+
|
|
138
|
+
# Replaces each worker process one at a time, waiting for the new
|
|
139
|
+
# worker to boot before moving on to the next. Triggered by SIGUSR2.
|
|
140
|
+
#
|
|
141
|
+
# @return [void]
|
|
142
|
+
#
|
|
143
|
+
# @rbs () -> void
|
|
144
|
+
def perform_phased_restart: () -> void
|
|
145
|
+
|
|
118
146
|
# Runs the full server stack inside a worker process.
|
|
119
147
|
#
|
|
120
148
|
# Sets up and coordinates the reactor, server, ractor pool, thread pool,
|
|
@@ -47,6 +47,8 @@ module Raptor
|
|
|
47
47
|
|
|
48
48
|
HOP_BY_HOP_HEADERS: untyped
|
|
49
49
|
|
|
50
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
51
|
+
|
|
50
52
|
@server_port: Integer
|
|
51
53
|
|
|
52
54
|
@app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
|
|
@@ -55,10 +57,11 @@ module Raptor
|
|
|
55
57
|
#
|
|
56
58
|
# @param app [#call] the Rack application to dispatch requests to
|
|
57
59
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
60
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
58
61
|
# @return [void]
|
|
59
62
|
#
|
|
60
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
61
|
-
def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port) -> void
|
|
63
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
64
|
+
def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
62
65
|
|
|
63
66
|
# Builds the initial server SETTINGS frame to send on connection establishment.
|
|
64
67
|
#
|
|
@@ -33,7 +33,9 @@ module Raptor
|
|
|
33
33
|
|
|
34
34
|
STATUS_WITH_NO_ENTITY_BODY: untyped
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
INTERNAL_SERVER_ERROR_RESPONSE: ::String
|
|
37
|
+
|
|
38
|
+
CONTENT_TOO_LARGE_RESPONSE: ::String
|
|
37
39
|
|
|
38
40
|
CONNECTION_CLOSE: ::String
|
|
39
41
|
|
|
@@ -65,15 +67,22 @@ module Raptor
|
|
|
65
67
|
|
|
66
68
|
# Decodes a chunked transfer-encoded body buffer.
|
|
67
69
|
#
|
|
68
|
-
# Returns the decoded bytes and a
|
|
69
|
-
# zero-length chunk was found
|
|
70
|
-
#
|
|
70
|
+
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
71
|
+
# terminating zero-length chunk was found, `:too_large` when the decoded
|
|
72
|
+
# size would exceed `max_size`, or `:incomplete` otherwise.
|
|
71
73
|
#
|
|
72
74
|
# @param buffer [String] the raw body buffer to decode
|
|
73
|
-
# @
|
|
75
|
+
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
76
|
+
# @return [Array(String, Symbol)] decoded body and completion state
|
|
74
77
|
#
|
|
75
|
-
# @rbs (String buffer) -> [String,
|
|
76
|
-
def self.decode_chunked: (String buffer) -> [ String,
|
|
78
|
+
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
79
|
+
def self.decode_chunked: (String buffer, ?Integer? max_size) -> [ String, Symbol ]
|
|
80
|
+
|
|
81
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
82
|
+
|
|
83
|
+
@body_spool_threshold: Integer?
|
|
84
|
+
|
|
85
|
+
@max_body_size: Integer?
|
|
77
86
|
|
|
78
87
|
@server_port: Integer
|
|
79
88
|
|
|
@@ -83,10 +92,14 @@ module Raptor
|
|
|
83
92
|
#
|
|
84
93
|
# @param app [#call] the Rack application to dispatch complete requests to
|
|
85
94
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
95
|
+
# @param client_options [Hash] client limits configuration
|
|
96
|
+
# @option client_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
97
|
+
# @option client_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
|
|
98
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
86
99
|
# @return [void]
|
|
87
100
|
#
|
|
88
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
89
|
-
def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port) -> void
|
|
101
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?client_options: Hash[Symbol, untyped], ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
102
|
+
def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port, ?client_options: Hash[Symbol, untyped], ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
90
103
|
|
|
91
104
|
# Eagerly reads and parses the first request on a freshly accepted
|
|
92
105
|
# connection on the server thread, dispatching directly to the thread pool
|
|
@@ -203,6 +216,15 @@ module Raptor
|
|
|
203
216
|
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
|
|
204
217
|
def fallback_to_reactor: (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
|
|
205
218
|
|
|
219
|
+
# Writes a 413 response and closes the socket. Used when a request body
|
|
220
|
+
# exceeds the configured maximum size.
|
|
221
|
+
#
|
|
222
|
+
# @param socket [TCPSocket] the client socket
|
|
223
|
+
# @return [void]
|
|
224
|
+
#
|
|
225
|
+
# @rbs (TCPSocket socket) -> void
|
|
226
|
+
def reject_oversized: (TCPSocket socket) -> void
|
|
227
|
+
|
|
206
228
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
207
229
|
#
|
|
208
230
|
# Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
|
|
@@ -219,6 +241,16 @@ module Raptor
|
|
|
219
241
|
# @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
|
|
220
242
|
def build_rack_env: (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
|
|
221
243
|
|
|
244
|
+
# Builds the `rack.input` IO object for the request body. Returns an
|
|
245
|
+
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
246
|
+
# for larger bodies to bound per-worker memory.
|
|
247
|
+
#
|
|
248
|
+
# @param body [String, nil] decoded request body
|
|
249
|
+
# @return [IO] an IO-like object positioned at the start of the body
|
|
250
|
+
#
|
|
251
|
+
# @rbs (String? body) -> IO
|
|
252
|
+
def build_rack_input: (String? body) -> IO
|
|
253
|
+
|
|
222
254
|
# Determines whether the connection should be kept alive after the response.
|
|
223
255
|
#
|
|
224
256
|
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|
data/sig/generated/raptor.rbs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# Main module for the Raptor web server.
|
|
4
4
|
#
|
|
5
|
-
# Raptor is a high-performance, multi-
|
|
6
|
-
#
|
|
7
|
-
# extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
|
|
5
|
+
# Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server
|
|
6
|
+
# implementing Rack 3+, leveraging Ractors for parallel HTTP/1.1 and HTTP/2 request processing,
|
|
7
|
+
# native C extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
|
|
8
8
|
module Raptor
|
|
9
9
|
end
|