Class: Philiprehberger::Lru::Cache

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/philiprehberger/lru.rb

Overview

Thread-safe LRU cache with TTL, eviction callbacks, and hit/miss statistics

Examples:

cache = Philiprehberger::Lru::Cache.new(max_size: 100, ttl: 300)
cache.set(:user, { name: 'Alice' })
cache.get(:user) # => { name: 'Alice' }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_size:, ttl: nil) ⇒ Cache

Create a new LRU cache

Parameters:

  • max_size (Integer)

    maximum number of entries

  • ttl (Numeric, nil) (defaults to: nil)

    time-to-live in seconds (nil for no expiration)

Raises:



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_sizeInteger (readonly)

Return the configured maximum size

Returns:

  • (Integer)


256
257
258
# File 'lib/philiprehberger/lru.rb', line 256

def max_size
  @max_size
end

#ttlNumeric? (readonly)

Return the configured TTL in seconds

Returns:

  • (Numeric, nil)


261
262
263
# File 'lib/philiprehberger/lru.rb', line 261

def ttl
  @ttl
end

Instance Method Details

#clearvoid

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

Parameters:

  • key (Object)

    the cache key

Returns:

  • (Object, nil)

    the removed value or nil



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

Parameters:

  • keys (Array<Object>)

    the cache keys to delete

Returns:

  • (Integer)

    number of keys actually deleted



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.

Yields:

  • (key, value)

Returns:

  • (Enumerator)

    when no block is given



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

Returns:

  • (Boolean)


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

Parameters:

  • key (Object)

    the cache key

Yields:

  • computes the value if the key is not found

Returns:

  • (Object)

    the cached or computed value



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

Parameters:

  • key (Object)

    the cache key

Returns:

  • (Object, nil)

    the cached value or nil if not found/expired



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

Parameters:

  • keys (Array<Object>)

    the cache keys

Returns:

  • (Hash)

    key => value for each key (nil for misses)



190
191
192
# File 'lib/philiprehberger/lru.rb', line 190

def get_many(*keys)
  keys.flatten.to_h { |k| [k, get(k)] }
end

#hit_rateFloat

Return the hit rate as a float between 0.0 and 1.0

Returns:

  • (Float)


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

Parameters:

  • key (Object)

    the cache key

Returns:

  • (Boolean)


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

#keysArray

Return all keys in the cache (most recently used first)

Returns:

  • (Array)


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_rateFloat

Return the miss rate as a float between 0.0 and 1.0

Returns:

  • (Float)


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

Yields:

  • (key, value)

    called when an entry is evicted



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.

Parameters:

  • key (Object)

    the cache key

Returns:

  • (Object, nil)

    the cached value or nil if not found/expired



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_statsvoid

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.

Parameters:

  • new_max (Integer)

    the new maximum size

Raises:

  • (Error)

    if new_max is not a positive integer



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

Parameters:

  • key (Object)

    the cache key

  • value (Object)

    the value to store

Returns:

  • (Object)

    the stored value



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

Parameters:

  • hash (Hash)

    key-value pairs to store



181
182
183
184
# File 'lib/philiprehberger/lru.rb', line 181

def set_many(hash)
  hash.each { |k, v| set(k, v) }
  nil
end

#sizeInteger

Return the current number of entries

Returns:

  • (Integer)


249
250
251
# File 'lib/philiprehberger/lru.rb', line 249

def size
  @mutex.synchronize { @map.size }
end

#statsHash

Return cache statistics

Returns:

  • (Hash)

    hits, misses, evictions, and current size



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

#valuesArray

Return all values in the cache (most recently used first)

Returns:

  • (Array)


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