safe_memoize 0.9.0 → 1.1.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 +29 -0
- data/README.md +181 -2
- data/ROADMAP.md +5 -36
- data/Rakefile +14 -1
- 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 +173 -19
- data/lib/safe_memoize/configuration.rb +47 -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/stores/base.rb +85 -0
- data/lib/safe_memoize/stores/memory.rb +70 -0
- data/lib/safe_memoize/stores/rails_cache.rb +128 -0
- data/lib/safe_memoize/stores/redis.rb +111 -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 +43 -6
- metadata +8 -1
|
@@ -1,8 +1,52 @@
|
|
|
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
|
|
5
|
-
|
|
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
|
+
# @param store [Stores::Base, nil] custom cache store adapter. Must be a
|
|
29
|
+
# {Stores::Base} subclass instance. The store is shared across all instances of the
|
|
30
|
+
# class. When +nil+, the default per-instance in-process hash is used.
|
|
31
|
+
# Cannot be combined with +max_size:+ or +shared:+.
|
|
32
|
+
# @return [void]
|
|
33
|
+
# @raise [ArgumentError] if the method does not exist, or option values are invalid
|
|
34
|
+
#
|
|
35
|
+
# @example Zero-argument method
|
|
36
|
+
# def expensive_query = db.run("SELECT …")
|
|
37
|
+
# memoize :expensive_query
|
|
38
|
+
#
|
|
39
|
+
# @example With TTL and LRU cap
|
|
40
|
+
# def fetch(id) = http_get(id)
|
|
41
|
+
# memoize :fetch, ttl: 60, max_size: 500
|
|
42
|
+
#
|
|
43
|
+
# @example Conditional — only cache successful responses
|
|
44
|
+
# memoize :fetch, if: ->(v) { v[:status] == 200 }
|
|
45
|
+
#
|
|
46
|
+
# @example With a custom store
|
|
47
|
+
# STORE = SafeMemoize::Stores::Memory.new
|
|
48
|
+
# memoize :fetch, store: STORE, ttl: 300
|
|
49
|
+
def memoize(method_name, ttl: nil, max_size: nil, ttl_refresh: false, if: nil, unless: nil, shared: false, key: nil, store: nil)
|
|
6
50
|
method_name = method_name.to_sym
|
|
7
51
|
|
|
8
52
|
unless method_defined?(method_name) || private_method_defined?(method_name) || protected_method_defined?(method_name)
|
|
@@ -49,6 +93,26 @@ module SafeMemoize
|
|
|
49
93
|
raise ArgumentError, ":unless must be callable" if cond_unless && !cond_unless.respond_to?(:call)
|
|
50
94
|
raise ArgumentError, ":key must be callable" if key && !key.respond_to?(:call)
|
|
51
95
|
|
|
96
|
+
if store
|
|
97
|
+
raise ArgumentError, "store: must be a SafeMemoize::Stores::Base instance (got #{store.class})" unless store.is_a?(SafeMemoize::Stores::Base)
|
|
98
|
+
raise ArgumentError, "max_size: is not supported with store: — use the store adapter's own eviction" if max_size
|
|
99
|
+
raise ArgumentError, "shared: and store: cannot be combined" if shared
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Resolve effective store: explicit store: wins; global default applies when
|
|
103
|
+
# compatible (max_size: and shared: are incompatible — fall back silently).
|
|
104
|
+
effective_store = store
|
|
105
|
+
if effective_store.nil? && !max_size && !shared
|
|
106
|
+
global_default = SafeMemoize.configuration.default_store
|
|
107
|
+
if global_default
|
|
108
|
+
unless global_default.is_a?(SafeMemoize::Stores::Base)
|
|
109
|
+
raise ArgumentError,
|
|
110
|
+
"SafeMemoize.configuration.default_store must be a Stores::Base instance (got #{global_default.class})"
|
|
111
|
+
end
|
|
112
|
+
effective_store = global_default
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
52
116
|
__safe_memo_class_key_generators__[method_name] = key if key
|
|
53
117
|
|
|
54
118
|
# Normalize to a single "should cache?" predicate
|
|
@@ -58,6 +122,49 @@ module SafeMemoize
|
|
|
58
122
|
->(result) { !cond_unless.call(result) }
|
|
59
123
|
end
|
|
60
124
|
|
|
125
|
+
if effective_store
|
|
126
|
+
miss = SafeMemoize::Stores::Base::MISS
|
|
127
|
+
|
|
128
|
+
mod = Module.new do
|
|
129
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
130
|
+
return super(*args, **kwargs, &block) if block
|
|
131
|
+
|
|
132
|
+
cache_key = compute_cache_key(method_name, args, kwargs)
|
|
133
|
+
cached = effective_store.read(cache_key)
|
|
134
|
+
|
|
135
|
+
unless cached.equal?(miss)
|
|
136
|
+
effective_store.write(cache_key, cached, expires_in: ttl) if ttl_refresh
|
|
137
|
+
record_cache_hit(method_name, args, kwargs)
|
|
138
|
+
call_memo_hooks(:on_hit, cache_key, {value: cached, expires_at: nil, cached_at: nil})
|
|
139
|
+
return cached
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
143
|
+
value = Adapters::OpenTelemetry.trace(
|
|
144
|
+
SafeMemoize.configuration.opentelemetry_tracer, method_name, self.class.name
|
|
145
|
+
) { super(*args, **kwargs) }
|
|
146
|
+
elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
147
|
+
|
|
148
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
149
|
+
if !condition || condition.call(value)
|
|
150
|
+
effective_store.write(cache_key, value, expires_in: ttl)
|
|
151
|
+
call_memo_hooks(:on_store, cache_key, {value: value, expires_at: nil, cached_at: now})
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
record_cache_miss(method_name, args, kwargs, elapsed_time)
|
|
155
|
+
call_memo_hooks(:on_miss, cache_key, {value: value, expires_at: nil, cached_at: now})
|
|
156
|
+
|
|
157
|
+
value
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
send(visibility, method_name)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
prepend mod
|
|
164
|
+
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
61
168
|
if shared
|
|
62
169
|
klass = self
|
|
63
170
|
shared_mutex = klass.send(:__safe_memo_shared_mutex__)
|
|
@@ -196,6 +303,45 @@ module SafeMemoize
|
|
|
196
303
|
prepend mod
|
|
197
304
|
end
|
|
198
305
|
|
|
306
|
+
# Memoizes every eligible public instance method defined directly on the class.
|
|
307
|
+
#
|
|
308
|
+
# Accepts all options that {#memoize} accepts, plus +:except:+ and +:only:+.
|
|
309
|
+
# Raises +ArgumentError+ when both +:only:+ and +:except:+ are given.
|
|
310
|
+
#
|
|
311
|
+
# @param except [Array<Symbol, String>] method names to skip
|
|
312
|
+
# @param only [Array<Symbol, String>] when non-empty, only these methods are memoized
|
|
313
|
+
# @param include_protected [Boolean] also memoize +protected+ methods
|
|
314
|
+
# @param include_private [Boolean] also memoize +private+ methods
|
|
315
|
+
# @param options [Hash] any additional options forwarded to {#memoize}
|
|
316
|
+
# @return [void]
|
|
317
|
+
# @raise [ArgumentError] if both +:only:+ and +:except:+ are given
|
|
318
|
+
def memoize_all(except: [], only: [], include_protected: false, include_private: false, **options)
|
|
319
|
+
raise ArgumentError, "cannot specify both :only and :except" if only.any? && except.any?
|
|
320
|
+
|
|
321
|
+
excluded = Array(except).map(&:to_sym)
|
|
322
|
+
included = Array(only).map(&:to_sym)
|
|
323
|
+
|
|
324
|
+
methods = public_instance_methods(false)
|
|
325
|
+
methods |= protected_instance_methods(false) if include_protected
|
|
326
|
+
methods |= private_instance_methods(false) if include_private
|
|
327
|
+
|
|
328
|
+
methods.each do |method_name|
|
|
329
|
+
next if excluded.include?(method_name)
|
|
330
|
+
next if included.any? && !included.include?(method_name)
|
|
331
|
+
|
|
332
|
+
memoize(method_name, **options)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Clears one or all entries from the class-level shared cache.
|
|
337
|
+
#
|
|
338
|
+
# With no positional args after +method_name+, clears *all* shared entries for
|
|
339
|
+
# that method. With args/kwargs, clears only the matching entry.
|
|
340
|
+
#
|
|
341
|
+
# @param method_name [Symbol, String] the memoized method
|
|
342
|
+
# @param args [Array] positional arguments identifying the entry to clear
|
|
343
|
+
# @param kwargs [Hash] keyword arguments identifying the entry to clear
|
|
344
|
+
# @return [void]
|
|
199
345
|
def reset_shared_memo(method_name, *args, **kwargs)
|
|
200
346
|
method_name = method_name.to_sym
|
|
201
347
|
specific_key = (args.empty? && kwargs.empty?) ? nil : [method_name, args, kwargs]
|
|
@@ -211,6 +357,8 @@ module SafeMemoize
|
|
|
211
357
|
end
|
|
212
358
|
end
|
|
213
359
|
|
|
360
|
+
# Clears the entire class-level shared cache for this class.
|
|
361
|
+
# @return [void]
|
|
214
362
|
def reset_all_shared_memos
|
|
215
363
|
__safe_memo_shared_mutex__.synchronize do
|
|
216
364
|
@__safe_memo_shared_cache__ = {}
|
|
@@ -218,6 +366,12 @@ module SafeMemoize
|
|
|
218
366
|
end
|
|
219
367
|
end
|
|
220
368
|
|
|
369
|
+
# Returns +true+ if a live shared cache entry exists for the given call signature.
|
|
370
|
+
#
|
|
371
|
+
# @param method_name [Symbol, String]
|
|
372
|
+
# @param args [Array] positional arguments
|
|
373
|
+
# @param kwargs [Hash] keyword arguments
|
|
374
|
+
# @return [Boolean]
|
|
221
375
|
def shared_memoized?(method_name, *args, **kwargs)
|
|
222
376
|
method_name = method_name.to_sym
|
|
223
377
|
cache_key = [method_name, args, kwargs]
|
|
@@ -233,6 +387,11 @@ module SafeMemoize
|
|
|
233
387
|
end
|
|
234
388
|
end
|
|
235
389
|
|
|
390
|
+
# Returns the number of live entries in the class-level shared cache.
|
|
391
|
+
#
|
|
392
|
+
# @param method_name [Symbol, String, nil] when given, counts only entries for
|
|
393
|
+
# that method; when +nil+, counts all methods.
|
|
394
|
+
# @return [Integer]
|
|
236
395
|
def shared_memo_count(method_name = nil)
|
|
237
396
|
__safe_memo_shared_mutex__.synchronize do
|
|
238
397
|
cache = @__safe_memo_shared_cache__ || {}
|
|
@@ -242,6 +401,13 @@ module SafeMemoize
|
|
|
242
401
|
end
|
|
243
402
|
end
|
|
244
403
|
|
|
404
|
+
# Returns how many seconds ago the shared entry was cached, or +nil+ if not cached
|
|
405
|
+
# or already expired.
|
|
406
|
+
#
|
|
407
|
+
# @param method_name [Symbol, String]
|
|
408
|
+
# @param args [Array]
|
|
409
|
+
# @param kwargs [Hash]
|
|
410
|
+
# @return [Float, nil]
|
|
245
411
|
def shared_memo_age(method_name, *args, **kwargs)
|
|
246
412
|
method_name = method_name.to_sym
|
|
247
413
|
cache_key = [method_name, args, kwargs]
|
|
@@ -263,6 +429,12 @@ module SafeMemoize
|
|
|
263
429
|
end
|
|
264
430
|
end
|
|
265
431
|
|
|
432
|
+
# Returns +true+ if the shared entry exists but its TTL has elapsed.
|
|
433
|
+
#
|
|
434
|
+
# @param method_name [Symbol, String]
|
|
435
|
+
# @param args [Array]
|
|
436
|
+
# @param kwargs [Hash]
|
|
437
|
+
# @return [Boolean]
|
|
266
438
|
def shared_memo_stale?(method_name, *args, **kwargs)
|
|
267
439
|
method_name = method_name.to_sym
|
|
268
440
|
cache_key = [method_name, args, kwargs]
|
|
@@ -281,24 +453,6 @@ module SafeMemoize
|
|
|
281
453
|
end
|
|
282
454
|
end
|
|
283
455
|
|
|
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
456
|
private
|
|
303
457
|
|
|
304
458
|
def __safe_memo_shared_cache__
|
|
@@ -1,10 +1,54 @@
|
|
|
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
|
+
# @return [Stores::Base, nil] Default cache store applied to every {ClassMethods#memoize}
|
|
46
|
+
# call that does not specify its own +store:+. +nil+ uses the built-in per-instance
|
|
47
|
+
# hash cache. Methods using +max_size:+ or +shared:+ are incompatible with an external
|
|
48
|
+
# store and will silently continue using the per-instance hash even when this is set.
|
|
49
|
+
attr_accessor :default_store
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
8
52
|
def initialize
|
|
9
53
|
@default_ttl = nil
|
|
10
54
|
@default_max_size = nil
|
|
@@ -13,6 +57,7 @@ module SafeMemoize
|
|
|
13
57
|
@active_support_notifications = false
|
|
14
58
|
@statsd_client = nil
|
|
15
59
|
@opentelemetry_tracer = nil
|
|
60
|
+
@default_store = nil
|
|
16
61
|
end
|
|
17
62
|
end
|
|
18
63
|
end
|
|
@@ -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
|