safe_memoize 0.8.0 → 1.0.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/.yardopts +10 -0
- data/CHANGELOG.md +23 -0
- data/README.md +555 -8
- data/ROADMAP.md +7 -22
- data/Rakefile +5 -0
- data/UPGRADING.md +197 -0
- data/benchmarks/README.md +68 -0
- data/benchmarks/benchmark.rb +225 -0
- data/codecov.yml +17 -0
- data/lib/safe_memoize/adapters/opentelemetry.rb +44 -0
- data/lib/safe_memoize/adapters/statsd.rb +56 -0
- data/lib/safe_memoize/cache_metrics_methods.rb +1 -0
- data/lib/safe_memoize/cache_record_methods.rb +1 -0
- data/lib/safe_memoize/cache_store_methods.rb +1 -0
- data/lib/safe_memoize/class_methods.rb +106 -21
- data/lib/safe_memoize/configuration.rb +43 -1
- data/lib/safe_memoize/custom_key_methods.rb +1 -0
- data/lib/safe_memoize/hooks_methods.rb +31 -0
- data/lib/safe_memoize/inspection_methods.rb +2 -1
- data/lib/safe_memoize/instance_methods.rb +1 -0
- data/lib/safe_memoize/lru_methods.rb +1 -0
- data/lib/safe_memoize/public_custom_key_methods.rb +23 -0
- data/lib/safe_memoize/public_methods.rb +162 -5
- data/lib/safe_memoize/public_metrics_methods.rb +20 -0
- data/lib/safe_memoize/rails/middleware.rb +25 -0
- data/lib/safe_memoize/rails/request_scoped.rb +40 -0
- data/lib/safe_memoize/rails.rb +28 -0
- data/lib/safe_memoize/release_tooling.rb +1 -0
- data/lib/safe_memoize/version.rb +2 -1
- data/lib/safe_memoize.rb +56 -0
- data/rbi/safe_memoize.rbi +245 -0
- data/sig/safe_memoize.rbs +41 -5
- metadata +12 -1
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# Public instance methods mixed into every class that prepends {SafeMemoize}.
|
|
4
5
|
module PublicMethods
|
|
6
|
+
# Returns +true+ if the given call is currently cached (and not expired).
|
|
7
|
+
#
|
|
8
|
+
# Always returns +false+ when a block is provided, because block-taking methods
|
|
9
|
+
# cannot be safely keyed by arguments alone.
|
|
10
|
+
#
|
|
11
|
+
# @param method_name [Symbol, String]
|
|
12
|
+
# @param args [Array] positional arguments used to look up the entry
|
|
13
|
+
# @param kwargs [Hash] keyword arguments used to look up the entry
|
|
14
|
+
# @return [Boolean]
|
|
5
15
|
def memoized?(method_name, *args, **kwargs, &block)
|
|
6
16
|
return false if block
|
|
7
17
|
|
|
8
|
-
cache_key =
|
|
18
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
9
19
|
|
|
10
20
|
with_memo_lock do
|
|
11
21
|
memo_cache_hit?(cache_key)
|
|
12
22
|
end
|
|
13
23
|
end
|
|
14
24
|
|
|
25
|
+
# Returns the number of seconds until the cached entry expires.
|
|
26
|
+
#
|
|
27
|
+
# @param method_name [Symbol, String]
|
|
28
|
+
# @param args [Array]
|
|
29
|
+
# @param kwargs [Hash]
|
|
30
|
+
# @return [Float] seconds remaining (may be 0 if already expired)
|
|
31
|
+
# @return [nil] if the entry has no TTL or is not cached
|
|
15
32
|
def memo_ttl_remaining(method_name, *args, **kwargs)
|
|
16
|
-
cache_key =
|
|
33
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
17
34
|
|
|
18
35
|
with_memo_lock do
|
|
19
36
|
record = memo_cache_record(cache_key)
|
|
@@ -27,6 +44,10 @@ module SafeMemoize
|
|
|
27
44
|
end
|
|
28
45
|
end
|
|
29
46
|
|
|
47
|
+
# Returns the number of live cached entries for a method (or all methods).
|
|
48
|
+
#
|
|
49
|
+
# @param method_name [Symbol, String, nil] when omitted, counts all methods
|
|
50
|
+
# @return [Integer]
|
|
30
51
|
def memo_count(*method_name)
|
|
31
52
|
scoped_method = safe_memo_scoped_method(method_name)
|
|
32
53
|
|
|
@@ -35,6 +56,13 @@ module SafeMemoize
|
|
|
35
56
|
end
|
|
36
57
|
end
|
|
37
58
|
|
|
59
|
+
# Returns metadata hashes describing each cached entry.
|
|
60
|
+
#
|
|
61
|
+
# Each hash contains +:args+, +:kwargs+ (or +:custom_key+ for custom-keyed entries),
|
|
62
|
+
# and +:method+ when no +method_name+ filter is applied.
|
|
63
|
+
#
|
|
64
|
+
# @param method_name [Symbol, String, nil] when omitted, returns entries for all methods
|
|
65
|
+
# @return [Array<Hash>]
|
|
38
66
|
def memo_keys(*method_name)
|
|
39
67
|
scoped_method = safe_memo_scoped_method(method_name)
|
|
40
68
|
|
|
@@ -43,6 +71,12 @@ module SafeMemoize
|
|
|
43
71
|
end
|
|
44
72
|
end
|
|
45
73
|
|
|
74
|
+
# Returns metadata hashes including the cached value for each entry.
|
|
75
|
+
#
|
|
76
|
+
# Each hash contains all fields from {#memo_keys} plus +:value+.
|
|
77
|
+
#
|
|
78
|
+
# @param method_name [Symbol, String, nil] when omitted, returns entries for all methods
|
|
79
|
+
# @return [Array<Hash>]
|
|
46
80
|
def memo_values(*method_name)
|
|
47
81
|
scoped_method = safe_memo_scoped_method(method_name)
|
|
48
82
|
|
|
@@ -51,42 +85,90 @@ module SafeMemoize
|
|
|
51
85
|
end
|
|
52
86
|
end
|
|
53
87
|
|
|
88
|
+
# Registers a hook that fires on every cache hit.
|
|
89
|
+
#
|
|
90
|
+
# @yield [cache_key, record] called synchronously inside the cache lock
|
|
91
|
+
# @yieldparam cache_key [Array] the internal cache key
|
|
92
|
+
# @yieldparam record [Hash] the cache record (+:value+, +:expires_at+, +:cached_at+)
|
|
93
|
+
# @return [void]
|
|
94
|
+
# @raise [ArgumentError] if no block is given
|
|
54
95
|
def on_memo_expire(&block)
|
|
55
96
|
raise ArgumentError, "block required" unless block
|
|
56
97
|
|
|
57
98
|
register_memo_hook(:on_expire, &block)
|
|
58
99
|
end
|
|
59
100
|
|
|
101
|
+
# Registers a hook that fires when an LRU eviction occurs.
|
|
102
|
+
#
|
|
103
|
+
# @yield [cache_key, record]
|
|
104
|
+
# @return [void]
|
|
105
|
+
# @raise [ArgumentError] if no block is given
|
|
60
106
|
def on_memo_evict(&block)
|
|
61
107
|
raise ArgumentError, "block required" unless block
|
|
62
108
|
|
|
63
109
|
register_memo_hook(:on_evict, &block)
|
|
64
110
|
end
|
|
65
111
|
|
|
112
|
+
# Registers a hook that fires on every cache hit.
|
|
113
|
+
#
|
|
114
|
+
# @yield [cache_key, record]
|
|
115
|
+
# @return [void]
|
|
116
|
+
# @raise [ArgumentError] if no block is given
|
|
66
117
|
def on_memo_hit(&block)
|
|
67
118
|
raise ArgumentError, "block required" unless block
|
|
68
119
|
|
|
69
120
|
register_memo_hook(:on_hit, &block)
|
|
70
121
|
end
|
|
71
122
|
|
|
123
|
+
# Registers a hook that fires on every cache miss (before the value is stored).
|
|
124
|
+
#
|
|
125
|
+
# @yield [cache_key, record]
|
|
126
|
+
# @return [void]
|
|
127
|
+
# @raise [ArgumentError] if no block is given
|
|
72
128
|
def on_memo_miss(&block)
|
|
73
129
|
raise ArgumentError, "block required" unless block
|
|
74
130
|
|
|
75
131
|
register_memo_hook(:on_miss, &block)
|
|
76
132
|
end
|
|
77
133
|
|
|
134
|
+
# Registers a hook that fires whenever a value is written to the cache
|
|
135
|
+
# (miss, {#warm_memo}, or {#load_memo}).
|
|
136
|
+
#
|
|
137
|
+
# @yield [cache_key, record]
|
|
138
|
+
# @return [void]
|
|
139
|
+
# @raise [ArgumentError] if no block is given
|
|
78
140
|
def on_memo_store(&block)
|
|
79
141
|
raise ArgumentError, "block required" unless block
|
|
80
142
|
|
|
81
143
|
register_memo_hook(:on_store, &block)
|
|
82
144
|
end
|
|
83
145
|
|
|
146
|
+
# Removes all registered hooks, or only hooks of a specific type.
|
|
147
|
+
#
|
|
148
|
+
# @param hook_type [Symbol, nil] one of +:on_hit+, +:on_miss+, +:on_store+,
|
|
149
|
+
# +:on_expire+, +:on_evict+; when +nil+ all hooks are cleared
|
|
150
|
+
# @return [void]
|
|
84
151
|
def clear_memo_hooks(hook_type = nil)
|
|
85
152
|
with_memo_lock do
|
|
86
153
|
_clear_memo_hooks(hook_type)
|
|
87
154
|
end
|
|
88
155
|
end
|
|
89
156
|
|
|
157
|
+
# Pre-populates a cache entry with the value returned by the block without
|
|
158
|
+
# calling the memoized method itself.
|
|
159
|
+
#
|
|
160
|
+
# Useful for warming caches from a serialized snapshot or an external source.
|
|
161
|
+
#
|
|
162
|
+
# @param method_name [Symbol, String]
|
|
163
|
+
# @param args [Array] positional arguments that identify the cache slot
|
|
164
|
+
# @param ttl [Numeric, nil] optional expiry for the warmed entry
|
|
165
|
+
# @param kwargs [Hash] keyword arguments that identify the cache slot
|
|
166
|
+
# @yield [] must return the value to store
|
|
167
|
+
# @return [Object] the value returned by the block
|
|
168
|
+
# @raise [ArgumentError] if no block is given
|
|
169
|
+
#
|
|
170
|
+
# @example
|
|
171
|
+
# obj.warm_memo(:find, 42) { User.new(id: 42, name: "cached") }
|
|
90
172
|
def warm_memo(method_name, *args, ttl: nil, **kwargs, &block)
|
|
91
173
|
raise ArgumentError, "block required" unless block
|
|
92
174
|
|
|
@@ -104,6 +186,17 @@ module SafeMemoize
|
|
|
104
186
|
value
|
|
105
187
|
end
|
|
106
188
|
|
|
189
|
+
# Calls the memoized method for each argument set and caches all results.
|
|
190
|
+
#
|
|
191
|
+
# Equivalent to calling the method for each arg set individually, but expressed
|
|
192
|
+
# as a single call for clarity.
|
|
193
|
+
#
|
|
194
|
+
# @param method_name [Symbol, String]
|
|
195
|
+
# @param arg_sets [Array<Array>] each element is an argument list for one call
|
|
196
|
+
# @return [Array] cached values in input order
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# obj.memo_preload(:find, [1], [2], [3])
|
|
107
200
|
def memo_preload(method_name, *arg_sets)
|
|
108
201
|
method_name = method_name.to_sym
|
|
109
202
|
arg_sets.map do |args|
|
|
@@ -111,6 +204,11 @@ module SafeMemoize
|
|
|
111
204
|
end
|
|
112
205
|
end
|
|
113
206
|
|
|
207
|
+
# Exports live cache entries as a plain hash suitable for serialization.
|
|
208
|
+
#
|
|
209
|
+
# @param method_name [Symbol, String, nil] when given, exports only entries for
|
|
210
|
+
# that method; when +nil+, exports all methods
|
|
211
|
+
# @return [Hash] mapping cache keys to their cached values (expired entries excluded)
|
|
114
212
|
def dump_memo(method_name = nil)
|
|
115
213
|
method_name = method_name&.to_sym
|
|
116
214
|
|
|
@@ -122,6 +220,14 @@ module SafeMemoize
|
|
|
122
220
|
end
|
|
123
221
|
end
|
|
124
222
|
|
|
223
|
+
# Restores cache entries from a snapshot produced by {#dump_memo}.
|
|
224
|
+
#
|
|
225
|
+
# Existing entries are not cleared; snapshot keys are merged in.
|
|
226
|
+
# Each restored entry fires the +:on_store+ hook.
|
|
227
|
+
#
|
|
228
|
+
# @param snapshot [Hash] a hash previously returned by {#dump_memo}
|
|
229
|
+
# @return [nil]
|
|
230
|
+
# @raise [ArgumentError] if +snapshot+ is not a +Hash+
|
|
125
231
|
def load_memo(snapshot)
|
|
126
232
|
raise ArgumentError, "snapshot must be a Hash" unless snapshot.is_a?(Hash)
|
|
127
233
|
|
|
@@ -137,9 +243,17 @@ module SafeMemoize
|
|
|
137
243
|
nil
|
|
138
244
|
end
|
|
139
245
|
|
|
246
|
+
# Resets the expiry clock on a live cached entry without recomputing its value.
|
|
247
|
+
#
|
|
248
|
+
# @param method_name [Symbol, String]
|
|
249
|
+
# @param args [Array]
|
|
250
|
+
# @param ttl [Numeric, nil] new TTL to apply; when +nil+, uses the original TTL
|
|
251
|
+
# derived from the entry's +cached_at+ and +expires_at+ timestamps
|
|
252
|
+
# @param kwargs [Hash]
|
|
253
|
+
# @return [Boolean] +true+ if the entry existed and was touched; +false+ otherwise
|
|
140
254
|
def memo_touch(method_name, *args, ttl: nil, **kwargs)
|
|
141
255
|
method_name = method_name.to_sym
|
|
142
|
-
cache_key =
|
|
256
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
143
257
|
|
|
144
258
|
with_memo_lock do
|
|
145
259
|
cache = memo_cache_or_nil
|
|
@@ -162,14 +276,27 @@ module SafeMemoize
|
|
|
162
276
|
end
|
|
163
277
|
end
|
|
164
278
|
|
|
279
|
+
# Clears the cached entry and immediately re-calls the method to populate a
|
|
280
|
+
# fresh value.
|
|
281
|
+
#
|
|
282
|
+
# @param method_name [Symbol, String]
|
|
283
|
+
# @param args [Array]
|
|
284
|
+
# @param kwargs [Hash]
|
|
285
|
+
# @return [Object] the freshly computed and cached value
|
|
165
286
|
def memo_refresh(method_name, *args, **kwargs)
|
|
166
287
|
method_name = method_name.to_sym
|
|
167
288
|
reset_memo(method_name, *args, **kwargs)
|
|
168
289
|
send(method_name, *args, **kwargs)
|
|
169
290
|
end
|
|
170
291
|
|
|
292
|
+
# Returns how many seconds ago the entry was cached, or +nil+ if not cached.
|
|
293
|
+
#
|
|
294
|
+
# @param method_name [Symbol, String]
|
|
295
|
+
# @param args [Array]
|
|
296
|
+
# @param kwargs [Hash]
|
|
297
|
+
# @return [Float, nil]
|
|
171
298
|
def memo_age(method_name, *args, **kwargs)
|
|
172
|
-
cache_key =
|
|
299
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
173
300
|
|
|
174
301
|
with_memo_lock do
|
|
175
302
|
record = memo_cache_record(cache_key)
|
|
@@ -182,8 +309,14 @@ module SafeMemoize
|
|
|
182
309
|
end
|
|
183
310
|
end
|
|
184
311
|
|
|
312
|
+
# Returns +true+ if the entry exists but its TTL has elapsed.
|
|
313
|
+
#
|
|
314
|
+
# @param method_name [Symbol, String]
|
|
315
|
+
# @param args [Array]
|
|
316
|
+
# @param kwargs [Hash]
|
|
317
|
+
# @return [Boolean]
|
|
185
318
|
def memo_stale?(method_name, *args, **kwargs)
|
|
186
|
-
cache_key =
|
|
319
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
187
320
|
|
|
188
321
|
with_memo_lock do
|
|
189
322
|
cache = memo_cache_or_nil
|
|
@@ -196,6 +329,16 @@ module SafeMemoize
|
|
|
196
329
|
end
|
|
197
330
|
end
|
|
198
331
|
|
|
332
|
+
# Removes one or all cached entries for a method.
|
|
333
|
+
#
|
|
334
|
+
# When called with only +method_name+, all entries for that method are cleared.
|
|
335
|
+
# When called with +method_name+ *and* arguments, only the exact matching entry
|
|
336
|
+
# is cleared. Each evicted entry fires the +:on_evict+ hook.
|
|
337
|
+
#
|
|
338
|
+
# @param method_name [Symbol, String]
|
|
339
|
+
# @param args [Array] positional arguments identifying a specific entry
|
|
340
|
+
# @param kwargs [Hash] keyword arguments identifying a specific entry
|
|
341
|
+
# @return [void]
|
|
199
342
|
def reset_memo(method_name, *args, **kwargs)
|
|
200
343
|
method_name = method_name.to_sym
|
|
201
344
|
|
|
@@ -215,6 +358,10 @@ module SafeMemoize
|
|
|
215
358
|
end
|
|
216
359
|
end
|
|
217
360
|
|
|
361
|
+
# Clears all cached entries for every method on this instance.
|
|
362
|
+
# Each evicted entry fires the +:on_evict+ hook.
|
|
363
|
+
#
|
|
364
|
+
# @return [void]
|
|
218
365
|
def reset_all_memos
|
|
219
366
|
with_memo_lock do
|
|
220
367
|
if defined?(@__safe_memo_cache__) && @__safe_memo_cache__
|
|
@@ -227,6 +374,16 @@ module SafeMemoize
|
|
|
227
374
|
end
|
|
228
375
|
end
|
|
229
376
|
|
|
377
|
+
# Returns a detailed snapshot of a single cached entry, or +nil+ if not cached.
|
|
378
|
+
#
|
|
379
|
+
# All reads are performed inside a single mutex hold.
|
|
380
|
+
#
|
|
381
|
+
# @param method_name [Symbol, String]
|
|
382
|
+
# @param args [Array]
|
|
383
|
+
# @param kwargs [Hash]
|
|
384
|
+
# @return [Hash, nil] hash with keys +:cached+, +:value+, +:hits+, +:misses+,
|
|
385
|
+
# +:ttl_remaining+, +:age+, +:custom_key+, +:lru_position+; or +nil+ when
|
|
386
|
+
# the entry is not present
|
|
230
387
|
def memo_inspect(method_name, *args, **kwargs)
|
|
231
388
|
method_name = method_name.to_sym
|
|
232
389
|
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# Per-instance cache metrics: hit/miss counts and average computation time.
|
|
4
5
|
module PublicMetricsMethods
|
|
6
|
+
# Returns aggregate metrics across all memoized methods on this instance.
|
|
7
|
+
#
|
|
8
|
+
# @return [Hash] with keys +:total_hits+, +:total_misses+, +:hit_rate+,
|
|
9
|
+
# +:miss_rate+, +:average_computation_time+, and +:entries+ (one entry
|
|
10
|
+
# per cached argument combination)
|
|
5
11
|
def cache_stats
|
|
6
12
|
with_memo_lock do
|
|
7
13
|
metrics = memo_metrics_store
|
|
@@ -11,6 +17,11 @@ module SafeMemoize
|
|
|
11
17
|
end
|
|
12
18
|
end
|
|
13
19
|
|
|
20
|
+
# Returns metrics for a single memoized method.
|
|
21
|
+
#
|
|
22
|
+
# @param method_name [Symbol, String]
|
|
23
|
+
# @return [Hash] same shape as {#cache_stats} but scoped to one method,
|
|
24
|
+
# with an extra +:method+ key
|
|
14
25
|
def cache_stats_for(method_name)
|
|
15
26
|
method_name = method_name.to_sym
|
|
16
27
|
|
|
@@ -22,14 +33,23 @@ module SafeMemoize
|
|
|
22
33
|
end
|
|
23
34
|
end
|
|
24
35
|
|
|
36
|
+
# Returns the overall cache hit rate as a percentage (0.0–100.0).
|
|
37
|
+
# @return [Float]
|
|
25
38
|
def cache_hit_rate
|
|
26
39
|
cache_stats[:hit_rate]
|
|
27
40
|
end
|
|
28
41
|
|
|
42
|
+
# Returns the overall cache miss rate as a percentage (0.0–100.0).
|
|
43
|
+
# @return [Float]
|
|
29
44
|
def cache_miss_rate
|
|
30
45
|
cache_stats[:miss_rate]
|
|
31
46
|
end
|
|
32
47
|
|
|
48
|
+
# Resets hit/miss counters, either for one method or for all methods.
|
|
49
|
+
#
|
|
50
|
+
# @param method_name [Symbol, String, nil] when given, resets only that method's
|
|
51
|
+
# metrics; when +nil+, resets all
|
|
52
|
+
# @return [void]
|
|
33
53
|
def cache_metrics_reset(method_name = nil)
|
|
34
54
|
with_memo_lock do
|
|
35
55
|
if method_name
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeMemoize
|
|
4
|
+
module Rails
|
|
5
|
+
# Rack middleware that resets all thread-tracked memoized instances at the
|
|
6
|
+
# end of each request. Useful for service objects that are instantiated
|
|
7
|
+
# per-request and register themselves via `SafeMemoize::Rails.track(self)`.
|
|
8
|
+
#
|
|
9
|
+
# Add to your Rack stack in config/application.rb:
|
|
10
|
+
# config.middleware.use SafeMemoize::Rails::Middleware
|
|
11
|
+
class Middleware
|
|
12
|
+
def initialize(app)
|
|
13
|
+
@app = app
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param env [Hash] Rack environment
|
|
17
|
+
# @return [Array] Rack response triplet
|
|
18
|
+
def call(env)
|
|
19
|
+
@app.call(env)
|
|
20
|
+
ensure
|
|
21
|
+
SafeMemoize::Rails.reset_tracked!
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeMemoize
|
|
4
|
+
module Rails
|
|
5
|
+
# Include in a Rails controller to automatically reset instance memos after
|
|
6
|
+
# each request. In non-controller classes (service objects, models), include
|
|
7
|
+
# it to gain `reset_request_memos` and call it manually at the end of a
|
|
8
|
+
# request or job.
|
|
9
|
+
#
|
|
10
|
+
# The class must also `prepend SafeMemoize` for `reset_all_memos` to exist.
|
|
11
|
+
#
|
|
12
|
+
# Example — controller:
|
|
13
|
+
# class ApplicationController < ActionController::Base
|
|
14
|
+
# prepend SafeMemoize
|
|
15
|
+
# include SafeMemoize::Rails::RequestScoped
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Example — service object with middleware tracking:
|
|
19
|
+
# class ReportService
|
|
20
|
+
# prepend SafeMemoize
|
|
21
|
+
# include SafeMemoize::Rails::RequestScoped
|
|
22
|
+
#
|
|
23
|
+
# def initialize
|
|
24
|
+
# SafeMemoize::Rails.track(self)
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
module RequestScoped
|
|
28
|
+
# @api private
|
|
29
|
+
def self.included(base)
|
|
30
|
+
base.after_action :reset_all_memos if base.respond_to?(:after_action)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Resets all memoized values on this instance. Delegates to {PublicMethods#reset_all_memos}.
|
|
34
|
+
# @return [void]
|
|
35
|
+
def reset_request_memos
|
|
36
|
+
reset_all_memos
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "safe_memoize"
|
|
4
|
+
require_relative "rails/request_scoped"
|
|
5
|
+
require_relative "rails/middleware"
|
|
6
|
+
|
|
7
|
+
module SafeMemoize
|
|
8
|
+
# Optional Rails integration. Not auto-required — add to your initializer:
|
|
9
|
+
# require "safe_memoize/rails"
|
|
10
|
+
module Rails
|
|
11
|
+
# Register an instance to have its memos reset at the end of the current
|
|
12
|
+
# request (via Middleware). Thread-local; each thread maintains its own list.
|
|
13
|
+
def self.track(instance)
|
|
14
|
+
(Thread.current[:safe_memoize_tracked] ||= []) << instance
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Reset all tracked instances and clear the list. Called automatically by
|
|
18
|
+
# Middleware after each request. Safe to call with an empty list.
|
|
19
|
+
def self.reset_tracked!
|
|
20
|
+
instances = Thread.current[:safe_memoize_tracked] || []
|
|
21
|
+
instances.each do |instance|
|
|
22
|
+
instance.reset_all_memos if instance.respond_to?(:reset_all_memos)
|
|
23
|
+
end
|
|
24
|
+
ensure
|
|
25
|
+
Thread.current[:safe_memoize_tracked] = []
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/safe_memoize/version.rb
CHANGED
data/lib/safe_memoize.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "safe_memoize/version"
|
|
4
4
|
require_relative "safe_memoize/configuration"
|
|
5
|
+
require_relative "safe_memoize/adapters/statsd"
|
|
6
|
+
require_relative "safe_memoize/adapters/opentelemetry"
|
|
5
7
|
require_relative "safe_memoize/class_methods"
|
|
6
8
|
require_relative "safe_memoize/public_methods"
|
|
7
9
|
require_relative "safe_memoize/cache_store_methods"
|
|
@@ -15,27 +17,81 @@ require_relative "safe_memoize/public_custom_key_methods"
|
|
|
15
17
|
require_relative "safe_memoize/lru_methods"
|
|
16
18
|
require_relative "safe_memoize/instance_methods"
|
|
17
19
|
|
|
20
|
+
# Thread-safe memoization for Ruby that correctly handles +nil+ and +false+ values.
|
|
21
|
+
#
|
|
22
|
+
# Prepend this module into any class, then call {ClassMethods#memoize} to wrap
|
|
23
|
+
# instance methods with a per-instance cache backed by a +Mutex+.
|
|
24
|
+
#
|
|
25
|
+
# @example Basic usage
|
|
26
|
+
# class UserService
|
|
27
|
+
# prepend SafeMemoize
|
|
28
|
+
#
|
|
29
|
+
# def current_user
|
|
30
|
+
# User.find_by(session_id: session_id)
|
|
31
|
+
# end
|
|
32
|
+
# memoize :current_user
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @example With TTL and LRU cap
|
|
36
|
+
# class ApiClient
|
|
37
|
+
# prepend SafeMemoize
|
|
38
|
+
#
|
|
39
|
+
# def fetch(id)
|
|
40
|
+
# http_get("/items/#{id}")
|
|
41
|
+
# end
|
|
42
|
+
# memoize :fetch, ttl: 60, max_size: 500
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# @see ClassMethods#memoize
|
|
46
|
+
# @see https://github.com/eclectic-coding/safe_memoize README
|
|
18
47
|
module SafeMemoize
|
|
48
|
+
# Base class for all SafeMemoize-specific exceptions.
|
|
49
|
+
# Rescue this to catch any error raised by the library itself.
|
|
19
50
|
class Error < StandardError; end
|
|
20
51
|
|
|
21
52
|
include InstanceMethods
|
|
22
53
|
|
|
54
|
+
# @api private
|
|
23
55
|
def self.prepended(base)
|
|
24
56
|
base.extend(ClassMethods)
|
|
25
57
|
end
|
|
26
58
|
|
|
59
|
+
# Yields the global {Configuration} object for mutation.
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# SafeMemoize.configure do |c|
|
|
63
|
+
# c.default_ttl = 300
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# @yield [config] The current {Configuration} instance.
|
|
67
|
+
# @yieldparam config [Configuration]
|
|
68
|
+
# @return [void]
|
|
27
69
|
def self.configure
|
|
28
70
|
yield configuration
|
|
29
71
|
end
|
|
30
72
|
|
|
73
|
+
# Returns the global {Configuration} instance, creating it on first access.
|
|
74
|
+
#
|
|
75
|
+
# @return [Configuration]
|
|
31
76
|
def self.configuration
|
|
32
77
|
@configuration ||= Configuration.new
|
|
33
78
|
end
|
|
34
79
|
|
|
80
|
+
# Resets the global configuration to all defaults.
|
|
81
|
+
#
|
|
82
|
+
# Useful in test suites to prevent configuration leaking between examples.
|
|
83
|
+
#
|
|
84
|
+
# @return [Configuration] the new blank configuration
|
|
35
85
|
def self.reset_configuration!
|
|
36
86
|
@configuration = Configuration.new
|
|
37
87
|
end
|
|
38
88
|
|
|
89
|
+
# Emits a structured deprecation warning through the configured handler.
|
|
90
|
+
#
|
|
91
|
+
# @param subject [String] short identifier of the deprecated symbol
|
|
92
|
+
# @param message [String] migration instructions
|
|
93
|
+
# @param horizon [String] version when the symbol will be removed (e.g. +"v2.0.0"+)
|
|
94
|
+
# @return [void]
|
|
39
95
|
def self.deprecate(subject, message:, horizon:)
|
|
40
96
|
text = "[SafeMemoize] #{subject} is deprecated and will be removed in #{horizon}. #{message}"
|
|
41
97
|
handler = configuration.on_deprecation
|