hyperion-rb 1.4.1 → 1.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64221d424c994e0757262dca6984cd2b65bf9ec3da6c85094daa717c716da906
4
- data.tar.gz: 236188ff1777b49178bb4b2bfe75fbea9309ec6f5d56ec01329943dfa1afbb36
3
+ metadata.gz: fd27fc15210a2436de9b88664413eeb7cd7cf129e0921f88d7d15edd87b76d11
4
+ data.tar.gz: 8dd98630e170ee1467197542d69d034b15aa1d1a1bc2441e780d3290c2c59f1d
5
5
  SHA512:
6
- metadata.gz: 9b196f8d046c828546f8f5fbac91e0300e8704a70f11b47096a9b0fb05caf2ec34f63e2f33768ab53b41413aaa4e957ac499aa4779ecd54b6a987deef7fd464e
7
- data.tar.gz: cb3d64f757736bcbc2632492e24b0794a67c98bfdcae7a1e53d89f583cf2eecc404e060fd3ec10e441ddfdcd11273c41e0d46fa9b80be904b29a7718fe0258ba
6
+ metadata.gz: a2ae7959ab5fe99207c2c53db22060385a6eeb3aad355d05b4b6f14b16c16d1057592eda2f63b2a72cbfeb62ec7ebae5ec5945f317e84a27c5bac9c6fdfbe614
7
+ data.tar.gz: 6fb3d77d7c631b98e366ef921859dcc9af1f5f2e3f6eb6e97bebfd9453f6403c6846d46c66628204785bd9c0253e01984696b59eab690c53a79f3238c6f1f15a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.2] - 2026-04-27
4
+
5
+ Audit-driven cleanup. No behaviour changes; fiber-correctness + docs polish.
6
+
7
+ ### Fixed
8
+ - **`Hyperion::Logger` access buffer was fiber-local, not thread-local** — pre-1.4.2 the access-log write buffer was stored via `Thread.current[@buffer_key]`. Under an `Async::Scheduler` (TLS / h2 / `--async-io` plain HTTP/1.1) every handler fiber got its own private buffer, so the 4 KiB `ACCESS_FLUSH_BYTES` batching never fired — each fiber's buffer accumulated 1-3 lines before its connection closed and `flush_access_buffer` wrote them. At 24k r/s this meant ~12-24k `write(2)` syscalls/sec instead of the designed ~750/sec. Switched to `Thread#thread_variable_*` so all fibers on the same OS thread share one buffer and the batching actually fires. Same root cause as the 1.4.1 Metrics fix; surfaced by a code-audit grep for residual `Thread.current[:key]` patterns.
9
+ - **`Logger#cached_timestamp` and `ResponseWriter#cached_date`** — same fix. Pre-1.4.2 the per-second / per-millisecond Time-formatting caches were per-fiber, so under Async every fiber rebuilt the iso8601 / httpdate String on its first call after a tick. Now per-OS-thread, shared across fibers; one allocation per second per thread total.
10
+
11
+ ### Added
12
+ - **Prometheus exporter example output** in the README's Metrics section — shows what `curl -H 'X-Hyperion-Admin-Token: ...' /-/metrics` actually returns (HELP/TYPE lines, status-code labels, auto-export of unknown counters), plus the Prometheus scraper config sketch.
13
+ - **Regression spec** for the access-buffer cross-fiber bug — two fibers on the same OS thread write through one logger; verifies a single buffer is registered (not one per fiber) and both lines land via `flush_all`.
14
+ - **4 new Metrics specs** (already shipped in 1.4.1; called out here for coverage tracking) — cross-fiber on same thread, cross-thread, cross-fiber-on-different-thread, many-fibers-on-same-thread.
15
+
16
+ ### Changed
17
+ - **README benchmark section** version-stamped: clarifies that the headline numbers were measured against the noted Hyperion version (most are 1.2.0 hello-world / 1.3.0 PG-bound) and that 1.3.0+ `--async-io` + 1.4.0+ TLS-inline + 1.4.1+ Metrics fix preserve or improve these numbers. We re-run the headline configs each release.
18
+
3
19
  ## [1.4.1] - 2026-04-27
4
20
 
5
21
  ### Fixed
data/README.md CHANGED
@@ -25,7 +25,7 @@ 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).
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.
29
29
 
30
30
  ### Hello-world Rack app
31
31
 
@@ -363,6 +363,31 @@ Hyperion.stats
363
363
  # => {connections_accepted: 1234, connections_active: 7, requests_total: 8910, …}
