hyperion-rb 1.6.2 → 2.11.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 +4768 -0
- data/README.md +222 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +499 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +618 -19
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
data/lib/hyperion/config.rb
CHANGED
|
@@ -8,7 +8,20 @@ module Hyperion
|
|
|
8
8
|
# All settings have safe defaults that match the per-class DEFAULT_* constants
|
|
9
9
|
# so that running Hyperion without a config file works identically to the
|
|
10
10
|
# pre-rc14 behaviour.
|
|
11
|
+
#
|
|
12
|
+
# 1.7.0 (RFC A4): grouped settings live in nested subconfigs —
|
|
13
|
+
# `config.h2.*`, `config.admin.*`, `config.worker_health.*`,
|
|
14
|
+
# `config.logging.*`. 1.7 added the nested DSL alongside the legacy
|
|
15
|
+
# flat keys; 1.8 deprecation-warned the flat keys; 2.0 removed them.
|
|
16
|
+
# The nested DSL is the only configuration surface — flat aliases
|
|
17
|
+
# like `h2_max_concurrent_streams` no longer exist on the DSL or on
|
|
18
|
+
# `Config` itself.
|
|
11
19
|
class Config
|
|
20
|
+
# Top-level (un-nested) defaults. Flat fields that don't group
|
|
21
|
+
# naturally are deliberately kept here per the RFC's "only 8 fields
|
|
22
|
+
# warrant nesting in A4" guidance — `max_pending`, `idle_keepalive`,
|
|
23
|
+
# `graceful_timeout`, the `tls_*` family, `read_timeout`, and the
|
|
24
|
+
# body/header byte caps stay flat.
|
|
12
25
|
DEFAULTS = {
|
|
13
26
|
host: '127.0.0.1',
|
|
14
27
|
port: 9292,
|
|
@@ -21,31 +34,250 @@ module Hyperion
|
|
|
21
34
|
graceful_timeout: 30,
|
|
22
35
|
max_header_bytes: 64 * 1024,
|
|
23
36
|
max_body_bytes: 16 * 1024 * 1024,
|
|
24
|
-
log_level: nil, # nil → Logger picks from env / default
|
|
25
|
-
log_format: nil, # nil → Logger picks via auto rule
|
|
26
|
-
log_requests: nil, # nil → Hyperion.log_requests? (default true)
|
|
27
37
|
fiber_local_shim: false,
|
|
28
38
|
yjit: nil, # nil → auto: enable on production/staging; true/false to force.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
max_pending: nil,
|
|
40
|
+
max_request_read_seconds: 60,
|
|
41
|
+
async_io: nil, # nil/true/false (validated strictly in 1.7.0+ via Server constructor).
|
|
42
|
+
accept_fibers_per_worker: 1, # RFC A6 — opt-in multi-fiber accept under :reuseport.
|
|
43
|
+
# 2.3-A: io_uring accept policy (Linux 5.6+ only). Tri-state, mirrors `tls.ktls`:
|
|
44
|
+
# :off — never use io_uring; epoll path always (2.3.0 default).
|
|
45
|
+
# :auto — use io_uring when supported; quietly fall back otherwise.
|
|
46
|
+
# :on — demand it; raise at boot if unsupported.
|
|
47
|
+
# Default flips to :auto in 2.4 only after soak. Operators flip on
|
|
48
|
+
# via `HYPERION_IO_URING={on,auto}` env var to A/B test.
|
|
49
|
+
io_uring: :off,
|
|
50
|
+
# 2.3-B: per-connection in-flight cap. nginx upstream keep-alive
|
|
51
|
+
# pipelines many client requests through one upstream connection;
|
|
52
|
+
# without this cap a single greedy upstream conn can hog the worker
|
|
53
|
+
# thread pool and starve siblings. Tri-state:
|
|
54
|
+
# * Integer >= 1 — explicit cap (e.g., `4` for `-t 16`).
|
|
55
|
+
# * :auto — `Config#finalize!` resolves to `thread_count / 4`
|
|
56
|
+
# (rounded down, minimum 1). Operator opt-in.
|
|
57
|
+
# * nil (default) — no cap; matches 2.2.0 behaviour. Hyperion is
|
|
58
|
+
# opt-in by default — the cap is a hardening tool
|
|
59
|
+
# that operators turn on, not a default flip.
|
|
60
|
+
max_in_flight_per_conn: nil,
|
|
61
|
+
# 2.10-E: explicit `preload_static "/path"` DSL entries plus the
|
|
62
|
+
# CLI's repeatable `--preload-static <dir>` flag accumulate here.
|
|
63
|
+
# Each element is a `{path: String, immutable: Boolean}` Hash.
|
|
64
|
+
# `Server#listen` walks the resolved list (which may also include
|
|
65
|
+
# auto-detected Rails asset paths — see `auto_preload_static_disabled`)
|
|
66
|
+
# and warms `Hyperion::Http::PageCache` before the accept loop spins.
|
|
67
|
+
# Default empty so a vanilla Rack app pays nothing.
|
|
68
|
+
preload_static_dirs: nil,
|
|
69
|
+
# 2.10-E: when truthy, suppress the Rails-aware auto-detect path
|
|
70
|
+
# (`Rails.configuration.assets.paths.first(N)`) at boot. Set by
|
|
71
|
+
# the `--no-preload-static` CLI flag; lets operators turn off
|
|
72
|
+
# auto-warming on a Rails app while still keeping the option to
|
|
73
|
+
# configure explicit dirs via `preload_static`.
|
|
74
|
+
auto_preload_static_disabled: false
|
|
39
75
|
}.freeze
|
|
40
76
|
|
|
41
77
|
HOOKS = %i[before_fork on_worker_boot on_worker_shutdown].freeze
|
|
42
78
|
|
|
79
|
+
# Plain top-level accessors. Subconfigs (h2/admin/worker_health/logging)
|
|
80
|
+
# and their flat-forwarder methods are defined further below.
|
|
43
81
|
attr_accessor(*DEFAULTS.keys)
|
|
44
82
|
attr_reader(*HOOKS)
|
|
45
83
|
|
|
84
|
+
# Nested subconfig readers. The DSL exposes them as block forms
|
|
85
|
+
# (`h2 do |h| ... end`) and the legacy flat forms (`h2_max_concurrent_streams 256`)
|
|
86
|
+
# both write into the same backing object.
|
|
87
|
+
attr_reader :h2, :admin, :worker_health, :logging, :tls, :websocket, :metrics
|
|
88
|
+
|
|
89
|
+
# H2 settings subconfig. RFC 7540 §6.5.2 settings + the new-in-1.7
|
|
90
|
+
# per-process `max_total_streams` admission cap (RFC A7).
|
|
91
|
+
class H2Settings
|
|
92
|
+
ATTRS = %i[max_concurrent_streams initial_window_size max_frame_size
|
|
93
|
+
max_header_list_size max_total_streams].freeze
|
|
94
|
+
attr_accessor(*ATTRS)
|
|
95
|
+
|
|
96
|
+
# 2.0 default for `max_total_streams`. The literal value `:auto`
|
|
97
|
+
# is a deferred sentinel: `Config#finalize!` resolves it to
|
|
98
|
+
# `max_concurrent_streams × workers × 4` once the worker count
|
|
99
|
+
# is known. Operators wanting the pre-2.0 unbounded behaviour
|
|
100
|
+
# write `:unbounded` (or `nil` after finalize); operators wanting
|
|
101
|
+
# a fixed cap write a positive integer.
|
|
102
|
+
AUTO = :auto
|
|
103
|
+
UNBOUNDED = :unbounded
|
|
104
|
+
|
|
105
|
+
def initialize
|
|
106
|
+
@max_concurrent_streams = 128
|
|
107
|
+
@initial_window_size = 1_048_576
|
|
108
|
+
@max_frame_size = 1_048_576
|
|
109
|
+
@max_header_list_size = 65_536
|
|
110
|
+
@max_total_streams = AUTO # 2.0 default — finalize! resolves it.
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Admin endpoint subconfig. `token` was `admin_token` in 1.6.x;
|
|
115
|
+
# `listener_port` / `listener_host` are new-in-1.7 (RFC A8 sibling
|
|
116
|
+
# listener; default nil keeps admin mounted in-app via AdminMiddleware).
|
|
117
|
+
class AdminConfig
|
|
118
|
+
ATTRS = %i[token listener_port listener_host].freeze
|
|
119
|
+
attr_accessor(*ATTRS)
|
|
120
|
+
|
|
121
|
+
def initialize
|
|
122
|
+
@token = nil
|
|
123
|
+
@listener_port = nil
|
|
124
|
+
@listener_host = '127.0.0.1'
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Worker health subconfig. `max_rss_mb` recycles a worker that
|
|
129
|
+
# exceeds the configured RSS; `check_interval` is the poll period
|
|
130
|
+
# in seconds. The new `timeout` field is reserved for 1.8+ worker-
|
|
131
|
+
# heartbeat work; ships now so operators can pre-configure.
|
|
132
|
+
class WorkerHealthConfig
|
|
133
|
+
ATTRS = %i[max_rss_mb check_interval timeout].freeze
|
|
134
|
+
attr_accessor(*ATTRS)
|
|
135
|
+
|
|
136
|
+
def initialize
|
|
137
|
+
@max_rss_mb = nil
|
|
138
|
+
@check_interval = 30
|
|
139
|
+
@timeout = nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Logging subconfig. `level` / `format` mirror the 1.6.x flat
|
|
144
|
+
# setters; `requests` is the new home for `log_requests`. nil =
|
|
145
|
+
# delegate to `Hyperion.log_requests?` (env + default ON).
|
|
146
|
+
class LoggingConfig
|
|
147
|
+
ATTRS = %i[level format requests].freeze
|
|
148
|
+
attr_accessor(*ATTRS)
|
|
149
|
+
|
|
150
|
+
def initialize
|
|
151
|
+
@level = nil
|
|
152
|
+
@format = nil
|
|
153
|
+
@requests = nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 2.3-C: WebSocket subconfig. The headline knob is
|
|
158
|
+
# `permessage_deflate` — RFC 7692 per-message DEFLATE compression
|
|
159
|
+
# for the WS payload. Tri-state, mirrors `tls.ktls`:
|
|
160
|
+
# :off — never advertise the extension.
|
|
161
|
+
# :auto — accept if the client offers it (default; backwards
|
|
162
|
+
# compatible with clients that don't offer it).
|
|
163
|
+
# :on — require it; reject the handshake (400) if the client
|
|
164
|
+
# doesn't offer a usable variant.
|
|
165
|
+
class WebSocketConfig
|
|
166
|
+
ATTRS = %i[permessage_deflate].freeze
|
|
167
|
+
attr_accessor(*ATTRS)
|
|
168
|
+
|
|
169
|
+
DEFAULT_PERMESSAGE_DEFLATE = :auto
|
|
170
|
+
|
|
171
|
+
def initialize
|
|
172
|
+
@permessage_deflate = DEFAULT_PERMESSAGE_DEFLATE
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# 2.4-C: Metrics subconfig. The headline knob is `path_templater`,
|
|
177
|
+
# which collapses raw request paths to low-cardinality templates
|
|
178
|
+
# for the per-route latency histogram (operators with Rails-style
|
|
179
|
+
# routes plug in their own templater). `enabled` flips the new
|
|
180
|
+
# 2.4-C histogram/gauge surface as a whole — counters in the legacy
|
|
181
|
+
# surface (requests, bytes_read, …) keep emitting regardless.
|
|
182
|
+
class MetricsConfig
|
|
183
|
+
ATTRS = %i[path_templater enabled].freeze
|
|
184
|
+
attr_accessor(*ATTRS)
|
|
185
|
+
|
|
186
|
+
def initialize
|
|
187
|
+
@path_templater = nil # lazily defaulted to PathTemplater.new on first read
|
|
188
|
+
@enabled = true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def path_templater
|
|
192
|
+
@path_templater ||= Hyperion::Metrics::PathTemplater.new
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# 2.3-B: top-level `:auto` sentinel for `max_in_flight_per_conn`.
|
|
197
|
+
# `Config#finalize!` resolves to `thread_count / 4`, floor 1. Plain
|
|
198
|
+
# symbol (no nested struct) because the only knob is the cap value.
|
|
199
|
+
MAX_IN_FLIGHT_PER_CONN_AUTO = :auto
|
|
200
|
+
|
|
201
|
+
# TLS subconfig. New in 1.8.0 (Phase 4 — TLS session resumption).
|
|
202
|
+
# `session_cache_size` controls the size of the in-process server-
|
|
203
|
+
# side session cache used to short-circuit the full handshake when a
|
|
204
|
+
# client returns with a previously-issued session id. The default of
|
|
205
|
+
# 20_480 is sized for ~16 MiB of cache memory at 800 B/session — well
|
|
206
|
+
# under the workload-default 128 MiB worker RSS cap.
|
|
207
|
+
#
|
|
208
|
+
# `ticket_key_rotation_signal` selects the OS signal that triggers
|
|
209
|
+
# a session-cache flush + ticket-key roll on the master. `:USR2`
|
|
210
|
+
# (default) is conventional for "rotate keys" signals (nginx uses
|
|
211
|
+
# SIGUSR2 for binary-upgrade, but here it's the rotation event).
|
|
212
|
+
# Set to `:NONE` to disable rotation entirely (workloads that don't
|
|
213
|
+
# care about ticket-key rotation security guarantees).
|
|
214
|
+
class TlsConfig
|
|
215
|
+
ATTRS = %i[session_cache_size ticket_key_rotation_signal ktls handshake_rate_limit].freeze
|
|
216
|
+
attr_accessor(*ATTRS)
|
|
217
|
+
|
|
218
|
+
DEFAULT_SESSION_CACHE_SIZE = 20_480
|
|
219
|
+
DEFAULT_ROTATION_SIGNAL = :USR2
|
|
220
|
+
# 2.2.0 (Phase 9): kernel TLS_TX policy.
|
|
221
|
+
# :auto — enable on Linux when supported, off elsewhere
|
|
222
|
+
# :on — force enable; raise at boot if unsupported
|
|
223
|
+
# :off — never enable, always use userspace SSL_write
|
|
224
|
+
DEFAULT_KTLS = :auto
|
|
225
|
+
# 2.3-B: TLS handshake CPU throttle. Token-bucket budget for
|
|
226
|
+
# SSL_accept calls per second per worker. Defends direct-exposure
|
|
227
|
+
# operators against handshake storms (e.g., many short-lived TLS
|
|
228
|
+
# clients reconnecting at once during a deployment). For the
|
|
229
|
+
# nginx-fronted topology this is mostly defensive — nginx keeps
|
|
230
|
+
# long-lived upstream conns so handshake rate is normally near-zero.
|
|
231
|
+
# * Integer >= 1 — handshakes/sec/worker (capacity == rate).
|
|
232
|
+
# * :unlimited (default) — no limit; matches 2.2.0 behaviour.
|
|
233
|
+
DEFAULT_HANDSHAKE_RATE_LIMIT = :unlimited
|
|
234
|
+
|
|
235
|
+
def initialize
|
|
236
|
+
@session_cache_size = DEFAULT_SESSION_CACHE_SIZE
|
|
237
|
+
@ticket_key_rotation_signal = DEFAULT_ROTATION_SIGNAL
|
|
238
|
+
@ktls = DEFAULT_KTLS
|
|
239
|
+
@handshake_rate_limit = DEFAULT_HANDSHAKE_RATE_LIMIT
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# CLI-only flat→nested setter map. The DSL surface no longer
|
|
244
|
+
# honours these names (2.0 removed the flat DSL forwarders), but
|
|
245
|
+
# `Config#merge_cli!` still receives flat-keyed cli_opts hashes
|
|
246
|
+
# built by the OptionParser branches in `Hyperion::CLI`. Routing
|
|
247
|
+
# them via this table keeps CLI flag spellings stable
|
|
248
|
+
# (`--admin-token`, `--log-level`, …) without re-introducing the
|
|
249
|
+
# deprecated DSL surface.
|
|
250
|
+
CLI_FLAT_TO_NESTED = {
|
|
251
|
+
h2_max_concurrent_streams: %i[h2 max_concurrent_streams],
|
|
252
|
+
h2_initial_window_size: %i[h2 initial_window_size],
|
|
253
|
+
h2_max_frame_size: %i[h2 max_frame_size],
|
|
254
|
+
h2_max_header_list_size: %i[h2 max_header_list_size],
|
|
255
|
+
h2_max_total_streams: %i[h2 max_total_streams],
|
|
256
|
+
admin_token: %i[admin token],
|
|
257
|
+
admin_listener_port: %i[admin listener_port],
|
|
258
|
+
admin_listener_host: %i[admin listener_host],
|
|
259
|
+
worker_max_rss_mb: %i[worker_health max_rss_mb],
|
|
260
|
+
worker_check_interval: %i[worker_health check_interval],
|
|
261
|
+
log_level: %i[logging level],
|
|
262
|
+
log_format: %i[logging format],
|
|
263
|
+
log_requests: %i[logging requests],
|
|
264
|
+
tls_handshake_rate_limit: %i[tls handshake_rate_limit]
|
|
265
|
+
}.freeze
|
|
266
|
+
|
|
46
267
|
def initialize
|
|
47
268
|
DEFAULTS.each { |k, v| public_send(:"#{k}=", v) }
|
|
48
269
|
HOOKS.each { |h| instance_variable_set(:"@#{h}", []) }
|
|
270
|
+
@h2 = H2Settings.new
|
|
271
|
+
@admin = AdminConfig.new
|
|
272
|
+
@worker_health = WorkerHealthConfig.new
|
|
273
|
+
@logging = LoggingConfig.new
|
|
274
|
+
@tls = TlsConfig.new
|
|
275
|
+
@websocket = WebSocketConfig.new
|
|
276
|
+
@metrics = MetricsConfig.new
|
|
277
|
+
# 2.10-E: per-instance Array — DEFAULTS is frozen so we can't share
|
|
278
|
+
# a literal `[]` across Config instances or every operator's DSL
|
|
279
|
+
# `preload_static` call would mutate the same backing list.
|
|
280
|
+
@preload_static_dirs = []
|
|
49
281
|
end
|
|
50
282
|
|
|
51
283
|
HOOKS.each do |hook|
|
|
@@ -64,21 +296,124 @@ module Hyperion
|
|
|
64
296
|
cfg
|
|
65
297
|
end
|
|
66
298
|
|
|
299
|
+
# Sentinel surfaced through `Config#h2.max_total_streams` when the
|
|
300
|
+
# operator hasn't touched the setting and 2.0's auto-default formula
|
|
301
|
+
# ought to compute on their behalf at finalize time. The `nil` value
|
|
302
|
+
# (RFC §3 1.7 default) used to mean "admission disabled forever";
|
|
303
|
+
# 2.0 redefines `nil` as "auto" and adds an explicit
|
|
304
|
+
# `H2Settings::UNBOUNDED` sentinel for operators who want the
|
|
305
|
+
# pre-2.0 unbounded behaviour.
|
|
306
|
+
#
|
|
307
|
+
# The Auto path is a sentinel-only wire — `H2Settings#initialize` no
|
|
308
|
+
# longer sets a hard `nil`; finalize! resolves it to
|
|
309
|
+
# `max_concurrent_streams × workers × 4` and writes the result back
|
|
310
|
+
# onto `h2.max_total_streams`. Operators reading the value before
|
|
311
|
+
# finalize see the sentinel; after finalize see the resolved
|
|
312
|
+
# integer.
|
|
313
|
+
# Resolve any "auto" sentinels to concrete integers based on
|
|
314
|
+
# finalized peer settings. Called once after `merge_cli!` and after
|
|
315
|
+
# the worker count is known (Master#initialize / CLI run_single).
|
|
316
|
+
# Idempotent — a finalized config can be re-finalized without
|
|
317
|
+
# changing values.
|
|
318
|
+
def finalize!(workers:)
|
|
319
|
+
case @h2.max_total_streams
|
|
320
|
+
when H2Settings::AUTO
|
|
321
|
+
@h2.max_total_streams = compute_h2_max_total_streams(workers: workers)
|
|
322
|
+
when H2Settings::UNBOUNDED
|
|
323
|
+
@h2.max_total_streams = nil
|
|
324
|
+
end
|
|
325
|
+
# 2.3-B: resolve the `:auto` sentinel for the per-conn fairness
|
|
326
|
+
# cap. `thread_count / 4` (floor 1) gives each conn at most 25% of
|
|
327
|
+
# the worker's thread budget — the recommended default. Operators
|
|
328
|
+
# who set an explicit integer at config time keep their value
|
|
329
|
+
# untouched; nil (no cap, 2.2.0 default) is also preserved.
|
|
330
|
+
@max_in_flight_per_conn = compute_max_in_flight_per_conn if @max_in_flight_per_conn == MAX_IN_FLIGHT_PER_CONN_AUTO
|
|
331
|
+
self
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# 2.0 default formula (RFC §3): per-conn cap × worker count × 4.
|
|
335
|
+
# The 4× headroom factor assumes the average connection holds 25%
|
|
336
|
+
# of the per-conn cap; well above realistic legitimate fan-out yet
|
|
337
|
+
# still bounds the OOM abuse window (5k conns × 128 streams = 640k
|
|
338
|
+
# fibers).
|
|
339
|
+
def compute_h2_max_total_streams(workers:)
|
|
340
|
+
cap_per_conn = @h2.max_concurrent_streams || H2Settings.new.max_concurrent_streams
|
|
341
|
+
worker_count = (workers && workers.positive? ? workers : 1)
|
|
342
|
+
cap_per_conn * worker_count * 4
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# 2.3-B per-conn fairness default: `thread_count / 4`, floor 1.
|
|
346
|
+
# Each conn caps at 25% of the worker's thread budget so a single
|
|
347
|
+
# greedy upstream connection can't starve siblings. Floor of 1
|
|
348
|
+
# ensures degenerate `-t 1` / `-t 2` / `-t 3` configurations still
|
|
349
|
+
# serve traffic (cap 1 = strictly serial per conn, but no rejects
|
|
350
|
+
# while no conn is currently dispatched).
|
|
351
|
+
def compute_max_in_flight_per_conn
|
|
352
|
+
threads = (@thread_count && @thread_count.positive? ? @thread_count : 1)
|
|
353
|
+
cap = threads / 4
|
|
354
|
+
cap = 1 if cap < 1
|
|
355
|
+
cap
|
|
356
|
+
end
|
|
357
|
+
|
|
67
358
|
# Apply CLI overrides on top of an existing config. Only non-nil values
|
|
68
359
|
# in `overrides` are applied — preserves the precedence ordering
|
|
69
360
|
# (CLI > env > config file > default).
|
|
361
|
+
#
|
|
362
|
+
# 2.0.0: flat keys that map to a nested subconfig
|
|
363
|
+
# (`admin_token` → `admin.token`, `log_level` → `logging.level`, …)
|
|
364
|
+
# are dispatched through `CLI_FLAT_TO_NESTED`. The DSL no longer
|
|
365
|
+
# accepts these names, but the CLI flag surface keeps its 1.x
|
|
366
|
+
# spellings — operators don't have to learn a new flag set.
|
|
367
|
+
#
|
|
368
|
+
# 2.10-E: `:preload_static` is special-cased — it's an Array of dir
|
|
369
|
+
# strings from the repeatable `--preload-static` flag, and we
|
|
370
|
+
# APPEND each as `{path:, immutable: true}` to the already-populated
|
|
371
|
+
# `preload_static_dirs` list. Operator config-file entries land
|
|
372
|
+
# first; CLI flags win by being applied last.
|
|
70
373
|
def merge_cli!(overrides)
|
|
71
374
|
overrides.each do |key, value|
|
|
72
375
|
next if value.nil?
|
|
73
376
|
|
|
74
|
-
|
|
377
|
+
if key == :preload_static
|
|
378
|
+
Array(value).each do |dir|
|
|
379
|
+
preload_static_dirs << { path: dir.to_s, immutable: true }
|
|
380
|
+
end
|
|
381
|
+
elsif (route = CLI_FLAT_TO_NESTED[key])
|
|
382
|
+
group, nested = route
|
|
383
|
+
public_send(group).public_send(:"#{nested}=", value)
|
|
384
|
+
elsif respond_to?(:"#{key}=")
|
|
385
|
+
public_send(:"#{key}=", value)
|
|
386
|
+
end
|
|
75
387
|
end
|
|
76
388
|
self
|
|
77
389
|
end
|
|
78
390
|
|
|
391
|
+
# 2.10-E — resolve the operator-supplied preload list, falling
|
|
392
|
+
# through to Rails auto-detect when no explicit dirs are configured
|
|
393
|
+
# AND auto-detect is not disabled by the operator. Always returns
|
|
394
|
+
# an Array of `{path:, immutable:}` Hashes (possibly empty).
|
|
395
|
+
#
|
|
396
|
+
# Precedence:
|
|
397
|
+
# 1. Operator-supplied (DSL `preload_static` or CLI flags) — used verbatim.
|
|
398
|
+
# 2. Otherwise, Rails-detected paths if auto-detect is enabled.
|
|
399
|
+
# 3. Otherwise, [] — no preload, 1.x cold-cache behaviour.
|
|
400
|
+
def resolved_preload_static_dirs
|
|
401
|
+
return preload_static_dirs.dup unless preload_static_dirs.empty?
|
|
402
|
+
return [] if auto_preload_static_disabled
|
|
403
|
+
|
|
404
|
+
Hyperion::StaticPreload.detect_rails_paths.map do |path|
|
|
405
|
+
{ path: path, immutable: true }
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
79
409
|
# DSL receiver. Each method call on the DSL maps to a Config setter or
|
|
80
410
|
# to a hook registration. Unknown methods raise NoMethodError so typos
|
|
81
411
|
# surface immediately at boot rather than as silent ignores.
|
|
412
|
+
#
|
|
413
|
+
# 1.7.0 (RFC A4) added nested block forms — `h2 do |h| ... end` and
|
|
414
|
+
# the bare-block `worker_health do; max_rss_mb 1024; end` shape. The
|
|
415
|
+
# flat `h2_max_concurrent_streams 256` form keeps working untouched
|
|
416
|
+
# in 1.7; deprecation warn lands in 1.8, removal in 2.0.
|
|
82
417
|
class DSL
|
|
83
418
|
def initialize(config)
|
|
84
419
|
@config = config
|
|
@@ -89,18 +424,46 @@ module Hyperion
|
|
|
89
424
|
@config.host = value
|
|
90
425
|
end
|
|
91
426
|
|
|
427
|
+
# Top-level flat setters. We define them dynamically off the
|
|
428
|
+
# DEFAULTS hash so adding a new top-level field auto-wires the DSL.
|
|
92
429
|
Config::DEFAULTS.each_key do |key|
|
|
93
430
|
define_method(key) do |value|
|
|
94
431
|
@config.public_send(:"#{key}=", value)
|
|
95
432
|
end
|
|
96
433
|
end
|
|
97
434
|
|
|
435
|
+
# 2.0.0: the flat DSL keys (`h2_max_concurrent_streams`, `admin_token`,
|
|
436
|
+
# `log_format`, …) are removed. Operators must use the nested DSL
|
|
437
|
+
# blocks defined below. Unknown DSL methods bubble up as
|
|
438
|
+
# `NoMethodError` from the DSL evaluator — typos surface at boot.
|
|
98
439
|
Config::HOOKS.each do |hook|
|
|
99
440
|
define_method(hook) do |&block|
|
|
100
441
|
@config.public_send(:"add_#{hook}", &block)
|
|
101
442
|
end
|
|
102
443
|
end
|
|
103
444
|
|
|
445
|
+
# Nested-block DSL — `h2 do |h| h.max_concurrent_streams 256 end`
|
|
446
|
+
# OR `h2 do; max_concurrent_streams 256; end`. The block is
|
|
447
|
+
# eval'd against a BlockProxy that proxies bareword method calls
|
|
448
|
+
# into the subconfig's accessors; explicit-arg form (`|h|`) gives
|
|
449
|
+
# callers the proxy directly so they can pass it around.
|
|
450
|
+
%i[h2 admin worker_health logging tls websocket metrics].each do |group|
|
|
451
|
+
define_method(group) do |&block|
|
|
452
|
+
subconfig = @config.public_send(group)
|
|
453
|
+
if block.nil?
|
|
454
|
+
subconfig
|
|
455
|
+
else
|
|
456
|
+
proxy = BlockProxy.new(subconfig)
|
|
457
|
+
if block.arity.zero? || block.arity.negative?
|
|
458
|
+
proxy.instance_eval(&block)
|
|
459
|
+
else
|
|
460
|
+
block.call(proxy)
|
|
461
|
+
end
|
|
462
|
+
subconfig
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
104
467
|
# `tls_cert_path` / `tls_key_path` are convenience aliases that read
|
|
105
468
|
# the file off disk so the DSL stays terse. The parsed cert/key are
|
|
106
469
|
# stored on the config and Server consumes them directly.
|
|
@@ -113,6 +476,69 @@ module Hyperion
|
|
|
113
476
|
require 'openssl'
|
|
114
477
|
@config.tls_key = OpenSSL::PKey.read(File.read(path))
|
|
115
478
|
end
|
|
479
|
+
|
|
480
|
+
# 2.10-E — `preload_static "/path", immutable: true` DSL key.
|
|
481
|
+
# Appends `{path:, immutable:}` onto `preload_static_dirs`. The
|
|
482
|
+
# `immutable:` kwarg defaults to true — the whole point of preload
|
|
483
|
+
# is "I promise these don't change without a restart" so the
|
|
484
|
+
# immutable flag is the operator-friendly default. Multiple calls
|
|
485
|
+
# accumulate.
|
|
486
|
+
#
|
|
487
|
+
# Overrides the auto-generated DEFAULTS-based setter for the
|
|
488
|
+
# backing field (which would write the entire array via `=`); this
|
|
489
|
+
# explicit method is the one the DSL actually exposes.
|
|
490
|
+
def preload_static(path, immutable: true)
|
|
491
|
+
@config.preload_static_dirs << { path: path.to_s, immutable: immutable }
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Block-form DSL proxy for nested subconfigs. Each bareword call
|
|
496
|
+
# inside `h2 do; max_concurrent_streams 256; end` lands on this
|
|
497
|
+
# proxy, which forwards into the wrapped subconfig's setter. Also
|
|
498
|
+
# supports explicit-arg form `h2 do |h| h.max_concurrent_streams 256 end`
|
|
499
|
+
# via the same accessor path. Unknown method names raise
|
|
500
|
+
# NoMethodError — typos surface at boot, matching the top-level
|
|
501
|
+
# DSL's strictness.
|
|
502
|
+
#
|
|
503
|
+
# Inherits from BasicObject so Ruby's `Kernel#format` / `Kernel#level`
|
|
504
|
+
# / etc. don't shadow our subconfig setters when callers write
|
|
505
|
+
# `logging do; format :json; end` — Kernel methods are absent on
|
|
506
|
+
# BasicObject, so the bareword falls through to method_missing.
|
|
507
|
+
class BlockProxy < BasicObject
|
|
508
|
+
def initialize(target)
|
|
509
|
+
@target = target
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def method_missing(name, *args, &block)
|
|
513
|
+
setter = :"#{name}="
|
|
514
|
+
if @target.respond_to?(setter)
|
|
515
|
+
# Single-arg sets (`max_concurrent_streams 256`) write the
|
|
516
|
+
# value through. Zero-arg calls (`max_concurrent_streams`)
|
|
517
|
+
# behave as readers — needed by the explicit-arg form so
|
|
518
|
+
# `h.max_concurrent_streams` returns the current value.
|
|
519
|
+
if args.length == 1
|
|
520
|
+
@target.public_send(setter, args.first)
|
|
521
|
+
elsif args.empty? && @target.respond_to?(name)
|
|
522
|
+
@target.public_send(name)
|
|
523
|
+
else
|
|
524
|
+
::Kernel.raise ::NoMethodError, "no DSL setter for #{name.inspect} on #{@target.class}"
|
|
525
|
+
end
|
|
526
|
+
elsif @target.respond_to?(name)
|
|
527
|
+
@target.public_send(name, *args, &block)
|
|
528
|
+
else
|
|
529
|
+
::Kernel.raise ::NoMethodError, "no DSL setter for #{name.inspect} on #{@target.class}"
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def respond_to_missing?(name, include_private = false)
|
|
534
|
+
setter = :"#{name}="
|
|
535
|
+
@target.respond_to?(setter) || @target.respond_to?(name, include_private)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# BasicObject lacks #class — provide it for tests + introspection.
|
|
539
|
+
def class
|
|
540
|
+
::Hyperion::Config::BlockProxy
|
|
541
|
+
end
|
|
116
542
|
end
|
|
117
543
|
end
|
|
118
544
|
end
|