Class: Philiprehberger::Lru::Cache
- Inherits:
-
Object
- Object
- Philiprehberger::Lru::Cache
- Includes:
- Enumerable
- Defined in:
- lib/philiprehberger/lru.rb
Overview
Thread-safe LRU cache with TTL, eviction callbacks, and hit/miss statistics
Instance Attribute Summary collapse
-
#max_size ⇒ Integer
readonly
Return the configured maximum size.
-
#ttl ⇒ Numeric?
readonly
Return the configured TTL in seconds.
Instance Method Summary collapse
-
#clear ⇒ void
Remove all entries from the cache.
-
#delete(key) ⇒ Object?
Delete a key from the cache.
-
#delete_many(*keys) ⇒ Integer
Bulk delete keys from the cache.
-
#each {|key, value| ... } ⇒ Enumerator
Iterate over non-expired entries in MRU order.
-
#empty? ⇒ Boolean
Check whether the cache is empty.
-
#fetch(key) { ... } ⇒ Object
Retrieve a value by key, or compute and store it using the block.
-
#get(key) ⇒ Object?
Retrieve a value by key.
-
#get_many(*keys) ⇒ Hash
Retrieve multiple values by key.
-
#hit_rate ⇒ Float
Return the hit rate as a float between 0.0 and 1.0.
-
#include?(key) ⇒ Boolean
(also: #key?)
Check whether a key exists in the cache and is not expired.
-
#initialize(max_size:, ttl: nil) ⇒ Cache
constructor
Create a new LRU cache.
-
#keys ⇒ Array
Return all keys in the cache (most recently used first).
-
#miss_rate ⇒ Float
Return the miss rate as a float between 0.0 and 1.0.
-
#on_evict {|key, value| ... } ⇒ void
Register an eviction callback.
-
#peek(key) ⇒ Object?
Read a value without promoting the key in the LRU order.
-
#reset_stats ⇒ void
Reset hit, miss, and eviction counters to zero.
-
#resize(new_max) ⇒ void
Change the maximum capacity at runtime.
-
#set(key, value) ⇒ Object
Store a key-value pair in the cache.
-
#set_many(hash) ⇒ void
Bulk insert from a hash.
-
#size ⇒ Integer
Return the current number of entries.
-
#stats ⇒ Hash
Return cache statistics.
-
#values ⇒ Array
Return all values in the cache (most recently used first).
Constructor Details
#initialize(max_size:, ttl: nil) ⇒ Cache
Create a new LRU cache
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/philiprehberger/lru.rb', line 44 def initialize(max_size:, ttl: nil) raise Error, 'max_size must be a positive integer' unless max_size.is_a?(Integer) && max_size.positive? raise Error, 'ttl must be a positive number' if ttl && (!ttl.is_a?(Numeric) || ttl <= 0) @max_size = max_size @ttl = ttl @map = {} @head = nil @tail = nil @mutex = Mutex.new @on_evict = nil @hits = 0 @misses = 0 @evictions = 0 end |
Instance Attribute Details
#max_size ⇒ Integer (readonly)
Return the configured maximum size
256 257 258 |
# File 'lib/philiprehberger/lru.rb', line 256 def max_size @max_size end |
#ttl ⇒ Numeric? (readonly)
Return the configured TTL in seconds
261 262 263 |
# File 'lib/philiprehberger/lru.rb', line 261 def ttl @ttl end |
Instance Method Details
#clear ⇒ void
This method returns an undefined value.
Remove all entries from the cache
159 160 161 162 163 164 165 |
# File 'lib/philiprehberger/lru.rb', line 159 def clear @mutex.synchronize do @map.clear @head = nil @tail = nil end end |
#delete(key) ⇒ Object?
Delete a key from the cache
146 147 148 149 150 151 152 153 154 |
# File 'lib/philiprehberger/lru.rb', line 146 def delete(key) @mutex.synchronize do node = @map.delete(key) return nil unless node remove_node(node) node.value end end |
#delete_many(*keys) ⇒ Integer
Bulk delete keys from the cache
198 199 200 |
# File 'lib/philiprehberger/lru.rb', line 198 def delete_many(*keys) keys.flatten.count { |k| !delete(k).nil? } end |
#each {|key, value| ... } ⇒ Enumerator
Iterate over non-expired entries in MRU order. Yields ‘[key, value]` pairs. Returns an Enumerator when called without a block.
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'lib/philiprehberger/lru.rb', line 351 def each(&block) return enum_for(:each) unless block snapshot = @mutex.synchronize do result = [] node = @head while node result << [node.key, node.value] unless node.expired? node = node.next_node end result end snapshot.each(&block) self end |
#empty? ⇒ Boolean
Check whether the cache is empty
342 343 344 |
# File 'lib/philiprehberger/lru.rb', line 342 def empty? @mutex.synchronize { @map.empty? } end |
#fetch(key) { ... } ⇒ Object
Retrieve a value by key, or compute and store it using the block
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/philiprehberger/lru.rb', line 115 def fetch(key, &block) @mutex.synchronize do node = @map[key] if node && !node.expired? move_to_front(node) @hits += 1 return node.value end if node&.expired? remove_node(node) @map.delete(key) @evictions += 1 notify_evict(key, node.value) end @misses += 1 end return nil unless block value = yield set(key, value) value end |
#get(key) ⇒ Object?
Retrieve a value by key
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/philiprehberger/lru.rb', line 87 def get(key) @mutex.synchronize do node = @map[key] if node.nil? @misses += 1 return nil end if node.expired? remove_node(node) @map.delete(key) @evictions += 1 notify_evict(key, node.value) @misses += 1 return nil end move_to_front(node) @hits += 1 node.value end end |
#get_many(*keys) ⇒ Hash
Retrieve multiple values by key
190 191 192 |
# File 'lib/philiprehberger/lru.rb', line 190 def get_many(*keys) keys.flatten.to_h { |k| [k, get(k)] } end |
#hit_rate ⇒ Float
Return the hit rate as a float between 0.0 and 1.0
205 206 207 208 209 210 211 212 |
# File 'lib/philiprehberger/lru.rb', line 205 def hit_rate @mutex.synchronize do total = @hits + @misses return 0.0 if total.zero? @hits.to_f / total end end |
#include?(key) ⇒ Boolean Also known as: key?
Check whether a key exists in the cache and is not expired
297 298 299 300 301 302 303 304 305 |
# File 'lib/philiprehberger/lru.rb', line 297 def include?(key) @mutex.synchronize do node = @map[key] return false if node.nil? return false if node.expired? true end end |
#keys ⇒ Array
Return all keys in the cache (most recently used first)
266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/philiprehberger/lru.rb', line 266 def keys @mutex.synchronize do result = [] node = @head while node result << node.key unless node.expired? node = node.next_node end result end end |
#miss_rate ⇒ Float
Return the miss rate as a float between 0.0 and 1.0
217 218 219 220 221 222 223 224 |
# File 'lib/philiprehberger/lru.rb', line 217 def miss_rate @mutex.synchronize do total = @hits + @misses return 0.0 if total.zero? @misses.to_f / total end end |
#on_evict {|key, value| ... } ⇒ void
This method returns an undefined value.
Register an eviction callback
171 172 173 174 175 |
# File 'lib/philiprehberger/lru.rb', line 171 def on_evict(&block) @mutex.synchronize do @on_evict = block end end |
#peek(key) ⇒ Object?
Read a value without promoting the key in the LRU order. Does not affect hit/miss statistics.
314 315 316 317 318 319 320 321 |
# File 'lib/philiprehberger/lru.rb', line 314 def peek(key) @mutex.synchronize do node = @map[key] return nil if node.nil? || node.expired? node.value end end |
#reset_stats ⇒ void
This method returns an undefined value.
Reset hit, miss, and eviction counters to zero
229 230 231 232 233 234 235 |
# File 'lib/philiprehberger/lru.rb', line 229 def reset_stats @mutex.synchronize do @hits = 0 @misses = 0 @evictions = 0 end end |
#resize(new_max) ⇒ void
This method returns an undefined value.
Change the maximum capacity at runtime. If the new size is smaller than the current number of entries, least-recently-used entries are evicted until the cache fits.
330 331 332 333 334 335 336 337 |
# File 'lib/philiprehberger/lru.rb', line 330 def resize(new_max) raise Error, 'max_size must be a positive integer' unless new_max.is_a?(Integer) && new_max.positive? @mutex.synchronize do @max_size = new_max evict_one while @map.size > @max_size end end |
#set(key, value) ⇒ Object
Store a key-value pair in the cache
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/philiprehberger/lru.rb', line 65 def set(key, value) @mutex.synchronize do if @map.key?(key) node = @map[key] node.value = value node.expires_at = @ttl ? Time.now + @ttl : nil move_to_front(node) else evict_one while @map.size >= @max_size expires_at = @ttl ? Time.now + @ttl : nil node = Node.new(key, value, expires_at: expires_at) @map[key] = node prepend_node(node) end value end end |
#set_many(hash) ⇒ void
This method returns an undefined value.
Bulk insert from a hash
181 182 183 184 |
# File 'lib/philiprehberger/lru.rb', line 181 def set_many(hash) hash.each { |k, v| set(k, v) } nil end |
#size ⇒ Integer
Return the current number of entries
249 250 251 |
# File 'lib/philiprehberger/lru.rb', line 249 def size @mutex.synchronize { @map.size } end |
#stats ⇒ Hash
Return cache statistics
240 241 242 243 244 |
# File 'lib/philiprehberger/lru.rb', line 240 def stats @mutex.synchronize do { hits: @hits, misses: @misses, evictions: @evictions, size: @map.size } end end |
#values ⇒ Array
Return all values in the cache (most recently used first)
281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/philiprehberger/lru.rb', line 281 def values @mutex.synchronize do result = [] node = @head while node result << node.value unless node.expired? node = node.next_node end result end end |