redis-memo 0.0.0.beta.1 → 0.0.0.beta.6
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/lib/redis-memo.rb +4 -0
- data/lib/redis_memo.rb +51 -2
- data/lib/redis_memo/after_commit.rb +6 -1
- data/lib/redis_memo/cache.rb +18 -5
- data/lib/redis_memo/connection_pool.rb +27 -0
- data/lib/redis_memo/future.rb +3 -3
- data/lib/redis_memo/memoizable.rb +7 -4
- data/lib/redis_memo/memoizable/dependency.rb +42 -12
- data/lib/redis_memo/memoizable/invalidation.rb +39 -20
- data/lib/redis_memo/memoize_method.rb +44 -11
- data/lib/redis_memo/memoize_query.rb +21 -12
- data/lib/redis_memo/memoize_query/cached_select.rb +46 -3
- data/lib/redis_memo/memoize_query/invalidation.rb +133 -10
- data/lib/redis_memo/options.rb +17 -18
- data/lib/redis_memo/redis.rb +2 -1
- data/lib/redis_memo/tracer.rb +4 -2
- metadata +38 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10f721bc9b55f26c51674ce16f120bc643c306748dd9ff5aa5f54a7d9b944b51
|
4
|
+
data.tar.gz: 3cf3fefe9a5119d231bf973da22aa7787fcac1cf71f46ab0cadc5421cc82da52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a576fa80cef7462769055bdf657e0ee0f70fc9fb63a08f5b4ceabb931faaa8c6c8d310f01424c2124c7842b00d3c069d7c31499fc4dff166544e85b52989d0bd
|
7
|
+
data.tar.gz: 45cc8638df93f0c22fd050ad39b25fd1d8671ac309fbee8e41979d51e0a6136e7c98d9fc9a31acd9e1e1828c592f5b2068349f6a0e1bb294664fcacea8911f48
|
data/lib/redis-memo.rb
CHANGED
data/lib/redis_memo.rb
CHANGED
@@ -1,18 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'active_support/all'
|
3
4
|
require 'digest'
|
4
5
|
require 'json'
|
6
|
+
require 'securerandom'
|
5
7
|
|
6
8
|
module RedisMemo
|
7
9
|
require 'redis_memo/memoize_method'
|
8
10
|
require 'redis_memo/memoize_query'
|
9
11
|
|
12
|
+
# A process-level +RedisMemo::Options+ instance that stores the global
|
13
|
+
# options. This object can be modified by +RedisMemo.configure+.
|
14
|
+
#
|
15
|
+
# +memoize_method+ allows users to provide method-level configuration.
|
16
|
+
# When no callsite-level configuration specified we will use the values in
|
17
|
+
# +DefaultOptions+ as the default value.
|
10
18
|
DefaultOptions = RedisMemo::Options.new
|
11
19
|
|
20
|
+
# @todo Move thread keys to +RedisMemo::ThreadKey+
|
21
|
+
THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
|
22
|
+
|
23
|
+
# Configure global-level default options. Those options will be used unless
|
24
|
+
# some options specified at +memoize_method+ callsite level. See
|
25
|
+
# +RedisMemo::Options+ for all the possible options.
|
26
|
+
#
|
27
|
+
# @yieldparam [RedisMemo::Options] default_options
|
28
|
+
# +RedisMemo::DefaultOptions+
|
29
|
+
# @return [void]
|
12
30
|
def self.configure(&blk)
|
13
31
|
blk.call(DefaultOptions)
|
14
32
|
end
|
15
33
|
|
34
|
+
# Batch Redis calls triggered by +memoize_method+ to minimize the round trips
|
35
|
+
# to Redis.
|
36
|
+
# - Batches cannot be nested
|
37
|
+
# - When a batch is still open (while still in the +RedisMemo.batch+ block)
|
38
|
+
# the return value of any memoized method is a +RedisMemo::Future+ instead of
|
39
|
+
# the actual method value
|
40
|
+
# - The actual method values are returned as a list, in the same order as
|
41
|
+
# invoking, after exiting the block
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# results = RedisMemo.batch do
|
45
|
+
# 5.times { |i| memoized_calculation(i) }
|
46
|
+
# nil # Not the return value of the block
|
47
|
+
# end
|
48
|
+
# results.size == 5 # true
|
49
|
+
#
|
50
|
+
# See +RedisMemo::Batch+ for more information.
|
16
51
|
def self.batch(&blk)
|
17
52
|
RedisMemo::Batch.open
|
18
53
|
blk.call
|
@@ -21,10 +56,17 @@ module RedisMemo
|
|
21
56
|
RedisMemo::Batch.close
|
22
57
|
end
|
23
58
|
|
59
|
+
# @todo Move this method out of the top namespace
|
24
60
|
def self.checksum(serialized)
|
25
61
|
Digest::SHA1.base64digest(serialized)
|
26
62
|
end
|
27
63
|
|
64
|
+
# @todo Move this method out of the top namespace
|
65
|
+
def self.uuid
|
66
|
+
SecureRandom.uuid
|
67
|
+
end
|
68
|
+
|
69
|
+
# @todo Move this method out of the top namespace
|
28
70
|
def self.deep_sort_hash(orig_hash)
|
29
71
|
{}.tap do |new_hash|
|
30
72
|
orig_hash.sort.each do |k, v|
|
@@ -33,12 +75,17 @@ module RedisMemo
|
|
33
75
|
end
|
34
76
|
end
|
35
77
|
|
36
|
-
|
37
|
-
|
78
|
+
# Whether the current execution context has been configured to skip
|
79
|
+
# memoization and use the uncached code path.
|
80
|
+
#
|
81
|
+
# @return [Boolean]
|
38
82
|
def self.without_memo?
|
39
83
|
Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
|
40
84
|
end
|
41
85
|
|
86
|
+
# Configure the wrapped code in the block to skip memoization.
|
87
|
+
#
|
88
|
+
# @yield [] no_args The block of code to skip memoization.
|
42
89
|
def self.without_memo
|
43
90
|
prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
|
44
91
|
Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
|
@@ -47,6 +94,8 @@ module RedisMemo
|
|
47
94
|
Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
|
48
95
|
end
|
49
96
|
|
97
|
+
# @todo Move errors to a separate file errors.rb
|
50
98
|
class ArgumentError < ::ArgumentError; end
|
51
99
|
class RuntimeError < ::RuntimeError; end
|
100
|
+
class WithoutMemoization < Exception; end
|
52
101
|
end
|
@@ -66,7 +66,12 @@ class RedisMemo::AfterCommit
|
|
66
66
|
@@pending_memo_versions.each do |key, version|
|
67
67
|
invalidation_queue =
|
68
68
|
RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue)
|
69
|
-
|
69
|
+
|
70
|
+
invalidation_queue << RedisMemo::Memoizable::Invalidation::Task.new(
|
71
|
+
key,
|
72
|
+
version,
|
73
|
+
@@previous_memo_versions[key],
|
74
|
+
)
|
70
75
|
end
|
71
76
|
|
72
77
|
RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
|
data/lib/redis_memo/cache.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require_relative 'options'
|
3
3
|
require_relative 'redis'
|
4
|
+
require_relative 'connection_pool'
|
4
5
|
|
5
6
|
class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
6
7
|
class Rescuable < Exception; end
|
7
8
|
|
8
|
-
THREAD_KEY_LOCAL_CACHE
|
9
|
-
|
9
|
+
THREAD_KEY_LOCAL_CACHE = :__redis_memo_cache_local_cache__
|
10
|
+
THREAD_KEY_LOCAL_DEPENDENCY_CACHE = :__redis_memo_local_cache_dependency_cache__
|
11
|
+
THREAD_KEY_RAISE_ERROR = :__redis_memo_cache_raise_error__
|
10
12
|
|
11
13
|
@@redis = nil
|
12
14
|
@@redis_store = nil
|
@@ -23,7 +25,12 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def self.redis
|
26
|
-
@@redis ||=
|
28
|
+
@@redis ||=
|
29
|
+
if RedisMemo::DefaultOptions.connection_pool
|
30
|
+
RedisMemo::ConnectionPool.new(**RedisMemo::DefaultOptions.connection_pool)
|
31
|
+
else
|
32
|
+
RedisMemo::DefaultOptions.redis
|
33
|
+
end
|
27
34
|
end
|
28
35
|
|
29
36
|
def self.redis_store
|
@@ -44,16 +51,22 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
|
44
51
|
Thread.current[THREAD_KEY_LOCAL_CACHE]
|
45
52
|
end
|
46
53
|
|
54
|
+
def self.local_dependency_cache
|
55
|
+
Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE]
|
56
|
+
end
|
57
|
+
|
47
58
|
class << self
|
48
59
|
def with_local_cache(&blk)
|
49
60
|
Thread.current[THREAD_KEY_LOCAL_CACHE] = {}
|
61
|
+
Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE] = {}
|
50
62
|
blk.call
|
51
63
|
ensure
|
52
64
|
Thread.current[THREAD_KEY_LOCAL_CACHE] = nil
|
65
|
+
Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE] = nil
|
53
66
|
end
|
54
67
|
|
55
68
|
# RedisCacheStore doesn't read from the local cache before reading from redis
|
56
|
-
def read_multi(*keys, raise_error: false)
|
69
|
+
def read_multi(*keys, raw: false, raise_error: false)
|
57
70
|
return {} if keys.empty?
|
58
71
|
|
59
72
|
Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
|
@@ -64,7 +77,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
|
64
77
|
keys_to_fetch -= local_entries.keys unless local_entries.empty?
|
65
78
|
return local_entries if keys_to_fetch.empty?
|
66
79
|
|
67
|
-
remote_entries = redis_store.read_multi(*keys_to_fetch)
|
80
|
+
remote_entries = redis_store.read_multi(*keys_to_fetch, raw: raw)
|
68
81
|
local_cache&.merge!(remote_entries)
|
69
82
|
|
70
83
|
if local_entries.empty?
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'connection_pool'
|
3
|
+
require_relative 'redis'
|
4
|
+
|
5
|
+
class RedisMemo::ConnectionPool
|
6
|
+
def initialize(**options)
|
7
|
+
@connection_pool = ::ConnectionPool.new(**options) do
|
8
|
+
# Construct a new client every time the block gets called
|
9
|
+
RedisMemo::Redis.new(RedisMemo::DefaultOptions.redis_config)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Avoid method_missing when possible for better performance
|
14
|
+
%i(get mget mapped_mget set eval).each do |method_name|
|
15
|
+
define_method method_name do |*args, &blk|
|
16
|
+
@connection_pool.with do |redis|
|
17
|
+
redis.send(method_name, *args, &blk)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(method_name, *args, &blk)
|
23
|
+
@connection_pool.with do |redis|
|
24
|
+
redis.send(method_name, *args, &blk)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/redis_memo/future.rb
CHANGED
@@ -9,14 +9,14 @@ class RedisMemo::Future
|
|
9
9
|
ref,
|
10
10
|
method_id,
|
11
11
|
method_args,
|
12
|
-
|
12
|
+
dependent_memos,
|
13
13
|
cache_options,
|
14
14
|
method_name_without_memo
|
15
15
|
)
|
16
16
|
@ref = ref
|
17
17
|
@method_id = method_id
|
18
18
|
@method_args = method_args
|
19
|
-
@
|
19
|
+
@dependent_memos = dependent_memos
|
20
20
|
@cache_options = cache_options
|
21
21
|
@method_name_without_memo = method_name_without_memo
|
22
22
|
@method_cache_key = nil
|
@@ -28,7 +28,7 @@ class RedisMemo::Future
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def context
|
31
|
-
[@
|
31
|
+
[@method_id, @method_args, @dependent_memos]
|
32
32
|
end
|
33
33
|
|
34
34
|
def method_cache_key
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'securerandom'
|
3
2
|
|
4
3
|
class RedisMemo::Memoizable
|
5
4
|
require_relative 'memoizable/dependency'
|
@@ -52,7 +51,7 @@ class RedisMemo::Memoizable
|
|
52
51
|
cache_key = instance.cache_key
|
53
52
|
RedisMemo::Memoizable::Invalidation.bump_version_later(
|
54
53
|
cache_key,
|
55
|
-
|
54
|
+
RedisMemo.uuid,
|
56
55
|
)
|
57
56
|
end
|
58
57
|
|
@@ -83,7 +82,11 @@ class RedisMemo::Memoizable
|
|
83
82
|
if keys_to_fetch.empty?
|
84
83
|
{}
|
85
84
|
else
|
86
|
-
RedisMemo::Cache.read_multi(
|
85
|
+
RedisMemo::Cache.read_multi(
|
86
|
+
*keys_to_fetch,
|
87
|
+
raw: true,
|
88
|
+
raise_error: true,
|
89
|
+
)
|
87
90
|
end
|
88
91
|
memo_versions.merge!(cached_versions) unless cached_versions.empty?
|
89
92
|
|
@@ -96,7 +99,7 @@ class RedisMemo::Memoizable
|
|
96
99
|
# cached result.
|
97
100
|
need_to_bump_versions = true
|
98
101
|
|
99
|
-
new_version =
|
102
|
+
new_version = RedisMemo.uuid
|
100
103
|
RedisMemo::Memoizable::Invalidation.bump_version_later(
|
101
104
|
key,
|
102
105
|
new_version,
|
@@ -12,25 +12,55 @@ class RedisMemo::Memoizable::Dependency
|
|
12
12
|
@nodes.values
|
13
13
|
end
|
14
14
|
|
15
|
-
def depends_on(
|
16
|
-
|
15
|
+
def depends_on(dependency, **conditions)
|
16
|
+
case dependency
|
17
|
+
when self.class
|
18
|
+
nodes.merge!(dependency.nodes)
|
19
|
+
when RedisMemo::Memoizable
|
20
|
+
memo = dependency
|
21
|
+
return if nodes.include?(memo.cache_key)
|
22
|
+
nodes[memo.cache_key] = memo
|
23
|
+
|
24
|
+
if memo.depends_on
|
25
|
+
# Extract dependencies from the current memoizable and recurse
|
26
|
+
instance_exec(&memo.depends_on)
|
27
|
+
end
|
28
|
+
when ActiveRecord::Relation
|
29
|
+
extracted = extract_dependencies_for_relation(dependency)
|
30
|
+
nodes.merge!(extracted.nodes)
|
31
|
+
when UsingActiveRecord
|
17
32
|
[
|
18
|
-
|
19
|
-
RedisMemo::MemoizeQuery.create_memo(
|
33
|
+
dependency.redis_memo_class_memoizable,
|
34
|
+
RedisMemo::MemoizeQuery.create_memo(dependency, **conditions),
|
20
35
|
].each do |memo|
|
21
36
|
nodes[memo.cache_key] = memo
|
22
37
|
end
|
23
|
-
|
24
|
-
|
38
|
+
else
|
39
|
+
raise(
|
40
|
+
RedisMemo::ArgumentError,
|
41
|
+
"Invalid dependency #{dependency}"
|
42
|
+
)
|
25
43
|
end
|
44
|
+
end
|
26
45
|
|
27
|
-
|
28
|
-
|
29
|
-
|
46
|
+
def extract_dependencies_for_relation(relation)
|
47
|
+
# Extract the dependent memos of an Arel without calling exec_query to actually execute the query
|
48
|
+
RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
|
49
|
+
connection = ActiveRecord::Base.connection
|
50
|
+
query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
|
51
|
+
RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
|
52
|
+
is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
|
53
|
+
raise(
|
54
|
+
RedisMemo::WithoutMemoization,
|
55
|
+
"Arel query is not cached using RedisMemo."
|
56
|
+
) unless is_query_cached
|
57
|
+
extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
|
58
|
+
end
|
59
|
+
end
|
30
60
|
|
31
|
-
|
32
|
-
|
33
|
-
|
61
|
+
class UsingActiveRecord
|
62
|
+
def self.===(dependency)
|
63
|
+
RedisMemo::MemoizeQuery.using_active_record?(dependency)
|
34
64
|
end
|
35
65
|
end
|
36
66
|
end
|
@@ -3,12 +3,29 @@ require_relative '../after_commit'
|
|
3
3
|
require_relative '../cache'
|
4
4
|
|
5
5
|
module RedisMemo::Memoizable::Invalidation
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
class Task
|
7
|
+
attr_reader :key
|
8
|
+
attr_reader :version
|
9
|
+
attr_reader :previous_version
|
10
|
+
|
11
|
+
def initialize(key, version, previous_version)
|
12
|
+
@key = key
|
13
|
+
@version = version
|
14
|
+
@previous_version = previous_version
|
15
|
+
@created_at = current_timestamp
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_timestamp
|
19
|
+
Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
20
|
+
end
|
21
|
+
|
22
|
+
def duration
|
23
|
+
current_timestamp - @created_at
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# This is a thread safe data structure to handle transient network errors
|
28
|
+
# during cache invalidation
|
12
29
|
#
|
13
30
|
# When an invalidation call arrives at Redis, we only bump to the specified
|
14
31
|
# version (so the cached results using that version will become visible) if
|
@@ -28,8 +45,6 @@ module RedisMemo::Memoizable::Invalidation
|
|
28
45
|
@@invalidation_queue = Queue.new
|
29
46
|
|
30
47
|
def self.bump_version_later(key, version, previous_version: nil)
|
31
|
-
RedisMemo::DefaultOptions.logger&.info("[received] Bump memo key #{key}")
|
32
|
-
|
33
48
|
if RedisMemo::AfterCommit.in_transaction?
|
34
49
|
previous_version ||= RedisMemo::AfterCommit.pending_memo_versions[key]
|
35
50
|
end
|
@@ -41,7 +56,10 @@ module RedisMemo::Memoizable::Invalidation
|
|
41
56
|
# Fill an expected previous version so the later calculation results
|
42
57
|
# based on this version can still be rolled out if this version
|
43
58
|
# does not change
|
44
|
-
previous_version ||= RedisMemo::Cache.read_multi(
|
59
|
+
previous_version ||= RedisMemo::Cache.read_multi(
|
60
|
+
key,
|
61
|
+
raw: true,
|
62
|
+
)[key]
|
45
63
|
end
|
46
64
|
|
47
65
|
local_cache&.send(:[]=, key, version)
|
@@ -52,7 +70,7 @@ module RedisMemo::Memoizable::Invalidation
|
|
52
70
|
previous_version: previous_version,
|
53
71
|
)
|
54
72
|
else
|
55
|
-
@@invalidation_queue <<
|
73
|
+
@@invalidation_queue << Task.new(key, version, previous_version)
|
56
74
|
end
|
57
75
|
end
|
58
76
|
|
@@ -89,31 +107,32 @@ module RedisMemo::Memoizable::Invalidation
|
|
89
107
|
return redis.call('set', key, new_version, unpack(px))
|
90
108
|
LUA
|
91
109
|
|
92
|
-
def self.bump_version(
|
93
|
-
RedisMemo::Tracer.trace('redis_memo.memoizable.bump_version',
|
110
|
+
def self.bump_version(task)
|
111
|
+
RedisMemo::Tracer.trace('redis_memo.memoizable.bump_version', task.key) do
|
94
112
|
ttl = RedisMemo::DefaultOptions.expires_in
|
95
113
|
ttl = (ttl * 1000.0).to_i if ttl
|
96
114
|
RedisMemo::Cache.redis.eval(
|
97
115
|
LUA_BUMP_VERSION,
|
98
|
-
keys: [
|
99
|
-
argv: [previous_version, version,
|
116
|
+
keys: [task.key],
|
117
|
+
argv: [task.previous_version, task.version, RedisMemo.uuid, ttl],
|
100
118
|
)
|
119
|
+
RedisMemo::Tracer.set_tag(enqueue_to_finish: task.duration)
|
101
120
|
end
|
102
|
-
RedisMemo::DefaultOptions.logger&.info("[performed] Bump memo key #{cache_key}")
|
103
121
|
end
|
104
122
|
|
105
123
|
def self.drain_invalidation_queue_now
|
106
124
|
retry_queue = []
|
107
125
|
until @@invalidation_queue.empty?
|
108
|
-
|
126
|
+
task = @@invalidation_queue.pop
|
109
127
|
begin
|
110
|
-
bump_version(
|
111
|
-
rescue SignalException, Redis::BaseConnectionError
|
112
|
-
|
128
|
+
bump_version(task)
|
129
|
+
rescue SignalException, Redis::BaseConnectionError,
|
130
|
+
::ConnectionPool::TimeoutError
|
131
|
+
retry_queue << task
|
113
132
|
end
|
114
133
|
end
|
115
134
|
ensure
|
116
|
-
retry_queue.each { |
|
135
|
+
retry_queue.each { |task| @@invalidation_queue << task }
|
117
136
|
end
|
118
137
|
|
119
138
|
at_exit do
|
@@ -15,6 +15,12 @@ module RedisMemo::MemoizeMethod
|
|
15
15
|
define_method method_name_with_memo do |*args|
|
16
16
|
return send(method_name_without_memo, *args) if RedisMemo.without_memo?
|
17
17
|
|
18
|
+
dependent_memos = nil
|
19
|
+
if depends_on
|
20
|
+
dependency = RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *args, &depends_on)
|
21
|
+
dependent_memos = dependency.memos
|
22
|
+
end
|
23
|
+
|
18
24
|
future = RedisMemo::Future.new(
|
19
25
|
self,
|
20
26
|
case method_id
|
@@ -26,7 +32,7 @@ module RedisMemo::MemoizeMethod
|
|
26
32
|
method_id.call(self, *args)
|
27
33
|
end,
|
28
34
|
args,
|
29
|
-
|
35
|
+
dependent_memos,
|
30
36
|
options,
|
31
37
|
method_name_without_memo,
|
32
38
|
)
|
@@ -37,9 +43,25 @@ module RedisMemo::MemoizeMethod
|
|
37
43
|
end
|
38
44
|
|
39
45
|
future.execute
|
46
|
+
rescue RedisMemo::WithoutMemoization
|
47
|
+
send(method_name_without_memo, *args)
|
40
48
|
end
|
41
49
|
|
42
50
|
alias_method method_name, method_name_with_memo
|
51
|
+
|
52
|
+
@__redis_memo_method_dependencies ||= Hash.new
|
53
|
+
@__redis_memo_method_dependencies[method_name] = depends_on
|
54
|
+
|
55
|
+
define_method :dependency_of do |method_name, *method_args|
|
56
|
+
method_depends_on = self.class.instance_variable_get(:@__redis_memo_method_dependencies)[method_name]
|
57
|
+
unless method_depends_on
|
58
|
+
raise(
|
59
|
+
RedisMemo::ArgumentError,
|
60
|
+
"#{method_name} is not a memoized method"
|
61
|
+
)
|
62
|
+
end
|
63
|
+
RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *method_args, &method_depends_on)
|
64
|
+
end
|
43
65
|
end
|
44
66
|
|
45
67
|
def self.method_id(ref, method_name)
|
@@ -49,23 +71,34 @@ module RedisMemo::MemoizeMethod
|
|
49
71
|
"#{class_name}#{is_class_method ? '::' : '#'}#{method_name}"
|
50
72
|
end
|
51
73
|
|
52
|
-
def self.
|
53
|
-
|
54
|
-
future_contexts.each_with_index do |(ref, _, method_args, depends_on), i|
|
55
|
-
if depends_on
|
56
|
-
dependency = RedisMemo::Memoizable::Dependency.new
|
74
|
+
def self.extract_dependencies(ref, *method_args, &depends_on)
|
75
|
+
dependency = RedisMemo::Memoizable::Dependency.new
|
57
76
|
|
58
|
-
|
59
|
-
|
77
|
+
# Resolve the dependency recursively
|
78
|
+
dependency.instance_exec(ref, *method_args, &depends_on)
|
79
|
+
dependency
|
80
|
+
end
|
60
81
|
|
61
|
-
|
62
|
-
|
82
|
+
def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
|
83
|
+
if RedisMemo::Cache.local_dependency_cache
|
84
|
+
RedisMemo::Cache.local_dependency_cache[ref] ||= {}
|
85
|
+
RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
|
86
|
+
RedisMemo::Cache.local_dependency_cache[ref][depends_on][method_args] ||= extract_dependencies(ref, *method_args, &depends_on)
|
87
|
+
else
|
88
|
+
extract_dependencies(ref, *method_args, &depends_on)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.method_cache_keys(future_contexts)
|
93
|
+
memos = Array.new(future_contexts.size)
|
94
|
+
future_contexts.each_with_index do |(_, _, dependent_memos), i|
|
95
|
+
memos[i] = dependent_memos
|
63
96
|
end
|
64
97
|
|
65
98
|
j = 0
|
66
99
|
memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
|
67
100
|
method_cache_key_versions = Array.new(future_contexts.size)
|
68
|
-
future_contexts.each_with_index do |(
|
101
|
+
future_contexts.each_with_index do |(method_id, method_args, _), i|
|
69
102
|
if memos[i]
|
70
103
|
method_cache_key_versions[i] = [method_id, memo_checksums[j]]
|
71
104
|
j += 1
|
@@ -13,6 +13,8 @@ if defined?(ActiveRecord)
|
|
13
13
|
# after each record save
|
14
14
|
def memoize_table_column(*raw_columns, editable: true)
|
15
15
|
RedisMemo::MemoizeQuery.using_active_record!(self)
|
16
|
+
return if ENV["REDIS_MEMO_DISABLE_#{self.table_name.upcase}"] == 'true'
|
17
|
+
|
16
18
|
columns = raw_columns.map(&:to_sym).sort
|
17
19
|
|
18
20
|
RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: true) << columns if editable
|
@@ -44,11 +46,15 @@ if defined?(ActiveRecord)
|
|
44
46
|
end
|
45
47
|
|
46
48
|
def self.using_active_record!(model_class)
|
47
|
-
unless
|
49
|
+
unless using_active_record?(model_class)
|
48
50
|
raise RedisMemo::ArgumentError, "'#{model_class.name}' does not use ActiveRecord"
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
54
|
+
def self.using_active_record?(model_class)
|
55
|
+
model_class.respond_to?(:<) && model_class < ActiveRecord::Base
|
56
|
+
end
|
57
|
+
|
52
58
|
@@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
|
53
59
|
|
54
60
|
def self.memoized_columns(model_or_table, editable_only: false)
|
@@ -68,26 +74,23 @@ if defined?(ActiveRecord)
|
|
68
74
|
)
|
69
75
|
end
|
70
76
|
|
71
|
-
extra_props.each do |key,
|
77
|
+
extra_props.each do |key, value|
|
72
78
|
# The data type is ensured by the database, thus we don't need to cast
|
73
79
|
# types here for better performance
|
74
80
|
column_name = key.to_s
|
75
|
-
values = [values] unless values.is_a?(Enumerable)
|
76
81
|
extra_props[key] =
|
77
82
|
if model_class.defined_enums.include?(column_name)
|
78
83
|
enum_mapping = model_class.defined_enums[column_name]
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
(enum_mapping[value.to_s] || value).to_s
|
83
|
-
end
|
84
|
+
# Assume a value is a converted enum if it does not exist in the
|
85
|
+
# enum mapping
|
86
|
+
(enum_mapping[value.to_s] || value).to_s
|
84
87
|
else
|
85
|
-
|
88
|
+
value.to_s
|
86
89
|
end
|
87
90
|
end
|
88
91
|
|
89
92
|
RedisMemo::Memoizable.new(
|
90
|
-
|
93
|
+
__redis_memo_memoize_query_table_name__: model_class.table_name,
|
91
94
|
**extra_props,
|
92
95
|
)
|
93
96
|
end
|
@@ -101,7 +104,13 @@ if defined?(ActiveRecord)
|
|
101
104
|
end
|
102
105
|
end
|
103
106
|
|
104
|
-
def self.invalidate(
|
107
|
+
def self.invalidate(*records)
|
108
|
+
RedisMemo::Memoizable.invalidate(
|
109
|
+
records.map { |record| to_memos(record) }.flatten,
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.to_memos(record)
|
105
114
|
# Invalidate memos with current values
|
106
115
|
memos_to_invalidate = memoized_columns(record.class).map do |columns|
|
107
116
|
props = {}
|
@@ -136,7 +145,7 @@ if defined?(ActiveRecord)
|
|
136
145
|
end
|
137
146
|
end
|
138
147
|
|
139
|
-
|
148
|
+
memos_to_invalidate
|
140
149
|
end
|
141
150
|
end
|
142
151
|
end
|
@@ -123,7 +123,14 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
123
123
|
|
124
124
|
depends_on RedisMemo::Memoizable.new(
|
125
125
|
__redis_memo_memoize_query_memoize_query_sql__: sql,
|
126
|
-
__redis_memo_memoize_query_memoize_query_binds__: binds.map
|
126
|
+
__redis_memo_memoize_query_memoize_query_binds__: binds.map do |bind|
|
127
|
+
if bind.respond_to?(:value_for_database)
|
128
|
+
bind.value_for_database
|
129
|
+
else
|
130
|
+
# In activerecord >= 6, a bind could be an actual database value
|
131
|
+
bind
|
132
|
+
end
|
133
|
+
end
|
127
134
|
)
|
128
135
|
end
|
129
136
|
end
|
@@ -187,6 +194,19 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
187
194
|
Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = nil
|
188
195
|
end
|
189
196
|
|
197
|
+
def self.with_new_query_context
|
198
|
+
prev_arel = Thread.current[THREAD_KEY_AREL]
|
199
|
+
prev_substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
|
200
|
+
prev_bind_params = Thread.current[THREAD_KEY_AREL_BIND_PARAMS]
|
201
|
+
RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
|
202
|
+
|
203
|
+
yield
|
204
|
+
ensure
|
205
|
+
Thread.current[THREAD_KEY_AREL] = prev_arel
|
206
|
+
Thread.current[THREAD_KEY_SUBSTITUTES] = prev_substitutes
|
207
|
+
Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = prev_bind_params
|
208
|
+
end
|
209
|
+
|
190
210
|
private
|
191
211
|
|
192
212
|
# A pre-order Depth First Search
|
@@ -197,7 +217,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
197
217
|
bind_params = BindParams.new
|
198
218
|
|
199
219
|
case node
|
200
|
-
when
|
220
|
+
when NodeHasFilterCondition
|
201
221
|
attr_node = node.left
|
202
222
|
return unless attr_node.is_a?(Arel::Attributes::Attribute)
|
203
223
|
|
@@ -234,7 +254,13 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
234
254
|
}
|
235
255
|
when Arel::Nodes::Casted
|
236
256
|
bind_params.params[binding_relation] << {
|
237
|
-
right.attribute.name.to_sym =>
|
257
|
+
right.attribute.name.to_sym =>
|
258
|
+
if right.respond_to?(:val)
|
259
|
+
right.val
|
260
|
+
else
|
261
|
+
# activerecord >= 6
|
262
|
+
right.value
|
263
|
+
end,
|
238
264
|
}
|
239
265
|
else
|
240
266
|
bind_params = bind_params.union(extract_bind_params_recurse(right))
|
@@ -330,6 +356,23 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
330
356
|
enabled_models[table_node.try(:name)]
|
331
357
|
end
|
332
358
|
|
359
|
+
class NodeHasFilterCondition
|
360
|
+
def self.===(node)
|
361
|
+
case node
|
362
|
+
when Arel::Nodes::Equality, Arel::Nodes::In
|
363
|
+
true
|
364
|
+
else
|
365
|
+
# In activerecord >= 6, a new arel node HomogeneousIn is introduced
|
366
|
+
if defined?(Arel::Nodes::HomogeneousIn) &&
|
367
|
+
node.is_a?(Arel::Nodes::HomogeneousIn)
|
368
|
+
true
|
369
|
+
else
|
370
|
+
false
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
333
376
|
# Thread locals to exchange information between RedisMemo and ActiveRecord
|
334
377
|
THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
|
335
378
|
THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
|
@@ -33,7 +33,6 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
33
33
|
# Methods that won't trigger model callbacks
|
34
34
|
# https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
|
35
35
|
%i(
|
36
|
-
import
|
37
36
|
decrement_counter
|
38
37
|
delete_all delete_by
|
39
38
|
increment_counter
|
@@ -43,7 +42,7 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
43
42
|
upsert upsert_all
|
44
43
|
).each do |method_name|
|
45
44
|
# Example: Model.update_all
|
46
|
-
|
45
|
+
rewrite_default_method(
|
47
46
|
model_class,
|
48
47
|
model_class,
|
49
48
|
method_name,
|
@@ -51,7 +50,7 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
51
50
|
)
|
52
51
|
|
53
52
|
# Example: Model.where(...).update_all
|
54
|
-
|
53
|
+
rewrite_default_method(
|
55
54
|
model_class,
|
56
55
|
model_class.const_get(:ActiveRecord_Relation),
|
57
56
|
method_name,
|
@@ -59,20 +58,28 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
59
58
|
)
|
60
59
|
end
|
61
60
|
|
61
|
+
%i(
|
62
|
+
import import!
|
63
|
+
).each do |method_name|
|
64
|
+
rewrite_import_method(
|
65
|
+
model_class,
|
66
|
+
method_name,
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
62
70
|
model_class.class_variable_set(var_name, true)
|
63
71
|
end
|
64
72
|
|
65
73
|
private
|
66
74
|
|
67
75
|
#
|
68
|
-
# There’s no good way to perform fine-grind cache invalidation when
|
69
|
-
# are bulk update operations such as
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
# bulk_updates.
|
76
|
+
# There’s no good way to perform fine-grind cache invalidation when
|
77
|
+
# operations are bulk update operations such as update_all, and delete_all
|
78
|
+
# witout fetching additional data from the database, which might lead to
|
79
|
+
# performance degradation. Thus, by default, we simply invalidate all
|
80
|
+
# existing cached records after each bulk_updates.
|
74
81
|
#
|
75
|
-
def self.
|
82
|
+
def self.rewrite_default_method(model_class, klass, method_name, class_method:)
|
76
83
|
methods = class_method ? :methods : :instance_methods
|
77
84
|
return unless klass.send(methods).include?(method_name)
|
78
85
|
|
@@ -87,4 +94,120 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
87
94
|
end
|
88
95
|
end
|
89
96
|
end
|
97
|
+
|
98
|
+
def self.rewrite_import_method(model_class, method_name)
|
99
|
+
# This optimization to avoid over-invalidation only works on postgres
|
100
|
+
unless ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
101
|
+
rewrite_default_method(model_class, model_class, method_name, class_method: true)
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
model_class.singleton_class.class_eval do
|
106
|
+
alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
|
107
|
+
|
108
|
+
# For the args format, see
|
109
|
+
# https://github.com/zdennis/activerecord-import/blob/master/lib/activerecord-import/import.rb#L128
|
110
|
+
define_method method_name do |*args, &blk|
|
111
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
112
|
+
records = args[args.last.is_a?(Hash) ? -2 : -1]
|
113
|
+
columns_to_update = options[:on_duplicate_key_update]
|
114
|
+
if columns_to_update.is_a?(Hash)
|
115
|
+
columns_to_update = columns_to_update[:columns]
|
116
|
+
end
|
117
|
+
|
118
|
+
if records.last.is_a?(Hash)
|
119
|
+
records.map! { |hash| model_class.new(hash) }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Invalidate the records before and after the import to resolve
|
123
|
+
# - default values filled by the database
|
124
|
+
# - updates on conflict conditions
|
125
|
+
records_to_invalidate =
|
126
|
+
if columns_to_update
|
127
|
+
RedisMemo::MemoizeQuery::Invalidation.send(
|
128
|
+
:select_by_columns,
|
129
|
+
model_class,
|
130
|
+
records,
|
131
|
+
columns_to_update,
|
132
|
+
)
|
133
|
+
else
|
134
|
+
[]
|
135
|
+
end
|
136
|
+
|
137
|
+
result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
|
138
|
+
|
139
|
+
# Offload the records to invalidate while selecting the next set of
|
140
|
+
# records to invalidate
|
141
|
+
case records_to_invalidate
|
142
|
+
when Array
|
143
|
+
RedisMemo::MemoizeQuery.invalidate(*records_to_invalidate) unless records_to_invalidate.empty?
|
144
|
+
|
145
|
+
RedisMemo::MemoizeQuery.invalidate(*RedisMemo::MemoizeQuery::Invalidation.send(
|
146
|
+
:select_by_id,
|
147
|
+
model_class,
|
148
|
+
# Not all databases support "RETURNING", which is useful when
|
149
|
+
# invaldating records after bulk creation
|
150
|
+
result.ids,
|
151
|
+
))
|
152
|
+
else
|
153
|
+
RedisMemo::MemoizeQuery.invalidate_all(model_class)
|
154
|
+
end
|
155
|
+
|
156
|
+
result
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.select_by_columns(model_class, records, columns_to_update)
|
162
|
+
return [] if records.empty?
|
163
|
+
|
164
|
+
or_chain = nil
|
165
|
+
columns_to_select = columns_to_update & RedisMemo::MemoizeQuery
|
166
|
+
.memoized_columns(model_class)
|
167
|
+
.to_a.flatten.uniq
|
168
|
+
|
169
|
+
# Nothing to invalidate here
|
170
|
+
return [] if columns_to_select.empty?
|
171
|
+
|
172
|
+
RedisMemo::Tracer.trace(
|
173
|
+
'redis_memo.memoize_query.invalidation',
|
174
|
+
"#{__method__}##{model_class.name}",
|
175
|
+
) do
|
176
|
+
records.each do |record|
|
177
|
+
conditions = {}
|
178
|
+
columns_to_select.each do |column|
|
179
|
+
conditions[column] = record.send(column)
|
180
|
+
end
|
181
|
+
if or_chain
|
182
|
+
or_chain = or_chain.or(model_class.where(conditions))
|
183
|
+
else
|
184
|
+
or_chain = model_class.where(conditions)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
record_count = RedisMemo.without_memo { or_chain.count }
|
189
|
+
if record_count > bulk_operations_invalidation_limit
|
190
|
+
nil
|
191
|
+
else
|
192
|
+
RedisMemo.without_memo { or_chain.to_a }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.select_by_id(model_class, ids)
|
198
|
+
RedisMemo::Tracer.trace(
|
199
|
+
'redis_memo.memoize_query.invalidation',
|
200
|
+
"#{__method__}##{model_class.name}",
|
201
|
+
) do
|
202
|
+
RedisMemo.without_memo do
|
203
|
+
model_class.where(model_class.primary_key => ids).to_a
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.bulk_operations_invalidation_limit
|
209
|
+
ENV['REDIS_MEMO_BULK_OPERATIONS_INVALIDATION_LIMIT']&.to_i ||
|
210
|
+
RedisMemo::DefaultOptions.bulk_operations_invalidation_limit ||
|
211
|
+
10000
|
212
|
+
end
|
90
213
|
end
|
data/lib/redis_memo/options.rb
CHANGED
@@ -13,7 +13,7 @@ class RedisMemo::Options
|
|
13
13
|
)
|
14
14
|
@compress = compress.nil? ? true : compress
|
15
15
|
@compress_threshold = compress_threshold || 1.kilobyte
|
16
|
-
@
|
16
|
+
@redis_config = redis
|
17
17
|
@redis_client = nil
|
18
18
|
@redis_error_handler = redis_error_handler
|
19
19
|
@tracer = tracer
|
@@ -22,20 +22,18 @@ class RedisMemo::Options
|
|
22
22
|
@expires_in = expires_in
|
23
23
|
end
|
24
24
|
|
25
|
-
def redis
|
26
|
-
|
27
|
-
|
25
|
+
def redis
|
26
|
+
@redis_client ||= RedisMemo::Redis.new(redis_config)
|
27
|
+
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
@redis = blk
|
38
|
-
end
|
29
|
+
def redis_config
|
30
|
+
@redis_config || {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def redis=(config)
|
34
|
+
@redis_config = config
|
35
|
+
@redis_client = nil
|
36
|
+
redis
|
39
37
|
end
|
40
38
|
|
41
39
|
def tracer(&blk)
|
@@ -74,15 +72,16 @@ class RedisMemo::Options
|
|
74
72
|
end
|
75
73
|
|
76
74
|
attr_accessor :async
|
75
|
+
attr_accessor :bulk_operations_invalidation_limit
|
76
|
+
attr_accessor :cache_out_of_date_handler
|
77
|
+
attr_accessor :cache_validation_sampler
|
77
78
|
attr_accessor :compress
|
78
79
|
attr_accessor :compress_threshold
|
79
|
-
attr_accessor :
|
80
|
+
attr_accessor :connection_pool
|
80
81
|
attr_accessor :expires_in
|
81
|
-
attr_accessor :
|
82
|
-
attr_accessor :cache_out_of_date_handler
|
82
|
+
attr_accessor :redis_error_handler
|
83
83
|
|
84
84
|
attr_writer :global_cache_key_version
|
85
|
-
attr_writer :redis
|
86
85
|
attr_writer :tracer
|
87
86
|
attr_writer :logger
|
88
87
|
end
|
data/lib/redis_memo/redis.rb
CHANGED
@@ -31,7 +31,8 @@ class RedisMemo::Redis < Redis::Distributed
|
|
31
31
|
end
|
32
32
|
|
33
33
|
class WithReplicas < ::Redis
|
34
|
-
def initialize(
|
34
|
+
def initialize(orig_options)
|
35
|
+
options = orig_options.dup
|
35
36
|
primary_option = options.shift
|
36
37
|
@replicas = options.map do |option|
|
37
38
|
option[:logger] ||= RedisMemo::DefaultOptions.logger
|
data/lib/redis_memo/tracer.rb
CHANGED
@@ -11,13 +11,15 @@ class RedisMemo::Tracer
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
def self.set_tag(
|
14
|
+
def self.set_tag(**tags)
|
15
15
|
tracer = RedisMemo::DefaultOptions.tracer
|
16
16
|
return if tracer.nil? || !tracer.respond_to?(:active_span)
|
17
17
|
|
18
18
|
active_span = tracer.active_span
|
19
19
|
return if !active_span.respond_to?(:set_tag)
|
20
20
|
|
21
|
-
|
21
|
+
tags.each do |name, value|
|
22
|
+
active_span.set_tag(name, value)
|
23
|
+
end
|
22
24
|
end
|
23
25
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-memo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.0.beta.
|
4
|
+
version: 0.0.0.beta.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chan Zuckerberg Initiative
|
@@ -14,44 +14,72 @@ dependencies:
|
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '5.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: redis
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 4.0.1
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.0.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: connection_pool
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.2.3
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
54
|
+
version: 2.2.3
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: activerecord
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
|
-
- - "
|
59
|
+
- - ">="
|
46
60
|
- !ruby/object:Gem::Version
|
47
61
|
version: '5.2'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
|
-
- - "
|
66
|
+
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '5.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord-import
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: codecov
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -147,6 +175,7 @@ files:
|
|
147
175
|
- lib/redis_memo/after_commit.rb
|
148
176
|
- lib/redis_memo/batch.rb
|
149
177
|
- lib/redis_memo/cache.rb
|
178
|
+
- lib/redis_memo/connection_pool.rb
|
150
179
|
- lib/redis_memo/future.rb
|
151
180
|
- lib/redis_memo/memoizable.rb
|
152
181
|
- lib/redis_memo/memoizable/dependency.rb
|