Class: Philiprehberger::CacheKit::Store
- Inherits:
-
Object
- Object
- Philiprehberger::CacheKit::Store
- Includes:
- Batch, Callbacks, Eviction, Serializable, TagStats
- Defined in:
- lib/philiprehberger/cache_kit/store.rb
Overview
Thread-safe in-memory LRU cache with TTL and tag-based invalidation.
Instance Method Summary collapse
- #[](key) ⇒ Object
- #[]=(key, value) ⇒ Object
- #clear ⇒ Object
-
#compact ⇒ Integer
Prune expired entries and return the count of evicted items.
-
#decrement(key, by: 1, ttl: nil) ⇒ Numeric
Atomically decrement a numeric entry.
-
#delete(key) ⇒ Boolean
True if the key existed.
-
#delete_many(*keys) ⇒ Integer
Bulk-delete multiple keys in a single lock acquisition.
-
#expire_at(key) ⇒ Time?
Absolute expiration time of the entry.
-
#fetch(key, ttl: nil, tags: [], &block) ⇒ Object
Get or compute a value (thread-safe).
-
#get(key) ⇒ Object
Get a value by key.
-
#increment(key, by: 1, ttl: nil) ⇒ Numeric
Atomically increment a numeric entry.
-
#initialize(max_size: 1000) ⇒ Store
constructor
A new instance of Store.
-
#invalidate_tag(tag) ⇒ Object
Invalidate all entries with a given tag.
- #key?(key) ⇒ Boolean
- #keys ⇒ Object
-
#keys_by_tag(tag) ⇒ Array<String>
Return the keys associated with a given tag.
-
#peek(key) ⇒ Object?
Read a value without affecting LRU order or hit/miss counters.
- #prune ⇒ Object
-
#refresh(key, ttl: nil) ⇒ Boolean
Reset the TTL of an existing entry without changing its value.
-
#replace_if_equal(key, expected, new_value, ttl: nil) ⇒ Boolean
Compare-and-swap: only replace the value when the current stored value equals ‘expected`.
-
#set(key, value, ttl: nil, tags: []) ⇒ Object
Store a value with optional TTL and tags.
-
#set_many(hash, ttl: nil, tags: []) ⇒ void
Bulk set multiple entries in a single lock acquisition.
- #size ⇒ Object
-
#stats(tag: nil) ⇒ Hash
Cache statistics, optionally filtered by tag.
-
#touch(key, ttl: nil) ⇒ Boolean
Promote a key to most-recently-used in LRU order.
-
#ttl(key) ⇒ Float?
Remaining seconds until the entry expires.
-
#values ⇒ Array
Returns all non-expired values in the cache.
Methods included from Serializable
Methods included from Batch
Methods included from Callbacks
Constructor Details
#initialize(max_size: 1000) ⇒ Store
Returns a new instance of Store.
14 15 16 17 18 19 20 21 22 23 24 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 14 def initialize(max_size: 1000) @max_size = max_size @data = {} @order = [] @mutex = Mutex.new @hits = 0 @misses = 0 @evictions = 0 init_callbacks init_tag_stats end |
Instance Method Details
#[](key) ⇒ Object
93 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 93 def [](key) = get(key) |
#[]=(key, value) ⇒ Object
95 96 97 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 95 def []=(key, value) set(key, value) end |
#clear ⇒ Object
51 52 53 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 51 def clear @mutex.synchronize { @data.clear && @order.clear } end |
#compact ⇒ Integer
Prune expired entries and return the count of evicted items
123 124 125 126 127 128 129 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 123 def compact @mutex.synchronize do expired_keys = @data.select { |_, entry| entry.expired? }.keys expired_keys.each { |key| remove_entry(key) } expired_keys.length end end |
#decrement(key, by: 1, ttl: nil) ⇒ Numeric
Atomically decrement a numeric entry. See #increment.
255 256 257 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 255 def decrement(key, by: 1, ttl: nil) @mutex.synchronize { apply_counter_delta(key, -by, ttl: ttl) } end |
#delete(key) ⇒ Boolean
Returns true if the key existed.
42 43 44 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 42 def delete(key) @mutex.synchronize { delete_entry(key) } end |
#delete_many(*keys) ⇒ Integer
Bulk-delete multiple keys in a single lock acquisition. Does not fire eviction callbacks (matches #delete semantics).
206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 206 def delete_many(*keys) keys = keys.flatten @mutex.synchronize do removed = 0 keys.each do |key| next unless @data.key?(key) remove_entry(key) removed += 1 end removed end end |
#expire_at(key) ⇒ Time?
Absolute expiration time of the entry.
Returns nil when the key is missing, expired, or has no TTL.
192 193 194 195 196 197 198 199 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 192 def expire_at(key) @mutex.synchronize do entry = @data[key] next nil if entry.nil? || entry.expired? entry.expire_at end end |
#fetch(key, ttl: nil, tags: [], &block) ⇒ Object
Get or compute a value (thread-safe).
37 38 39 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 37 def fetch(key, ttl: nil, tags: [], &block) @mutex.synchronize { fetch_or_compute(key, ttl: ttl, tags: , &block) } end |
#get(key) ⇒ Object
Get a value by key. Returns nil if missing or expired.
27 28 29 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 27 def get(key) @mutex.synchronize { fetch_entry(key) } end |
#increment(key, by: 1, ttl: nil) ⇒ Numeric
Atomically increment a numeric entry. Initializes missing or expired keys to 0 before applying the delta.
244 245 246 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 244 def increment(key, by: 1, ttl: nil) @mutex.synchronize { apply_counter_delta(key, by, ttl: ttl) } end |
#invalidate_tag(tag) ⇒ Object
Invalidate all entries with a given tag.
47 48 49 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 47 def invalidate_tag(tag) @mutex.synchronize { invalidate_by_tag(tag) } end |
#key?(key) ⇒ Boolean
59 60 61 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 59 def key?(key) @mutex.synchronize { key_present?(key) } end |
#keys ⇒ Object
63 64 65 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 63 def keys @mutex.synchronize { @data.reject { |_, e| e.expired? }.keys } end |
#keys_by_tag(tag) ⇒ Array<String>
Return the keys associated with a given tag. Excludes expired entries.
225 226 227 228 229 230 231 232 233 234 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 225 def keys_by_tag(tag) tag_s = tag.to_s @mutex.synchronize do @data.each_with_object([]) do |(key, entry), acc| next if entry.expired? acc << key if entry..include?(tag_s) end end end |
#peek(key) ⇒ Object?
Read a value without affecting LRU order or hit/miss counters.
Returns the stored value when the entry is present and not expired, otherwise nil. Unlike #get, peek does not promote the entry to most-recently-used, does not record a hit or miss, and does not remove expired entries — it is a pure read for inspection paths.
84 85 86 87 88 89 90 91 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 84 def peek(key) @mutex.synchronize do entry = @data[key] next nil if entry.nil? || entry.expired? entry.value end end |
#prune ⇒ Object
104 105 106 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 104 def prune @mutex.synchronize { prune_expired } end |
#refresh(key, ttl: nil) ⇒ Boolean
Reset the TTL of an existing entry without changing its value
136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 136 def refresh(key, ttl: nil) @mutex.synchronize do entry = @data[key] return false if entry.nil? || entry.expired? remove_entry(key) @data[key] = Entry.new(entry.value, ttl: ttl, tags: entry.) @order.push(key) true end end |
#replace_if_equal(key, expected, new_value, ttl: nil) ⇒ Boolean
Compare-and-swap: only replace the value when the current stored value equals ‘expected`. Fires no swap when the key is missing, expired, or holds a different value. Preserves the existing tags, but refreshes the TTL from `ttl` when provided (`nil` keeps the current TTL).
Returns true on a successful swap and false otherwise, enabling optimistic locking patterns.
272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 272 def replace_if_equal(key, expected, new_value, ttl: nil) @mutex.synchronize do entry = @data[key] return false if entry.nil? || entry.expired? return false unless entry.value == expected preserved_ttl = ttl.nil? ? entry.ttl : ttl = entry. remove_entry(key) @data[key] = Entry.new(new_value, ttl: preserved_ttl, tags: ) @order.push(key) true end end |
#set(key, value, ttl: nil, tags: []) ⇒ Object
Store a value with optional TTL and tags.
32 33 34 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 32 def set(key, value, ttl: nil, tags: []) @mutex.synchronize { store_entry(key, value, ttl: ttl, tags: ) } end |
#set_many(hash, ttl: nil, tags: []) ⇒ void
This method returns an undefined value.
Bulk set multiple entries in a single lock acquisition
114 115 116 117 118 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 114 def set_many(hash, ttl: nil, tags: []) @mutex.synchronize do hash.each { |key, value| store_entry(key, value, ttl: ttl, tags: ) } end end |
#size ⇒ Object
55 56 57 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 55 def size @mutex.synchronize { @data.size } end |
#stats(tag: nil) ⇒ Hash
Returns cache statistics, optionally filtered by tag.
100 101 102 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 100 def stats(tag: nil) @mutex.synchronize { tag ? tag_stats_for(tag) : global_stats } end |
#touch(key, ttl: nil) ⇒ Boolean
Promote a key to most-recently-used in LRU order. Optionally resets the entry’s TTL to ‘now + ttl` seconds when a `ttl:` is provided. Expired entries are removed as a side effect.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 155 def touch(key, ttl: nil) @mutex.synchronize do entry = @data[key] return false if entry.nil? if entry.expired? evict_entry(key) return false end entry.reset_ttl!(ttl) unless ttl.nil? promote_key(key) true end end |
#ttl(key) ⇒ Float?
Remaining seconds until the entry expires.
Returns nil when the key is missing, expired, or has no TTL.
177 178 179 180 181 182 183 184 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 177 def ttl(key) @mutex.synchronize do entry = @data[key] next nil if entry.nil? || entry.expired? entry.remaining_ttl end end |
#values ⇒ Array
Returns all non-expired values in the cache. Read-only introspection: does not affect LRU ordering.
71 72 73 |
# File 'lib/philiprehberger/cache_kit/store.rb', line 71 def values @mutex.synchronize { @data.reject { |_, e| e.expired? }.values.map(&:value) } end |