364
364
  ```
365
365
 
366
+ ### Prometheus exporter
367
+
368
+ When `admin_token` is set in your config, Hyperion mounts a `/-/metrics` endpoint that emits Prometheus text-format v0.0.4. Same token guards both `/-/metrics` (GET) and `/-/quit` (POST); auth is via the `X-Hyperion-Admin-Token` header.
369
+
370
+ ```sh
371
+ $ curl -s -H 'X-Hyperion-Admin-Token: secret' http://127.0.0.1:9292/-/metrics
372
+ # HELP hyperion_requests_total Total HTTP requests handled
373
+ # TYPE hyperion_requests_total counter
374
+ hyperion_requests_total 8910
375
+ # HELP hyperion_bytes_written_total Total bytes written to response sockets
376
+ # TYPE hyperion_bytes_written_total counter
377
+ hyperion_bytes_written_total 2351023
378
+ # HELP hyperion_responses_status_total Responses by HTTP status code
379
+ # TYPE hyperion_responses_status_total counter
380
+ hyperion_responses_status_total{status="200"} 8521
381
+ hyperion_responses_status_total{status="404"} 12
382
+ hyperion_responses_status_total{status="500"} 3
383
+ # … and so on for sendfile_responses_total, rejected_connections_total,
384
+ # slow_request_aborts_total, requests_async_dispatched_total, etc.
385
+ ```
386
+
387
+ Any counter not in the known set (added by app middleware via `Hyperion.metrics.increment(:custom_thing)`) is auto-exported as `hyperion_custom_thing` with a generic HELP line — no Hyperion config change required.
388
+
389
+ Point your scraper at it: in Prometheus' `scrape_configs`, set `metrics_path: /-/metrics` and `bearer_token` (or use a custom header relabel — Prometheus 2.42+ supports `authorization.credentials_file` paired with a custom `header` block). Network-isolate the admin endpoints if the listener is internet-facing — see [docs/REVERSE_PROXY.md](docs/REVERSE_PROXY.md) for the nginx `location /-/ { return 404; }` recipe.
390
+
366
391
  ## TLS + HTTP/2
367
392
 
368
393
  Provide a PEM cert + key:
@@ -150,7 +150,7 @@ module Hyperion
150
150
  build_access_text(ts, method, path, query, status, duration_ms, remote_addr, http_version)
151
151
  end
152
152
 
153
- buf = Thread.current[@buffer_key] || allocate_access_buffer
153
+ buf = Thread.current.thread_variable_get(@buffer_key) || allocate_access_buffer
154
154
  buf << line
155
155
  return if buf.bytesize < ACCESS_FLUSH_BYTES
156
156
 
@@ -164,7 +164,7 @@ module Hyperion
164
164
  # loop when a connection closes (so log lines from a closing keep-alive
165
165
  # session don't get stuck behind the buffer until the next connection).
166
166
  def flush_access_buffer
167
- buf = Thread.current[@buffer_key]
167
+ buf = Thread.current.thread_variable_get(@buffer_key)
168
168
  return if buf.nil? || buf.empty?
169
169
 
170
170
  @out.write(buf)
@@ -215,7 +215,7 @@ module Hyperion
215
215
  # Mutex is taken once per thread (not per request).
216
216
  def allocate_access_buffer
217
217
  buf = +''
218
- Thread.current[@buffer_key] = buf
218
+ Thread.current.thread_variable_set(@buffer_key, buf)
219
219
  @access_buffers_mutex.synchronize { @access_buffers << buf }
220
220
  buf
221
221
  end
@@ -229,11 +229,21 @@ module Hyperion
229
229
  end
230
230
 
231
231
  # Cached UTC iso8601(3) timestamp, refreshed at most once per millisecond
232
- # per thread. At 24k r/s with 16 threads we render ~1500 r/s/thread; only
233
- # ~1000 of those allocate a new String. The other 500 reuse the cached one.
232
+ # per OS thread. At 24k r/s with 16 threads we render ~1500 r/s/thread;
233
+ # only ~1000 of those allocate a new String. The other 500 reuse the
234
+ # cached one. Stored as a thread variable (truly thread-local across
235
+ # fibers) so under Async every fiber on this thread shares the same
236
+ # cache and the per-second amortisation actually fires; with the prior
237
+ # `Thread.current[:k]` storage each fiber would re-build the iso8601
238
+ # String on its first call after a millisecond tick.
234
239
  def cached_timestamp
235
240
  now_ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
236
- cache = (Thread.current[:__hyperion_ts_cache__] ||= [-1, ''])
241
+ thread = Thread.current
242
+ cache = thread.thread_variable_get(:__hyperion_ts_cache__)
243
+ if cache.nil?
244
+ cache = [-1, '']
245
+ thread.thread_variable_set(:__hyperion_ts_cache__, cache)
246
+ end
237
247
  return cache[1] if cache[0] == now_ms
238
248
 
239
249
  cache[0] = now_ms
@@ -142,10 +142,19 @@ module Hyperion
142
142
 
143
143
  # Cached HTTP `Date:` header at second resolution. `Time.now.httpdate`
144
144
  # allocates several strings; at high r/s the cache reuses one String per
145
- # second per thread instead of allocating per response.
145
+ # second per OS thread instead of allocating per response. Stored as a
146
+ # thread variable (truly thread-local across fibers) so under Async
147
+ # every fiber on this thread shares the same cache — otherwise each
148
+ # fiber would rebuild the httpdate String on its first response after
149
+ # a second tick.
146
150
  def cached_date
147
151
  now_s = Process.clock_gettime(Process::CLOCK_REALTIME, :second)
148
- cache = (Thread.current[:__hyperion_date_cache__] ||= [-1, ''])
152
+ thread = Thread.current
153
+ cache = thread.thread_variable_get(:__hyperion_date_cache__)
154
+ if cache.nil?
155
+ cache = [-1, '']
156
+ thread.thread_variable_set(:__hyperion_date_cache__, cache)
157
+ end
149
158
  return cache[1] if cache[0] == now_s
150
159
 
151
160
  cache[0] = now_s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperion
4
- VERSION = '1.4.1'
4
+ VERSION = '1.4.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperion-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Lobanov