safe_memoize 0.9.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 +12 -0
- data/README.md +181 -2
- data/ROADMAP.md +7 -22
- data/Rakefile +5 -0
- data/UPGRADING.md +197 -0
- data/lib/safe_memoize/adapters/opentelemetry.rb +25 -0
- data/lib/safe_memoize/adapters/statsd.rb +31 -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 +101 -18
- data/lib/safe_memoize/configuration.rb +40 -2
- data/lib/safe_memoize/custom_key_methods.rb +1 -0
- data/lib/safe_memoize/hooks_methods.rb +1 -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 +2 -0
- data/lib/safe_memoize/rails/request_scoped.rb +3 -0
- data/lib/safe_memoize/release_tooling.rb +1 -0
- data/lib/safe_memoize/version.rb +2 -1
- data/lib/safe_memoize.rb +54 -0
- data/rbi/safe_memoize.rbi +245 -0
- data/sig/safe_memoize.rbs +15 -5
- metadata +4 -1
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# Class-level DSL added to any class that does +prepend SafeMemoize+.
|
|
4
5
|
module ClassMethods
|
|
6
|
+
# Wraps an existing instance method with a thread-safe per-instance cache.
|
|
7
|
+
#
|
|
8
|
+
# Must be called *after* the method is defined. Raises +ArgumentError+ immediately
|
|
9
|
+
# at class-definition time if no such method exists.
|
|
10
|
+
#
|
|
11
|
+
# @param method_name [Symbol, String] name of the instance method to memoize
|
|
12
|
+
# @param ttl [Numeric, nil] seconds until the cached value expires; +nil+ means
|
|
13
|
+
# the entry never expires. Falls back to {Configuration#default_ttl} when +nil+.
|
|
14
|
+
# @param max_size [Integer, nil] maximum number of cached entries per instance
|
|
15
|
+
# for this method; the least-recently-used entry is evicted when the limit is
|
|
16
|
+
# reached. Falls back to {Configuration#default_max_size} when +nil+.
|
|
17
|
+
# @param ttl_refresh [Boolean] when +true+, every cache *hit* resets the expiry
|
|
18
|
+
# clock (sliding-window TTL). Requires +ttl:+ to be set.
|
|
19
|
+
# @param if [Proc, nil] callable predicate; the result is cached only when the
|
|
20
|
+
# predicate returns truthy. Receives the computed return value as its argument.
|
|
21
|
+
# @param unless [Proc, nil] inverse of +:if+; the result is *not* cached when the
|
|
22
|
+
# predicate returns truthy.
|
|
23
|
+
# @param shared [Boolean] when +true+, results are stored on the class rather than
|
|
24
|
+
# per instance — all instances share one cache.
|
|
25
|
+
# @param key [Proc, nil] class-level custom cache key generator. Receives the same
|
|
26
|
+
# arguments as the method and should return a single comparable value. Instance-level
|
|
27
|
+
# keys set via {PublicCustomKeyMethods#memoize_with_custom_key} take priority.
|
|
28
|
+
# @return [void]
|
|
29
|
+
# @raise [ArgumentError] if the method does not exist, or option values are invalid
|
|
30
|
+
#
|
|
31
|
+
# @example Zero-argument method
|
|
32
|
+
# def expensive_query = db.run("SELECT …")
|
|
33
|
+
# memoize :expensive_query
|
|
34
|
+
#
|
|
35
|
+
# @example With TTL and LRU cap
|
|
36
|
+
# def fetch(id) = http_get(id)
|
|
37
|
+
# memoize :fetch, ttl: 60, max_size: 500
|
|
38
|
+
#
|
|
39
|
+
# @example Conditional — only cache successful responses
|
|
40
|
+
# memoize :fetch, if: ->(v) { v[:status] == 200 }
|
|
5
41
|
def memoize(method_name, ttl: nil, max_size: nil, ttl_refresh: false, if: nil, unless: nil, shared: false, key: nil)
|
|
6
42
|
method_name = method_name.to_sym
|
|
7
43
|
|
|
@@ -196,6 +232,45 @@ module SafeMemoize
|
|
|
196
232
|
prepend mod
|
|
197
233
|
end
|
|
198
234
|
|
|
235
|
+
# Memoizes every eligible public instance method defined directly on the class.
|
|
236
|
+
#
|
|
237
|
+
# Accepts all options that {#memoize} accepts, plus +:except:+ and +:only:+.
|
|
238
|
+
# Raises +ArgumentError+ when both +:only:+ and +:except:+ are given.
|
|
239
|
+
#
|
|
240
|
+
# @param except [Array<Symbol, String>] method names to skip
|
|
241
|
+
# @param only [Array<Symbol, String>] when non-empty, only these methods are memoized
|
|
242
|
+
# @param include_protected [Boolean] also memoize +protected+ methods
|
|
243
|
+
# @param include_private [Boolean] also memoize +private+ methods
|
|
244
|
+
# @param options [Hash] any additional options forwarded to {#memoize}
|
|
245
|
+
# @return [void]
|
|
246
|
+
# @raise [ArgumentError] if both +:only:+ and +:except:+ are given
|
|
247
|
+
def memoize_all(except: [], only: [], include_protected: false, include_private: false, **options)
|
|
248
|
+
raise ArgumentError, "cannot specify both :only and :except" if only.any? && except.any?
|
|
249
|
+
|
|
250
|
+
excluded = Array(except).map(&:to_sym)
|
|
251
|
+
included = Array(only).map(&:to_sym)
|
|
252
|
+
|
|
253
|
+
methods = public_instance_methods(false)
|
|
254
|
+
methods |= protected_instance_methods(false) if include_protected
|
|
255
|
+
methods |= private_instance_methods(false) if include_private
|
|
256
|
+
|
|
257
|
+
methods.each do |method_name|
|
|
258
|
+
next if excluded.include?(method_name)
|
|
259
|
+
next if included.any? && !included.include?(method_name)
|
|
260
|
+
|
|
261
|
+
memoize(method_name, **options)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Clears one or all entries from the class-level shared cache.
|
|
266
|
+
#
|
|
267
|
+
# With no positional args after +method_name+, clears *all* shared entries for
|
|
268
|
+
# that method. With args/kwargs, clears only the matching entry.
|
|
269
|
+
#
|
|
270
|
+
# @param method_name [Symbol, String] the memoized method
|
|
271
|
+
# @param args [Array] positional arguments identifying the entry to clear
|
|
272
|
+
# @param kwargs [Hash] keyword arguments identifying the entry to clear
|
|
273
|
+
# @return [void]
|
|
199
274
|
def reset_shared_memo(method_name, *args, **kwargs)
|
|
200
275
|
method_name = method_name.to_sym
|
|
201
276
|
specific_key = (args.empty? && kwargs.empty?) ? nil : [method_name, args, kwargs]
|
|
@@ -211,6 +286,8 @@ module SafeMemoize
|
|
|
211
286
|
end
|
|
212
287
|
end
|
|
213
288
|
|
|
289
|
+
# Clears the entire class-level shared cache for this class.
|
|
290
|
+
# @return [void]
|
|
214
291
|
def reset_all_shared_memos
|
|
215
292
|
__safe_memo_shared_mutex__.synchronize do
|
|
216
293
|
@__safe_memo_shared_cache__ = {}
|
|
@@ -218,6 +295,12 @@ module SafeMemoize
|
|
|
218
295
|
end
|
|
219
296
|
end
|
|
220
297
|
|
|
298
|
+
# Returns +true+ if a live shared cache entry exists for the given call signature.
|
|
299
|
+
#
|
|
300
|
+
# @param method_name [Symbol, String]
|
|
301
|
+
# @param args [Array] positional arguments
|
|
302
|
+
# @param kwargs [Hash] keyword arguments
|
|
303
|
+
# @return [Boolean]
|
|
221
304
|
def shared_memoized?(method_name, *args, **kwargs)
|
|
222
305
|
method_name = method_name.to_sym
|
|
223
306
|
cache_key = [method_name, args, kwargs]
|
|
@@ -233,6 +316,11 @@ module SafeMemoize
|
|
|
233
316
|
end
|
|
234
317
|
end
|
|
235
318
|
|
|
319
|
+
# Returns the number of live entries in the class-level shared cache.
|
|
320
|
+
#
|
|
321
|
+
# @param method_name [Symbol, String, nil] when given, counts only entries for
|
|
322
|
+
# that method; when +nil+, counts all methods.
|
|
323
|
+
# @return [Integer]
|
|
236
324
|
def shared_memo_count(method_name = nil)
|
|
237
325
|
__safe_memo_shared_mutex__.synchronize do
|
|
238
326
|
cache = @__safe_memo_shared_cache__ || {}
|
|
@@ -242,6 +330,13 @@ module SafeMemoize
|
|
|
242
330
|
end
|
|
243
331
|
end
|
|
244
332
|
|
|
333
|
+
# Returns how many seconds ago the shared entry was cached, or +nil+ if not cached
|
|
334
|
+
# or already expired.
|
|
335
|
+
#
|
|
336
|
+
# @param method_name [Symbol, String]
|
|
337
|
+
# @param args [Array]
|
|
338
|
+
# @param kwargs [Hash]
|
|
339
|
+
# @return [Float, nil]
|
|
245
340
|
def shared_memo_age(method_name, *args, **kwargs)
|
|
246
341
|
method_name = method_name.to_sym
|
|
247
342
|
cache_key = [method_name, args, kwargs]
|
|
@@ -263,6 +358,12 @@ module SafeMemoize
|
|
|
263
358
|
end
|
|
264
359
|
end
|
|
265
360
|
|
|
361
|
+
# Returns +true+ if the shared entry exists but its TTL has elapsed.
|
|
362
|
+
#
|
|
363
|
+
# @param method_name [Symbol, String]
|
|
364
|
+
# @param args [Array]
|
|
365
|
+
# @param kwargs [Hash]
|
|
366
|
+
# @return [Boolean]
|
|
266
367
|
def shared_memo_stale?(method_name, *args, **kwargs)
|
|
267
368
|
method_name = method_name.to_sym
|
|
268
369
|
cache_key = [method_name, args, kwargs]
|
|
@@ -281,24 +382,6 @@ module SafeMemoize
|
|
|
281
382
|
end
|
|
282
383
|
end
|
|
283
384
|
|
|
284
|
-
def memoize_all(except: [], only: [], include_protected: false, include_private: false, **options)
|
|
285
|
-
raise ArgumentError, "cannot specify both :only and :except" if only.any? && except.any?
|
|
286
|
-
|
|
287
|
-
excluded = Array(except).map(&:to_sym)
|
|
288
|
-
included = Array(only).map(&:to_sym)
|
|
289
|
-
|
|
290
|
-
methods = public_instance_methods(false)
|
|
291
|
-
methods |= protected_instance_methods(false) if include_protected
|
|
292
|
-
methods |= private_instance_methods(false) if include_private
|
|
293
|
-
|
|
294
|
-
methods.each do |method_name|
|
|
295
|
-
next if excluded.include?(method_name)
|
|
296
|
-
next if included.any? && !included.include?(method_name)
|
|
297
|
-
|
|
298
|
-
memoize(method_name, **options)
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
385
|
private
|
|
303
386
|
|
|
304
387
|
def __safe_memo_shared_cache__
|
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# Global configuration for SafeMemoize.
|
|
5
|
+
#
|
|
6
|
+
# Obtain an instance via {SafeMemoize.configure} or {SafeMemoize.configuration}.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# SafeMemoize.configure do |c|
|
|
10
|
+
# c.default_ttl = 300
|
|
11
|
+
# c.default_max_size = 100
|
|
12
|
+
# c.on_hook_error = ->(err, type, key) { Bugsnag.notify(err) }
|
|
13
|
+
# end
|
|
4
14
|
class Configuration
|
|
5
|
-
|
|
6
|
-
|
|
15
|
+
# @return [Numeric, nil] Default TTL (seconds) applied to every {ClassMethods#memoize}
|
|
16
|
+
# call that does not specify its own +ttl:+. +nil+ means no expiry.
|
|
17
|
+
attr_accessor :default_ttl
|
|
7
18
|
|
|
19
|
+
# @return [Integer, nil] Default LRU size cap applied to every {ClassMethods#memoize}
|
|
20
|
+
# call that does not specify its own +max_size:+. +nil+ means unlimited.
|
|
21
|
+
attr_accessor :default_max_size
|
|
22
|
+
|
|
23
|
+
# @return [Proc, nil] Custom handler for deprecation warnings.
|
|
24
|
+
# Receives a single +String+ message. When +nil+, warnings are written to +$stderr+.
|
|
25
|
+
attr_accessor :on_deprecation
|
|
26
|
+
|
|
27
|
+
# @return [Proc, nil] Custom handler for errors raised inside lifecycle hooks.
|
|
28
|
+
# Receives +(Exception, Symbol hook_type, cache_key)+. When +nil+, a warning is
|
|
29
|
+
# written to +$stderr+ and the error is swallowed.
|
|
30
|
+
attr_accessor :on_hook_error
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] When +true+, SafeMemoize emits +ActiveSupport::Notifications+
|
|
33
|
+
# events for cache hits, misses, stores, evictions, and expirations.
|
|
34
|
+
# Requires +activesupport+ to be loaded; has zero overhead when it is not.
|
|
35
|
+
attr_accessor :active_support_notifications
|
|
36
|
+
|
|
37
|
+
# @return [Object, nil] Any StatsD-compatible client (responds to +#increment+).
|
|
38
|
+
# When set, {Adapters::StatsD} routes lifecycle events to this client.
|
|
39
|
+
attr_accessor :statsd_client
|
|
40
|
+
|
|
41
|
+
# @return [Object, nil] An OpenTelemetry tracer (responds to +#in_span+).
|
|
42
|
+
# When set, {Adapters::OpenTelemetry} wraps each cache-miss computation in a span.
|
|
43
|
+
attr_accessor :opentelemetry_tracer
|
|
44
|
+
|
|
45
|
+
# @api private
|
|
8
46
|
def initialize
|
|
9
47
|
@default_ttl = nil
|
|
10
48
|
@default_max_size = nil
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# @api private
|
|
4
5
|
module InspectionMethods
|
|
5
6
|
private
|
|
6
7
|
|
|
@@ -14,7 +15,7 @@ module SafeMemoize
|
|
|
14
15
|
if args.empty? && kwargs.empty?
|
|
15
16
|
->(key) { key[0] == method_name }
|
|
16
17
|
else
|
|
17
|
-
cache_key =
|
|
18
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
18
19
|
->(key) { key == cache_key }
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# Instance-level custom cache key registration.
|
|
4
5
|
module PublicCustomKeyMethods
|
|
6
|
+
# Registers a per-instance custom key generator for a memoized method.
|
|
7
|
+
#
|
|
8
|
+
# The block receives the same arguments as the method and should return a
|
|
9
|
+
# single value used as the cache key. Two calls that produce the same key
|
|
10
|
+
# value share one cached result, regardless of their raw arguments.
|
|
11
|
+
#
|
|
12
|
+
# Instance-level keys take priority over the class-level +key:+ option set
|
|
13
|
+
# in {ClassMethods#memoize}.
|
|
14
|
+
#
|
|
15
|
+
# @param method_name [Symbol, String]
|
|
16
|
+
# @yield [*args, **kwargs] called with the method's arguments on each invocation
|
|
17
|
+
# @yieldreturn [Object] the key value (must be comparable with +==+)
|
|
18
|
+
# @return [void]
|
|
19
|
+
# @raise [ArgumentError] if no block is given
|
|
20
|
+
#
|
|
21
|
+
# @example Collapse all option hashes that share the same user ID
|
|
22
|
+
# obj.memoize_with_custom_key(:fetch) { |user_id, _options| user_id }
|
|
5
23
|
def memoize_with_custom_key(method_name, &key_generator)
|
|
6
24
|
raise ArgumentError, "block required for key generation" unless key_generator
|
|
7
25
|
|
|
8
26
|
register_custom_key(method_name, &key_generator)
|
|
9
27
|
end
|
|
10
28
|
|
|
29
|
+
# Removes the custom key generator for one method, or all generators.
|
|
30
|
+
#
|
|
31
|
+
# @param method_name [Symbol, String, nil] when given, removes only that method's
|
|
32
|
+
# generator; when +nil+, removes all generators on this instance
|
|
33
|
+
# @return [void]
|
|
11
34
|
def clear_custom_keys(method_name = nil)
|
|
12
35
|
if method_name
|
|
13
36
|
with_memo_lock do
|
|
@@ -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
|