Class: Hyperion::Metrics::PathTemplater

Inherits:
Object
  • Object
show all
Defined in:
lib/hyperion/metrics/path_templater.rb

Overview

2.4-C — turn raw request paths into low-cardinality templates so the per-route histogram doesn’t blow up to one label-set per ‘/users/<id>`.

The default rules collapse ‘/users/123` → `/users/:id` and `/orders/3fa85f64-5717-4562-b3fc-2c963f66afa6` → `/orders/:uuid`. They cover the bulk of real-world REST paths; operators with Rails-style routes (`/articles/cool-slug-2024`) plug in their own rules via `Hyperion::Config#metrics.path_templater = MyTemplater.new`.

An LRU cache keyed on the raw path side-steps repeating the regex walk on every keep-alive request to the same handler. 1000 entries is sized for typical Rails-shape apps (sub-1000 unique route templates); apps with more should pass ‘lru_size:` explicitly.

Constant Summary collapse

DEFAULT_RULES =
[
  [/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, ':uuid'],
  [/\b\d+\b/, ':id']
].freeze
DEFAULT_LRU_SIZE =
1000
DEFAULT_THREAD_CACHE_SIZE =
64

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rules: DEFAULT_RULES, lru_size: DEFAULT_LRU_SIZE) ⇒ PathTemplater

Returns a new instance of PathTemplater.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/hyperion/metrics/path_templater.rb', line 28

def initialize(rules: DEFAULT_RULES, lru_size: DEFAULT_LRU_SIZE)
  @rules    = rules
  @lru_size = lru_size
  @cache    = {} # Insertion-ordered Hash doubles as an LRU.
  @mutex    = Mutex.new
  # PR3-2 — per-thread shadow cache. On a keep-alive benchmark
  # connection the same path is seen on every request; the shared
  # LRU's mutex acquire (even uncontended) costs a syscall-comparable
  # overhead under high concurrency. Each worker thread keeps its own
  # small (DEFAULT_THREAD_CACHE_SIZE-entry) Hash; on a hit we return
  # without touching the mutex at all.  On a miss we fall through to
  # the shared LRU and backfill the thread cache.  The thread cache
  # is stored with Thread#thread_variable_* (true thread-local, not
  # fiber-local) so it survives async-io scheduler yields correctly.
  @thread_cache_key = :"__hyperion_pt_cache_#{object_id}__"
  @thread_size_key  = :"__hyperion_pt_size_#{object_id}__"
end

Instance Attribute Details

#lru_sizeObject (readonly)

Returns the value of attribute lru_size.



26
27
28
# File 'lib/hyperion/metrics/path_templater.rb', line 26

def lru_size
  @lru_size
end

Instance Method Details

#cache_sizeObject



82
83
84
# File 'lib/hyperion/metrics/path_templater.rb', line 82

def cache_size
  @mutex.synchronize { @cache.size }
end

#template(path) ⇒ Object

Translate a raw request path into its template form. The result is memoized in the LRU; a cache hit is a single Hash#[] + re-insert (touch). On miss we run the regex chain and trim the oldest entry if we exceed ‘lru_size`.

PR3-2: Fast path checks the per-thread shadow cache first (no mutex).



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/hyperion/metrics/path_templater.rb', line 54

def template(path)
  return path if path.nil? || path.empty?

  thread = Thread.current
  tc = thread.thread_variable_get(@thread_cache_key)
  if tc && (hit = tc[path])
    return hit
  end

  @mutex.synchronize do
    if (cached = @cache.delete(path))
      # Re-insert to mark "recently used" (Ruby Hashes preserve
      # insertion order, oldest = first key).
      @cache[path] = cached
      tc = thread_cache_for(thread)
      tc[path] = cached
      return cached
    end

    templated = compute(path)
    @cache[path] = templated
    @cache.shift if @cache.size > @lru_size
    tc = thread_cache_for(thread)
    tc[path] = templated
    templated
  end
end