Class: Philiprehberger::ExpiringMap::Map
- Inherits:
-
Object
- Object
- Philiprehberger::ExpiringMap::Map
- Includes:
- Enumerable
- Defined in:
- lib/philiprehberger/expiring_map/map.rb
Overview
Thread-safe hash with per-key TTL and automatic expiration
Instance Method Summary collapse
-
#clear ⇒ void
Remove all entries.
-
#delete(key) ⇒ Object?
Delete a key.
-
#delete_if {|key, value| ... } ⇒ Integer
Remove entries where the block returns true.
-
#each {|key, value| ... } ⇒ Enumerator
Iterate over non-expired entries.
-
#fetch(key, ttl: nil) ⇒ Object
Retrieve a value by key, or atomically compute and store it on miss.
-
#get(key) ⇒ Object?
Retrieve a value by key.
-
#get_many(*keys) ⇒ Hash
Bulk retrieve values by keys.
-
#initialize(default_ttl: 60, max_size: nil) ⇒ Map
constructor
A new instance of Map.
-
#keys ⇒ Array<Object>
Return all non-expired keys.
-
#on_expire {|key, value| ... } ⇒ Object
Register a callback for expired entries.
-
#set(key, value, ttl: nil) ⇒ Object
Store a value with an optional per-key TTL.
-
#set_many(hash, ttl: nil) ⇒ void
Bulk insert from a hash.
-
#size ⇒ Integer
Return the number of non-expired entries.
-
#stats ⇒ Hash
Return statistics about the map.
-
#touch(key) ⇒ Boolean
Reset the TTL for a key to the default.
-
#ttl(key) ⇒ Float?
Return remaining TTL for a key.
-
#values ⇒ Array<Object>
Return all non-expired values.
Constructor Details
#initialize(default_ttl: 60, max_size: nil) ⇒ Map
Returns a new instance of Map.
13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 13 def initialize(default_ttl: 60, max_size: nil) @default_ttl = default_ttl @max_size = max_size @store = {} @mutex = Mutex.new @on_expire_callback = nil @hits = 0 @misses = 0 @expirations = 0 @evictions = 0 end |
Instance Method Details
#clear ⇒ void
This method returns an undefined value.
Remove all entries
175 176 177 178 179 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 175 def clear @mutex.synchronize do @store.clear end end |
#delete(key) ⇒ Object?
Delete a key
117 118 119 120 121 122 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 117 def delete(key) @mutex.synchronize do entry = @store.delete(key) entry&.value end end |
#delete_if {|key, value| ... } ⇒ Integer
Remove entries where the block returns true
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 233 def delete_if(&block) raise ArgumentError, 'block required' unless block @mutex.synchronize do sweep_expired count = 0 @store.delete_if do |key, entry| if block.call(key, entry.value) count += 1 true else false end end count end end |
#each {|key, value| ... } ⇒ Enumerator
Iterate over non-expired entries
185 186 187 188 189 190 191 192 193 194 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 185 def each(&block) pairs = @mutex.synchronize do sweep_expired @store.map { |k, e| [k, e.value] } end return pairs.each unless block pairs.each(&block) end |
#fetch(key, ttl: nil) ⇒ Object
Retrieve a value by key, or atomically compute and store it on miss
If the key is present and not expired, returns the stored value. Otherwise evaluates the block, stores the result under key with the default TTL (or ttl: override), and returns it. The get/compute/set path runs under the same mutex used by #get and #set so there is no race with concurrent writers.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 82 def fetch(key, ttl: nil) @mutex.synchronize do entry = @store[key] if entry && !entry.expired? @hits += 1 return entry.value end if entry&.expired? @expirations += 1 fire_expire(key, entry.value) @store.delete(key) end @misses += 1 raise KeyError, "key not found: #{key.inspect}" unless block_given? value = yield effective_ttl = ttl || @default_ttl expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + effective_ttl sweep_expired evict_oldest if @max_size && @store.size >= @max_size && !@store.key?(key) @store[key] = Entry.new(value, expires_at) value end end |
#get(key) ⇒ Object?
Retrieve a value by key
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 48 def get(key) @mutex.synchronize do entry = @store[key] unless entry @misses += 1 return nil end if entry.expired? @expirations += 1 @misses += 1 fire_expire(key, entry.value) @store.delete(key) return nil end @hits += 1 entry.value end end |
#get_many(*keys) ⇒ Hash
Bulk retrieve values by keys
225 226 227 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 225 def get_many(*keys) keys.flatten.to_h { |k| [k, get(k)] } end |
#keys ⇒ Array<Object>
Return all non-expired keys
254 255 256 257 258 259 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 254 def keys @mutex.synchronize do sweep_expired @store.keys end end |
#on_expire {|key, value| ... } ⇒ Object
Register a callback for expired entries
166 167 168 169 170 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 166 def on_expire(&block) @mutex.synchronize do @on_expire_callback = block end end |
#set(key, value, ttl: nil) ⇒ Object
Store a value with an optional per-key TTL
31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 31 def set(key, value, ttl: nil) ttl ||= @default_ttl expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + ttl @mutex.synchronize do sweep_expired evict_oldest if @max_size && @store.size >= @max_size && !@store.key?(key) @store[key] = Entry.new(value, expires_at) end value end |
#set_many(hash, ttl: nil) ⇒ void
This method returns an undefined value.
Bulk insert from a hash
217 218 219 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 217 def set_many(hash, ttl: nil) hash.each { |k, v| set(k, v, ttl: ttl) } end |
#size ⇒ Integer
Return the number of non-expired entries
156 157 158 159 160 161 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 156 def size @mutex.synchronize do sweep_expired @store.size end end |
#stats ⇒ Hash
Return statistics about the map
199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 199 def stats @mutex.synchronize do sweep_expired { hits: @hits, misses: @misses, expirations: @expirations, evictions: @evictions, size: @store.size } end end |
#touch(key) ⇒ Boolean
Reset the TTL for a key to the default
142 143 144 145 146 147 148 149 150 151 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 142 def touch(key) @mutex.synchronize do entry = @store[key] return false unless entry return false if entry.expired? entry.expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @default_ttl true end end |
#ttl(key) ⇒ Float?
Return remaining TTL for a key
128 129 130 131 132 133 134 135 136 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 128 def ttl(key) @mutex.synchronize do entry = @store[key] return nil unless entry return nil if entry.expired? entry.ttl end end |
#values ⇒ Array<Object>
Return all non-expired values
264 265 266 267 268 269 |
# File 'lib/philiprehberger/expiring_map/map.rb', line 264 def values @mutex.synchronize do sweep_expired @store.values.map(&:value) end end |