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.
@@ -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
- attr_accessor :default_ttl, :default_max_size, :on_deprecation, :on_hook_error,
6
- :active_support_notifications, :statsd_client, :opentelemetry_tracer
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 CustomKeyMethods
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafeMemoize
4
+ # @api private
4
5
  module HooksMethods
5
6
  NOTIFICATION_EVENT_NAMES = {
6
7
  on_hit: "cache_hit.safe_memoize",
@@ -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 = safe_memo_cache_key(method_name, args, kwargs)
18
+ cache_key = compute_cache_key(method_name, args, kwargs)
18
19
  ->(key) { key == cache_key }
19
20
  end
20
21
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafeMemoize
4
+ # @api private
4
5
  module InstanceMethods
5
6
  include PublicMethods
6
7
  include CacheStoreMethods
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafeMemoize
4
+ # @api private
4
5
  module LruMethods
5
6
  private
6
7
 
@@ -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 = safe_memo_cache_key(method_name, args, kwargs)
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 = safe_memo_cache_key(method_name, args, kwargs)
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 = safe_memo_cache_key(method_name, args, kwargs)
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 = safe_memo_cache_key(method_name, args, kwargs)
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 = safe_memo_cache_key(method_name, args, kwargs)
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
@@ -13,6 +13,8 @@ module SafeMemoize
13
13
  @app = app
14
14
  end
15
15
 
16
+ # @param env [Hash] Rack environment
17
+ # @return [Array] Rack response triplet
16
18
  def call(env)
17
19
  @app.call(env)
18
20
  ensure