hyperion-rb 1.6.0 → 1.6.1
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 +9 -0
- data/README.md +59 -1
- data/lib/hyperion/cli.rb +37 -0
- data/lib/hyperion/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 388377a54507d370411ae4b229ff575e191742ba6e3dc044c9c8990552bff5ff
|
|
4
|
+
data.tar.gz: 8cc9cd083c9450948ba3a710cb5514f16bc31b8a421ebd405c4064129b0b031c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 389098362215d01ce8fa08add90d29871390e0c9c5e38d384caa50ee2605210005c252ecbaea191f379d59580e2c4d4573b94ef8c9308259e1959abee81e4397
|
|
7
|
+
data.tar.gz: f3d2664e553a2b3c24f8518ed9b65e73a49bbefd0dee03f0c602d7867fdd37ed0a8bf7030abcee68b9020c54ee80fe3cc039c93e4e004fbd372cea02313592bd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.1] - 2026-04-27
|
|
4
|
+
|
|
5
|
+
Audit follow-up from the [BENCH_2026_04_27.md](docs/BENCH_2026_04_27.md) sweep. No code-path changes; doc surface and operator-UX polish.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`## Operator guidance` README section** — concrete "when do I pick which config?" tables. Translates the bench numbers into decisions: `-w 1 + larger pool` vs `-w N + smaller pool` for I/O-bound (multi-worker is 2.6× memory for 0.77× rps if you pick wrong on PG-wait); the `--async-io` decision tree (default OFF unless you're paired with a fiber-cooperative library); how to read p50 vs p99 (tail wins are 5-200× larger than the rps story suggests — size capacity by p99).
|
|
9
|
+
- **Boot-time advisory warn for orphan `--async-io`** — if `async_io: true` is set but no fiber-cooperative library is loaded (`hyperion-async-pg`, `async-redis`, `async-http`), Hyperion logs a single advisory warn at boot pointing at the operator-guidance docs. The setting is still honoured; the warn just helps operators who flipped the flag expecting a free perf bump (bench showed `--async-io` on hello-world = 47% rps regression + 3.65 s p99 spike).
|
|
10
|
+
- **4 new specs in `spec/hyperion/cli_async_io_warn_spec.rb`** covering all four warn-fire cases (true + no library, false, nil, true + library detected via stub_const).
|
|
11
|
+
|
|
3
12
|
## [1.6.0] - 2026-04-27
|
|
4
13
|
|
|
5
14
|
Two parallel improvements landing in 1.6.0:
|
data/README.md
CHANGED
|
@@ -25,7 +25,9 @@ bundle exec hyperion config.ru
|
|
|
25
25
|
|
|
26
26
|
## Benchmarks
|
|
27
27
|
|
|
28
|
-
All numbers are real wrk runs against published Hyperion configs. Hyperion ships **with default-ON structured access logs**; Puma comparisons use Puma defaults (no per-request log emission). Each section is stamped with the Hyperion version it was measured against — newer versions (1.3.0+ `--async-io`, 1.4.0+ TLS h1 inline, 1.4.1+ Metrics fiber-key fix) preserve or improve these numbers; we re-run the headline configs each release and have not seen regressions on these workloads.
|
|
28
|
+
All numbers are real wrk runs against published Hyperion configs. Hyperion ships **with default-ON structured access logs**; Puma comparisons use Puma defaults (no per-request log emission). Each section is stamped with the Hyperion version it was measured against — newer versions (1.3.0+ `--async-io`, 1.4.0+ TLS h1 inline, 1.4.1+ Metrics fiber-key fix, 1.6.0+ HTTP/2 writer fiber + 3 C-ext additions) preserve or improve these numbers; we re-run the headline configs each release and have not seen regressions on these workloads.
|
|
29
|
+
|
|
30
|
+
> **Comprehensive matrix for 1.6.0 + hyperion-async-pg 0.5.0 (16-vCPU Linux, 9 workloads × 25+ configs)**: see [`docs/BENCH_2026_04_27.md`](docs/BENCH_2026_04_27.md). Headline: 98,818 r/s on hello `-w 16`, 21,215 r/s `-w 4` at p99 < 2 ms, 2,180 r/s on a 50 ms-waiting PG workload (4.1× the best Puma), 1,667 req/s HTTP/2 multiplexed at 0 errors, 155 MB RSS for 10k idle keep-alive connections.
|
|
29
31
|
|
|
30
32
|
### Hello-world Rack app
|
|
31
33
|
|
|
@@ -320,6 +322,62 @@ Strict DSL: unknown methods raise `NoMethodError` at boot — typos surface imme
|
|
|
320
322
|
|
|
321
323
|
A documented sample lives at [`config/hyperion.example.rb`](config/hyperion.example.rb).
|
|
322
324
|
|
|
325
|
+
## Operator guidance
|
|
326
|
+
|
|
327
|
+
Concrete tradeoffs distilled from [`docs/BENCH_2026_04_27.md`](docs/BENCH_2026_04_27.md). If the bench numbers cited below feel surprising, check that doc for the full matrix + caveats.
|
|
328
|
+
|
|
329
|
+
### When to use `-w N`
|
|
330
|
+
|
|
331
|
+
| Workload shape | Recommended | Why |
|
|
332
|
+
|---|---|---|
|
|
333
|
+
| **Pure I/O-bound** (PG / Redis / external HTTP, no significant CPU) | `-w 1` + larger pool | Bench: `-w 1 pool=200` = 87 MB / 2,180 r/s vs `-w 4 pool=64` = 224 MB / 1,680 r/s. **2.6× more memory, 0.77× rps** if you pick multi-worker on a wait-bound workload. |
|
|
334
|
+
| **Pure CPU-bound** (heavy JSON / template render / image processing) | `-w N` matching CPU count | Each worker's accept loop is single-threaded under `--async-io`; multi-worker gives CPU-parallelism. Bench: `-w 16 -t 5` hits 98,818 r/s on a 16-vCPU box, 4.7× a `-w 1` ceiling on the same hardware. |
|
|
335
|
+
| **Mixed** (Rails-shaped: ~5 ms CPU + 50 ms PG wait per request) | `-w N/2` (half cores) + medium pool | Lets CPU work parallelise while keeping per-worker memory tractable. Bench `pg_mixed.ru` at `-w 4 -t 5 pool=128` = 1,740 r/s with no cold-start spike (ForkSafe `prefill_in_child: true`). |
|
|
336
|
+
|
|
337
|
+
Multi-worker on PG-wait workloads is the **wrong** default for most apps — the headline rps doesn't justify the memory and PG-connection cost. Verify your shape with the bench before scaling out.
|
|
338
|
+
|
|
339
|
+
### When to use `--async-io`
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
Are you using a fiber-cooperative I/O library?
|
|
343
|
+
(hyperion-async-pg, async-redis, async-http)
|
|
344
|
+
│
|
|
345
|
+
┌─────────────┴─────────────┐
|
|
346
|
+
yes no
|
|
347
|
+
│ │
|
|
348
|
+
Pair with a fiber-aware Leave --async-io OFF.
|
|
349
|
+
connection pool Default thread-pool dispatch
|
|
350
|
+
(FiberPool, async-pool — is faster for synchronous
|
|
351
|
+
NOT connection_pool gem, Rails apps. Bench: --async-io
|
|
352
|
+
which uses non-fiber Mutex). on hello-world = 47% rps
|
|
353
|
+
│ regression + p99 spike to
|
|
354
|
+
Set --async-io. 3.65 s under no-yield workloads.
|
|
355
|
+
Pool size is the real No reason to flip the flag.
|
|
356
|
+
concurrency knob; -t is
|
|
357
|
+
decorative for wait-bound.
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Hyperion warns at boot if you set `--async-io` without any fiber-cooperative library loaded. The setting is still honoured; the warn just nudges operators who flipped it expecting a free perf bump.
|
|
361
|
+
|
|
362
|
+
### Tuning `-t` and pool sizes
|
|
363
|
+
|
|
364
|
+
- **Without `--async-io`** (sync server, default): `-t` is the concurrency knob. Each in-flight request holds an OS thread; pool size should match `-t`. Bench shows Puma-style behaviour — at 200 wrk conns hitting a 5-thread server, queue depth dominates p99 (Hyperion `-t 5 -w 1` p50 = 0.95 ms vs Puma's same shape at 59.5 ms — Hyperion's queueing is cheaper but the model still serializes at `-t`).
|
|
365
|
+
- **With `--async-io` + a fiber-aware pool**: pool size is the concurrency knob. `-t` is decorative for wait-bound workloads; one accept-loop fiber serves all in-flight queries via the pool. Linear scaling: pool=64 → ~780 r/s, pool=128 → ~1,344 r/s, pool=200 → ~2,180 r/s on 50 ms PG queries.
|
|
366
|
+
- **Pool over WAN**: if `PG.connect` round-trip is >50 ms, expect pool fill at startup to take `pool_size / parallel_fill_threads × RTT`. `hyperion-async-pg 0.5.1+` auto-scales `parallel_fill_threads` so pool=200 fills in ~1-2 s.
|
|
367
|
+
|
|
368
|
+
### How to read p50 vs p99
|
|
369
|
+
|
|
370
|
+
Tail latency tells the queueing story; rps tells the throughput story. Hyperion's tail wins are **always** bigger than its rps wins — sometimes the rps numbers look close to a competitor while p99 is 5-200× lower:
|
|
371
|
+
|
|
372
|
+
| Workload | Hyperion rps / p99 | Closest competitor | rps ratio | p99 ratio |
|
|
373
|
+
|---|---|---|---:|---:|
|
|
374
|
+
| Hello `-w 4` | 21,215 r/s / 1.87 ms | Falcon 24,061 / 9.78 ms | 0.88× | **5.2× lower** |
|
|
375
|
+
| CPU JSON `-w 4` | 15,582 r/s / 2.47 ms | Falcon 18,643 / 13.51 ms | 0.84× | **5.5× lower** |
|
|
376
|
+
| Static 1 MiB | 1,919 r/s / 4.22 ms | Puma 2,074 / 55 ms | 0.93× | **13× lower** |
|
|
377
|
+
| PG-wait `-w 1` pool=200 | 2,180 r/s / 668 ms | Puma 530 r/s + 200 timeouts | **4.1×** | qualitative crush |
|
|
378
|
+
|
|
379
|
+
**Size capacity by p99, not by mean.** Throughput peaks are easy to fake under controlled bench conditions; tail latency reflects what your slowest user actually experiences when the load balancer fans them onto a busy worker.
|
|
380
|
+
|
|
323
381
|
## Logging
|
|
324
382
|
|
|
325
383
|
Default behaviour (rc16+):
|
data/lib/hyperion/cli.rb
CHANGED
|
@@ -26,6 +26,14 @@ module Hyperion
|
|
|
26
26
|
Hyperion.logger = Hyperion::Logger.new(level: config.log_level, format: config.log_format)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Advisory: operators frequently flip --async-io expecting "fast mode"
|
|
30
|
+
# without installing a fiber-cooperative I/O library. On hello-world this
|
|
31
|
+
# costs ~5% rps; on no-I/O workloads more. The flag only pays off when
|
|
32
|
+
# paired with `hyperion-async-pg` / `async-redis` / `async-http`. We log
|
|
33
|
+
# once at boot pointing at the operator-guidance docs; the operator's
|
|
34
|
+
# setting is still honoured.
|
|
35
|
+
warn_orphan_async_io(config)
|
|
36
|
+
|
|
29
37
|
# Propagate log_requests so every Connection picks it up via
|
|
30
38
|
# `Hyperion.log_requests?` without needing to thread it through
|
|
31
39
|
# Server/ThreadPool/Master plumbing. Default is ON; nil means "don't
|
|
@@ -261,6 +269,35 @@ WARNING: argv is visible via `ps`; prefer --admin-token-file PATH for production
|
|
|
261
269
|
end
|
|
262
270
|
private_class_method :maybe_enable_yjit
|
|
263
271
|
|
|
272
|
+
# Probe table for fiber-cooperative I/O libraries. If `async_io: true` is
|
|
273
|
+
# set but none of these are loaded, the operator has likely flipped the
|
|
274
|
+
# flag without reading the bench numbers — `--async-io` adds Async-loop
|
|
275
|
+
# overhead and only pays off when paired with a library whose I/O calls
|
|
276
|
+
# yield to the scheduler. Hello-world bench (BENCH_2026_04_27.md) showed
|
|
277
|
+
# a 47% rps regression + 3.65 s p99 spike on this shape.
|
|
278
|
+
ASYNC_IO_PROBE_LIBS = {
|
|
279
|
+
'hyperion-async-pg' => -> { defined?(::Hyperion::AsyncPg) },
|
|
280
|
+
'async-redis' => -> { defined?(::Async::Redis) },
|
|
281
|
+
'async-http' => -> { defined?(::Async::HTTP) }
|
|
282
|
+
}.freeze
|
|
283
|
+
|
|
284
|
+
def self.warn_orphan_async_io(config)
|
|
285
|
+
return unless config.async_io == true # nil and false are both no-ops here
|
|
286
|
+
|
|
287
|
+
detected = ASYNC_IO_PROBE_LIBS.select { |_name, probe| probe.call }.keys
|
|
288
|
+
return unless detected.empty?
|
|
289
|
+
|
|
290
|
+
Hyperion.logger.warn do
|
|
291
|
+
{
|
|
292
|
+
message: 'async_io enabled but no fiber-cooperative I/O library detected',
|
|
293
|
+
libraries_checked: ASYNC_IO_PROBE_LIBS.keys,
|
|
294
|
+
impact: 'async_io adds Async-loop overhead (~5-47% rps depending on workload) and only pays off when paired with a library that yields to the Async scheduler on socket waits.',
|
|
295
|
+
docs: 'https://github.com/andrew-woblavobla/hyperion#operator-guidance'
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
private_class_method :warn_orphan_async_io
|
|
300
|
+
|
|
264
301
|
# When admin_token is configured, wrap the app in AdminMiddleware so
|
|
265
302
|
# POST /-/quit and GET /-/metrics become token-protected admin endpoints.
|
|
266
303
|
# Skipped when the token is unset — those paths fall through to the app,
|
data/lib/hyperion/version.rb
CHANGED