raptor 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4a7acc08848f71017602d3abfb38b7d352a48d7bc7929e54f3a693dc3a0fe83
4
- data.tar.gz: 1ee020abeb67db2a2eba6ff64f59054bf2e91447bd3996464f5a3279c3d3e748
3
+ metadata.gz: a9e7bfef1752060035a5e76f4d427097486aaa57c8bb184a47a3d8806d9749c8
4
+ data.tar.gz: e7c86ec6597a2315d0352c90c0ea89451e7301a00cfb4592d6c63760686ac7e6
5
5
  SHA512:
6
- metadata.gz: b5b25843f29afe7e47a90c85f48ba5f9d06e7f5dc819d24595821940f8207fa46054208f38601a455271d026246bfd32144329782f1a842f44cd500bf3d10bbf
7
- data.tar.gz: 5c1f9f18fbf946a8e4a648bed675f83ba7d71bdc9a85354b5df1ccc6ba39bd65b5740c31e1ffd11f4f88960b52aaee0ce2b46747fac52907bae6b6598f4e390e
6
+ metadata.gz: c5d04b6d4412b95b4131bcdcd6223a0b96ecc906a53a2522b1423195c4cd9b0746998d6d0588f6cf01a269eca4c67af091fd649512080ea98376de099d8eb291
7
+ data.tar.gz: 0e6b0fc536dccbe056c178bace6040550598aedf56ca05434714b299c09d7795092a85a41f6ffe7cb8e9a79ce6e88bd14ba231ce8a5afa6049ca4788ee6648fe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
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
+
3
22
  ## [0.7.0] - 2026-06-12
4
23
 
5
24
  - Eagerly consume back-to-back HTTP/2 frame batches in the pipeline collector
data/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # Raptor
2
2
 
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.
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 91348|main|main] Cluster initializing:
34
- [Raptor 91348|main|main] ├─ Version: 0.6.0
35
- [Raptor 91348|main|main] ├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
36
- [Raptor 91348|main|main] ├─ Master PID: 91348
37
- [Raptor 91348|main|main] │ └─ 4 worker processes
38
- [Raptor 91348|main|main] │ ├─ 1 server thread
39
- [Raptor 91348|main|main] │ ├─ 1 reactor thread
40
- [Raptor 91348|main|main] │ ├─ 1 pipeline ractor
41
- [Raptor 91348|main|main] │ ├─ 1 pipeline collector thread
42
- [Raptor 91348|main|main] │ ├─ 3 worker threads
43
- [Raptor 91348|main|main] │ └─ 1 stats thread
44
- [Raptor 91348|main|main] └─ Listening on 0.0.0.0:9292
45
- [Raptor 91350|main|main] Worker 0 booted
46
- [Raptor 91351|main|main] Worker 1 booted
47
- [Raptor 91352|main|main] Worker 2 booted
48
- [Raptor 91353|main|main] Worker 3 booted
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.6.0 vs Puma 8.0.2:
190
+ Raptor 0.8.0 vs Puma 8.0.2:
66
191
 
67
192
  | Protocol | Raptor | Puma |
68
193
  | --------------------- | ----------- | ----------- |
69
- | HTTP/1.1 | 17.9k req/s | 16.8k req/s |
70
- | HTTP/1.1 (keep-alive) | 60k req/s | 29.6k req/s |
71
- | HTTP/2 | 57.2k req/s | N/A |
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[:client] = cli_defaults[:client].merge(config[:client] || {})
88
- result[:worker_timeout] = (config[:worker_timeout] || cli_defaults[:worker_timeout]).to_i
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
- @listeners = @bind_uris.map do |bind_uri|
146
- uri = URI.parse(bind_uri)
147
- case uri.scheme
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
- raise UnknownBindSchemeError.new(uri.scheme)
181
+ [bind_uri, create_listeners(bind_uri)]
156
182
  end
157
- end.tap(&:flatten!)
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 SOCKET_BACKLOG
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
- server = UNIXServer.new(path)
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
- ssl_context = OpenSSL::SSL::SSLContext.new
228
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
229
- ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
230
- ssl_context.alpn_protocols = ["h2", "http/1.1"]
231
- ssl_context.alpn_select_cb = ->(protocols) { protocols.include?("h2") ? "h2" : "http/1.1" }
232
- ssl_context.freeze
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
- tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
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.