Class: Hyperion::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/hyperion/config.rb

Overview

Mutable configuration container — populated by the DSL evaluator (Hyperion::Config.load) and then read by CLI / Server / Master / Worker / Connection / Logger.

All settings have safe defaults that match the per-class DEFAULT_* constants so that running Hyperion without a config file works identically to the pre-rc14 behaviour.

1.7.0 (RFC A4): grouped settings live in nested subconfigs —‘config.h2.*`, `config.admin.*`, `config.worker_health.*`, `config.logging.*`. 1.7 added the nested DSL alongside the legacy flat keys; 1.8 deprecation-warned the flat keys; 2.0 removed them. The nested DSL is the only configuration surface — flat aliases like `h2_max_concurrent_streams` no longer exist on the DSL or on `Config` itself.

Defined Under Namespace

Classes: AdminConfig, BlockProxy, DSL, H2Settings, LoggingConfig, MetricsConfig, TlsConfig, WebSocketConfig, WorkerHealthConfig

Constant Summary collapse

DEFAULTS =

Top-level (un-nested) defaults. Flat fields that don’t group naturally are deliberately kept here per the RFC’s “only 8 fields warrant nesting in A4” guidance — ‘max_pending`, `idle_keepalive`, `graceful_timeout`, the `tls_*` family, `read_timeout`, and the body/header byte caps stay flat.

{
  host: '127.0.0.1',
  port: 9292,
  workers: 1,
  thread_count: 5,
  tls_cert: nil,
  tls_key: nil,
  read_timeout: 30,
  idle_keepalive: 5,
  graceful_timeout: 30,
  max_header_bytes: 64 * 1024,
  max_body_bytes: 16 * 1024 * 1024,
  fiber_local_shim: false,
  yjit: nil, # nil → auto: enable on production/staging; true/false to force.
  max_pending: nil,
  max_request_read_seconds: 60,
  async_io: nil, # nil/true/false (validated strictly in 1.7.0+ via Server constructor).
  accept_fibers_per_worker: 1, # RFC A6 — opt-in multi-fiber accept under :reuseport.
  # 2.3-A: io_uring accept policy (Linux 5.6+ only). Tri-state, mirrors `tls.ktls`:
  #   :off  — never use io_uring; epoll path always (2.3.0 default).
  #   :auto — use io_uring when supported; quietly fall back otherwise.
  #   :on   — demand it; raise at boot if unsupported.
  # Default flips to :auto in 2.4 only after soak. Operators flip on
  # via `HYPERION_IO_URING={on,auto}` env var to A/B test.
  io_uring: :off,
  # 2.3-B: per-connection in-flight cap. nginx upstream keep-alive
  # pipelines many client requests through one upstream connection;
  # without this cap a single greedy upstream conn can hog the worker
  # thread pool and starve siblings. Tri-state:
  #   * Integer >= 1 — explicit cap (e.g., `4` for `-t 16`).
  #   * :auto         — `Config#finalize!` resolves to `thread_count / 4`
  #                     (rounded down, minimum 1). Operator opt-in.
  #   * nil (default) — no cap; matches 2.2.0 behaviour. Hyperion is
  #                     opt-in by default — the cap is a hardening tool
  #                     that operators turn on, not a default flip.
  max_in_flight_per_conn: nil,
  # 2.10-E: explicit `preload_static "/path"` DSL entries plus the
  # CLI's repeatable `--preload-static <dir>` flag accumulate here.
  # Each element is a `{path: String, immutable: Boolean}` Hash.
  # `Server#listen` walks the resolved list (which may also include
  # auto-detected Rails asset paths — see `auto_preload_static_disabled`)
  # and warms `Hyperion::Http::PageCache` before the accept loop spins.
  # Default empty so a vanilla Rack app pays nothing.
  preload_static_dirs: nil,
  # 2.10-E: when truthy, suppress the Rails-aware auto-detect path
  # (`Rails.configuration.assets.paths.first(N)`) at boot.  Set by
  # the `--no-preload-static` CLI flag; lets operators turn off
  # auto-warming on a Rails app while still keeping the option to
  # configure explicit dirs via `preload_static`.
  auto_preload_static_disabled: false
}.freeze
HOOKS =
%i[before_fork on_worker_boot on_worker_shutdown].freeze
MAX_IN_FLIGHT_PER_CONN_AUTO =

