hyperion-rb 2.11.0 → 2.13.0
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 +1079 -0
- data/README.md +220 -5
- data/ext/hyperion_http/extconf.rb +41 -0
- data/ext/hyperion_http/io_uring_loop.c +710 -0
- data/ext/hyperion_http/page_cache.c +1032 -0
- data/ext/hyperion_http/page_cache_internal.h +132 -0
- data/ext/hyperion_http/parser.c +382 -51
- data/lib/hyperion/adapter/rack.rb +18 -4
- data/lib/hyperion/connection.rb +78 -3
- data/lib/hyperion/dispatch_mode.rb +19 -1
- data/lib/hyperion/http2_handler.rb +458 -13
- data/lib/hyperion/metrics.rb +212 -38
- data/lib/hyperion/prometheus_exporter.rb +76 -1
- data/lib/hyperion/server/connection_loop.rb +159 -0
- data/lib/hyperion/server.rb +183 -0
- data/lib/hyperion/thread_pool.rb +23 -7
- data/lib/hyperion/version.rb +1 -1
- metadata +4 -1
data/lib/hyperion/connection.rb
CHANGED
|
@@ -135,6 +135,24 @@ module Hyperion
|
|
|
135
135
|
# keep the existing pattern of caching boot-time refs as ivars so
|
|
136
136
|
# the per-request observe stays a single Hash lookup.
|
|
137
137
|
@path_templater = path_templater || Hyperion::Metrics.default_path_templater
|
|
138
|
+
# 2.12-E — per-worker request counter label. Cached once per
|
|
139
|
+
# Connection (Process.pid is process-constant — re-reading it per
|
|
140
|
+
# request would allocate the to_s String every time the operator
|
|
141
|
+
# asked Ruby for the symbol/label). Each Connection lives in
|
|
142
|
+
# exactly one process, so the cache is tight and never stale.
|
|
143
|
+
@worker_id = Process.pid.to_s
|
|
144
|
+
# 2.13-A — pre-build the frozen single-element label tuple that
|
|
145
|
+
# `tick_worker_request` would otherwise allocate every request
|
|
146
|
+
# (`[@worker_id]` per call). Per-Connection caching is safe
|
|
147
|
+
# because @worker_id is process-constant and the tuple is
|
|
148
|
+
# frozen so consumers can't mutate the shared instance.
|
|
149
|
+
@worker_id_label_tuple = [@worker_id].freeze
|
|
150
|
+
# 2.13-A — register the labeled-counter family ONCE here (used
|
|
151
|
+
# to fire on every `tick_worker_request` via an `unless`-flag
|
|
152
|
+
# check; the early-return cost is small but real on the
|
|
153
|
+
# 8000 r/s -c1 single-thread profile). After this, the
|
|
154
|
+
# request loop calls `increment_labeled_counter` directly.
|
|
155
|
+
@metrics.ensure_worker_request_family_registered!
|
|
138
156
|
# 2.10-D — direct-dispatch route table. The hot-path lookup
|
|
139
157
|
# is `@route_table&.lookup(method, path)` so the nil-default
|
|
140
158
|
# case (no operator-registered direct routes — the
|
|
@@ -307,6 +325,23 @@ module Hyperion
|
|
|
307
325
|
@metrics.increment(:bytes_read, body_end)
|
|
308
326
|
@metrics.increment(:requests_total)
|
|
309
327
|
@metrics.increment(:requests_in_flight)
|
|
328
|
+
# 2.12-E — per-worker request counter for the SO_REUSEPORT
|
|
329
|
+
# load-balancing audit. Worker_id is the OS pid (matches the
|
|
330
|
+
# 2.4-C `hyperion_io_uring_workers_active` convention). Single
|
|
331
|
+
# location for every Ruby-side dispatch shape: regular Rack
|
|
332
|
+
# via `dispatch_request`, direct dispatch via `dispatch_direct!`,
|
|
333
|
+
# and the StaticEntry fast path via `dispatch_direct_static!`
|
|
334
|
+
# all flow through this point in `serve`.
|
|
335
|
+
#
|
|
336
|
+
# 2.13-A — call `increment_labeled_counter` directly with the
|
|
337
|
+
# pre-built frozen `[@worker_id]` tuple instead of going
|
|
338
|
+
# through `tick_worker_request`. The wrapper allocates a
|
|
339
|
+
# fresh `[label]` array AND calls `worker_id.to_s` per
|
|
340
|
+
# request; cached tuple skips both. Family registration was
|
|
341
|
+
# done once in the constructor (idempotent on the Metrics
|
|
342
|
+
# instance) so the request loop is registration-free.
|
|
343
|
+
@metrics.increment_labeled_counter(Hyperion::Metrics::REQUESTS_DISPATCH_TOTAL,
|
|
344
|
+
@worker_id_label_tuple)
|
|
310
345
|
# 2.4-C: capture start time for the per-route duration histogram.
|
|
311
346
|
# Same Process.clock_gettime that the access-log path was already
|
|
312
347
|
# paying — at default-ON log_requests the second call here is
|
|
@@ -774,10 +809,35 @@ module Hyperion
|
|
|
774
809
|
)
|
|
775
810
|
end
|
|
776
811
|
|
|
812
|
+
# 2.13-A — Rack 3 (the version Hyperion advertises in
|
|
813
|
+
# `env['rack.version']`) requires response header keys to be
|
|
814
|
+
# lowercase Strings (Rack 3 spec §6.4 "Headers must be a Hash;
|
|
815
|
+
# the header keys must be lowercase Strings"). Pre-2.13-A this
|
|
816
|
+
# method scanned the whole Hash via `headers.find` + per-key
|
|
817
|
+
# `k.to_s.downcase` to find the Connection header — that's an
|
|
818
|
+
# O(N) walk + N transient string allocations on EVERY response
|
|
819
|
+
# (and most responses don't carry a Connection header at all,
|
|
820
|
+
# so the loop ran to completion every time).
|
|
821
|
+
#
|
|
822
|
+
# The new path is a single Hash lookup. Apps that violate the
|
|
823
|
+
# Rack 3 spec by returning mixed-case keys (some legacy gems
|
|
824
|
+
# still do; less common in 2026) lose the Connection-close
|
|
825
|
+
# signal and stay on keep-alive — that's a benign degradation
|
|
826
|
+
# (the connection is reused; the next request still goes through
|
|
827
|
+
# request-side `Connection: close` parsing) and the fix is to
|
|
828
|
+
# update the app to spec.
|
|
829
|
+
CONNECTION_HEADER_KEY_DOWNCASE = 'connection'
|
|
830
|
+
|
|
777
831
|
def should_keep_alive?(request, _status, headers)
|
|
778
|
-
# App-emitted Connection: close wins.
|
|
779
|
-
|
|
780
|
-
|
|
832
|
+
# App-emitted Connection: close wins. Rack-3 fast path: O(1)
|
|
833
|
+
# Hash lookup; non-Hash headers (Array-of-pairs, etc.) fall
|
|
834
|
+
# back to a single allocation-free scan.
|
|
835
|
+
conn_response_value = if headers.is_a?(Hash)
|
|
836
|
+
headers[CONNECTION_HEADER_KEY_DOWNCASE]
|
|
837
|
+
else
|
|
838
|
+
find_connection_header_array(headers)
|
|
839
|
+
end
|
|
840
|
+
return false if conn_response_value && conn_response_value.to_s.downcase == 'close'
|
|
781
841
|
|
|
782
842
|
# Request-side Connection header.
|
|
783
843
|
conn_request = request.header('connection')&.downcase
|
|
@@ -792,6 +852,21 @@ module Hyperion
|
|
|
792
852
|
end
|
|
793
853
|
end
|
|
794
854
|
|
|
855
|
+
# 2.13-A — non-Hash headers fallback (Array of [key, value] pairs).
|
|
856
|
+
# Rack 3 mandates Hash, but legacy code occasionally returns an
|
|
857
|
+
# Array; we walk it case-sensitively because Rack-3 lowercase is
|
|
858
|
+
# part of the contract for non-Hash returns too. Apps emitting
|
|
859
|
+
# `'Connection'`-cased keys via Array form fall through to no-
|
|
860
|
+
# match and stay on keep-alive — same benign degradation as the
|
|
861
|
+
# Hash branch.
|
|
862
|
+
def find_connection_header_array(headers)
|
|
863
|
+
headers.each do |pair|
|
|
864
|
+
next unless pair.is_a?(Array) && pair.length >= 2
|
|
865
|
+
return pair[1] if pair[0] == CONNECTION_HEADER_KEY_DOWNCASE
|
|
866
|
+
end
|
|
867
|
+
nil
|
|
868
|
+
end
|
|
869
|
+
|
|
795
870
|
def set_idle_timeout(socket)
|
|
796
871
|
socket.timeout = IDLE_KEEPALIVE_TIMEOUT_SECONDS if socket.respond_to?(:timeout=)
|
|
797
872
|
rescue StandardError
|
|
@@ -46,8 +46,26 @@ module Hyperion
|
|
|
46
46
|
# escape hatch via `env['hyperion.dispatch_mode']
|
|
47
47
|
# = :inline_blocking` for routes the auto-
|
|
48
48
|
# detect doesn't catch.
|
|
49
|
+
# 2.12-C — `:c_accept_loop_h1` is a connection-wide mode (NOT a
|
|
50
|
+
# per-response override): the entire accept-and-serve loop runs in
|
|
51
|
+
# C via `Hyperion::Http::PageCache.run_static_accept_loop`. Engaged
|
|
52
|
+
# only when the operator's route table is composed entirely of
|
|
53
|
+
# `Server.handle_static`-registered routes AND the listener is
|
|
54
|
+
# plain TCP. Counted under `:requests_dispatch_c_accept_loop_h1`
|
|
55
|
+
# at engage time (one bump per worker boot) so operators can see
|
|
56
|
+
# the path is on without scraping the per-request `:c_accept_loop_requests`
|
|
57
|
+
# counter.
|
|
58
|
+
# 2.12-D — `:c_accept_loop_io_uring_h1` is a sibling of
|
|
59
|
+
# `:c_accept_loop_h1`. Engaged when the operator opts into
|
|
60
|
+
# `HYPERION_IO_URING_ACCEPT=1` AND the C ext was compiled with
|
|
61
|
+
# liburing AND the runtime probe succeeded. Same eligibility gates
|
|
62
|
+
# as `:c_accept_loop_h1` (handle_static-only routes, plain TCP),
|
|
63
|
+
# different syscall shape (single `io_uring_enter` per N requests
|
|
64
|
+
# vs. N×3 syscalls). Counted under
|
|
65
|
+
# `:requests_dispatch_c_accept_loop_io_uring_h1` so operators can
|
|
66
|
+
# confirm the path is on without scraping logs.
|
|
49
67
|
MODES = %i[tls_h2 tls_h1_inline async_io_h1_inline threadpool_h1 inline_h1_no_pool
|
|
50
|
-
inline_blocking].freeze
|
|
68
|
+
inline_blocking c_accept_loop_h1 c_accept_loop_io_uring_h1].freeze
|
|
51
69
|
|
|
52
70
|
INLINE_MODES = %i[tls_h1_inline async_io_h1_inline inline_h1_no_pool inline_blocking].freeze
|
|
53
71
|
|