raptor 0.6.0 → 0.8.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 +148 -23
- data/lib/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +122 -28
- data/lib/raptor/cli.rb +82 -21
- data/lib/raptor/cluster.rb +188 -32
- data/lib/raptor/http.rb +75 -0
- data/lib/raptor/{request.rb → http1.rb} +202 -81
- data/lib/raptor/http2.rb +149 -61
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/server.rb +57 -37
- data/lib/raptor/stats.rb +1 -1
- data/lib/raptor/systemd.rb +69 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +72 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +89 -13
- data/sig/generated/raptor/http.rbs +52 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +107 -31
- data/sig/generated/raptor/http2.rbs +64 -14
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/server.rbs +32 -18
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9e7bfef1752060035a5e76f4d427097486aaa57c8bb184a47a3d8806d9749c8
|
|
4
|
+
data.tar.gz: e7c86ec6597a2315d0352c90c0ea89451e7301a00cfb4592d6c63760686ac7e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5d04b6d4412b95b4131bcdcd6223a0b96ecc906a53a2522b1423195c4cd9b0746998d6d0588f6cf01a269eca4c67af091fd649512080ea98376de099d8eb291
|
|
7
|
+
data.tar.gz: 0e6b0fc536dccbe056c178bace6040550598aedf56ca05434714b299c09d7795092a85a41f6ffe7cb8e9a79ce6e88bd14ba231ce8a5afa6049ca4788ee6648fe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-07-02
|
|
4
|
+
|
|
5
|
+
- Add systemd `LISTEN_FDS` socket activation and `sd_notify` lifecycle messages
|
|
6
|
+
- Add hot restart on `SIGUSR2`, inheriting listening sockets across the re-exec
|
|
7
|
+
- Drop `SIGUSR1` stats logging and move phased restart from `SIGUSR2` to `SIGUSR1`
|
|
8
|
+
- Add `chdir` and `environment` for Rack app loading, with fallback to `RAILS_ENV` and `RACK_ENV`
|
|
9
|
+
- Add `access_log_file` for Common Log Format access logging, reopened on `SIGHUP`
|
|
10
|
+
- Add `stdout_file` and `stderr_file` for redirecting stdout/stderr, reopened on `SIGHUP`
|
|
11
|
+
- Add `drain_accept_queue` for dispatching every queued connection on shutdown
|
|
12
|
+
- Add `worker_drain_timeout` for force-killing hung app threads during worker shutdown
|
|
13
|
+
- Reject `Content-Length` values containing non-digit characters with 400
|
|
14
|
+
- Populate `SERVER_SOFTWARE` and `HTTP_VERSION` in the Rack env
|
|
15
|
+
- Honour `X-Forwarded-Proto`, `X-Forwarded-Scheme`, and `X-Forwarded-Ssl` from upstream proxies
|
|
16
|
+
- Split newline-joined response header values into separate header lines
|
|
17
|
+
- Reject excessive chunked framing overhead with 400 (slow-trickle attack guard)
|
|
18
|
+
- Reject ambiguous request framing (`Transfer-Encoding` + `Content-Length`, or `chunked` not the final encoding) with 400
|
|
19
|
+
- Send `100 Continue` when an HTTP/1.1 client sends `Expect: 100-continue`
|
|
20
|
+
- Add new configuration options and split `client:` into protocol-scoped namespaces
|
|
21
|
+
|
|
22
|
+
## [0.7.0] - 2026-06-12
|
|
23
|
+
|
|
24
|
+
- Eagerly consume back-to-back HTTP/2 frame batches in the pipeline collector
|
|
25
|
+
|
|
3
26
|
## [0.6.0] - 2026-06-02
|
|
4
27
|
|
|
5
28
|
- Raise the backpressure threshold floor so low thread counts don't throttle prematurely
|
data/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Raptor
|
|
2
2
|
|
|
3
|
-
Raptor is a high-performance, preloading,
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Raptor is a high-performance, preloading, pre-forking, multi-threaded Ruby 4+ web server implementing Rack 3.2+, using
|
|
4
|
+
NIO for non-blocking I/O and Ractors for parallel HTTP/1.1 and HTTP/2 parsing via native C extensions, which also
|
|
5
|
+
implement HPACK compression.
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> **Your application does not need to be Ractor-safe.** Ractors handle protocol-level work only; your Rack application
|
|
9
|
+
> is invoked on a thread pool, so any thread-safe Rack app (including Rails) works as-is.
|
|
10
|
+
|
|
11
|
+
Reference documentation is published at <https://joshuay03.github.io/raptor>.
|
|
6
12
|
|
|
7
13
|
## Installation
|
|
8
14
|
|
|
@@ -30,22 +36,23 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
30
36
|
|
|
31
37
|
```
|
|
32
38
|
> bundle exec raptor -w 4 -t 3 hello_world.ru
|
|
33
|
-
[Raptor
|
|
34
|
-
[Raptor
|
|
35
|
-
[Raptor
|
|
36
|
-
[Raptor
|
|
37
|
-
[Raptor
|
|
38
|
-
[Raptor
|
|
39
|
-
[Raptor
|
|
40
|
-
[Raptor
|
|
41
|
-
[Raptor
|
|
42
|
-
[Raptor
|
|
43
|
-
[Raptor
|
|
44
|
-
[Raptor
|
|
45
|
-
[Raptor
|
|
46
|
-
[Raptor
|
|
47
|
-
[Raptor
|
|
48
|
-
[Raptor
|
|
39
|
+
[Raptor 76577|Main|Main] Cluster initializing:
|
|
40
|
+
[Raptor 76577|Main|Main] ├─ Version: 0.8.0
|
|
41
|
+
[Raptor 76577|Main|Main] ├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
42
|
+
[Raptor 76577|Main|Main] ├─ Environment: development
|
|
43
|
+
[Raptor 76577|Main|Main] ├─ Master PID: 76577
|
|
44
|
+
[Raptor 76577|Main|Main] │ └─ 4 worker processes
|
|
45
|
+
[Raptor 76577|Main|Main] │ ├─ 1 server thread
|
|
46
|
+
[Raptor 76577|Main|Main] │ ├─ 1 reactor thread
|
|
47
|
+
[Raptor 76577|Main|Main] │ ├─ 1 pipeline ractor
|
|
48
|
+
[Raptor 76577|Main|Main] │ ├─ 1 pipeline collector thread
|
|
49
|
+
[Raptor 76577|Main|Main] │ ├─ 3 worker threads
|
|
50
|
+
[Raptor 76577|Main|Main] │ └─ 1 stats thread
|
|
51
|
+
[Raptor 76577|Main|Main] └─ Listening on 0.0.0.0:9292
|
|
52
|
+
[Raptor 76579|Main|Main] Worker 0 booted
|
|
53
|
+
[Raptor 76580|Main|Main] Worker 1 booted
|
|
54
|
+
[Raptor 76581|Main|Main] Worker 2 booted
|
|
55
|
+
[Raptor 76582|Main|Main] Worker 3 booted
|
|
49
56
|
```
|
|
50
57
|
|
|
51
58
|
```
|
|
@@ -60,15 +67,133 @@ Also works with `rackup` and `rails server`:
|
|
|
60
67
|
> bundle exec rails server -u raptor
|
|
61
68
|
```
|
|
62
69
|
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
Raptor accepts configuration via command-line flags, a Ruby config file, or both (CLI flags override config file
|
|
73
|
+
values). Run `bundle exec raptor --help` for the full flag list.
|
|
74
|
+
|
|
75
|
+
The config file is a Ruby file that evaluates to a hash of options. By default Raptor loads `raptor.rb` then
|
|
76
|
+
`config/raptor.rb` from the working directory; pass `-c PATH` to point at a specific file. Settings are nested under
|
|
77
|
+
`connection:` (shared across protocols), `http1:` (HTTP/1.1-specific), and `http2:` (HTTP/2-specific).
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# raptor.rb
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
binds: ["tcp://0.0.0.0:9292"],
|
|
84
|
+
socket_backlog: 1024,
|
|
85
|
+
drain_accept_queue: false,
|
|
86
|
+
workers: 4,
|
|
87
|
+
ractors: 1,
|
|
88
|
+
threads: 3,
|
|
89
|
+
chdir: nil,
|
|
90
|
+
environment: nil,
|
|
91
|
+
connection: {
|
|
92
|
+
first_data_timeout: 30,
|
|
93
|
+
chunk_data_timeout: 10,
|
|
94
|
+
write_timeout: 5,
|
|
95
|
+
max_body_size: nil,
|
|
96
|
+
body_spool_threshold: 1024 * 1024,
|
|
97
|
+
},
|
|
98
|
+
http1: {
|
|
99
|
+
persistent_data_timeout: 65,
|
|
100
|
+
max_keepalive_requests: 100,
|
|
101
|
+
},
|
|
102
|
+
http2: {
|
|
103
|
+
max_concurrent_streams: 100,
|
|
104
|
+
},
|
|
105
|
+
worker_boot_timeout: 60,
|
|
106
|
+
worker_timeout: 60,
|
|
107
|
+
worker_drain_timeout: 25,
|
|
108
|
+
worker_shutdown_timeout: 30,
|
|
109
|
+
stats_file: "tmp/raptor.json",
|
|
110
|
+
pid_file: nil,
|
|
111
|
+
stdout_file: nil,
|
|
112
|
+
stderr_file: nil,
|
|
113
|
+
access_log_file: nil,
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Bindings
|
|
118
|
+
|
|
119
|
+
Raptor accepts multiple `binds:` URIs across three schemes.
|
|
120
|
+
|
|
121
|
+
- `tcp://host:port` for TCP. Host can be a specific IP, `0.0.0.0` / `[::]`, or `localhost` (expanded to both IPv4 and
|
|
122
|
+
IPv6 loopback addresses).
|
|
123
|
+
- `unix:///path/to/socket` for a Unix domain socket. Stale sockets left by crashed processes are cleaned up
|
|
124
|
+
automatically.
|
|
125
|
+
- `ssl://host:port?cert=/path/to.crt&key=/path/to.key` for TLS. HTTP/1.1 and HTTP/2 are negotiated via ALPN.
|
|
126
|
+
|
|
127
|
+
Multiple binds can be combined freely.
|
|
128
|
+
|
|
129
|
+
## Signals
|
|
130
|
+
|
|
131
|
+
Send to the master process.
|
|
132
|
+
|
|
133
|
+
| Signal | Effect |
|
|
134
|
+
| ------ | ----------------------------------------------------------- |
|
|
135
|
+
| `INT` | Graceful shutdown |
|
|
136
|
+
| `TERM` | Graceful shutdown |
|
|
137
|
+
| `HUP` | Reopen `stdout_file`, `stderr_file`, and `access_log_file` |
|
|
138
|
+
| `USR1` | Phased restart (rolling worker replacement) |
|
|
139
|
+
| `USR2` | Hot restart (re-exec master, inheriting listening sockets) |
|
|
140
|
+
|
|
141
|
+
## Restarts
|
|
142
|
+
|
|
143
|
+
- **Phased restart** (`USR1`) replaces workers one at a time, waiting for each new worker to boot before retiring the
|
|
144
|
+
previous one. The master process keeps running, so existing workers continue serving until they are individually
|
|
145
|
+
replaced. Use to pick up code changes that don't affect the master's boot path.
|
|
146
|
+
- **Hot restart** (`USR2`) re-execs the master process with its original command line, inheriting the listening sockets
|
|
147
|
+
so accepted connections continue to be served across the swap. The successor master re-runs initialization from
|
|
148
|
+
scratch. Use to pick up changes that affect master-level state (config layout, dependency upgrades, Raptor itself).
|
|
149
|
+
|
|
150
|
+
## systemd
|
|
151
|
+
|
|
152
|
+
Raptor implements socket activation (`LISTEN_FDS`) and `sd_notify`, so it integrates cleanly with `Type=notify` units.
|
|
153
|
+
When the socket unit is active, systemd hands the pre-bound listening file descriptors to Raptor, which serves them in
|
|
154
|
+
place of `binds:`. `READY=1`, `STOPPING=1`, and `RELOADING=1` lifecycle messages are emitted automatically.
|
|
155
|
+
|
|
156
|
+
```ini
|
|
157
|
+
# /etc/systemd/system/myapp.socket
|
|
158
|
+
[Socket]
|
|
159
|
+
ListenStream=0.0.0.0:9292
|
|
160
|
+
|
|
161
|
+
[Install]
|
|
162
|
+
WantedBy=sockets.target
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```ini
|
|
166
|
+
# /etc/systemd/system/myapp.service
|
|
167
|
+
[Service]
|
|
168
|
+
Type=notify
|
|
169
|
+
WorkingDirectory=/srv/myapp
|
|
170
|
+
ExecStart=/usr/bin/bundle exec raptor
|
|
171
|
+
ExecReload=/bin/kill -USR2 $MAINPID
|
|
172
|
+
KillMode=mixed
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Stats
|
|
176
|
+
|
|
177
|
+
Each worker writes per-worker stats (request count, busy threads, backlog, last check-in) to shared memory and to a
|
|
178
|
+
JSON file (default `tmp/raptor.json`; set via `stats_file`).
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
> bundle exec raptor stats
|
|
182
|
+
Master PID: 91348
|
|
183
|
+
Worker 0 (phase 0): pid=91350, requests=1234, busy=2/3, backlog=0, booted, last_checkin=10:42:01
|
|
184
|
+
Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_checkin=10:42:01
|
|
185
|
+
...
|
|
186
|
+
```
|
|
187
|
+
|
|
63
188
|
## (Micro) Benchmarks
|
|
64
189
|
|
|
65
|
-
Raptor 0.
|
|
190
|
+
Raptor 0.8.0 vs Puma 8.0.2:
|
|
66
191
|
|
|
67
192
|
| Protocol | Raptor | Puma |
|
|
68
193
|
| --------------------- | ----------- | ----------- |
|
|
69
|
-
| HTTP/1.1 |
|
|
70
|
-
| HTTP/1.1 (keep-alive) |
|
|
71
|
-
| HTTP/2 |
|
|
194
|
+
| HTTP/1.1 | 18.1k req/s | 15.8k req/s |
|
|
195
|
+
| HTTP/1.1 (keep-alive) | 58.2k req/s | 29.5k req/s |
|
|
196
|
+
| HTTP/2 | 57k req/s | N/A |
|
|
72
197
|
|
|
73
198
|
> ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
74
199
|
> 4 workers, 3 threads, 12 concurrent connections
|
|
@@ -78,18 +78,28 @@ module Rackup
|
|
|
78
78
|
else
|
|
79
79
|
config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
|
|
80
80
|
end,
|
|
81
|
+
socket_backlog: (config[:socket_backlog] || cli_defaults[:socket_backlog]).to_i,
|
|
82
|
+
drain_accept_queue: config.key?(:drain_accept_queue) ? config[:drain_accept_queue] : cli_defaults[:drain_accept_queue],
|
|
81
83
|
workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
|
|
82
84
|
ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
|
|
83
85
|
threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
|
|
84
86
|
app: app
|
|
85
87
|
}
|
|
86
88
|
result[:rackup] = config[:rackup] if config.key?(:rackup)
|
|
87
|
-
result[:
|
|
88
|
-
result[:
|
|
89
|
+
result[:chdir] = config[:chdir] if config.key?(:chdir)
|
|
90
|
+
result[:environment] = config[:environment] if config.key?(:environment)
|
|
91
|
+
::Raptor::CLI::NESTED_OPTION_KEYS.each do |key|
|
|
92
|
+
result[key] = cli_defaults[key].merge(config[key] || {})
|
|
93
|
+
end
|
|
89
94
|
result[:worker_boot_timeout] = (config[:worker_boot_timeout] || cli_defaults[:worker_boot_timeout]).to_i
|
|
95
|
+
result[:worker_timeout] = (config[:worker_timeout] || cli_defaults[:worker_timeout]).to_i
|
|
96
|
+
result[:worker_drain_timeout] = (config[:worker_drain_timeout] || cli_defaults[:worker_drain_timeout]).to_i
|
|
90
97
|
result[:worker_shutdown_timeout] = (config[:worker_shutdown_timeout] || cli_defaults[:worker_shutdown_timeout]).to_i
|
|
91
98
|
result[:stats_file] = config.key?(:stats_file) ? config[:stats_file] : cli_defaults[:stats_file]
|
|
92
99
|
result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
|
|
100
|
+
result[:stdout_file] = config[:stdout_file] if config.key?(:stdout_file)
|
|
101
|
+
result[:stderr_file] = config[:stderr_file] if config.key?(:stderr_file)
|
|
102
|
+
result[:access_log_file] = config[:access_log_file] if config.key?(:access_log_file)
|
|
93
103
|
result[:on_error] = config[:on_error] if config.key?(:on_error)
|
|
94
104
|
result
|
|
95
105
|
end
|
data/lib/raptor/binder.rb
CHANGED
|
@@ -56,7 +56,10 @@ module Raptor
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# @rbs @bind_uris: Array[String]
|
|
59
|
+
# @rbs @socket_backlog: Integer
|
|
60
|
+
# @rbs @inherited_fds: Hash[String, Array[Integer]]
|
|
59
61
|
# @rbs @listeners: Array[TCPServer | UNIXServer | SslListener]
|
|
62
|
+
# @rbs @uri_listeners: Hash[String, Array[TCPServer | UNIXServer | SslListener]]
|
|
60
63
|
|
|
61
64
|
# Array of listening sockets.
|
|
62
65
|
#
|
|
@@ -67,19 +70,26 @@ module Raptor
|
|
|
67
70
|
#
|
|
68
71
|
# Parses the provided bind URIs and creates listening sockets for each one.
|
|
69
72
|
# Supports tcp://, unix://, and ssl:// schemes. Localhost is expanded to
|
|
70
|
-
# all available loopback addresses (both IPv4 and IPv6).
|
|
73
|
+
# all available loopback addresses (both IPv4 and IPv6). When `inherited_fds`
|
|
74
|
+
# supplies file descriptors for a URI, the listener is reconstructed from
|
|
75
|
+
# those FDs instead of binding fresh.
|
|
71
76
|
#
|
|
72
77
|
# @param bind_uris [Array<String>] array of URI strings to bind to
|
|
78
|
+
# @param socket_backlog [Integer] kernel listen() queue depth for TCP/SSL listeners
|
|
79
|
+
# @param inherited_fds [Hash{String => Array<Integer>}] inherited listener FDs keyed by bind URI
|
|
73
80
|
# @return [void]
|
|
74
81
|
# @raise [UnknownBindSchemeError] if a URI has an unsupported scheme
|
|
75
82
|
#
|
|
76
83
|
# @example
|
|
77
84
|
# binder = Binder.new(["tcp://0.0.0.0:3000", "unix:///tmp/raptor.sock"])
|
|
78
85
|
#
|
|
79
|
-
# @rbs (Array[String] bind_uris) -> void
|
|
80
|
-
def initialize(bind_uris)
|
|
86
|
+
# @rbs (Array[String] bind_uris, ?socket_backlog: Integer, ?inherited_fds: Hash[String, Array[Integer]]) -> void
|
|
87
|
+
def initialize(bind_uris, socket_backlog: SOCKET_BACKLOG, inherited_fds: {})
|
|
81
88
|
@bind_uris = bind_uris
|
|
89
|
+
@socket_backlog = socket_backlog
|
|
90
|
+
@inherited_fds = inherited_fds
|
|
82
91
|
@listeners = nil
|
|
92
|
+
@uri_listeners = nil
|
|
83
93
|
parse
|
|
84
94
|
end
|
|
85
95
|
|
|
@@ -133,28 +143,91 @@ module Raptor
|
|
|
133
143
|
@listeners.each(&:close)
|
|
134
144
|
end
|
|
135
145
|
|
|
146
|
+
# Returns the file descriptors of every listener, grouped by the bind URI
|
|
147
|
+
# they were created from. The result is the payload to hand to a successor
|
|
148
|
+
# process via the `inherited_fds:` constructor argument.
|
|
149
|
+
#
|
|
150
|
+
# @return [Hash{String => Array<Integer>}]
|
|
151
|
+
#
|
|
152
|
+
# @rbs () -> Hash[String, Array[Integer]]
|
|
153
|
+
def inheritable_fds
|
|
154
|
+
@uri_listeners.transform_values { |listeners| listeners.map { |listener| listener.to_io.fileno } }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Clears the close-on-exec flag on every listener so the file descriptors
|
|
158
|
+
# survive `Kernel.exec`.
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
161
|
+
#
|
|
162
|
+
# @rbs () -> void
|
|
163
|
+
def clear_close_on_exec
|
|
164
|
+
@listeners.each { |listener| listener.to_io.close_on_exec = false }
|
|
165
|
+
end
|
|
166
|
+
|
|
136
167
|
private
|
|
137
168
|
|
|
138
|
-
# Parses bind URIs and creates listening sockets
|
|
169
|
+
# Parses bind URIs and creates listening sockets, reusing inherited file
|
|
170
|
+
# descriptors for URIs supplied in `@inherited_fds`.
|
|
139
171
|
#
|
|
140
172
|
# @return [void]
|
|
141
173
|
# @raise [UnknownBindSchemeError] if a URI scheme is not supported
|
|
142
174
|
#
|
|
143
175
|
# @rbs () -> void
|
|
144
176
|
def parse
|
|
145
|
-
@
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
when "tcp"
|
|
149
|
-
create_tcp_listeners(uri.host, uri.port)
|
|
150
|
-
when "unix"
|
|
151
|
-
create_unix_listeners(uri.path)
|
|
152
|
-
when "ssl"
|
|
153
|
-
create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
|
|
177
|
+
@uri_listeners = @bind_uris.to_h do |bind_uri|
|
|
178
|
+
if filenos = @inherited_fds[bind_uri]
|
|
179
|
+
[bind_uri, restore_listeners(bind_uri, filenos)]
|
|
154
180
|
else
|
|
155
|
-
|
|
181
|
+
[bind_uri, create_listeners(bind_uri)]
|
|
156
182
|
end
|
|
157
|
-
end
|
|
183
|
+
end
|
|
184
|
+
@listeners = @uri_listeners.values.flatten
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Creates fresh listeners for the given bind URI.
|
|
188
|
+
#
|
|
189
|
+
# @param bind_uri [String] the URI to bind
|
|
190
|
+
# @return [Array<TCPServer, UNIXServer, SslListener>]
|
|
191
|
+
# @raise [UnknownBindSchemeError] if the URI scheme is not supported
|
|
192
|
+
#
|
|
193
|
+
# @rbs (String bind_uri) -> Array[TCPServer | UNIXServer | SslListener]
|
|
194
|
+
def create_listeners(bind_uri)
|
|
195
|
+
uri = URI.parse(bind_uri)
|
|
196
|
+
case uri.scheme
|
|
197
|
+
when "tcp"
|
|
198
|
+
create_tcp_listeners(uri.host, uri.port)
|
|
199
|
+
when "unix"
|
|
200
|
+
create_unix_listeners(uri.path)
|
|
201
|
+
when "ssl"
|
|
202
|
+
create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
|
|
203
|
+
else
|
|
204
|
+
raise UnknownBindSchemeError.new(uri.scheme)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Reconstructs listeners for the given bind URI from inherited file
|
|
209
|
+
# descriptors.
|
|
210
|
+
#
|
|
211
|
+
# @param bind_uri [String] the URI the FDs were bound to
|
|
212
|
+
# @param filenos [Array<Integer>] file descriptors to wrap
|
|
213
|
+
# @return [Array<TCPServer, UNIXServer, SslListener>]
|
|
214
|
+
# @raise [UnknownBindSchemeError] if the URI scheme is not supported
|
|
215
|
+
#
|
|
216
|
+
# @rbs (String bind_uri, Array[Integer] filenos) -> Array[TCPServer | UNIXServer | SslListener]
|
|
217
|
+
def restore_listeners(bind_uri, filenos)
|
|
218
|
+
uri = URI.parse(bind_uri)
|
|
219
|
+
case uri.scheme
|
|
220
|
+
when "tcp"
|
|
221
|
+
filenos.map { |fileno| TCPServer.for_fd(fileno) }
|
|
222
|
+
when "unix"
|
|
223
|
+
register_unix_socket_cleanup(uri.path)
|
|
224
|
+
filenos.map { |fileno| UNIXServer.for_fd(fileno) }
|
|
225
|
+
when "ssl"
|
|
226
|
+
ssl_context = build_ssl_context(URI.decode_www_form(uri.query || "").to_h)
|
|
227
|
+
filenos.map { |fileno| SslListener.new(tcp_server: TCPServer.for_fd(fileno), ssl_context: ssl_context) }
|
|
228
|
+
else
|
|
229
|
+
raise UnknownBindSchemeError.new(uri.scheme)
|
|
230
|
+
end
|
|
158
231
|
end
|
|
159
232
|
|
|
160
233
|
# Creates TCP server sockets for the given host and port.
|
|
@@ -174,7 +247,7 @@ module Raptor
|
|
|
174
247
|
tcp_server = TCPServer.new(host, port)
|
|
175
248
|
tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
176
249
|
tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
|
|
177
|
-
tcp_server.listen
|
|
250
|
+
tcp_server.listen @socket_backlog
|
|
178
251
|
|
|
179
252
|
[tcp_server]
|
|
180
253
|
end
|
|
@@ -200,11 +273,22 @@ module Raptor
|
|
|
200
273
|
end
|
|
201
274
|
end
|
|
202
275
|
|
|
203
|
-
|
|
276
|
+
register_unix_socket_cleanup(path)
|
|
277
|
+
|
|
278
|
+
[UNIXServer.new(path)]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Registers an `at_exit` hook that removes the Unix socket file on the
|
|
282
|
+
# owning master's clean exit. Each call records the current process so
|
|
283
|
+
# forked workers won't delete a socket their master still owns.
|
|
284
|
+
#
|
|
285
|
+
# @param path [String] filesystem path of the Unix socket
|
|
286
|
+
# @return [void]
|
|
287
|
+
#
|
|
288
|
+
# @rbs (String path) -> void
|
|
289
|
+
def register_unix_socket_cleanup(path)
|
|
204
290
|
master_pid = Process.pid
|
|
205
291
|
at_exit { File.delete(path) rescue nil if Process.pid == master_pid }
|
|
206
|
-
|
|
207
|
-
[server]
|
|
208
292
|
end
|
|
209
293
|
|
|
210
294
|
# Creates SSL server sockets for the given host, port, and SSL parameters.
|
|
@@ -220,18 +304,28 @@ module Raptor
|
|
|
220
304
|
#
|
|
221
305
|
# @rbs (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]
|
|
222
306
|
def create_ssl_listeners(host, port, ssl_params)
|
|
223
|
-
require "openssl"
|
|
224
|
-
|
|
225
307
|
tcp_servers = create_tcp_listeners(host, port)
|
|
308
|
+
ssl_context = build_ssl_context(ssl_params)
|
|
309
|
+
tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
|
|
310
|
+
end
|
|
226
311
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
312
|
+
# Builds a frozen `OpenSSL::SSL::SSLContext` configured for HTTP/2 and
|
|
313
|
+
# HTTP/1.1 ALPN negotiation.
|
|
314
|
+
#
|
|
315
|
+
# @param ssl_params [Hash<String, String>] SSL options ("cert" and "key" paths)
|
|
316
|
+
# @return [OpenSSL::SSL::SSLContext]
|
|
317
|
+
#
|
|
318
|
+
# @rbs (Hash[String, String] ssl_params) -> OpenSSL::SSL::SSLContext
|
|
319
|
+
def build_ssl_context(ssl_params)
|
|
320
|
+
require "openssl"
|
|
233
321
|
|
|
234
|
-
|
|
322
|
+
OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
|
|
323
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
|
|
324
|
+
ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
|
|
325
|
+
ssl_context.alpn_protocols = ["h2", "http/1.1"]
|
|
326
|
+
ssl_context.alpn_select_cb = ->(protocols) { protocols.include?("h2") ? "h2" : "http/1.1" }
|
|
327
|
+
ssl_context.freeze
|
|
328
|
+
end
|
|
235
329
|
end
|
|
236
330
|
|
|
237
331
|
# Returns all available loopback IP addresses.
|