redis-memo 0.0.0.beta.3 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|