2.3-B: top-level ‘:auto` sentinel for `max_in_flight_per_conn`. `Config#finalize!` resolves to `thread_count / 4`, floor 1. Plain symbol (no nested struct) because the only knob is the cap value.

:auto
CLI_FLAT_TO_NESTED =

CLI-only flat→nested setter map. The DSL surface no longer honours these names (2.0 removed the flat DSL forwarders), but ‘Config#merge_cli!` still receives flat-keyed cli_opts hashes built by the OptionParser branches in `Hyperion::CLI`. Routing them via this table keeps CLI flag spellings stable (`–admin-token`, `–log-level`, …) without re-introducing the deprecated DSL surface.

{
  h2_max_concurrent_streams: %i[h2 max_concurrent_streams],
  h2_initial_window_size: %i[h2 initial_window_size],
  h2_max_frame_size: %i[h2 max_frame_size],
  h2_max_header_list_size: %i[h2 max_header_list_size],
  h2_max_total_streams: %i[h2 max_total_streams],
  admin_token: %i[admin token],
  admin_listener_port: %i[admin listener_port],
  admin_listener_host: %i[admin listener_host],
  worker_max_rss_mb: %i[worker_health max_rss_mb],
  worker_check_interval: %i[worker_health check_interval],
  log_level: %i[logging level],
  log_format: %i[logging format],
  log_requests: %i[logging requests],
  tls_handshake_rate_limit: %i[tls handshake_rate_limit]
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfig

Returns a new instance of Config.



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/hyperion/config.rb', line 267

def initialize
  DEFAULTS.each { |k, v| public_send(:"#{k}=", v) }
  HOOKS.each { |h| instance_variable_set(:"@#{h}", []) }
  @h2            = H2Settings.new
  @admin         = AdminConfig.new
  @worker_health = WorkerHealthConfig.new
  @logging       = LoggingConfig.new
  @tls           = TlsConfig.new
  @websocket     = WebSocketConfig.new
  @metrics       = MetricsConfig.new
  # 2.10-E: per-instance Array — DEFAULTS is frozen so we can't share
  # a literal `[]` across Config instances or every operator's DSL
  # `preload_static` call would mutate the same backing list.
  @preload_static_dirs = []
end

Instance Attribute Details

#adminObject (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def admin
  @admin
end

#h2Object (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def h2
  @h2
end

#loggingObject (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def logging
  @logging
end

#metricsObject (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def metrics
  @metrics
end

#tlsObject (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def tls
  @tls
end

#websocketObject (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def websocket
  @websocket
end

#worker_healthObject (readonly)

Nested subconfig readers. The DSL exposes them as block forms (‘h2 do |h| … end`) and the legacy flat forms (`h2_max_concurrent_streams 256`) both write into the same backing object.



87
88
89
# File 'lib/hyperion/config.rb', line 87

def worker_health
  @worker_health
end

Class Method Details

.load(path) ⇒ Object

Load a Ruby DSL config file. Returns the populated Config. Path is the operator-supplied –config argument; we evaluate it in a DSL context that maps method calls to attribute setters.



292
293
294
295
296
297
# File 'lib/hyperion/config.rb', line 292

def self.load(path)
  cfg = new
  contents = File.read(path)
  DSL.new(cfg).instance_eval(contents, path)
  cfg
end

Instance Method Details

#compute_h2_max_total_streams(workers:) ⇒ Object

2.0 default formula (RFC §3): per-conn cap × worker count × 4. The 4× headroom factor assumes the average connection holds 25% of the per-conn cap; well above realistic legitimate fan-out yet still bounds the OOM abuse window (5k conns × 128 streams = 640k fibers).



339
340
341
342
343
# File 'lib/hyperion/config.rb', line 339

def compute_h2_max_total_streams(workers:)
  cap_per_conn = @h2.max_concurrent_streams || H2Settings.new.max_concurrent_streams
  worker_count = (workers && workers.positive? ? workers : 1)
  cap_per_conn * worker_count * 4
end

#compute_max_in_flight_per_connObject

2.3-B per-conn fairness default: ‘thread_count / 4`, floor 1. Each conn caps at 25% of the worker’s thread budget so a single greedy upstream connection can’t starve siblings. Floor of 1 ensures degenerate ‘-t 1` / `-t 2` / `-t 3` configurations still serve traffic (cap 1 = strictly serial per conn, but no rejects while no conn is currently dispatched).



351
352
353
354
355
356
# File 'lib/hyperion/config.rb', line 351

def compute_max_in_flight_per_conn
  threads = (@thread_count && @thread_count.positive? ? @thread_count : 1)
  cap = threads / 4
  cap = 1 if cap < 1
  cap
end

#finalize!(workers:) ⇒ Object

Sentinel surfaced through ‘Config#h2.max_total_streams` when the operator hasn’t touched the setting and 2.0’s auto-default formula ought to compute on their behalf at finalize time. The ‘nil` value (RFC §3 1.7 default) used to mean “admission disabled forever”; 2.0 redefines `nil` as “auto” and adds an explicit `H2Settings::UNBOUNDED` sentinel for operators who want the pre-2.0 unbounded behaviour.

The Auto path is a sentinel-only wire — ‘H2Settings#initialize` no longer sets a hard `nil`; finalize! resolves it to `max_concurrent_streams × workers × 4` and writes the result back onto `h2.max_total_streams`. Operators reading the value before finalize see the sentinel; after finalize see the resolved integer. Resolve any “auto” sentinels to concrete integers based on finalized peer settings. Called once after `merge_cli!` and after the worker count is known (Master#initialize / CLI run_single). Idempotent — a finalized config can be re-finalized without changing values.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/hyperion/config.rb', line 318

def finalize!(workers:)
  case @h2.max_total_streams
  when H2Settings::AUTO
    @h2.max_total_streams = compute_h2_max_total_streams(workers: workers)
  when H2Settings::UNBOUNDED
    @h2.max_total_streams = nil
  end
  # 2.3-B: resolve the `:auto` sentinel for the per-conn fairness
  # cap. `thread_count / 4` (floor 1) gives each conn at most 25% of
  # the worker's thread budget — the recommended default. Operators
  # who set an explicit integer at config time keep their value
  # untouched; nil (no cap, 2.2.0 default) is also preserved.
  @max_in_flight_per_conn = compute_max_in_flight_per_conn if @max_in_flight_per_conn == MAX_IN_FLIGHT_PER_CONN_AUTO
  self
end

#merge_cli!(overrides) ⇒ Object

Apply CLI overrides on top of an existing config. Only non-nil values in ‘overrides` are applied — preserves the precedence ordering (CLI > env > config file > default).

2.0.0: flat keys that map to a nested subconfig (‘admin_token` → `admin.token`, `log_level` → `logging.level`, …) are dispatched through `CLI_FLAT_TO_NESTED`. The DSL no longer accepts these names, but the CLI flag surface keeps its 1.x spellings — operators don’t have to learn a new flag set.

2.10-E: ‘:preload_static` is special-cased — it’s an Array of dir strings from the repeatable ‘–preload-static` flag, and we APPEND each as `immutable: true` to the already-populated `preload_static_dirs` list. Operator config-file entries land first; CLI flags win by being applied last.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/hyperion/config.rb', line 373

def merge_cli!(overrides)
  overrides.each do |key, value|
    next if value.nil?

    if key == :preload_static
      Array(value).each do |dir|
        preload_static_dirs << { path: dir.to_s, immutable: true }
      end
    elsif (route = CLI_FLAT_TO_NESTED[key])
      group, nested = route
      public_send(group).public_send(:"#{nested}=", value)
    elsif respond_to?(:"#{key}=")
      public_send(:"#{key}=", value)
    end
  end
  self
end

#resolved_preload_static_dirsObject

2.10-E — resolve the operator-supplied preload list, falling through to Rails auto-detect when no explicit dirs are configured AND auto-detect is not disabled by the operator. Always returns an Array of ‘immutable:` Hashes (possibly empty).

Precedence:

1. Operator-supplied (DSL `preload_static` or CLI flags) — used verbatim.
2. Otherwise, Rails-detected paths if auto-detect is enabled.
3. Otherwise, [] — no preload, 1.x cold-cache behaviour.


400
401
402
403
404
405
406
407
# File 'lib/hyperion/config.rb', line 400

def resolved_preload_static_dirs
  return preload_static_dirs.dup unless preload_static_dirs.empty?
  return [] if auto_preload_static_disabled

  Hyperion::StaticPreload.detect_rails_paths.map do |path|
    { path: path, immutable: true }
  end
end