safe_memoize 1.0.0 → 1.2.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/.github/workflows/ci.yml +2 -2
- data/CHANGELOG.md +42 -1
- data/README.md +288 -6
- data/ROADMAP.md +0 -42
- data/Rakefile +13 -1
- data/lib/safe_memoize/adapters/concurrent_ruby.rb +98 -0
- data/lib/safe_memoize/class_methods.rb +260 -1
- data/lib/safe_memoize/configuration.rb +7 -0
- data/lib/safe_memoize/fiber_local_methods.rb +108 -0
- data/lib/safe_memoize/hooks_methods.rb +6 -1
- data/lib/safe_memoize/instance_methods.rb +1 -0
- data/lib/safe_memoize/ractor_shared_methods.rb +146 -0
- data/lib/safe_memoize/release_tooling.rb +25 -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 +1 -1
- data/lib/safe_memoize.rb +5 -0
- data/sig/safe_memoize.rbs +74 -2
- metadata +8 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeMemoize
|
|
4
|
+
module Stores
|
|
5
|
+
# Abstract base class for SafeMemoize cache store adapters.
|
|
6
|
+
#
|
|
7
|
+
# Subclass this and implement all abstract methods to plug in a custom backend
|
|
8
|
+
# (Redis, Memcached, Rails.cache, etc.). The {Stores::Memory} class is the
|
|
9
|
+
# built-in reference implementation.
|
|
10
|
+
#
|
|
11
|
+
# @abstract
|
|
12
|
+
#
|
|
13
|
+
# @example Minimal inline implementation
|
|
14
|
+
# class MyStore < SafeMemoize::Stores::Base
|
|
15
|
+
# def initialize = (@h = {})
|
|
16
|
+
# def read(key) = @h.fetch(key, MISS)
|
|
17
|
+
# def write(key, value, expires_in: nil) = (@h[key] = value)
|
|
18
|
+
# def delete(key) = @h.delete(key)
|
|
19
|
+
# def clear = @h.clear
|
|
20
|
+
# def keys = @h.keys
|
|
21
|
+
# end
|
|
22
|
+
class Base
|
|
23
|
+
# Sentinel returned by {#read} to signal a cache miss.
|
|
24
|
+
#
|
|
25
|
+
# Distinct from +nil+ and +false+, which are valid cached values.
|
|
26
|
+
MISS = Object.new.freeze
|
|
27
|
+
|
|
28
|
+
# Read a value from the store.
|
|
29
|
+
#
|
|
30
|
+
# @param key [Object] cache key
|
|
31
|
+
# @return [Object] the stored value, or {MISS} if absent or expired
|
|
32
|
+
# @abstract
|
|
33
|
+
def read(key)
|
|
34
|
+
raise NotImplementedError, "#{self.class}#read must be implemented"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Write a value to the store.
|
|
38
|
+
#
|
|
39
|
+
# @param key [Object] cache key
|
|
40
|
+
# @param value [Object] value to cache (may be +nil+ or +false+)
|
|
41
|
+
# @param expires_in [Numeric, nil] seconds until expiry; +nil+ means no expiry
|
|
42
|
+
# @return [void]
|
|
43
|
+
# @abstract
|
|
44
|
+
def write(key, value, expires_in: nil)
|
|
45
|
+
raise NotImplementedError, "#{self.class}#write must be implemented"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Delete a single entry.
|
|
49
|
+
#
|
|
50
|
+
# @param key [Object] cache key
|
|
51
|
+
# @return [void]
|
|
52
|
+
# @abstract
|
|
53
|
+
def delete(key)
|
|
54
|
+
raise NotImplementedError, "#{self.class}#delete must be implemented"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Remove all entries from the store.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @abstract
|
|
61
|
+
def clear
|
|
62
|
+
raise NotImplementedError, "#{self.class}#clear must be implemented"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Return all live (non-expired) keys.
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<Object>]
|
|
68
|
+
# @abstract
|
|
69
|
+
def keys
|
|
70
|
+
raise NotImplementedError, "#{self.class}#keys must be implemented"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check whether a live entry exists for the given key.
|
|
74
|
+
#
|
|
75
|
+
# The default delegates to {#read}; subclasses may override for stores
|
|
76
|
+
# with a native existence check.
|
|
77
|
+
#
|
|
78
|
+
# @param key [Object]
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def exist?(key)
|
|
81
|
+
read(key) != MISS
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeMemoize
|
|
4
|
+
module Stores
|
|
5
|
+
# Default in-process cache store backed by a plain +Hash+.
|
|
6
|
+
#
|
|
7
|
+
# Thread-safe via an internal +Mutex+. Supports per-entry TTL with lazy
|
|
8
|
+
# expiry: stale entries are not proactively removed but are treated as
|
|
9
|
+
# misses on read and excluded from {#keys}.
|
|
10
|
+
class Memory < Base
|
|
11
|
+
def initialize
|
|
12
|
+
@data = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param key [Object]
|
|
17
|
+
# @return [Object] stored value, or {MISS} if absent or expired
|
|
18
|
+
def read(key)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
entry = @data[key]
|
|
21
|
+
return MISS unless entry
|
|
22
|
+
return MISS if expired?(entry)
|
|
23
|
+
|
|
24
|
+
entry[:value]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param key [Object]
|
|
29
|
+
# @param value [Object]
|
|
30
|
+
# @param expires_in [Numeric, nil] seconds until expiry
|
|
31
|
+
# @return [void]
|
|
32
|
+
def write(key, value, expires_in: nil)
|
|
33
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
34
|
+
expires_at = expires_in ? now + expires_in.to_f : nil
|
|
35
|
+
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@data[key] = {value: value, expires_at: expires_at, cached_at: now}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param key [Object]
|
|
42
|
+
# @return [void]
|
|
43
|
+
def delete(key)
|
|
44
|
+
@mutex.synchronize { @data.delete(key) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Removes all entries.
|
|
48
|
+
# @return [void]
|
|
49
|
+
def clear
|
|
50
|
+
@mutex.synchronize { @data.clear }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns all live (non-expired) keys.
|
|
54
|
+
# @return [Array<Object>]
|
|
55
|
+
def keys
|
|
56
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
57
|
+
@mutex.synchronize do
|
|
58
|
+
@data.filter_map { |k, entry| k unless entry[:expires_at] && entry[:expires_at] <= now }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def expired?(entry)
|
|
65
|
+
expires_at = entry[:expires_at]
|
|
66
|
+
expires_at && expires_at <= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "safe_memoize"
|
|
4
|
+
|
|
5
|
+
module SafeMemoize
|
|
6
|
+
module Stores
|
|
7
|
+
# Cache store adapter backed by any +ActiveSupport::Cache::Store+.
|
|
8
|
+
#
|
|
9
|
+
# Not auto-required. Add to your Rails initializer:
|
|
10
|
+
# require "safe_memoize/stores/rails_cache"
|
|
11
|
+
#
|
|
12
|
+
# Compatible with any +ActiveSupport::Cache::Store+ implementation
|
|
13
|
+
# (+MemoryStore+, +FileStore+, +MemCacheStore+, +RedisCacheStore+, etc.)
|
|
14
|
+
# and with +Rails.cache+ directly.
|
|
15
|
+
#
|
|
16
|
+
# Because +ActiveSupport::Cache+ returns +nil+ for both a cache miss and
|
|
17
|
+
# a cached +nil+ value, this adapter wraps every value in a two-element
|
|
18
|
+
# sentinel envelope before writing. The envelope is transparent to callers.
|
|
19
|
+
#
|
|
20
|
+
# TTL is forwarded as +expires_in:+ to the cache, so the underlying store
|
|
21
|
+
# manages expiry natively — there is no lazy-expiry overhead on read.
|
|
22
|
+
#
|
|
23
|
+
# {#clear} uses +delete_matched+ scoped to the adapter's namespace, so it
|
|
24
|
+
# never clears entries belonging to other parts of the application. The
|
|
25
|
+
# backend must respond to +delete_matched+ (all standard Rails cache stores
|
|
26
|
+
# do); a +NotImplementedError+ is raised if it does not.
|
|
27
|
+
#
|
|
28
|
+
# {#keys} returns an empty array — +ActiveSupport::Cache::Store+ does not
|
|
29
|
+
# expose a standard key enumeration API. Override the method if your
|
|
30
|
+
# backend supports it.
|
|
31
|
+
#
|
|
32
|
+
# @example Basic setup
|
|
33
|
+
# # config/initializers/safe_memoize.rb
|
|
34
|
+
# require "safe_memoize/stores/rails_cache"
|
|
35
|
+
#
|
|
36
|
+
# MEMO_STORE = SafeMemoize::Stores::RailsCache.new(Rails.cache)
|
|
37
|
+
#
|
|
38
|
+
# class MyService
|
|
39
|
+
# prepend SafeMemoize
|
|
40
|
+
# def fetch(id) = http_get(id)
|
|
41
|
+
# memoize :fetch, store: MEMO_STORE, ttl: 300
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @example Dedicated cache store (recommended for production)
|
|
45
|
+
# MEMO_STORE = SafeMemoize::Stores::RailsCache.new(
|
|
46
|
+
# ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"]),
|
|
47
|
+
# namespace: "myapp:memo"
|
|
48
|
+
# )
|
|
49
|
+
class RailsCache < Base
|
|
50
|
+
# Tag prepended to every stored value so cached +nil+/+false+ are
|
|
51
|
+
# distinguishable from a cache miss.
|
|
52
|
+
VALUE_TAG = "safe_memoize:v1"
|
|
53
|
+
|
|
54
|
+
# @param cache [ActiveSupport::Cache::Store] the cache store to use;
|
|
55
|
+
# typically +Rails.cache+ or a dedicated store instance
|
|
56
|
+
# @param namespace [String] key prefix used to scope all entries;
|
|
57
|
+
# defaults to +"safe_memoize"+
|
|
58
|
+
def initialize(cache, namespace: "safe_memoize")
|
|
59
|
+
@cache = cache
|
|
60
|
+
@namespace = namespace
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param key [Object] cache key (serialized with Marshal + Base64)
|
|
64
|
+
# @return [Object] the stored value, or {MISS} if absent or unrecognised
|
|
65
|
+
def read(key)
|
|
66
|
+
raw = @cache.read(cache_key(key))
|
|
67
|
+
return MISS unless raw.is_a?(Array) && raw.length == 2 && raw[0] == VALUE_TAG
|
|
68
|
+
|
|
69
|
+
raw[1]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param key [Object] cache key
|
|
73
|
+
# @param value [Object] value to store (may be +nil+ or +false+)
|
|
74
|
+
# @param expires_in [Numeric, nil] TTL in seconds forwarded to the cache
|
|
75
|
+
# as +expires_in:+; +nil+ means no expiry
|
|
76
|
+
# @return [void]
|
|
77
|
+
def write(key, value, expires_in: nil)
|
|
78
|
+
opts = expires_in ? {expires_in: expires_in} : {}
|
|
79
|
+
@cache.write(cache_key(key), [VALUE_TAG, value], **opts)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param key [Object]
|
|
83
|
+
# @return [void]
|
|
84
|
+
def delete(key)
|
|
85
|
+
@cache.delete(cache_key(key))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Removes all entries written by this adapter (scoped to the namespace).
|
|
89
|
+
#
|
|
90
|
+
# Delegates to +delete_matched+ on the underlying store; raises
|
|
91
|
+
# +NotImplementedError+ if the store does not support it.
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
94
|
+
# @raise [NotImplementedError] if the backing store does not respond to
|
|
95
|
+
# +delete_matched+
|
|
96
|
+
def clear
|
|
97
|
+
unless @cache.respond_to?(:delete_matched)
|
|
98
|
+
raise NotImplementedError,
|
|
99
|
+
"#{@cache.class} does not support delete_matched — " \
|
|
100
|
+
"implement clear manually or use a store that supports it (e.g. MemoryStore, RedisCacheStore)"
|
|
101
|
+
end
|
|
102
|
+
@cache.delete_matched(/\A#{Regexp.escape(@namespace)}:/)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns an empty array.
|
|
106
|
+
#
|
|
107
|
+
# +ActiveSupport::Cache::Store+ does not expose a key enumeration API.
|
|
108
|
+
# Override this method if your backend supports key listing.
|
|
109
|
+
#
|
|
110
|
+
# @return [Array]
|
|
111
|
+
def keys
|
|
112
|
+
[]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param key [Object]
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def exist?(key)
|
|
118
|
+
@cache.exist?(cache_key(key))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def cache_key(key)
|
|
124
|
+
"#{@namespace}:#{[Marshal.dump(key)].pack("m0")}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "safe_memoize"
|
|
4
|
+
|
|
5
|
+
module SafeMemoize
|
|
6
|
+
module Stores
|
|
7
|
+
# Cache store adapter backed by Redis.
|
|
8
|
+
#
|
|
9
|
+
# Not auto-required. Add to your application:
|
|
10
|
+
# require "safe_memoize/stores/redis"
|
|
11
|
+
#
|
|
12
|
+
# Requires a Redis-compatible client that responds to +#get+, +#set+,
|
|
13
|
+
# +#del+, and +#scan_each+. Compatible with the +redis+ gem (v4+) and
|
|
14
|
+
# any drop-in replacement.
|
|
15
|
+
#
|
|
16
|
+
# Values and keys are serialized with +Marshal+ (Base64-encoded via
|
|
17
|
+
# +Array#pack("m0")+) so that any Ruby object, including +nil+ and
|
|
18
|
+
# +false+, can be stored and retrieved faithfully. TTL is forwarded to
|
|
19
|
+
# Redis as the +PX+ option (milliseconds, rounded up to the nearest
|
|
20
|
+
# millisecond; minimum 1 ms) to preserve sub-second precision.
|
|
21
|
+
#
|
|
22
|
+
# @example Basic setup
|
|
23
|
+
# require "redis"
|
|
24
|
+
# require "safe_memoize/stores/redis"
|
|
25
|
+
#
|
|
26
|
+
# REDIS_STORE = SafeMemoize::Stores::Redis.new(::Redis.new)
|
|
27
|
+
#
|
|
28
|
+
# class MyService
|
|
29
|
+
# prepend SafeMemoize
|
|
30
|
+
# def fetch(id) = http_get(id)
|
|
31
|
+
# memoize :fetch, store: REDIS_STORE, ttl: 300
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example With a custom namespace
|
|
35
|
+
# STORE = SafeMemoize::Stores::Redis.new(::Redis.new, namespace: "myapp:memo")
|
|
36
|
+
class Redis < Base
|
|
37
|
+
# @param client [Object] a Redis-compatible client responding to
|
|
38
|
+
# +#get+, +#set+, +#del+, and +#scan_each+
|
|
39
|
+
# @param namespace [String] key prefix used to scope all entries in Redis;
|
|
40
|
+
# defaults to +"safe_memoize"+
|
|
41
|
+
def initialize(client, namespace: "safe_memoize")
|
|
42
|
+
@client = client
|
|
43
|
+
@namespace = namespace
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param key [Object] cache key (serialized with Marshal + Base64)
|
|
47
|
+
# @return [Object] the stored value, or {MISS} if absent
|
|
48
|
+
def read(key)
|
|
49
|
+
raw = @client.get(redis_key(key))
|
|
50
|
+
return MISS if raw.nil?
|
|
51
|
+
|
|
52
|
+
Marshal.load(raw) # rubocop:disable Security/MarshalLoad
|
|
53
|
+
rescue TypeError, ArgumentError
|
|
54
|
+
MISS
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param key [Object] cache key
|
|
58
|
+
# @param value [Object] value to store (may be +nil+ or +false+)
|
|
59
|
+
# @param expires_in [Numeric, nil] TTL in seconds forwarded to Redis as +PX+
|
|
60
|
+
# (milliseconds, rounded up; minimum 1 ms); +nil+ means no expiry
|
|
61
|
+
# @return [void]
|
|
62
|
+
def write(key, value, expires_in: nil)
|
|
63
|
+
raw = Marshal.dump(value)
|
|
64
|
+
if expires_in
|
|
65
|
+
@client.set(redis_key(key), raw, px: [(expires_in * 1000).ceil, 1].max)
|
|
66
|
+
else
|
|
67
|
+
@client.set(redis_key(key), raw)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param key [Object]
|
|
72
|
+
# @return [void]
|
|
73
|
+
def delete(key)
|
|
74
|
+
@client.del(redis_key(key))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Removes all entries written by this adapter (scoped to the namespace).
|
|
78
|
+
# Uses +SCAN+ internally to avoid blocking Redis.
|
|
79
|
+
# @return [void]
|
|
80
|
+
def clear
|
|
81
|
+
to_delete = []
|
|
82
|
+
@client.scan_each(match: "#{@namespace}:*") { |k| to_delete << k }
|
|
83
|
+
@client.del(*to_delete) unless to_delete.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns all live keys in the namespace, deserialized back to their
|
|
87
|
+
# original Ruby form. Entries that cannot be deserialized are silently
|
|
88
|
+
# skipped. Because Redis handles TTL natively, every key returned by
|
|
89
|
+
# +SCAN+ is live.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Object>]
|
|
92
|
+
def keys
|
|
93
|
+
prefix = "#{@namespace}:"
|
|
94
|
+
result = []
|
|
95
|
+
@client.scan_each(match: "#{@namespace}:*") do |rk|
|
|
96
|
+
encoded = rk.delete_prefix(prefix)
|
|
97
|
+
result << Marshal.load(encoded.unpack1("m0")) # rubocop:disable Security/MarshalLoad
|
|
98
|
+
rescue ArgumentError, TypeError
|
|
99
|
+
# skip keys that cannot be deserialized (e.g. written by another serializer)
|
|
100
|
+
end
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def redis_key(key)
|
|
107
|
+
"#{@namespace}:#{[Marshal.dump(key)].pack("m0")}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/safe_memoize/version.rb
CHANGED
data/lib/safe_memoize.rb
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "safe_memoize/version"
|
|
4
4
|
require_relative "safe_memoize/configuration"
|
|
5
|
+
require_relative "safe_memoize/stores/base"
|
|
6
|
+
require_relative "safe_memoize/stores/memory"
|
|
5
7
|
require_relative "safe_memoize/adapters/statsd"
|
|
6
8
|
require_relative "safe_memoize/adapters/opentelemetry"
|
|
9
|
+
require_relative "safe_memoize/adapters/concurrent_ruby"
|
|
7
10
|
require_relative "safe_memoize/class_methods"
|
|
8
11
|
require_relative "safe_memoize/public_methods"
|
|
9
12
|
require_relative "safe_memoize/cache_store_methods"
|
|
@@ -15,6 +18,8 @@ require_relative "safe_memoize/public_metrics_methods"
|
|
|
15
18
|
require_relative "safe_memoize/custom_key_methods"
|
|
16
19
|
require_relative "safe_memoize/public_custom_key_methods"
|
|
17
20
|
require_relative "safe_memoize/lru_methods"
|
|
21
|
+
require_relative "safe_memoize/fiber_local_methods"
|
|
22
|
+
require_relative "safe_memoize/ractor_shared_methods"
|
|
18
23
|
require_relative "safe_memoize/instance_methods"
|
|
19
24
|
|
|
20
25
|
# Thread-safe memoization for Ruby that correctly handles +nil+ and +false+ values.
|
data/sig/safe_memoize.rbs
CHANGED
|
@@ -32,13 +32,16 @@ module SafeMemoize
|
|
|
32
32
|
attr_accessor active_support_notifications: bool
|
|
33
33
|
attr_accessor statsd_client: untyped
|
|
34
34
|
attr_accessor opentelemetry_tracer: untyped
|
|
35
|
+
attr_accessor default_store: Stores::Base?
|
|
35
36
|
|
|
36
37
|
def initialize: () -> void
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
module ClassMethods
|
|
40
|
-
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?) -> void
|
|
41
|
-
def
|
|
41
|
+
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool) -> void
|
|
42
|
+
def safe_memoize_store: () -> Stores::Base?
|
|
43
|
+
def safe_memoize_store=: (Stores::Base?) -> Stores::Base?
|
|
44
|
+
def memoize_all: (?except: Array[Symbol | String], ?only: Array[Symbol | String], ?include_protected: bool, ?include_private: bool, ?ttl: Numeric?, ?max_size: Integer?, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?fiber_local: bool) -> void
|
|
42
45
|
def reset_shared_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
43
46
|
def reset_all_shared_memos: () -> void
|
|
44
47
|
def shared_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
@@ -186,6 +189,37 @@ module SafeMemoize
|
|
|
186
189
|
def lru_clear_all: () -> void
|
|
187
190
|
end
|
|
188
191
|
|
|
192
|
+
module FiberLocalMethods
|
|
193
|
+
FIBER_STORE_KEY: Symbol
|
|
194
|
+
|
|
195
|
+
def fiber_local_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) ?{ () -> untyped } -> bool
|
|
196
|
+
def reset_fiber_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
197
|
+
def reset_all_fiber_memos: () -> void
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def fiber_root_store!: () -> Hash[untyped, untyped]
|
|
202
|
+
def fiber_memo_store!: () -> { cache: Hash[memo_key, memo_record], lru: Hash[Symbol, Hash[memo_key, true]] }
|
|
203
|
+
def fiber_memo_cache!: () -> Hash[memo_key, memo_record]
|
|
204
|
+
def fiber_memo_lru!: () -> Hash[Symbol, Hash[memo_key, true]]
|
|
205
|
+
def fiber_root_store_or_nil: () -> Hash[untyped, untyped]?
|
|
206
|
+
def fiber_memo_store_or_nil: () -> { cache: Hash[memo_key, memo_record], lru: Hash[Symbol, Hash[memo_key, true]] }?
|
|
207
|
+
def fiber_memo_cache_or_nil: () -> Hash[memo_key, memo_record]?
|
|
208
|
+
def fiber_memo_lru_or_nil: () -> Hash[Symbol, Hash[memo_key, true]]?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
module RactorSharedMethods
|
|
212
|
+
def reset_ractor_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
213
|
+
def reset_all_ractor_memos: () -> void
|
|
214
|
+
def ractor_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
215
|
+
def ractor_memo_count: (?(Symbol | String) method_name) -> Integer
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def __ractor_cache_send__: (untyped supervisor, Symbol op, *untyped args) -> untyped
|
|
220
|
+
def __safe_memo_ractor_supervisor__: () -> untyped
|
|
221
|
+
end
|
|
222
|
+
|
|
189
223
|
module InstanceMethods
|
|
190
224
|
include PublicMethods
|
|
191
225
|
include CacheStoreMethods
|
|
@@ -197,6 +231,33 @@ module SafeMemoize
|
|
|
197
231
|
include CustomKeyMethods
|
|
198
232
|
include PublicCustomKeyMethods
|
|
199
233
|
include LruMethods
|
|
234
|
+
include FiberLocalMethods
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
module Stores
|
|
238
|
+
class Base
|
|
239
|
+
MISS: Object
|
|
240
|
+
|
|
241
|
+
def read: (untyped key) -> untyped
|
|
242
|
+
def write: (untyped key, untyped value, ?expires_in: Numeric?) -> void
|
|
243
|
+
def delete: (untyped key) -> void
|
|
244
|
+
def clear: () -> void
|
|
245
|
+
def keys: () -> Array[untyped]
|
|
246
|
+
def exist?: (untyped key) -> bool
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
class Memory < Base
|
|
250
|
+
def initialize: () -> void
|
|
251
|
+
def read: (untyped key) -> untyped
|
|
252
|
+
def write: (untyped key, untyped value, ?expires_in: Numeric?) -> void
|
|
253
|
+
def delete: (untyped key) -> void
|
|
254
|
+
def clear: () -> void
|
|
255
|
+
def keys: () -> Array[untyped]
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def expired?: ({ expires_at: Float?, value: untyped, cached_at: Float }) -> bool
|
|
260
|
+
end
|
|
200
261
|
end
|
|
201
262
|
|
|
202
263
|
module Adapters
|
|
@@ -210,6 +271,17 @@ module SafeMemoize
|
|
|
210
271
|
SPAN_NAME: String
|
|
211
272
|
def self.trace: (untyped tracer, Symbol | String method_name, String? class_name) { () -> untyped } -> untyped
|
|
212
273
|
end
|
|
274
|
+
|
|
275
|
+
class ConcurrentRuby < Stores::Base
|
|
276
|
+
def initialize: () -> void
|
|
277
|
+
def read: (untyped key) -> untyped
|
|
278
|
+
def write: (untyped key, untyped value, ?expires_in: Numeric?) -> void
|
|
279
|
+
def delete: (untyped key) -> void
|
|
280
|
+
def clear: () -> void
|
|
281
|
+
def keys: () -> Array[untyped]
|
|
282
|
+
private
|
|
283
|
+
def expired?: ({ expires_at: Float?, value: untyped, cached_at: Float }) -> bool
|
|
284
|
+
end
|
|
213
285
|
end
|
|
214
286
|
|
|
215
287
|
module Rails
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe_memoize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- benchmarks/benchmark.rb
|
|
53
53
|
- codecov.yml
|
|
54
54
|
- lib/safe_memoize.rb
|
|
55
|
+
- lib/safe_memoize/adapters/concurrent_ruby.rb
|
|
55
56
|
- lib/safe_memoize/adapters/opentelemetry.rb
|
|
56
57
|
- lib/safe_memoize/adapters/statsd.rb
|
|
57
58
|
- lib/safe_memoize/cache_metrics_methods.rb
|
|
@@ -60,6 +61,7 @@ files:
|
|
|
60
61
|
- lib/safe_memoize/class_methods.rb
|
|
61
62
|
- lib/safe_memoize/configuration.rb
|
|
62
63
|
- lib/safe_memoize/custom_key_methods.rb
|
|
64
|
+
- lib/safe_memoize/fiber_local_methods.rb
|
|
63
65
|
- lib/safe_memoize/hooks_methods.rb
|
|
64
66
|
- lib/safe_memoize/inspection_methods.rb
|
|
65
67
|
- lib/safe_memoize/instance_methods.rb
|
|
@@ -67,10 +69,15 @@ files:
|
|
|
67
69
|
- lib/safe_memoize/public_custom_key_methods.rb
|
|
68
70
|
- lib/safe_memoize/public_methods.rb
|
|
69
71
|
- lib/safe_memoize/public_metrics_methods.rb
|
|
72
|
+
- lib/safe_memoize/ractor_shared_methods.rb
|
|
70
73
|
- lib/safe_memoize/rails.rb
|
|
71
74
|
- lib/safe_memoize/rails/middleware.rb
|
|
72
75
|
- lib/safe_memoize/rails/request_scoped.rb
|
|
73
76
|
- lib/safe_memoize/release_tooling.rb
|
|
77
|
+
- lib/safe_memoize/stores/base.rb
|
|
78
|
+
- lib/safe_memoize/stores/memory.rb
|
|
79
|
+
- lib/safe_memoize/stores/rails_cache.rb
|
|
80
|
+
- lib/safe_memoize/stores/redis.rb
|
|
74
81
|
- lib/safe_memoize/version.rb
|
|
75
82
|
- rbi/safe_memoize.rbi
|
|
76
83
|
- sig/safe_memoize.rbs
|