philiprehberger-expiring_map 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +43 -0
- data/lib/philiprehberger/expiring_map/map.rb +77 -0
- data/lib/philiprehberger/expiring_map/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9113519082c9f4bc5b232176d347f5bf07ffd282a63f27be15a68443a6aee90
|
|
4
|
+
data.tar.gz: 7b90b98bab68612c39f4723c4d7673a1149155c50531972754eb7ae0daea841c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f55f4d5d64f2b81cd57519784e8fdce758360f72c9cd85c6789f29bea0950de80eaf3a649b1d0c070c890179db3a126893e9933aa39f9e41817fcccc765c3e34
|
|
7
|
+
data.tar.gz: 40c275b4ef7abd9dfc531bcf6ef19e44d812e25b1eb5158c4d4b2cdea07f6e0952dff5e6543a6fad9cb67ea4d2a6389dfbda2c6c2ce4dee867173d86f374fb44
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Map#purge_expired!` — actively sweep expired entries and return the count removed; fires `on_expire` callbacks. Useful for explicit cleanup in long-running processes that read sparingly.
|
|
14
|
+
- `Map#expired?(key)` — predicate for whether an entry is present-but-expired without deleting it or firing `on_expire`. Distinct from `get(key).nil?`, which can't tell "missing" from "expired".
|
|
15
|
+
|
|
16
|
+
## [0.3.0] - 2026-04-17
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `Map#fetch(key, ttl: nil) { block }` atomically memoizes the block result on miss
|
|
20
|
+
|
|
10
21
|
## [0.2.0] - 2026-04-03
|
|
11
22
|
|
|
12
23
|
### Added
|
data/README.md
CHANGED
|
@@ -42,6 +42,19 @@ cache.set(:config, data, ttl: 3600) # expires in 1 hour
|
|
|
42
42
|
cache.ttl(:token) # => remaining seconds
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
### Fetch-or-compute
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
cache = Philiprehberger::ExpiringMap.new(default_ttl: 300)
|
|
49
|
+
|
|
50
|
+
# On miss, the block is evaluated, the result stored, and returned.
|
|
51
|
+
# On hit, the stored value is returned and the block is not called.
|
|
52
|
+
user = cache.fetch(:user_42) { User.find(42) }
|
|
53
|
+
|
|
54
|
+
# Override the TTL for this insert only:
|
|
55
|
+
token = cache.fetch(:token, ttl: 60) { Auth.issue_token }
|
|
56
|
+
```
|
|
57
|
+
|
|
45
58
|
### Max Size with Eviction
|
|
46
59
|
|
|
47
60
|
```ruby
|
|
@@ -108,6 +121,33 @@ cache.each { |key, value| puts "#{key}: #{value}" }
|
|
|
108
121
|
cache.select { |_k, v| v > 10 }
|
|
109
122
|
```
|
|
110
123
|
|
|
124
|
+
### Manual Sweep
|
|
125
|
+
|
|
126
|
+
Most read methods (`get`, `size`, `each`, `keys`, `values`) sweep expired
|
|
127
|
+
entries lazily on access. Long-running processes that read sparingly can
|
|
128
|
+
accumulate stale entries between reads — call `purge_expired!` to reclaim
|
|
129
|
+
memory and fire `on_expire` callbacks for everything that has expired:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
cache.set(:a, 1, ttl: 0.01)
|
|
133
|
+
cache.set(:b, 2, ttl: 10)
|
|
134
|
+
sleep 0.02
|
|
135
|
+
|
|
136
|
+
cache.purge_expired! # => 1 (number of entries removed)
|
|
137
|
+
cache.keys # => [:b]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`expired?(key)` returns whether a present key has elapsed its TTL — without
|
|
141
|
+
deleting the entry or firing `on_expire`. Distinct from `get(key).nil?`,
|
|
142
|
+
which can't tell "missing" from "expired":
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
cache.set(:dead, 'gone', ttl: 0.01)
|
|
146
|
+
sleep 0.02
|
|
147
|
+
cache.expired?(:dead) # => true
|
|
148
|
+
cache.expired?(:nope) # => false (missing, not expired)
|
|
149
|
+
```
|
|
150
|
+
|
|
111
151
|
## API
|
|
112
152
|
|
|
113
153
|
| Method | Description |
|
|
@@ -115,6 +155,7 @@ cache.select { |_k, v| v > 10 }
|
|
|
115
155
|
| `.new(default_ttl:, max_size:)` | Create a new expiring map |
|
|
116
156
|
| `#set(key, value, ttl:)` | Store a value with optional per-key TTL |
|
|
117
157
|
| `#get(key)` | Retrieve a value, nil if expired or missing |
|
|
158
|
+
| `#fetch(key, ttl:) { block }` | Return stored value or atomically memoize block result on miss |
|
|
118
159
|
| `#set_many(hash, ttl:)` | Bulk insert from a hash |
|
|
119
160
|
| `#get_many(*keys)` | Bulk get returning hash of key => value |
|
|
120
161
|
| `#delete(key)` | Remove and return a value |
|
|
@@ -128,6 +169,8 @@ cache.select { |_k, v| v > 10 }
|
|
|
128
169
|
| `#on_expire { \|k, v\| }` | Register expiration callback |
|
|
129
170
|
| `#clear` | Remove all entries |
|
|
130
171
|
| `#each { \|k, v\| }` | Iterate over non-expired entries |
|
|
172
|
+
| `#purge_expired!` | Actively sweep expired entries; fires `on_expire`; returns the count removed |
|
|
173
|
+
| `#expired?(key)` | Whether the entry at `key` is present-but-expired (does not delete or fire `on_expire`) |
|
|
131
174
|
|
|
132
175
|
## Development
|
|
133
176
|
|
|
@@ -66,6 +66,50 @@ module Philiprehberger
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Retrieve a value by key, or atomically compute and store it on miss
|
|
70
|
+
#
|
|
71
|
+
# If the key is present and not expired, returns the stored value.
|
|
72
|
+
# Otherwise evaluates the block, stores the result under +key+ with the
|
|
73
|
+
# default TTL (or +ttl:+ override), and returns it. The get/compute/set
|
|
74
|
+
# path runs under the same mutex used by +#get+ and +#set+ so there is
|
|
75
|
+
# no race with concurrent writers.
|
|
76
|
+
#
|
|
77
|
+
# @param key [Object] the key
|
|
78
|
+
# @param ttl [Numeric, nil] TTL in seconds for the inserted value on miss, uses default if nil
|
|
79
|
+
# @yieldreturn [Object] the value to memoize under +key+ on miss
|
|
80
|
+
# @raise [KeyError] if the key is missing/expired and no block is given
|
|
81
|
+
# @return [Object] the stored or freshly computed value
|
|
82
|
+
def fetch(key, ttl: nil)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
entry = @store[key]
|
|
85
|
+
|
|
86
|
+
if entry && !entry.expired?
|
|
87
|
+
@hits += 1
|
|
88
|
+
return entry.value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if entry&.expired?
|
|
92
|
+
@expirations += 1
|
|
93
|
+
fire_expire(key, entry.value)
|
|
94
|
+
@store.delete(key)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@misses += 1
|
|
98
|
+
|
|
99
|
+
raise KeyError, "key not found: #{key.inspect}" unless block_given?
|
|
100
|
+
|
|
101
|
+
value = yield
|
|
102
|
+
effective_ttl = ttl || @default_ttl
|
|
103
|
+
expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + effective_ttl
|
|
104
|
+
|
|
105
|
+
sweep_expired
|
|
106
|
+
evict_oldest if @max_size && @store.size >= @max_size && !@store.key?(key)
|
|
107
|
+
@store[key] = Entry.new(value, expires_at)
|
|
108
|
+
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
69
113
|
# Delete a key
|
|
70
114
|
#
|
|
71
115
|
# @param key [Object] the key
|
|
@@ -224,6 +268,39 @@ module Philiprehberger
|
|
|
224
268
|
end
|
|
225
269
|
end
|
|
226
270
|
|
|
271
|
+
# Actively sweep expired entries and return how many were removed.
|
|
272
|
+
#
|
|
273
|
+
# Most read methods (`get`, `size`, `each`, `keys`, `values`) sweep lazily,
|
|
274
|
+
# so long-running processes that read sparingly may accumulate stale
|
|
275
|
+
# entries before the next read triggers cleanup. Call this to reclaim
|
|
276
|
+
# memory and fire `on_expire` callbacks for everything that has expired.
|
|
277
|
+
#
|
|
278
|
+
# @return [Integer] number of entries removed
|
|
279
|
+
def purge_expired!
|
|
280
|
+
@mutex.synchronize do
|
|
281
|
+
before = @store.size
|
|
282
|
+
sweep_expired
|
|
283
|
+
before - @store.size
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Whether the entry at +key+ has expired.
|
|
288
|
+
#
|
|
289
|
+
# Returns +false+ when the key is missing or still valid; returns +true+
|
|
290
|
+
# when the entry exists but its TTL has elapsed. Unlike `get`, this does
|
|
291
|
+
# not delete the entry or fire `on_expire`.
|
|
292
|
+
#
|
|
293
|
+
# @param key [Object] the key
|
|
294
|
+
# @return [Boolean]
|
|
295
|
+
def expired?(key)
|
|
296
|
+
@mutex.synchronize do
|
|
297
|
+
entry = @store[key]
|
|
298
|
+
return false unless entry
|
|
299
|
+
|
|
300
|
+
entry.expired?
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
227
304
|
private
|
|
228
305
|
|
|
229
306
|
# Remove all expired entries, firing callbacks
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-expiring_map
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A thread-safe hash map where each key has its own TTL, with automatic
|
|
14
14
|
expiration, max size eviction, expiration callbacks, and Enumerable support.
|