hyperion-rb 2.12.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 +513 -0
- data/README.md +120 -2
- data/ext/hyperion_http/parser.c +382 -51
- data/lib/hyperion/adapter/rack.rb +18 -4
- data/lib/hyperion/connection.rb +65 -4
- data/lib/hyperion/http2_handler.rb +348 -21
- data/lib/hyperion/metrics.rb +174 -38
- data/lib/hyperion/version.rb +1 -1
- metadata +1 -1
data/lib/hyperion/metrics.rb
CHANGED
|
@@ -79,6 +79,30 @@ module Hyperion
|
|
|
79
79
|
# Snapshot block hooks for gauges whose value is read on demand
|
|
80
80
|
# (ThreadPool queue depth, etc.). `{ name => { labels_tuple => Proc } }`.
|
|
81
81
|
@gauge_blocks = {}
|
|
82
|
+
|
|
83
|
+
# 2.13-A — per-thread shards for the hot-path metrics that USED to
|
|
84
|
+
# take @hg_mutex on every observe / increment. The pre-2.13-A
|
|
85
|
+
# comment in `increment_labeled_counter` claimed those paths were
|
|
86
|
+
# "low-rate" — that turned out to be wrong: `tick_worker_request`
|
|
87
|
+
# fires once per Rack request, and `observe_histogram` fires once
|
|
88
|
+
# per request via the per-route duration histogram. Under -t 32
|
|
89
|
+
# the single mutex serialised every worker thread on the
|
|
90
|
+
# request-completion tail. Per-thread shards remove the
|
|
91
|
+
# contention; snapshots merge across threads under the mutex
|
|
92
|
+
# (snapshot is a low-rate operation — once per /-/metrics scrape).
|
|
93
|
+
#
|
|
94
|
+
# Thread-variable storage (NOT Thread.current[]) for the same
|
|
95
|
+
# reason as the unlabeled counter path: under an Async scheduler
|
|
96
|
+
# `Thread.current[:k]` is fiber-local, which would let snapshots
|
|
97
|
+
# miss observations made on a fiber that already exited.
|
|
98
|
+
@hg_thread_key = :"__hyperion_metrics_hg_#{object_id}__"
|
|
99
|
+
@lc_thread_key = :"__hyperion_metrics_lc_#{object_id}__"
|
|
100
|
+
# Holds direct references to every per-thread shard ever
|
|
101
|
+
# allocated through this Metrics instance (mirrors @thread_counters)
|
|
102
|
+
# so snapshots survive thread death.
|
|
103
|
+
@thread_histograms = []
|
|
104
|
+
@thread_labeled_counters = []
|
|
105
|
+
@hg_thread_mutex = Mutex.new
|
|
82
106
|
end
|
|
83
107
|
|
|
84
108
|
# Hot path: one thread-variable lookup + one hash op. No mutex on the
|
|
@@ -162,6 +186,12 @@ module Hyperion
|
|
|
162
186
|
end
|
|
163
187
|
|
|
164
188
|
# Tests can call .reset! between examples to avoid cross-spec leakage.
|
|
189
|
+
#
|
|
190
|
+
# 2.13-A — also clear per-thread histogram and labeled-counter
|
|
191
|
+
# shards. Without this, an observation made on thread A in spec X
|
|
192
|
+
# would leak into spec Y's snapshot because the shard hashes are
|
|
193
|
+
# held alive by `@thread_histograms` / `@thread_labeled_counters`
|
|
194
|
+
# for the lifetime of the Metrics instance.
|
|
165
195
|
def reset!
|
|
166
196
|
@counters_mutex.synchronize do
|
|
167
197
|
@thread_counters.each(&:clear)
|
|
@@ -171,6 +201,10 @@ module Hyperion
|
|
|
171
201
|
@gauges.each_value(&:clear)
|
|
172
202
|
@gauge_blocks.each_value(&:clear)
|
|
173
203
|
end
|
|
204
|
+
@hg_thread_mutex.synchronize do
|
|
205
|
+
@thread_histograms.each(&:clear)
|
|
206
|
+
@thread_labeled_counters.each(&:clear)
|
|
207
|
+
end
|
|
174
208
|
end
|
|
175
209
|
|
|
176
210
|
# ---- 2.4-C histogram + gauge API ---------------------------------
|
|
@@ -197,27 +231,37 @@ module Hyperion
|
|
|
197
231
|
|
|
198
232
|
# Observe `value` on a previously-registered histogram. `label_values`
|
|
199
233
|
# MUST be supplied in the same order as `label_keys` at registration.
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
234
|
+
#
|
|
235
|
+
# 2.13-A — Hot-path lock-free shard. Each thread keeps its own
|
|
236
|
+
# `{ name => { labels => HistogramAccumulator } }` map; observations
|
|
237
|
+
# never block on `@hg_mutex`. Snapshots merge across threads under
|
|
238
|
+
# the mutex (low-rate). Allocation footprint per observe: zero on
|
|
239
|
+
# the cached-key path; one frozen Array + one HistogramAccumulator
|
|
240
|
+
# on first observation for a given (name, label-set, thread).
|
|
241
|
+
#
|
|
242
|
+
# Bench impact (generic Rack hello, -t 32 -c 100):
|
|
243
|
+
# contention on `@hg_mutex` was the dominant tail latency
|
|
244
|
+
# contributor — this fires once per request via the per-route
|
|
245
|
+
# request-duration histogram, multiplied by N worker threads.
|
|
204
246
|
def observe_histogram(name, value, label_values = EMPTY_LABELS)
|
|
205
|
-
@
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
meta = @histograms_meta[name]
|
|
248
|
+
return unless meta # silently skip unregistered observations
|
|
249
|
+
|
|
250
|
+
thread = Thread.current
|
|
251
|
+
shard = thread.thread_variable_get(@hg_thread_key)
|
|
252
|
+
shard = register_thread_histograms(thread) if shard.nil?
|
|
253
|
+
|
|
254
|
+
family = (shard[name] ||= {})
|
|
255
|
+
accum = family[label_values]
|
|
256
|
+
unless accum
|
|
257
|
+
accum = HistogramAccumulator.new(meta[:buckets])
|
|
258
|
+
# Freeze the label tuple so future identical-content tuples
|
|
259
|
+
# hash to the same bucket — but we keep the original ref
|
|
260
|
+
# provided by the caller as the canonical key so subsequent
|
|
261
|
+
# observes with the same Array bypass the freeze step.
|
|
262
|
+
family[label_values.frozen? ? label_values : label_values.dup.freeze] = accum
|
|
220
263
|
end
|
|
264
|
+
accum.observe(value)
|
|
221
265
|
end
|
|
222
266
|
|
|
223
267
|
# Set a gauge value. `label_values` follows the same convention as
|
|
@@ -263,15 +307,40 @@ module Hyperion
|
|
|
263
307
|
|
|
264
308
|
# Snapshot helpers — read-only views of the current histogram /
|
|
265
309
|
# gauge state. The exporter uses these to render the scrape body.
|
|
310
|
+
#
|
|
311
|
+
# 2.13-A — Histograms merge across the per-thread shards on the
|
|
312
|
+
# snapshot path. The mutex is held only long enough to copy the
|
|
313
|
+
# shard list (every shard Hash is owned by one thread, so we can
|
|
314
|
+
# iterate its current contents safely while merging — torn reads
|
|
315
|
+
# of in-progress observations show as a slightly stale snapshot,
|
|
316
|
+
# never as a corrupted Accumulator).
|
|
266
317
|
def histogram_snapshot
|
|
267
318
|
out = {}
|
|
319
|
+
|
|
320
|
+
# Pre-seed names from registered families so a histogram with
|
|
321
|
+
# zero observations still appears in the scrape (matches the
|
|
322
|
+
# pre-2.13-A behaviour where `register_histogram` populated the
|
|
323
|
+
# `@histograms[name] = {}` slot eagerly).
|
|
268
324
|
@hg_mutex.synchronize do
|
|
269
|
-
@
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
325
|
+
@histograms_meta.each_key { |name| out[name] = { meta: @histograms_meta[name], series: {} } }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
shards = @hg_thread_mutex.synchronize { @thread_histograms.dup }
|
|
329
|
+
shards.each do |shard|
|
|
330
|
+
shard.each do |name, family|
|
|
331
|
+
slot = (out[name] ||= { meta: @histograms_meta[name], series: {} })
|
|
332
|
+
series = slot[:series]
|
|
333
|
+
family.each do |labels, accum|
|
|
334
|
+
existing = series[labels]
|
|
335
|
+
if existing.nil?
|
|
336
|
+
series[labels] = accum.snapshot
|
|
337
|
+
else
|
|
338
|
+
merge_histogram_snapshot!(existing, accum)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
273
341
|
end
|
|
274
342
|
end
|
|
343
|
+
|
|
275
344
|
out
|
|
276
345
|
end
|
|
277
346
|
|
|
@@ -309,19 +378,33 @@ module Hyperion
|
|
|
309
378
|
|
|
310
379
|
# Labeled counter — separate from the legacy thread-local counter
|
|
311
380
|
# surface (which is unlabeled and per-thread for hot-path
|
|
312
|
-
# contention-free increments).
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
#
|
|
381
|
+
# contention-free increments).
|
|
382
|
+
#
|
|
383
|
+
# 2.13-A — moved to a per-thread shard for the same reason as
|
|
384
|
+
# `observe_histogram`: the previous "low-rate paths" claim was wrong
|
|
385
|
+
# (`tick_worker_request` is per-Rack-request), and at -t 32 the
|
|
386
|
+
# single mutex serialised every worker thread on the request-
|
|
387
|
+
# completion tail. Per-thread shards remove the contention;
|
|
388
|
+
# `labeled_counter_snapshot` merges shards under the mutex.
|
|
316
389
|
def increment_labeled_counter(name, label_values = EMPTY_LABELS, by = 1)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
390
|
+
thread = Thread.current
|
|
391
|
+
shard = thread.thread_variable_get(@lc_thread_key)
|
|
392
|
+
shard = register_thread_labeled_counters(thread) if shard.nil?
|
|
393
|
+
|
|
394
|
+
# Defensive: ensure the family meta exists so `register_labeled_counter`
|
|
395
|
+
# is not strictly required for hot-path increments. Pre-2.13-A the
|
|
396
|
+
# mutex'd path lazily registered an unlabeled meta; we mirror that
|
|
397
|
+
# under @hg_mutex so the shape stays consistent across threads.
|
|
398
|
+
unless @labeled_counters_meta && @labeled_counters_meta[name]
|
|
399
|
+
@hg_mutex.synchronize do
|
|
400
|
+
@labeled_counters_meta ||= {}
|
|
401
|
+
@labeled_counters_meta[name] ||= { label_keys: [].freeze }
|
|
402
|
+
end
|
|
324
403
|
end
|
|
404
|
+
|
|
405
|
+
family = (shard[name] ||= {})
|
|
406
|
+
key = label_values.frozen? ? label_values : label_values.dup.freeze
|
|
407
|
+
family[key] = (family[key] || 0) + by
|
|
325
408
|
end
|
|
326
409
|
|
|
327
410
|
def register_labeled_counter(name, label_keys: [])
|
|
@@ -333,14 +416,27 @@ module Hyperion
|
|
|
333
416
|
end
|
|
334
417
|
end
|
|
335
418
|
|
|
419
|
+
# 2.13-A — Snapshot merges per-thread shards. Pre-seeded with
|
|
420
|
+
# `@labeled_counters_meta` so registered-but-unticked families still
|
|
421
|
+
# show up in the scrape (matches pre-2.13-A behaviour where
|
|
422
|
+
# `register_labeled_counter` eagerly created the `[name] = {}` slot).
|
|
336
423
|
def labeled_counter_snapshot
|
|
337
424
|
out = {}
|
|
338
425
|
@hg_mutex.synchronize do
|
|
339
|
-
(@
|
|
340
|
-
|
|
341
|
-
|
|
426
|
+
(@labeled_counters_meta || {}).each do |name, meta|
|
|
427
|
+
out[name] = { meta: meta, series: {} }
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
shards = @hg_thread_mutex.synchronize { @thread_labeled_counters.dup }
|
|
432
|
+
shards.each do |shard|
|
|
433
|
+
shard.each do |name, family|
|
|
342
434
|
meta = (@labeled_counters_meta || {})[name] || { label_keys: [].freeze }
|
|
343
|
-
out[name]
|
|
435
|
+
slot = (out[name] ||= { meta: meta, series: {} })
|
|
436
|
+
series = slot[:series]
|
|
437
|
+
family.each do |labels, count|
|
|
438
|
+
series[labels] = (series[labels] || 0) + count
|
|
439
|
+
end
|
|
344
440
|
end
|
|
345
441
|
end
|
|
346
442
|
out
|
|
@@ -392,6 +488,46 @@ module Hyperion
|
|
|
392
488
|
@counters_mutex.synchronize { @thread_counters << counters }
|
|
393
489
|
counters
|
|
394
490
|
end
|
|
491
|
+
|
|
492
|
+
# 2.13-A — allocate this thread's histogram shard and register it
|
|
493
|
+
# in `@thread_histograms` so snapshots find it. Idempotent per
|
|
494
|
+
# thread: callers always check `thread_variable_get` first.
|
|
495
|
+
def register_thread_histograms(thread)
|
|
496
|
+
shard = {}
|
|
497
|
+
thread.thread_variable_set(@hg_thread_key, shard)
|
|
498
|
+
@hg_thread_mutex.synchronize { @thread_histograms << shard }
|
|
499
|
+
shard
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# 2.13-A — same shape as register_thread_histograms but for labeled
|
|
503
|
+
# counters. Each thread gets its own `{ name => { labels => Integer } }`
|
|
504
|
+
# map; the snapshot path merges across shards.
|
|
505
|
+
def register_thread_labeled_counters(thread)
|
|
506
|
+
shard = {}
|
|
507
|
+
thread.thread_variable_set(@lc_thread_key, shard)
|
|
508
|
+
@hg_thread_mutex.synchronize { @thread_labeled_counters << shard }
|
|
509
|
+
shard
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# 2.13-A — fold a per-thread HistogramAccumulator's contents into an
|
|
513
|
+
# already-snapshotted entry (`{buckets:, counts:, sum:, count:}`).
|
|
514
|
+
# Both arguments share the same `:buckets` so the bucket index axis
|
|
515
|
+
# is identical; we sum `counts` per bucket plus the scalars. Used
|
|
516
|
+
# when two threads observed the SAME (name, labels) pair — a
|
|
517
|
+
# legitimate steady state on a Rack route hit by concurrent
|
|
518
|
+
# workers.
|
|
519
|
+
def merge_histogram_snapshot!(existing, accum)
|
|
520
|
+
counts = existing[:counts]
|
|
521
|
+
acc_counts = accum.counts
|
|
522
|
+
i = 0
|
|
523
|
+
len = counts.length
|
|
524
|
+
while i < len
|
|
525
|
+
counts[i] += acc_counts[i]
|
|
526
|
+
i += 1
|
|
527
|
+
end
|
|
528
|
+
existing[:sum] += accum.sum
|
|
529
|
+
existing[:count] += accum.count
|
|
530
|
+
end
|
|
395
531
|
end
|
|
396
532
|
end
|
|
397
533
|
|
data/lib/hyperion/version.rb
CHANGED