Class: Hyperion::Runtime
- Inherits:
-
Object
- Object
- Hyperion::Runtime
- Defined in:
- lib/hyperion/runtime.rb
Overview
Runtime services container — holds per-process or per-server services (metrics sink, logger, clock) that used to live as module-level singletons on ‘Hyperion`.
Pre-1.7 every Connection / Http2Handler / ResponseWriter reached for ‘Hyperion.metrics` / `Hyperion.logger` directly. That made:
1. Long-lived keep-alive connections impossible to swap services on
mid-flight — `Connection#initialize` cached the singletons in ivars
and never re-read them.
2. Multi-tenant apps unable to give each `Hyperion::Server` its own
metrics sink — the module-level singleton is process-global.
3. Tests messy: stubbing `Hyperion.metrics` is global state mutation
that bleeds across examples unless every spec resets explicitly.
1.7.0 introduces this Runtime and adds a ‘runtime:` kwarg to `Server` and `Connection`. The default is `Runtime.default`, a process-wide singleton — back-compat with the 1.6.x behaviour. Tests and library users construct their own `Runtime.new(metrics: …, logger: …)` and pass it explicitly; that runtime is then used exclusively by the connection/server it was given to.
‘Runtime.default` is intentionally NOT frozen after first read. RFC §5 Q4: tests need to swap metrics/logger on the default runtime, and freezing for no real safety benefit just adds ceremony.
Module-level ‘Hyperion.metrics` / `Hyperion.logger` (and their writers) keep working — they delegate to `Runtime.default`. They’re marked for deprecation in 1.8.0 and removal in 2.0.
Instance Attribute Summary collapse
-
#clock ⇒ Object
readonly
Returns the value of attribute clock.
- #logger ⇒ Object
-
#metrics ⇒ Object
The default Runtime’s metrics / logger readers honour module-level ivar overrides on ‘Hyperion` itself.
Class Method Summary collapse
-
.default ⇒ Object
Process-wide default Runtime.
-
.default=(runtime) ⇒ Object
Test seam: replace the process-wide default.
-
.reset_default! ⇒ Object
Test seam: clear the memoized default so the next ‘default` call builds a fresh one.
Instance Method Summary collapse
-
#default? ⇒ Boolean
True when this runtime is ‘Runtime.default`.
-
#fire_request_end(request, env, response, error) ⇒ Object
2.5-C — invoked by Adapter::Rack#call after ‘app.call` returns (or raises).
-
#fire_request_start(request, env) ⇒ Object
2.5-C — invoked by Adapter::Rack#call after env is built.
-
#has_request_hooks? ⇒ Boolean
2.5-C — zero-cost guard used by Adapter::Rack#call.
-
#initialize(metrics: nil, logger: nil, clock: Process) ⇒ Runtime
constructor
A new instance of Runtime.
-
#on_request_end(&block) ⇒ Object
2.5-C — register a Proc to fire AFTER ‘app.call` returns or raises.
-
#on_request_start(&block) ⇒ Object
2.5-C — register a Proc to fire AFTER env is built but BEFORE ‘app.call`.
Constructor Details
#initialize(metrics: nil, logger: nil, clock: Process) ⇒ Runtime
Returns a new instance of Runtime.
95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/hyperion/runtime.rb', line 95 def initialize(metrics: nil, logger: nil, clock: Process) @metrics = metrics || Hyperion::Metrics.new @logger = logger || Hyperion::Logger.new @clock = clock # 2.5-C: per-request lifecycle hooks. Pre-allocated empty Arrays so # `has_request_hooks?` can be a single `any?` check on each side # — no nil-guard, no lazy-init branch on the hot path. Hooks are # appended in registration order; FIFO dispatch. @before_request_hooks = [] @after_request_hooks = [] end |
Instance Attribute Details
#clock ⇒ Object (readonly)
Returns the value of attribute clock.
34 35 36 |
# File 'lib/hyperion/runtime.rb', line 34 def clock @clock end |
#logger ⇒ Object
53 54 55 56 |
# File 'lib/hyperion/runtime.rb', line 53 def logger override = Hyperion.instance_variable_get(:@logger) if default? override || @logger end |
#metrics ⇒ Object
The default Runtime’s metrics / logger readers honour module-level ivar overrides on ‘Hyperion` itself. This preserves a back-compat seam for 1.6.x specs that swap by reaching into private internals via `Hyperion.instance_variable_set(:@metrics, …)` — the new Runtime-routed code paths (Server / Connection / Http2Handler) all read `runtime.metrics`, so without this the override would only affect the legacy `Hyperion.metrics` reader and the new code path would still write to the original Runtime-owned object.
Custom Runtimes (‘Hyperion::Runtime.new(…)`) ignore the override entirely — they’re per-Server isolated by design.
48 49 50 51 |
# File 'lib/hyperion/runtime.rb', line 48 def metrics override = Hyperion.instance_variable_get(:@metrics) if default? override || @metrics end |
Class Method Details
.default ⇒ Object
Process-wide default Runtime. Lazily initialized on first read. Module-level ‘Hyperion.metrics` / `Hyperion.logger` accessors and writers all delegate to this instance, so legacy callers in 1.6.x shape (`Hyperion.metrics = MyAdapter.new`) keep working without any source change.
Tests can mutate ‘Runtime.default.metrics = …` directly or replace the whole default with `Runtime.default = Runtime.new(…)` (writer below). Resetting between examples is on the test author — there’s no auto-reset because the singleton is part of the public surface.
75 76 77 |
# File 'lib/hyperion/runtime.rb', line 75 def self.default @default ||= new end |
.default=(runtime) ⇒ Object
Test seam: replace the process-wide default. Used in specs that need to inject a known-state Runtime without reaching into ‘@default` directly.
82 83 84 85 86 |
# File 'lib/hyperion/runtime.rb', line 82 def self.default=(runtime) raise ArgumentError, 'expected a Hyperion::Runtime' unless runtime.is_a?(Runtime) @default = runtime end |
.reset_default! ⇒ Object
Test seam: clear the memoized default so the next ‘default` call builds a fresh one. Equivalent to `default = Runtime.new` but without forcing the caller to allocate.
91 92 93 |
# File 'lib/hyperion/runtime.rb', line 91 def self.reset_default! @default = nil end |
Instance Method Details
#default? ⇒ Boolean
True when this runtime is ‘Runtime.default`. The default runtime is the one consulted by legacy module-level accessors — see the `metrics` / `logger` readers above.
61 62 63 |
# File 'lib/hyperion/runtime.rb', line 61 def default? Runtime.instance_variable_get(:@default).equal?(self) end |
#fire_request_end(request, env, response, error) ⇒ Object
2.5-C — invoked by Adapter::Rack#call after ‘app.call` returns (or raises). `response` is the [status, headers, body] tuple on success, `nil` on error; `error` is the raised exception or nil. Same rescue contract as `fire_request_start`: each hook runs independently; one failure does not prevent later hooks from firing or the response from being written.
170 171 172 173 174 175 176 177 |
# File 'lib/hyperion/runtime.rb', line 170 def fire_request_end(request, env, response, error) @after_request_hooks.each do |hook| hook.call(request, env, response, error) rescue StandardError => e log_hook_failure(:after_request, hook, e) end nil end |
#fire_request_start(request, env) ⇒ Object
2.5-C — invoked by Adapter::Rack#call after env is built. Wraps each hook in a rescue so a misbehaving observer can’t break the dispatch chain — failures are logged with the block’s source location so operators can identify which hook went wrong.
155 156 157 158 159 160 161 162 |
# File 'lib/hyperion/runtime.rb', line 155 def fire_request_start(request, env) @before_request_hooks.each do |hook| hook.call(request, env) rescue StandardError => e log_hook_failure(:before_request, hook, e) end nil end |
#has_request_hooks? ⇒ Boolean
2.5-C — zero-cost guard used by Adapter::Rack#call. When both arrays are empty (the default — no hooks registered), the adapter skips the dispatch entirely: no Array iteration, no Proc invocation, no allocation. The audit harness (‘yjit_alloc_audit_spec`) verifies the per-request alloc count is unchanged from 2.5-B.
147 148 149 |
# File 'lib/hyperion/runtime.rb', line 147 def has_request_hooks? !@before_request_hooks.empty? || !@after_request_hooks.empty? end |
#on_request_end(&block) ⇒ Object
2.5-C — register a Proc to fire AFTER ‘app.call` returns or raises. Receives `(request, env, response, error)`:
* `response` is the `[status, headers, body]` tuple when the
app returned normally, or `nil` when the app raised.
* `error` is the `StandardError` the app raised, or `nil` on
success.
Use this to finish trace spans, attach response codes to the active transaction, increment per-route counters, etc. Hook errors are caught and logged — they never break dispatch.
134 135 136 137 138 139 |
# File 'lib/hyperion/runtime.rb', line 134 def on_request_end(&block) raise ArgumentError, 'block required' unless block @after_request_hooks << block block end |
#on_request_start(&block) ⇒ Object
2.5-C — register a Proc to fire AFTER env is built but BEFORE ‘app.call`. Receives `(request, env)` where `request` is the parsed `Hyperion::Request` and `env` is the mutable Rack env Hash — callbacks may stash trace context (NewRelic transactions, OpenTelemetry spans, AppSignal/DataDog handles) into the env so the corresponding after-hook can finish them.
Hook errors are caught and logged; they DO NOT abort dispatch. Multiple hooks fire in registration order (FIFO).
116 117 118 119 120 121 |
# File 'lib/hyperion/runtime.rb', line 116 def on_request_start(&block) raise ArgumentError, 'block required' unless block @before_request_hooks << block block end |