Class: Beacon::LRU
- Inherits:
-
Object
- Object
- Beacon::LRU
- Defined in:
- lib/beacon/lru.rb
Overview
Bounded LRU cache, Mutex-guarded.
Exploits the fact that Ruby’s Hash preserves insertion order: on a read hit we delete + re-insert (moves the key to the tail), and on overflow we shift the oldest entry. No linked list, no external dependency.
The critical section is intentionally minimal — [], []=, and compute_if_absent all run a handful of Hash operations under the Mutex. The middleware’s per-request path cache lives behind this class, so the overhead is on the hot path.
Footgun: this LRU does NOT distinguish a stored nil from a miss. ‘lru` returns nil in both cases. Callers that need to cache nil should store a sentinel (e.g. :__missing__) instead.
Instance Method Summary collapse
- #[](key) ⇒ Object
- #[]=(key, value) ⇒ Object
- #clear ⇒ Object
-
#compute_if_absent(key) ⇒ Object
Read-or-compute primitive.
-
#initialize(max:) ⇒ LRU
constructor
A new instance of LRU.
- #size ⇒ Object
Constructor Details
#initialize(max:) ⇒ LRU
Returns a new instance of LRU.
17 18 19 20 21 22 |
# File 'lib/beacon/lru.rb', line 17 def initialize(max:) raise ArgumentError, "max must be positive" unless max.is_a?(Integer) && max > 0 @max = max @store = {} @mutex = Mutex.new end |
Instance Method Details
#[](key) ⇒ Object
24 25 26 27 28 29 30 31 |
# File 'lib/beacon/lru.rb', line 24 def [](key) @mutex.synchronize do return nil unless @store.key?(key) value = @store.delete(key) @store[key] = value value end end |
#[]=(key, value) ⇒ Object
33 34 35 36 37 38 39 40 41 42 |
# File 'lib/beacon/lru.rb', line 33 def []=(key, value) @mutex.synchronize do @store.delete(key) if @store.key?(key) @store[key] = value # A single []= can grow the store by at most one entry, so an # `if` is sufficient — no need for `while`. @store.shift if @store.size > @max value end end |
#clear ⇒ Object
63 64 65 |
# File 'lib/beacon/lru.rb', line 63 def clear @mutex.synchronize { @store.clear } end |
#compute_if_absent(key) ⇒ Object
Read-or-compute primitive. On hit, returns the cached value. On miss, yields the key, stores the result, and returns it.
The compute block runs OUTSIDE the mutex. Two threads concurrently missing on the same key will both run the block — the second []= then overwrites the first. Use this only when the block is idempotent and cheap (e.g. pure transforms like PathNormalizer). Callers that need exactly-once compute under contention should build their own guard around this class.
53 54 55 56 57 |
# File 'lib/beacon/lru.rb', line 53 def compute_if_absent(key) hit = self[key] return hit unless hit.nil? self[key] = yield(key) end |
#size ⇒ Object
59 60 61 |
# File 'lib/beacon/lru.rb', line 59 def size @mutex.synchronize { @store.size } end |