redis-memo 0.0.0.beta.3 → 0.1.1
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 +1 -0
- data/lib/redis_memo/cache.rb +9 -3
- data/lib/redis_memo/connection_pool.rb +27 -0
- data/lib/redis_memo/future.rb +3 -3
- data/lib/redis_memo/memoizable.rb +5 -1
- data/lib/redis_memo/memoizable/dependency.rb +18 -17
- data/lib/redis_memo/memoizable/invalidation.rb +6 -2
- data/lib/redis_memo/memoize_method.rb +89 -10
- data/lib/redis_memo/memoize_query.rb +6 -2
- data/lib/redis_memo/memoize_query/cached_select.rb +35 -12
- data/lib/redis_memo/memoize_query/invalidation.rb +136 -42
- data/lib/redis_memo/options.rb +16 -18
- data/lib/redis_memo/redis.rb +2 -1
- metadata +26 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1f9810ab293851487a23911835eb644ce4401df360404a04236898b99e1ab64
|
4
|
+
data.tar.gz: edbd814f8e8c027207123340445f121e771e30260e7126fb91665a23139fd710
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a07892d98f25a3690ee254015be04f0164053be755fcce73478598ba65e28a39a4e99b47d18b12f1ec6e89facfba06e9d1b8390dab61ac3713e291f4451b782
|
7
|
+
data.tar.gz: 47f5e0d42b7957d6422277f01446b6659f8526371e556957626a68c44635d1ba456380152d48f3682959cbd84f4d2adf333a50697be386f554b000b4edf67d1f
|
data/lib/redis_memo.rb
CHANGED
data/lib/redis_memo/cache.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
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
|
@@ -24,7 +25,12 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def self.redis
|
27
|
-
@@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
|
28
34
|
end
|
29
35
|
|
30
36
|
def self.redis_store
|
@@ -60,7 +66,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
|
60
66
|
end
|
61
67
|
|
62
68
|
# RedisCacheStore doesn't read from the local cache before reading from redis
|
63
|
-
def read_multi(*keys, raise_error: false)
|
69
|
+
def read_multi(*keys, raw: false, raise_error: false)
|
64
70
|
return {} if keys.empty?
|
65
71
|
|
66
72
|
Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
|
@@ -71,7 +77,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
|
71
77
|
keys_to_fetch -= local_entries.keys unless local_entries.empty?
|
72
78
|
return local_entries if keys_to_fetch.empty?
|
73
79
|
|
74
|
-
remote_entries = redis_store.read_multi(*keys_to_fetch)
|
80
|
+
remote_entries = redis_store.read_multi(*keys_to_fetch, raw: raw)
|
75
81
|
local_cache&.merge!(remote_entries)
|
76
82
|
|
77
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
|
@@ -82,7 +82,11 @@ class RedisMemo::Memoizable
|
|
82
82
|
if keys_to_fetch.empty?
|
83
83
|
{}
|
84
84
|
else
|
85
|
-
RedisMemo::Cache.read_multi(
|
85
|
+
RedisMemo::Cache.read_multi(
|
86
|
+
*keys_to_fetch,
|
87
|
+
raw: true,
|
88
|
+
raise_error: true,
|
89
|
+
)
|
86
90
|
end
|
87
91
|
memo_versions.merge!(cached_versions) unless cached_versions.empty?
|
88
92
|
|
@@ -26,14 +26,18 @@ class RedisMemo::Memoizable::Dependency
|
|
26
26
|
instance_exec(&memo.depends_on)
|
27
27
|
end
|
28
28
|
when ActiveRecord::Relation
|
29
|
-
extracted =
|
29
|
+
extracted = self.class.extract_from_relation(dependency)
|
30
30
|
nodes.merge!(extracted.nodes)
|
31
|
-
when
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
].each do |memo|
|
31
|
+
when RedisMemo::MemoizeQuery::CachedSelect::BindParams
|
32
|
+
# A private API
|
33
|
+
dependency.params.each do |model, attrs_set|
|
34
|
+
memo = model.redis_memo_class_memoizable
|
36
35
|
nodes[memo.cache_key] = memo
|
36
|
+
|
37
|
+
attrs_set.each do |attrs|
|
38
|
+
memo = RedisMemo::MemoizeQuery.create_memo(model, **attrs)
|
39
|
+
nodes[memo.cache_key] = memo
|
40
|
+
end
|
37
41
|
end
|
38
42
|
else
|
39
43
|
raise(
|
@@ -43,24 +47,21 @@ class RedisMemo::Memoizable::Dependency
|
|
43
47
|
end
|
44
48
|
end
|
45
49
|
|
46
|
-
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.extract_from_relation(relation)
|
47
53
|
# Extract the dependent memos of an Arel without calling exec_query to actually execute the query
|
48
54
|
RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
|
49
55
|
connection = ActiveRecord::Base.connection
|
50
56
|
query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
|
51
57
|
RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
|
52
58
|
is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
|
53
|
-
raise(
|
54
|
-
RedisMemo::ArgumentError,
|
55
|
-
"Invalid Arel dependency. Query is not enabled for RedisMemo caching."
|
56
|
-
) unless is_query_cached
|
57
|
-
extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
|
58
|
-
end
|
59
|
-
end
|
60
59
|
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
unless is_query_cached
|
61
|
+
raise RedisMemo::WithoutMemoization, 'Arel query is not cached using RedisMemo'
|
62
|
+
end
|
63
|
+
|
64
|
+
connection.dependency_of(:exec_query, query, nil, binds)
|
64
65
|
end
|
65
66
|
end
|
66
67
|
end
|
@@ -56,7 +56,10 @@ module RedisMemo::Memoizable::Invalidation
|
|
56
56
|
# Fill an expected previous version so the later calculation results
|
57
57
|
# based on this version can still be rolled out if this version
|
58
58
|
# does not change
|
59
|
-
previous_version ||= RedisMemo::Cache.read_multi(
|
59
|
+
previous_version ||= RedisMemo::Cache.read_multi(
|
60
|
+
key,
|
61
|
+
raw: true,
|
62
|
+
)[key]
|
60
63
|
end
|
61
64
|
|
62
65
|
local_cache&.send(:[]=, key, version)
|
@@ -123,7 +126,8 @@ module RedisMemo::Memoizable::Invalidation
|
|
123
126
|
task = @@invalidation_queue.pop
|
124
127
|
begin
|
125
128
|
bump_version(task)
|
126
|
-
rescue SignalException, Redis::BaseConnectionError
|
129
|
+
rescue SignalException, Redis::BaseConnectionError,
|
130
|
+
::ConnectionPool::TimeoutError
|
127
131
|
retry_queue << task
|
128
132
|
end
|
129
133
|
end
|
@@ -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,6 +43,8 @@ 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
|
@@ -73,27 +81,98 @@ module RedisMemo::MemoizeMethod
|
|
73
81
|
|
74
82
|
def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
|
75
83
|
if RedisMemo::Cache.local_dependency_cache
|
76
|
-
RedisMemo::Cache.local_dependency_cache[ref] ||= {}
|
77
|
-
RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
|
78
|
-
|
84
|
+
RedisMemo::Cache.local_dependency_cache[ref.class] ||= {}
|
85
|
+
RedisMemo::Cache.local_dependency_cache[ref.class][depends_on] ||= {}
|
86
|
+
named_args = exclude_anonymous_args(depends_on, ref, method_args)
|
87
|
+
RedisMemo::Cache.local_dependency_cache[ref.class][depends_on][named_args] ||= extract_dependencies(ref, *method_args, &depends_on)
|
79
88
|
else
|
80
89
|
extract_dependencies(ref, *method_args, &depends_on)
|
81
90
|
end
|
82
91
|
end
|
83
92
|
|
93
|
+
# We only look at named method parameters in the dependency block in order to define its dependent
|
94
|
+
# memos and ignore anonymous parameters, following the convention that nil or :_ is an anonymous parameter.
|
95
|
+
# Example:
|
96
|
+
# ```
|
97
|
+
# def method(param1, param2)
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# memoize_method :method do |_, _, param2|`
|
101
|
+
# depends_on RedisMemo::Memoizable.new(param2: param2)
|
102
|
+
# end
|
103
|
+
# ```
|
104
|
+
# `exclude_anonymous_args(depends_on, ref, [1, 2])` returns [2]
|
105
|
+
def self.exclude_anonymous_args(depends_on, ref, args)
|
106
|
+
return [] if depends_on.parameters.empty? or args.empty?
|
107
|
+
|
108
|
+
positional_args = []
|
109
|
+
kwargs = {}
|
110
|
+
depends_on_args = [ref] + args
|
111
|
+
options = depends_on_args.extract_options!
|
112
|
+
|
113
|
+
# Keep track of the splat start index, and the number of positional args before and after the splat,
|
114
|
+
# so we can map which args belong to positional args and which args belong to the splat.
|
115
|
+
named_splat = false
|
116
|
+
splat_index = nil
|
117
|
+
num_positional_args_after_splat = 0
|
118
|
+
num_positional_args_before_splat = 0
|
119
|
+
|
120
|
+
depends_on.parameters.each_with_index do |param, i|
|
121
|
+
# Defined by https://github.com/ruby/ruby/blob/22b8ddfd1049c3fd1e368684c4fd03bceb041b3a/proc.c#L3048-L3059
|
122
|
+
case param.first
|
123
|
+
when :opt, :req
|
124
|
+
if splat_index
|
125
|
+
num_positional_args_after_splat += 1
|
126
|
+
else
|
127
|
+
num_positional_args_before_splat += 1
|
128
|
+
end
|
129
|
+
when :rest
|
130
|
+
named_splat = is_named?(param)
|
131
|
+
splat_index = i
|
132
|
+
when :key, :keyreq
|
133
|
+
kwargs[param.last] = options[param.last] if is_named?(param)
|
134
|
+
when :keyrest
|
135
|
+
kwargs.merge!(options) if is_named?(param)
|
136
|
+
else
|
137
|
+
raise(RedisMemo::ArgumentError, "#{param.first} argument isn't supported in the dependency block")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Determine the named positional and splat arguments after we know the # of pos. arguments before and after splat
|
142
|
+
after_splat_index = depends_on_args.size - num_positional_args_after_splat
|
143
|
+
depends_on_args.each_with_index do |arg, i|
|
144
|
+
# if the index is within the splat
|
145
|
+
if i >= num_positional_args_before_splat && i < after_splat_index
|
146
|
+
positional_args << arg if named_splat
|
147
|
+
else
|
148
|
+
j = i < num_positional_args_before_splat ? i : i - (after_splat_index - splat_index) - 1
|
149
|
+
positional_args << arg if is_named?(depends_on.parameters[j])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if !kwargs.empty?
|
154
|
+
positional_args + [kwargs]
|
155
|
+
elsif named_splat && !options.empty?
|
156
|
+
positional_args + [options]
|
157
|
+
else
|
158
|
+
positional_args
|
159
|
+
end
|
160
|
+
end
|
161
|
+
private
|
162
|
+
def self.is_named?(param)
|
163
|
+
param.size == 2 && param.last != :_
|
164
|
+
end
|
165
|
+
|
84
166
|
def self.method_cache_keys(future_contexts)
|
85
167
|
memos = Array.new(future_contexts.size)
|
86
|
-
future_contexts.each_with_index do |(
|
87
|
-
|
88
|
-
dependency = get_or_extract_dependencies(ref, *method_args, &depends_on)
|
89
|
-
memos[i] = dependency.memos
|
90
|
-
end
|
168
|
+
future_contexts.each_with_index do |(_, _, dependent_memos), i|
|
169
|
+
memos[i] = dependent_memos
|
91
170
|
end
|
92
171
|
|
93
172
|
j = 0
|
94
173
|
memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
|
95
174
|
method_cache_key_versions = Array.new(future_contexts.size)
|
96
|
-
future_contexts.each_with_index do |(
|
175
|
+
future_contexts.each_with_index do |(method_id, method_args, _), i|
|
97
176
|
if memos[i]
|
98
177
|
method_cache_key_versions[i] = [method_id, memo_checksums[j]]
|
99
178
|
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
|
@@ -102,8 +104,10 @@ if defined?(ActiveRecord)
|
|
102
104
|
end
|
103
105
|
end
|
104
106
|
|
105
|
-
def self.invalidate(
|
106
|
-
RedisMemo::Memoizable.invalidate(
|
107
|
+
def self.invalidate(*records)
|
108
|
+
RedisMemo::Memoizable.invalidate(
|
109
|
+
records.map { |record| to_memos(record) }.flatten,
|
110
|
+
)
|
107
111
|
end
|
108
112
|
|
109
113
|
def self.to_memos(record)
|
@@ -111,19 +111,19 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
111
111
|
sql.gsub(/(\$\d+)/, '?') # $1 -> ?
|
112
112
|
.gsub(/((, *)*\?)+/, '?') # (?, ?, ? ...) -> (?)
|
113
113
|
end,
|
114
|
-
) do |_, sql,
|
115
|
-
RedisMemo::MemoizeQuery::CachedSelect
|
116
|
-
.current_query_bind_params
|
117
|
-
.params
|
118
|
-
.each do |model, attrs_set|
|
119
|
-
attrs_set.each do |attrs|
|
120
|
-
depends_on model, **attrs
|
121
|
-
end
|
122
|
-
end
|
114
|
+
) do |_, sql, _, binds, **|
|
115
|
+
depends_on RedisMemo::MemoizeQuery::CachedSelect.current_query_bind_params
|
123
116
|
|
124
117
|
depends_on RedisMemo::Memoizable.new(
|
125
118
|
__redis_memo_memoize_query_memoize_query_sql__: sql,
|
126
|
-
__redis_memo_memoize_query_memoize_query_binds__: binds.map
|
119
|
+
__redis_memo_memoize_query_memoize_query_binds__: binds.map do |bind|
|
120
|
+
if bind.respond_to?(:value_for_database)
|
121
|
+
bind.value_for_database
|
122
|
+
else
|
123
|
+
# In activerecord >= 6, a bind could be an actual database value
|
124
|
+
bind
|
125
|
+
end
|
126
|
+
end
|
127
127
|
)
|
128
128
|
end
|
129
129
|
end
|
@@ -210,7 +210,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
210
210
|
bind_params = BindParams.new
|
211
211
|
|
212
212
|
case node
|
213
|
-
when
|
213
|
+
when NodeHasFilterCondition
|
214
214
|
attr_node = node.left
|
215
215
|
return unless attr_node.is_a?(Arel::Attributes::Attribute)
|
216
216
|
|
@@ -247,7 +247,13 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
247
247
|
}
|
248
248
|
when Arel::Nodes::Casted
|
249
249
|
bind_params.params[binding_relation] << {
|
250
|
-
right.attribute.name.to_sym =>
|
250
|
+
right.attribute.name.to_sym =>
|
251
|
+
if right.respond_to?(:val)
|
252
|
+
right.val
|
253
|
+
else
|
254
|
+
# activerecord >= 6
|
255
|
+
right.value
|
256
|
+
end,
|
251
257
|
}
|
252
258
|
else
|
253
259
|
bind_params = bind_params.union(extract_bind_params_recurse(right))
|
@@ -343,6 +349,23 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
343
349
|
enabled_models[table_node.try(:name)]
|
344
350
|
end
|
345
351
|
|
352
|
+
class NodeHasFilterCondition
|
353
|
+
def self.===(node)
|
354
|
+
case node
|
355
|
+
when Arel::Nodes::Equality, Arel::Nodes::In
|
356
|
+
true
|
357
|
+
else
|
358
|
+
# In activerecord >= 6, a new arel node HomogeneousIn is introduced
|
359
|
+
if defined?(Arel::Nodes::HomogeneousIn) &&
|
360
|
+
node.is_a?(Arel::Nodes::HomogeneousIn)
|
361
|
+
true
|
362
|
+
else
|
363
|
+
false
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
346
369
|
# Thread locals to exchange information between RedisMemo and ActiveRecord
|
347
370
|
THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
|
348
371
|
THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
|
@@ -36,10 +36,8 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
36
36
|
decrement_counter
|
37
37
|
delete_all delete_by
|
38
38
|
increment_counter
|
39
|
-
insert insert! insert_all insert_all!
|
40
39
|
touch_all
|
41
40
|
update_column update_columns update_all update_counters
|
42
|
-
upsert upsert_all
|
43
41
|
).each do |method_name|
|
44
42
|
# Example: Model.update_all
|
45
43
|
rewrite_default_method(
|
@@ -58,6 +56,24 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
58
56
|
)
|
59
57
|
end
|
60
58
|
|
59
|
+
%i(
|
60
|
+
insert insert! insert_all insert_all!
|
61
|
+
).each do |method_name|
|
62
|
+
rewrite_insert_method(
|
63
|
+
model_class,
|
64
|
+
method_name,
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
%i(
|
69
|
+
upsert upsert_all
|
70
|
+
).each do |method_name|
|
71
|
+
rewrite_upsert_method(
|
72
|
+
model_class,
|
73
|
+
method_name,
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
61
77
|
%i(
|
62
78
|
import import!
|
63
79
|
).each do |method_name|
|
@@ -70,6 +86,36 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
70
86
|
model_class.class_variable_set(var_name, true)
|
71
87
|
end
|
72
88
|
|
89
|
+
def self.invalidate_new_records(model_class, &blk)
|
90
|
+
current_id = model_class.maximum(model_class.primary_key)
|
91
|
+
result = blk.call
|
92
|
+
records = select_by_new_ids(model_class, current_id)
|
93
|
+
RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
|
94
|
+
result
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.invalidate_records_by_conflict_target(model_class, records:, conflict_target: nil, &blk)
|
98
|
+
if conflict_target.nil?
|
99
|
+
# When the conflict_target is not set, we are basically inserting new
|
100
|
+
# records since duplicate rows are simply skipped
|
101
|
+
return invalidate_new_records(model_class, &blk)
|
102
|
+
end
|
103
|
+
|
104
|
+
relation = build_relation_by_conflict_target(model_class, records, conflict_target)
|
105
|
+
# Invalidate records before updating
|
106
|
+
records = select_by_conflict_target_relation(model_class, relation)
|
107
|
+
RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
|
108
|
+
|
109
|
+
# Perform updating
|
110
|
+
result = blk.call
|
111
|
+
|
112
|
+
# Invalidate records after updating
|
113
|
+
records = select_by_conflict_target_relation(model_class, relation)
|
114
|
+
RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
73
119
|
private
|
74
120
|
|
75
121
|
#
|
@@ -95,12 +141,48 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
95
141
|
end
|
96
142
|
end
|
97
143
|
|
98
|
-
def self.
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
144
|
+
def self.rewrite_insert_method(model_class, method_name)
|
145
|
+
return unless model_class.respond_to?(method_name)
|
146
|
+
|
147
|
+
model_class.singleton_class.class_eval do
|
148
|
+
alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
|
149
|
+
|
150
|
+
define_method method_name do |*args, &blk|
|
151
|
+
RedisMemo::MemoizeQuery::Invalidation.invalidate_new_records(model_class) do
|
152
|
+
send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.rewrite_upsert_method(model_class, method_name)
|
159
|
+
return unless model_class.respond_to?(method_name)
|
160
|
+
|
161
|
+
model_class.singleton_class.class_eval do
|
162
|
+
alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
|
163
|
+
|
164
|
+
define_method method_name do |attributes, unique_by: nil, **kwargs, &blk|
|
165
|
+
RedisMemo::MemoizeQuery::Invalidation.invalidate_records_by_conflict_target(
|
166
|
+
model_class,
|
167
|
+
records: nil, # not used
|
168
|
+
# upsert does not support on_duplicate_key_update yet at activerecord
|
169
|
+
# HEAD (6.1.3)
|
170
|
+
conflict_target: nil,
|
171
|
+
) do
|
172
|
+
send(
|
173
|
+
:"#{method_name}_without_redis_memo_invalidation",
|
174
|
+
attributes,
|
175
|
+
unique_by: unique_by,
|
176
|
+
**kwargs,
|
177
|
+
&blk
|
178
|
+
)
|
179
|
+
end
|
180
|
+
end
|
103
181
|
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.rewrite_import_method(model_class, method_name)
|
185
|
+
return unless model_class.respond_to?(method_name)
|
104
186
|
|
105
187
|
model_class.singleton_class.class_eval do
|
106
188
|
alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
|
@@ -110,54 +192,42 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
110
192
|
define_method method_name do |*args, &blk|
|
111
193
|
options = args.last.is_a?(Hash) ? args.last : {}
|
112
194
|
records = args[args.last.is_a?(Hash) ? -2 : -1]
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
# - default values filled by the database
|
124
|
-
# - updates on conflict conditions
|
125
|
-
records_to_invalidate =
|
126
|
-
if unique_by
|
127
|
-
RedisMemo::MemoizeQuery::Invalidation.send(
|
128
|
-
:select_by_uniq_index,
|
129
|
-
records,
|
130
|
-
unique_by,
|
131
|
-
)
|
195
|
+
on_duplicate_key_update = options[:on_duplicate_key_update]
|
196
|
+
conflict_target =
|
197
|
+
case on_duplicate_key_update
|
198
|
+
when Hash
|
199
|
+
# The conflict_target option is only supported in PostgreSQL. In
|
200
|
+
# MySQL, the primary_key is used as the conflict_target
|
201
|
+
on_duplicate_key_update[:conflict_target] || [model_class.primary_key.to_sym]
|
202
|
+
when Array
|
203
|
+
# The default conflict_target is just the primary_key
|
204
|
+
[model_class.primary_key.to_sym]
|
132
205
|
else
|
133
|
-
|
206
|
+
# Ignore duplicate rows
|
207
|
+
nil
|
134
208
|
end
|
135
209
|
|
136
|
-
|
137
|
-
|
138
|
-
records_to_invalidate += RedisMemo.without_memo do
|
139
|
-
# Not all databases support "RETURNING", which is useful when
|
140
|
-
# invaldating records after bulk creation
|
141
|
-
model_class.where(model_class.primary_key => result.ids).to_a
|
210
|
+
if conflict_target && records.last.is_a?(Hash)
|
211
|
+
records.map! { |hash| model_class.new(hash) }
|
142
212
|
end
|
143
213
|
|
144
|
-
|
145
|
-
|
214
|
+
RedisMemo::MemoizeQuery::Invalidation.invalidate_records_by_conflict_target(
|
215
|
+
model_class,
|
216
|
+
records: records,
|
217
|
+
conflict_target: conflict_target,
|
218
|
+
) do
|
219
|
+
send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
|
146
220
|
end
|
147
|
-
RedisMemo::Memoizable.invalidate(memos_to_invalidate.flatten)
|
148
|
-
|
149
|
-
result
|
150
221
|
end
|
151
222
|
end
|
152
223
|
end
|
153
224
|
|
154
|
-
def self.
|
155
|
-
model_class = records.first.class
|
225
|
+
def self.build_relation_by_conflict_target(model_class, records, conflict_target)
|
156
226
|
or_chain = nil
|
157
227
|
|
158
228
|
records.each do |record|
|
159
229
|
conditions = {}
|
160
|
-
|
230
|
+
conflict_target.each do |column|
|
161
231
|
conditions[column] = record.send(column)
|
162
232
|
end
|
163
233
|
if or_chain
|
@@ -167,6 +237,30 @@ class RedisMemo::MemoizeQuery::Invalidation
|
|
167
237
|
end
|
168
238
|
end
|
169
239
|
|
170
|
-
|
240
|
+
or_chain
|
241
|
+
end
|
242
|
+
|
243
|
+
def self.select_by_new_ids(model_class, target_id)
|
244
|
+
RedisMemo::Tracer.trace(
|
245
|
+
'redis_memo.memoize_query.invalidation',
|
246
|
+
"#{__method__}##{model_class.name}",
|
247
|
+
) do
|
248
|
+
RedisMemo.without_memo do
|
249
|
+
model_class.where(
|
250
|
+
model_class.arel_table[model_class.primary_key].gt(target_id),
|
251
|
+
).to_a
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def self.select_by_conflict_target_relation(model_class, relation)
|
257
|
+
return [] unless relation
|
258
|
+
|
259
|
+
RedisMemo::Tracer.trace(
|
260
|
+
'redis_memo.memoize_query.invalidation',
|
261
|
+
"#{__method__}##{model_class.name}",
|
262
|
+
) do
|
263
|
+
RedisMemo.without_memo { relation.reload }
|
264
|
+
end
|
171
265
|
end
|
172
266
|
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,15 @@ class RedisMemo::Options
|
|
74
72
|
end
|
75
73
|
|
76
74
|
attr_accessor :async
|
75
|
+
attr_accessor :cache_out_of_date_handler
|
76
|
+
attr_accessor :cache_validation_sampler
|
77
77
|
attr_accessor :compress
|
78
78
|
attr_accessor :compress_threshold
|
79
|
-
attr_accessor :
|
79
|
+
attr_accessor :connection_pool
|
80
80
|
attr_accessor :expires_in
|
81
|
-
attr_accessor :
|
82
|
-
attr_accessor :cache_out_of_date_handler
|
81
|
+
attr_accessor :redis_error_handler
|
83
82
|
|
84
83
|
attr_writer :global_cache_key_version
|
85
|
-
attr_writer :redis
|
86
84
|
attr_writer :tracer
|
87
85
|
attr_writer :logger
|
88
86
|
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
|
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.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chan Zuckerberg Initiative
|
@@ -14,42 +14,56 @@ 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'
|
55
69
|
- !ruby/object:Gem::Dependency
|
@@ -161,6 +175,7 @@ files:
|
|
161
175
|
- lib/redis_memo/after_commit.rb
|
162
176
|
- lib/redis_memo/batch.rb
|
163
177
|
- lib/redis_memo/cache.rb
|
178
|
+
- lib/redis_memo/connection_pool.rb
|
164
179
|
- lib/redis_memo/future.rb
|
165
180
|
- lib/redis_memo/memoizable.rb
|
166
181
|
- lib/redis_memo/memoizable/dependency.rb
|
@@ -193,9 +208,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
193
208
|
version: 2.5.0
|
194
209
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
210
|
requirements:
|
196
|
-
- - "
|
211
|
+
- - ">="
|
197
212
|
- !ruby/object:Gem::Version
|
198
|
-
version:
|
213
|
+
version: '0'
|
199
214
|
requirements: []
|
200
215
|
rubygems_version: 3.0.8
|
201
216
|
signing_key:
|