hyperion-rb 1.0.0.rc17
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 +7 -0
- data/CHANGELOG.md +133 -0
- data/LICENSE +21 -0
- data/README.md +260 -0
- data/bin/hyperion +6 -0
- data/ext/hyperion_http/extconf.rb +19 -0
- data/ext/hyperion_http/llhttp/api.c +509 -0
- data/ext/hyperion_http/llhttp/http.c +170 -0
- data/ext/hyperion_http/llhttp/llhttp.c +10103 -0
- data/ext/hyperion_http/llhttp/llhttp.h +907 -0
- data/ext/hyperion_http/parser.c +428 -0
- data/lib/hyperion/adapter/rack.rb +143 -0
- data/lib/hyperion/c_parser.rb +19 -0
- data/lib/hyperion/cli.rb +151 -0
- data/lib/hyperion/config.rb +107 -0
- data/lib/hyperion/connection.rb +338 -0
- data/lib/hyperion/fiber_local.rb +104 -0
- data/lib/hyperion/http2_handler.rb +312 -0
- data/lib/hyperion/logger.rb +269 -0
- data/lib/hyperion/master.rb +221 -0
- data/lib/hyperion/metrics.rb +68 -0
- data/lib/hyperion/parser.rb +128 -0
- data/lib/hyperion/pool.rb +34 -0
- data/lib/hyperion/request.rb +25 -0
- data/lib/hyperion/response_writer.rb +98 -0
- data/lib/hyperion/server.rb +198 -0
- data/lib/hyperion/thread_pool.rb +116 -0
- data/lib/hyperion/tls.rb +29 -0
- data/lib/hyperion/version.rb +5 -0
- data/lib/hyperion/worker.rb +91 -0
- data/lib/hyperion.rb +82 -0
- metadata +193 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bcb08723cb65420d073fe0e239c039946649ed4b2d5ec913391edaa9f9022f5c
|
|
4
|
+
data.tar.gz: 87f88b3956f6879defe23ba4f9e4b6a5b41bcf4db787b553ed7a3608ef9f6760
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b0fecfb34f4cd2a2b272858305fc43a46a9f09d0ac9a77e13b2977c0793bd92a5eac024d8d6f080eaaa565f99f87e092d919a1ff4662eb3a205a29cefa9826fd
|
|
7
|
+
data.tar.gz: 4a86ffdf69d7ba24e3dbac045c26c26aed00ee07f43db3eaabe725c3ce5a52b9b3af7f94cfefa80ca743a75db0c882d931e1ae4953131acc70a20b5104ccdbf0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0.rc17] - 2026-04-26
|
|
4
|
+
|
|
5
|
+
### Performance
|
|
6
|
+
- **C-extension response head builder** (`Hyperion::CParser.build_response_head`).
|
|
7
|
+
Per-response status-line, normalized hash, and per-header String#<< allocations now happen in C — same wire output, ~44% faster on the synthetic head-build microbench (430 k vs 298 k writes/sec on macOS).
|
|
8
|
+
- `ResponseWriter` keeps a pure-Ruby `build_head_ruby` fallback for JRuby / TruffleRuby / build failures; behaviour is identical.
|
|
9
|
+
- Ships alongside the frozen `HTTP_KEY_CACHE` Rack-adapter optimisation already on the rc16 tip.
|
|
10
|
+
|
|
11
|
+
## [1.0.0.rc16] - 2026-04-26
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Logger split (12-factor)**: `info` / `debug` route to stdout; `warn` / `error` / `fatal` route to stderr. Previously everything went to stderr.
|
|
15
|
+
- `Logger.new` accepts `out:` and `err:` kwargs (default `$stdout` / `$stderr`); `io:` kwarg still works as a back-compat alias for tests.
|
|
16
|
+
- Per-request access logs go to **stdout** (info level).
|
|
17
|
+
|
|
18
|
+
All notable changes to Hyperion are documented here.
|
|
19
|
+
|
|
20
|
+
## [1.0.0.rc15] - 2026-04-26
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Per-OS worker model**: master auto-detects host OS and picks the right multi-worker strategy.
|
|
24
|
+
- **macOS / BSD**: master binds the listening socket once, forks workers that inherit the FD (Puma's pattern). Single accept queue, race-fair across workers.
|
|
25
|
+
- **Linux**: each worker independently binds with `SO_REUSEPORT` (kernel-fair distribution, no thundering herd).
|
|
26
|
+
- Override via `HYPERION_WORKER_MODEL=share|reuseport`.
|
|
27
|
+
- macOS `-w 4` now scales correctly: hello-world bench at `-w 4 -t 10` serves 44 k r/s vs Puma's 38 k r/s (1.17× throughput, 15× lower p99).
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- macOS `SO_REUSEPORT` distribution issue (Darwin's kernel funnels connections to one socket, never load-balancing siblings). The share-model fix sidesteps it entirely.
|
|
31
|
+
|
|
32
|
+
## [1.0.0.rc14] - 2026-04-26
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **Ruby DSL config file** (`config/hyperion.rb`) with the same shape as Puma's `config/puma.rb`. Auto-loaded if present in cwd; explicit path via `-C / --config PATH`.
|
|
36
|
+
- **Lifecycle hooks**: `before_fork`, `on_worker_boot`, `on_worker_shutdown`. Same API as Puma's hooks.
|
|
37
|
+
- DSL settings: `bind`, `port`, `workers`, `thread_count`, `tls_cert_path`, `tls_key_path`, `read_timeout`, `idle_keepalive`, `graceful_timeout`, `max_header_bytes`, `max_body_bytes`, `log_level`, `log_format`, `log_requests`, `fiber_local_shim`.
|
|
38
|
+
- `config/hyperion.example.rb` with documented sample config.
|
|
39
|
+
- Strict DSL: unknown methods raise `NoMethodError` at boot — typos surface immediately.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Precedence: explicit CLI flag > env var > config file > built-in default.
|
|
43
|
+
|
|
44
|
+
## [1.0.0.rc13] - 2026-04-26
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- **Per-request access logs default ON** (matches Puma + Rails operator expectations). Disable with `--no-log-requests` or `HYPERION_LOG_REQUESTS=0`.
|
|
48
|
+
- Logger emit is now lock-free: dropped the per-write mutex (POSIX `write(2)` is atomic for writes ≤ PIPE_BUF).
|
|
49
|
+
- Access-log hot path: per-thread cached iso8601 timestamp, hand-rolled single-interpolation line builder, per-thread 4 KiB write buffer flushed in Connection's `ensure`.
|
|
50
|
+
|
|
51
|
+
### Performance
|
|
52
|
+
- Default-ON Hyperion still beats Puma: hello-world at `-t 16` serves 20 k r/s vs Puma's 19 k r/s, with 24× lower p99 — and Hyperion emits 200 k+ access lines that Puma doesn't.
|
|
53
|
+
|
|
54
|
+
## [1.0.0.rc12] - 2026-04-26
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- `--log-requests` flag (initially default OFF) emitting one structured INFO line per response.
|
|
58
|
+
|
|
59
|
+
## [1.0.0.rc11] - 2026-04-26
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- Smart log format auto-detect: `--log-format auto` (default).
|
|
63
|
+
- `RAILS_ENV` / `RACK_ENV` / `HYPERION_ENV` ∈ `{production, staging}` → JSON.
|
|
64
|
+
- Stderr is a TTY → colored text (ANSI level colors).
|
|
65
|
+
- Otherwise (piped output, no env hint) → JSON.
|
|
66
|
+
|
|
67
|
+
## [1.0.0.rc10] - 2026-04-26
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- **Lock-free per-thread metrics counters**, no global mutex. Hot-path cost: one TLS lookup + one Hash op per increment.
|
|
71
|
+
- Connection caches `Hyperion.logger` and `Hyperion.metrics` once at init so the request loop skips method dispatch.
|
|
72
|
+
|
|
73
|
+
## [1.0.0.rc9] - 2026-04-26
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
- `Hyperion::Logger` — structured logger with levels (`debug`, `info`, `warn`, `error`, `fatal`), text + JSON formats. Hash-based payload: `logger.info { { message: 'foo', key: val } }`.
|
|
77
|
+
- `Hyperion::Metrics` — `Hyperion.stats` returns a hash of counters: `connections_accepted`, `connections_active`, `requests_total`, `requests_in_flight`, `responses_<code>`, `parse_errors`, `app_errors`, `read_timeouts`.
|
|
78
|
+
|
|
79
|
+
## [1.0.0.rc8] - 2026-04-26
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- **Worker thread owns the HTTP/1.1 connection** (Puma's model). The `app.call(env)` per-request hop through a cross-thread Queue is gone.
|
|
83
|
+
- `ThreadPool#submit_connection(socket, app)` hands the entire connection to a worker thread; workers run `Connection#serve` directly with no pool indirection.
|
|
84
|
+
- HTTP/2 path keeps fiber-per-stream (correct because h2 multiplexes streams onto one socket).
|
|
85
|
+
|
|
86
|
+
### Performance
|
|
87
|
+
- Microbench: per-connection serve goes from 12 k r/s (rc7 hop-per-request) to 29 k r/s (rc8 worker-owns).
|
|
88
|
+
- Hello-world `-t 16`: 1.27× Puma throughput, 30× lower p99.
|
|
89
|
+
|
|
90
|
+
## [1.0.0.rc7] - 2026-04-26
|
|
91
|
+
|
|
92
|
+
### Performance
|
|
93
|
+
- Pooled the per-call reply `Queue` in `ThreadPool` (was `Queue.new` on every dispatch).
|
|
94
|
+
- Inlined `Connection#read_chunk`'s hot path.
|
|
95
|
+
|
|
96
|
+
## [1.0.0.rc6] - 2026-04-26
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
- Production-mode Rails benchmark suite. At Puma's default 3-thread config Hyperion `-t 16` serves 3.18× Puma's throughput with 2.10× lower p99.
|
|
100
|
+
|
|
101
|
+
## [1.0.0.rc5] - 2026-04-26
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
- **Hybrid async/thread-pool architecture**: accept/parse/write stays on fibers; `app.call(env)` dispatched to OS-thread pool (default 5, `-t/--threads N`).
|
|
105
|
+
|
|
106
|
+
## [1.0.0.rc4] - 2026-04-26
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
- Adapter sets `REMOTE_ADDR` from socket peer (was missing — broke Rack::Attack and any IP-based middleware).
|
|
110
|
+
- Pinned `openssl < 4.0` so apps mutating `OpenSSL::SSL::SSLContext::DEFAULT_PARAMS` (e.g. AWS SDK initializers) don't crash on boot.
|
|
111
|
+
|
|
112
|
+
## [1.0.0.rc3] - 2026-04-26
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
- HTTP/2 dispatch via `protocol-http2` with per-stream fiber multiplexing and WINDOW_UPDATE-aware flow control.
|
|
116
|
+
- rake-compiler integration: `bundle exec rake compile` builds the C ext.
|
|
117
|
+
|
|
118
|
+
## [1.0.0.rc1] - 2026-04-26
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
- TLS + ALPN (HTTPS over HTTP/1.1; HTTP/2 via ALPN handshake).
|
|
122
|
+
- FiberLocal compatibility shim (`Hyperion::FiberLocal.install!`) for Rails apps using `Thread.current.thread_variable_*`.
|
|
123
|
+
- Object pooling for Rack `env` Hash and `rack.input` StringIO.
|
|
124
|
+
- Vendored llhttp 9.3.0 C parser (`Hyperion::CParser`); falls back to pure-Ruby `Hyperion::Parser`.
|
|
125
|
+
- Pre-fork cluster (`-w N`); graceful shutdown with 30 s drain.
|
|
126
|
+
- `Async::Scheduler` integration; fiber per connection.
|
|
127
|
+
- HTTP/1.1 keep-alive, pipelining, chunked Transfer-Encoding.
|
|
128
|
+
- Smuggling defenses: `Content-Length` + `Transfer-Encoding` together → 400; non-chunked TE → 501.
|
|
129
|
+
|
|
130
|
+
## v0.0.1 - 2026-04-26
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
- Phase 1 skeleton: pure-Ruby HTTP/1.1 parser, Rack 3 adapter, Connection state machine, Server accept loop, CLI.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Lobanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Hyperion
|
|
2
|
+
|
|
3
|
+
High-performance Ruby HTTP server. Falcon-class fiber concurrency, Puma-class compatibility.
|
|
4
|
+
|
|
5
|
+
[]()
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
gem install hyperion
|
|
11
|
+
bundle exec hyperion config.ru
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Highlights
|
|
15
|
+
|
|
16
|
+
- **HTTP/1.1 + HTTP/2 + TLS** out of the box (HTTP/2 with per-stream fiber multiplexing, WINDOW_UPDATE-aware flow control, ALPN auto-negotiation).
|
|
17
|
+
- **Pre-fork cluster** with per-OS worker model: `SO_REUSEPORT` on Linux, master-bind + worker-fd-share on macOS/BSD (Darwin's `SO_REUSEPORT` doesn't load-balance).
|
|
18
|
+
- **Hybrid concurrency**: fiber-per-connection for I/O, OS-thread pool for `app.call(env)` — synchronous Rack handlers (Rails, ActiveRecord, anything holding a global mutex) get true OS-thread concurrency.
|
|
19
|
+
- **Vendored llhttp 9.3.0** C parser; pure-Ruby fallback for non-MRI runtimes.
|
|
20
|
+
- **Default-ON structured access logs** (one JSON or text line per request) with hot-path optimisations: per-thread cached timestamp, hand-rolled line builder, lock-free per-thread write buffer.
|
|
21
|
+
- **12-factor logger split**: info/debug → stdout, warn/error/fatal → stderr.
|
|
22
|
+
- **Ruby DSL config file** (`config/hyperion.rb`) with lifecycle hooks (`before_fork`, `on_worker_boot`, `on_worker_shutdown`).
|
|
23
|
+
- **Object pooling** for the Rack `env` hash and `rack.input` IO — amortizes per-request allocations across the worker's lifetime.
|
|
24
|
+
- **`Hyperion::FiberLocal`** opt-in shim for older Rails idioms that store request-scoped data via `Thread.current.thread_variable_*`.
|
|
25
|
+
|
|
26
|
+
## Benchmarks
|
|
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).
|
|
29
|
+
|
|
30
|
+
### Hello-world Rack app
|
|
31
|
+
|
|
32
|
+
`bench/hello.ru`, single worker, parity threads (`-t 16` vs Puma `-t 16:16`), 4 wrk threads / 50 connections / 10s, macOS arm64 / Ruby 3.3.3:
|
|
33
|
+
|
|
34
|
+
| | r/s | p99 |
|
|
35
|
+
|---|---:|---:|
|
|
36
|
+
| **Hyperion default (logs ON)** | **23,885** | **1.05 ms** |
|
|
37
|
+
| Hyperion `--no-log-requests` | 24,222 | 1.00 ms |
|
|
38
|
+
| Puma `-t 16:16` | 18,794 | 30.89 ms |
|
|
39
|
+
|
|
40
|
+
**1.27× Puma throughput, ~30× lower p99 — while emitting structured JSON access logs Puma doesn't.**
|
|
41
|
+
|
|
42
|
+
### Production cluster config (`-w 4`)
|
|
43
|
+
|
|
44
|
+
Same bench app, `-w 4` cluster, parity threads. macOS arm64:
|
|
45
|
+
|
|
46
|
+
| | r/s | p99 |
|
|
47
|
+
|---|---:|---:|
|
|
48
|
+
| **Hyperion `-w 4 -t 10`** | **44,221** | **1.15 ms** |
|
|
49
|
+
| Puma `-w 4 -t 10:10` | 37,929 | 17.06 ms |
|
|
50
|
+
|
|
51
|
+
**1.17× Puma throughput, ~15× lower p99.**
|
|
52
|
+
|
|
53
|
+
### Linux production-config (DB-backed Rack)
|
|
54
|
+
|
|
55
|
+
`-w 4 -t 10` on Ubuntu 24.04 / Ruby 3.3.3. Rack app does one Postgres `SELECT 1` + one Redis `GET` per request, real network round-trip. wrk `-t4 -c50 -d10s` × 3 runs (median):
|
|
56
|
+
|
|
57
|
+
| | r/s (median) | vs Puma default |
|
|
58
|
+
|---|---:|---:|
|
|
59
|
+
| **Hyperion default (rc17, logs ON)** | **5,786** | **1.012×** |
|
|
60
|
+
| Hyperion `--no-log-requests` | 6,364 | 1.114× |
|
|
61
|
+
| Puma `-w 4 -t 10:10` (no per-req logs) | 5,715 | 1.000× |
|
|
62
|
+
|
|
63
|
+
Bench is network-bound (~3-4 ms median is the PG + Redis round-trip). Hyperion's lead comes from cheaper per-request CPU: lock-free per-thread metrics, per-thread cached iso8601 timestamps in the access log, hand-rolled single-interpolation log line builder, no logger mutex (POSIX `write(2)` atomicity), C-extension response-head builder.
|
|
64
|
+
|
|
65
|
+
### Real Rails 8.1 app (single worker, parity threads `-t 16`)
|
|
66
|
+
|
|
67
|
+
Health endpoint that traverses the full middleware chain (rack-attack, locale redirect, structured tagger, geo-location, etc.). Plus a Grape API endpoint reading cached data, and a Rails controller doing a Redis GET + an ActiveRecord query.
|
|
68
|
+
|
|
69
|
+
| endpoint | server | r/s | p99 | wrk timeouts |
|
|
70
|
+
|---|---|---:|---:|---:|
|
|
71
|
+
| `/up` (health) | **Hyperion** | **19.03** | **1.12 s** | **0** |
|
|
72
|
+
| `/up` (health) | Puma `-t 16:16` | 16.64 | 1.95 s | **138** |
|
|
73
|
+
| Grape `/api/v1/cached_data` | **Hyperion** | **16.15** | **779 ms** | 16 |
|
|
74
|
+
| Grape `/api/v1/cached_data` | Puma `-t 16:16` | 10.90 | (>2 s, censored) | **110** |
|
|
75
|
+
| Rails `/api/v1/health` | **Hyperion** | **15.95** | **992 ms** | 16 |
|
|
76
|
+
| Rails `/api/v1/health` | Puma `-t 16:16` | 11.29 | (>2 s, censored) | **114** |
|
|
77
|
+
|
|
78
|
+
On Grape and Rails-controller workloads Puma hits wrk's 2 s timeout cap on ~⅔ of requests — its real p99 is censored above 2 s. Hyperion serves all of its requests under 1.2 s with 0 to 16 timeouts. **1.14–1.48× Puma throughput** depending on endpoint.
|
|
79
|
+
|
|
80
|
+
### Reproduce
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# hello-world
|
|
84
|
+
bundle exec ruby bench/compare.rb
|
|
85
|
+
HYPERION_WORKERS=4 PUMA_WORKERS=4 FALCON_COUNT=4 bundle exec ruby bench/compare.rb
|
|
86
|
+
|
|
87
|
+
# Real Rails / Grape: see bench/db.ru for the schema
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
bundle install
|
|
94
|
+
bundle exec rake compile # build the llhttp C ext
|
|
95
|
+
bundle exec hyperion config.ru # single-process default
|
|
96
|
+
bundle exec hyperion -w 4 -t 10 config.ru # 4-worker cluster, 10 threads each
|
|
97
|
+
bundle exec hyperion -w 0 config.ru # 1 worker per CPU
|
|
98
|
+
bundle exec hyperion --tls-cert cert.pem --tls-key key.pem -p 9443 config.ru # HTTPS
|
|
99
|
+
curl http://127.0.0.1:9292/ # => hello
|
|
100
|
+
|
|
101
|
+
# Chunked POST works:
|
|
102
|
+
curl -X POST -H "Transfer-Encoding: chunked" --data-binary @file http://127.0.0.1:9292/
|
|
103
|
+
|
|
104
|
+
# HTTP/2 (over TLS, ALPN-negotiated):
|
|
105
|
+
curl --http2 -k https://127.0.0.1:9443/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`bundle exec rake spec` (and the `default` task) auto-invoke `compile`, so a fresh checkout just needs `bundle install && bundle exec rake` to get a green run.
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
Three layers, in precedence order: explicit CLI flag > environment variable > `config/hyperion.rb` > built-in default.
|
|
113
|
+
|
|
114
|
+
### CLI flags
|
|
115
|
+
|
|
116
|
+
| Flag | Default | Notes |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `-b, --bind HOST` | `127.0.0.1` | |
|
|
119
|
+
| `-p, --port PORT` | `9292` | |
|
|
120
|
+
| `-w, --workers N` | `1` | `0` → `Etc.nprocessors` |
|
|
121
|
+
| `-t, --threads N` | `5` | OS-thread Rack handler pool per worker. `0` → run inline (no pool, debugging only). |
|
|
122
|
+
| `-C, --config PATH` | `config/hyperion.rb` if present | Ruby DSL file. |
|
|
123
|
+
| `--tls-cert PATH` | nil | PEM certificate. |
|
|
124
|
+
| `--tls-key PATH` | nil | PEM private key. |
|
|
125
|
+
| `--log-level LEVEL` | `info` | `debug` / `info` / `warn` / `error` / `fatal`. |
|
|
126
|
+
| `--log-format FORMAT` | `auto` | `text` / `json` / `auto`. Auto: JSON when `RAILS_ENV`/`RACK_ENV` is `production`/`staging`, colored text on TTY, JSON otherwise. |
|
|
127
|
+
| `--[no-]log-requests` | ON | Per-request access log. |
|
|
128
|
+
| `--fiber-local-shim` | off | Patches `Thread#thread_variable_*` to fiber storage for older Rails idioms. |
|
|
129
|
+
|
|
130
|
+
### Environment variables
|
|
131
|
+
|
|
132
|
+
`HYPERION_LOG_LEVEL`, `HYPERION_LOG_FORMAT`, `HYPERION_LOG_REQUESTS` (`0|1|true|false|yes|no|on|off`), `HYPERION_ENV`, `HYPERION_WORKER_MODEL` (`share|reuseport`).
|
|
133
|
+
|
|
134
|
+
### Config file
|
|
135
|
+
|
|
136
|
+
`config/hyperion.rb` — same shape as Puma's `puma.rb`. Auto-loaded if present.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# config/hyperion.rb
|
|
140
|
+
bind '0.0.0.0'
|
|
141
|
+
port 9292
|
|
142
|
+
|
|
143
|
+
workers 4
|
|
144
|
+
thread_count 10
|
|
145
|
+
|
|
146
|
+
# tls_cert_path 'config/cert.pem'
|
|
147
|
+
# tls_key_path 'config/key.pem'
|
|
148
|
+
|
|
149
|
+
read_timeout 30
|
|
150
|
+
idle_keepalive 5
|
|
151
|
+
graceful_timeout 30
|
|
152
|
+
|
|
153
|
+
max_header_bytes 64 * 1024
|
|
154
|
+
max_body_bytes 16 * 1024 * 1024
|
|
155
|
+
|
|
156
|
+
log_level :info
|
|
157
|
+
log_format :auto
|
|
158
|
+
log_requests true
|
|
159
|
+
|
|
160
|
+
fiber_local_shim false
|
|
161
|
+
|
|
162
|
+
before_fork do
|
|
163
|
+
ActiveRecord::Base.connection_handler.clear_all_connections! if defined?(ActiveRecord)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
on_worker_boot do |worker_index|
|
|
167
|
+
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
on_worker_shutdown do |worker_index|
|
|
171
|
+
ActiveRecord::Base.connection_handler.clear_all_connections! if defined?(ActiveRecord)
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Strict DSL: unknown methods raise `NoMethodError` at boot — typos surface immediately rather than getting silently ignored.
|
|
176
|
+
|
|
177
|
+
A documented sample lives at [`config/hyperion.example.rb`](config/hyperion.example.rb).
|
|
178
|
+
|
|
179
|
+
## Logging
|
|
180
|
+
|
|
181
|
+
Default behaviour (rc16+):
|
|
182
|
+
|
|
183
|
+
- **`info` / `debug` → stdout**, **`warn` / `error` / `fatal` → stderr** (12-factor).
|
|
184
|
+
- **One structured access-log line per response**, info level, on stdout. Disable with `--no-log-requests` or `HYPERION_LOG_REQUESTS=0`.
|
|
185
|
+
- **Format auto-selects**: production envs → JSON (line-delimited, parseable by every log aggregator); TTY → coloured text; piped output without env hint → JSON.
|
|
186
|
+
|
|
187
|
+
### Sample access log lines
|
|
188
|
+
|
|
189
|
+
Text format (TTY default):
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
2026-04-26T18:40:04.112Z INFO [hyperion] message=request method=GET path=/api/v1/health status=200 duration_ms=46.63 remote_addr=127.0.0.1 http_version=HTTP/1.1
|
|
193
|
+
2026-04-26T18:40:04.123Z INFO [hyperion] message=request method=GET path=/api/v1/cached_data query="currency=USD" status=200 duration_ms=43.87 remote_addr=127.0.0.1 http_version=HTTP/1.1
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
JSON format (auto-selected on `RAILS_ENV=production`/`staging` or piped output):
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{"ts":"2026-04-26T18:38:49.405Z","level":"info","source":"hyperion","message":"request","method":"GET","path":"/api/v1/health","status":200,"duration_ms":46.63,"remote_addr":"127.0.0.1","http_version":"HTTP/1.1"}
|
|
200
|
+
{"ts":"2026-04-26T18:38:49.411Z","level":"info","source":"hyperion","message":"request","method":"GET","path":"/api/v1/cached_data","query":"currency=USD","status":200,"duration_ms":40.64,"remote_addr":"127.0.0.1","http_version":"HTTP/1.1"}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Hot-path optimisations
|
|
204
|
+
|
|
205
|
+
The default-ON access log path is engineered to stay near-zero cost:
|
|
206
|
+
|
|
207
|
+
- **Per-thread cached `iso8601(3)` timestamp** — one allocation per millisecond per thread, reused across all requests in that millisecond.
|
|
208
|
+
- **Hand-rolled single-interpolation line builder** — bypasses generic `Hash#map.join`.
|
|
209
|
+
- **Per-thread 4 KiB write buffer** — flushes to stdout when full or on connection close. Cuts ~32× the syscalls under load.
|
|
210
|
+
- **Lock-free emit** — POSIX `write(2)` is atomic for writes ≤ PIPE_BUF (4096 B); a log line is ~200 B. No logger mutex.
|
|
211
|
+
|
|
212
|
+
## Metrics
|
|
213
|
+
|
|
214
|
+
`Hyperion.stats` returns a snapshot Hash with the following counters (lock-free per-thread aggregation):
|
|
215
|
+
|
|
216
|
+
| Counter | Meaning |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `connections_accepted` | Lifetime accept count. |
|
|
219
|
+
| `connections_active` | Currently in-flight connections. |
|
|
220
|
+
| `requests_total` | Lifetime request count. |
|
|
221
|
+
| `requests_in_flight` | Currently in-flight requests. |
|
|
222
|
+
| `responses_<code>` | One counter per status code emitted (`responses_200`, `responses_400`, …). |
|
|
223
|
+
| `parse_errors` | HTTP parse failures → 400. |
|
|
224
|
+
| `app_errors` | Rack app raised → 500. |
|
|
225
|
+
| `read_timeouts` | Per-connection read deadline hit. |
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
require 'hyperion'
|
|
229
|
+
Hyperion.stats
|
|
230
|
+
# => {connections_accepted: 1234, connections_active: 7, requests_total: 8910, …}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## TLS + HTTP/2
|
|
234
|
+
|
|
235
|
+
Provide a PEM cert + key:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
bundle exec hyperion --tls-cert config/cert.pem --tls-key config/key.pem -p 9443 config.ru
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
ALPN auto-negotiates `h2` (HTTP/2) or `http/1.1` per connection. HTTP/2 multiplexes streams onto fibers within a single connection — slow handlers don't head-of-line-block other streams. Cluster-mode TLS works (`-w N` + `--tls-cert` / `--tls-key`).
|
|
242
|
+
|
|
243
|
+
Smuggling defenses for HTTP/1.1: `Content-Length` + `Transfer-Encoding` together → 400; non-chunked `Transfer-Encoding` → 501; CRLF in response header values → `ArgumentError` (response-splitting guard).
|
|
244
|
+
|
|
245
|
+
## Compatibility
|
|
246
|
+
|
|
247
|
+
- **Ruby 3.2+** required.
|
|
248
|
+
- **Rack 3** (auto-sets `SERVER_SOFTWARE`, `rack.version`, `REMOTE_ADDR`, IPv6-safe `Host` parsing, CRLF guard).
|
|
249
|
+
- **`Hyperion::FiberLocal.install!`** opt-in shim for older Rails apps that store request-scoped data via `Thread.current.thread_variable_*` (modern Rails 7.1+ already uses Fiber storage natively; the shim handles the residual footgun).
|
|
250
|
+
- **`Hyperion::FiberLocal.verify_environment!`** runtime check that `Thread.current[:k]` is fiber-local on the current Ruby (it is on 3.2+).
|
|
251
|
+
|
|
252
|
+
## Credits
|
|
253
|
+
|
|
254
|
+
- Vendored [llhttp](https://github.com/nodejs/llhttp) (Node.js's HTTP parser, MIT) under `ext/hyperion_http/llhttp/`.
|
|
255
|
+
- HTTP/2 framing and HPACK via [`protocol-http2`](https://github.com/socketry/protocol-http2).
|
|
256
|
+
- Fiber scheduler via [`async`](https://github.com/socketry/async).
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT.
|
data/bin/hyperion
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mkmf'
|
|
4
|
+
|
|
5
|
+
# llhttp source files vendored under ext/hyperion_http/llhttp/.
|
|
6
|
+
# We compile them in-tree (single static unit) rather than linking a
|
|
7
|
+
# system library, so the gem builds standalone on any host with a C
|
|
8
|
+
# compiler + Ruby headers.
|
|
9
|
+
$srcs = %w[
|
|
10
|
+
parser.c
|
|
11
|
+
llhttp.c
|
|
12
|
+
api.c
|
|
13
|
+
http.c
|
|
14
|
+
]
|
|
15
|
+
$VPATH << '$(srcdir)/llhttp'
|
|
16
|
+
$INCFLAGS << ' -I$(srcdir)/llhttp'
|
|
17
|
+
$CFLAGS << ' -O2 -fno-strict-aliasing'
|
|
18
|
+
|
|
19
|
+
create_makefile('hyperion_http/hyperion_http')
|