redis-memo 0.0.0.beta → 0.0.0.beta.5
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 +4 -0
- data/lib/redis_memo.rb +52 -3
- 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 +53 -13
- data/lib/redis_memo/memoize_query.rb +151 -0
- data/lib/redis_memo/{memoize_records → memoize_query}/cached_select.rb +80 -199
- data/lib/redis_memo/memoize_query/cached_select/bind_params.rb +127 -0
- data/lib/redis_memo/memoize_query/cached_select/connection_adapter.rb +41 -0
- data/lib/redis_memo/memoize_query/cached_select/statement_cache.rb +16 -0
- data/lib/redis_memo/memoize_query/invalidation.rb +211 -0
- data/lib/redis_memo/memoize_query/memoize_table_column.rb +5 -0
- data/lib/redis_memo/{memoize_records → memoize_query}/model_callback.rb +3 -3
- 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 +46 -14
- data/lib/redis_memo/memoize_method.rbi +0 -10
- data/lib/redis_memo/memoize_records.rb +0 -146
- data/lib/redis_memo/memoize_records/invalidation.rb +0 -85
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::MemoizeQuery::CachedSelect
|
4
|
+
class BindParams
|
5
|
+
def params
|
6
|
+
#
|
7
|
+
# Bind params is hash of sets: each key is a model class, each value is a
|
8
|
+
# set of hashes for memoized column conditions. Example:
|
9
|
+
#
|
10
|
+
# {
|
11
|
+
# Site => [
|
12
|
+
# {name: 'a', city: 'b'},
|
13
|
+
# {name: 'a', city: 'c'},
|
14
|
+
# {name: 'b', city: 'b'},
|
15
|
+
# {name: 'b', city: 'c'},
|
16
|
+
# ],
|
17
|
+
# }
|
18
|
+
#
|
19
|
+
@params ||= Hash.new do |models, model|
|
20
|
+
models[model] = []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def union(other)
|
25
|
+
return unless other
|
26
|
+
|
27
|
+
# The tree is almost always right-heavy. Merge into the right node for better
|
28
|
+
# performance.
|
29
|
+
other.params.merge!(params) do |_, other_attrs_set, attrs_set|
|
30
|
+
if other_attrs_set.empty?
|
31
|
+
attrs_set
|
32
|
+
elsif attrs_set.empty?
|
33
|
+
other_attrs_set
|
34
|
+
else
|
35
|
+
attrs_set + other_attrs_set
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
other
|
40
|
+
end
|
41
|
+
|
42
|
+
def product(other)
|
43
|
+
# Example:
|
44
|
+
#
|
45
|
+
# and(
|
46
|
+
# [{a: 1}, {a: 2}],
|
47
|
+
# [{b: 1}, {b: 2}],
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
# =>
|
51
|
+
#
|
52
|
+
# [
|
53
|
+
# {a: 1, b: 1},
|
54
|
+
# {a: 1, b: 2},
|
55
|
+
# {a: 2, b: 1},
|
56
|
+
# {a: 2, b: 2},
|
57
|
+
# ]
|
58
|
+
return unless other
|
59
|
+
|
60
|
+
# The tree is almost always right-heavy. Merge into the right node for better
|
61
|
+
# performance.
|
62
|
+
params.each do |model, attrs_set|
|
63
|
+
next if attrs_set.empty?
|
64
|
+
|
65
|
+
# The other model does not have any conditions so far: carry the
|
66
|
+
# attributes over to the other node
|
67
|
+
if other.params[model].empty?
|
68
|
+
other.params[model] = attrs_set
|
69
|
+
next
|
70
|
+
end
|
71
|
+
|
72
|
+
# Distribute the current attrs into the other
|
73
|
+
other_attrs_set_size = other.params[model].size
|
74
|
+
other_attrs_set = other.params[model]
|
75
|
+
merged_attrs_set = Array.new(other_attrs_set_size * attrs_set.size)
|
76
|
+
|
77
|
+
attrs_set.each_with_index do |attrs, i|
|
78
|
+
other_attrs_set.each_with_index do |other_attrs, j|
|
79
|
+
k = i * other_attrs_set_size + j
|
80
|
+
merged_attrs = merged_attrs_set[k] = other_attrs.dup
|
81
|
+
attrs.each do |name, val|
|
82
|
+
# Conflict detected. For example:
|
83
|
+
#
|
84
|
+
# (a = 1 or b = 1) and (a = 2 or b = 2)
|
85
|
+
#
|
86
|
+
# Keep: a = 1 and b = 2, a = 2 and b = 1
|
87
|
+
# Discard: a = 1 and a = 2, b = 1 and b = 2
|
88
|
+
if merged_attrs.include?(name) && merged_attrs[name] != val
|
89
|
+
merged_attrs_set[k] = nil
|
90
|
+
break
|
91
|
+
end
|
92
|
+
|
93
|
+
merged_attrs[name] = val
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
merged_attrs_set.compact!
|
99
|
+
other.params[model] = merged_attrs_set
|
100
|
+
end
|
101
|
+
|
102
|
+
other
|
103
|
+
end
|
104
|
+
|
105
|
+
def uniq!
|
106
|
+
params.each do |_, attrs_set|
|
107
|
+
attrs_set.uniq!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def memoizable?
|
112
|
+
return false if params.empty?
|
113
|
+
|
114
|
+
params.each do |model, attrs_set|
|
115
|
+
return false if attrs_set.empty?
|
116
|
+
|
117
|
+
attrs_set.each do |attrs|
|
118
|
+
return false unless RedisMemo::MemoizeQuery
|
119
|
+
.memoized_columns(model)
|
120
|
+
.include?(attrs.keys.sort)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::MemoizeQuery::CachedSelect
|
4
|
+
module ConnectionAdapter
|
5
|
+
def cacheable_query(*args)
|
6
|
+
query, binds = super(*args)
|
7
|
+
|
8
|
+
# Persist the arel object to StatementCache#execute
|
9
|
+
query.instance_variable_set(:@__redis_memo_memoize_query_memoize_query_arel, args.last)
|
10
|
+
|
11
|
+
[query, binds]
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec_query(*args)
|
15
|
+
# An Arel AST in Thread local is set prior to supported query methods
|
16
|
+
if !RedisMemo.without_memo? &&
|
17
|
+
RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(args[0])
|
18
|
+
# [Reids $model Load] $sql $binds
|
19
|
+
RedisMemo::DefaultOptions.logger&.info(
|
20
|
+
"[Redis] \u001b[36;1m#{args[1]} \u001b[34;1m#{args[0]}\u001b[0m #{
|
21
|
+
args[2].map { |bind| [bind.name, bind.value_for_database]}
|
22
|
+
}"
|
23
|
+
)
|
24
|
+
|
25
|
+
super(*args)
|
26
|
+
else
|
27
|
+
RedisMemo.without_memo { super(*args) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def select_all(*args)
|
32
|
+
if args[0].is_a?(Arel::SelectManager)
|
33
|
+
RedisMemo::MemoizeQuery::CachedSelect.current_query = args[0]
|
34
|
+
end
|
35
|
+
|
36
|
+
super(*args)
|
37
|
+
ensure
|
38
|
+
RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::MemoizeQuery::CachedSelect
|
4
|
+
module StatementCache
|
5
|
+
def execute(*args)
|
6
|
+
arel = query_builder.instance_variable_get(:@__redis_memo_memoize_query_memoize_query_arel)
|
7
|
+
RedisMemo::MemoizeQuery::CachedSelect.current_query = arel
|
8
|
+
RedisMemo::MemoizeQuery::CachedSelect.current_substitutes =
|
9
|
+
bind_map.map_substitutes(args[0])
|
10
|
+
|
11
|
+
super(*args)
|
12
|
+
ensure
|
13
|
+
RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Automatically invalidate memoizable when modifying ActiveRecords objects.
|
5
|
+
# You still need to invalidate memos when you are using SQL queries to perform
|
6
|
+
# update / delete (does not trigger record callbacks)
|
7
|
+
#
|
8
|
+
class RedisMemo::MemoizeQuery::Invalidation
|
9
|
+
def self.install(model_class)
|
10
|
+
var_name = :@@__redis_memo_memoize_query_invalidation_installed__
|
11
|
+
return if model_class.class_variable_defined?(var_name)
|
12
|
+
|
13
|
+
model_class.class_eval do
|
14
|
+
# A memory-persistent memoizable used for invalidating all queries of a
|
15
|
+
# particular model
|
16
|
+
def self.redis_memo_class_memoizable
|
17
|
+
@redis_memo_class_memoizable ||= RedisMemo::MemoizeQuery.create_memo(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
%i(delete decrement! increment!).each do |method_name|
|
21
|
+
alias_method :"without_redis_memo_invalidation_#{method_name}", method_name
|
22
|
+
|
23
|
+
define_method method_name do |*args|
|
24
|
+
result = send(:"without_redis_memo_invalidation_#{method_name}", *args)
|
25
|
+
|
26
|
+
RedisMemo::MemoizeQuery.invalidate(self)
|
27
|
+
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Methods that won't trigger model callbacks
|
34
|
+
# https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
|
35
|
+
%i(
|
36
|
+
decrement_counter
|
37
|
+
delete_all delete_by
|
38
|
+
increment_counter
|
39
|
+
insert insert! insert_all insert_all!
|
40
|
+
touch_all
|
41
|
+
update_column update_columns update_all update_counters
|
42
|
+
upsert upsert_all
|
43
|
+
).each do |method_name|
|
44
|
+
# Example: Model.update_all
|
45
|
+
rewrite_default_method(
|
46
|
+
model_class,
|
47
|
+
model_class,
|
48
|
+
method_name,
|
49
|
+
class_method: true,
|
50
|
+
)
|
51
|
+
|
52
|
+
# Example: Model.where(...).update_all
|
53
|
+
rewrite_default_method(
|
54
|
+
model_class,
|
55
|
+
model_class.const_get(:ActiveRecord_Relation),
|
56
|
+
method_name,
|
57
|
+
class_method: false,
|
58
|
+
)
|
59
|
+
end
|
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
|
+
|
70
|
+
model_class.class_variable_set(var_name, true)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
#
|
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.
|
81
|
+
#
|
82
|
+
def self.rewrite_default_method(model_class, klass, method_name, class_method:)
|
83
|
+
methods = class_method ? :methods : :instance_methods
|
84
|
+
return unless klass.send(methods).include?(method_name)
|
85
|
+
|
86
|
+
klass = klass.singleton_class if class_method
|
87
|
+
klass.class_eval do
|
88
|
+
alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
|
89
|
+
|
90
|
+
define_method method_name do |*args|
|
91
|
+
result = send(:"#{method_name}_without_redis_memo_invalidation", *args)
|
92
|
+
RedisMemo::MemoizeQuery.invalidate_all(model_class)
|
93
|
+
result
|
94
|
+
end
|
95
|
+
end
|
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
|
+
records,
|
130
|
+
columns_to_update,
|
131
|
+
)
|
132
|
+
else
|
133
|
+
[]
|
134
|
+
end
|
135
|
+
|
136
|
+
result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
|
137
|
+
|
138
|
+
# Offload the records to invalidate while selecting the next set of
|
139
|
+
# records to invalidate
|
140
|
+
case records_to_invalidate
|
141
|
+
when Array
|
142
|
+
RedisMemo::MemoizeQuery.invalidate(*records_to_invalidate) unless records_to_invalidate.empty?
|
143
|
+
|
144
|
+
RedisMemo::MemoizeQuery.invalidate(*RedisMemo::MemoizeQuery::Invalidation.send(
|
145
|
+
:select_by_id,
|
146
|
+
model_class,
|
147
|
+
# Not all databases support "RETURNING", which is useful when
|
148
|
+
# invaldating records after bulk creation
|
149
|
+
result.ids,
|
150
|
+
))
|
151
|
+
else
|
152
|
+
RedisMemo::MemoizeQuery.invalidate_all(model_class)
|
153
|
+
end
|
154
|
+
|
155
|
+
result
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.select_by_columns(records, columns_to_update)
|
161
|
+
model_class = records.first.class
|
162
|
+
or_chain = nil
|
163
|
+
columns_to_select = columns_to_update & RedisMemo::MemoizeQuery
|
164
|
+
.memoized_columns(model_class)
|
165
|
+
.to_a.flatten.uniq
|
166
|
+
|
167
|
+
# Nothing to invalidate here
|
168
|
+
return [] if columns_to_select.empty?
|
169
|
+
|
170
|
+
RedisMemo::Tracer.trace(
|
171
|
+
'redis_memo.memoize_query.invalidation',
|
172
|
+
"#{__method__}##{model_class.name}",
|
173
|
+
) do
|
174
|
+
records.each do |record|
|
175
|
+
conditions = {}
|
176
|
+
columns_to_select.each do |column|
|
177
|
+
conditions[column] = record.send(column)
|
178
|
+
end
|
179
|
+
if or_chain
|
180
|
+
or_chain = or_chain.or(model_class.where(conditions))
|
181
|
+
else
|
182
|
+
or_chain = model_class.where(conditions)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
record_count = RedisMemo.without_memo { or_chain.count }
|
187
|
+
if record_count > bulk_operations_invalidation_limit
|
188
|
+
nil
|
189
|
+
else
|
190
|
+
RedisMemo.without_memo { or_chain.to_a }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.select_by_id(model_class, ids)
|
196
|
+
RedisMemo::Tracer.trace(
|
197
|
+
'redis_memo.memoize_query.invalidation',
|
198
|
+
"#{__method__}##{model_class.name}",
|
199
|
+
) do
|
200
|
+
RedisMemo.without_memo do
|
201
|
+
model_class.where(model_class.primary_key => ids).to_a
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.bulk_operations_invalidation_limit
|
207
|
+
ENV['REDIS_MEMO_BULK_OPERATIONS_INVALIDATION_LIMIT']&.to_i ||
|
208
|
+
RedisMemo::DefaultOptions.bulk_operations_invalidation_limit ||
|
209
|
+
10000
|
210
|
+
end
|
211
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class RedisMemo::
|
3
|
+
class RedisMemo::MemoizeQuery::ModelCallback
|
4
4
|
def self.install(model_class)
|
5
5
|
var_name = :@@__redis_memo_memoize_record_after_save_callback_installed__
|
6
6
|
return if model_class.class_variable_defined?(var_name)
|
@@ -12,10 +12,10 @@ class RedisMemo::MemoizeRecords::ModelCallback
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def after_save(record)
|
15
|
-
RedisMemo::
|
15
|
+
RedisMemo::MemoizeQuery.invalidate(record)
|
16
16
|
end
|
17
17
|
|
18
18
|
def after_destroy(record)
|
19
|
-
RedisMemo::
|
19
|
+
RedisMemo::MemoizeQuery.invalidate(record)
|
20
20
|
end
|
21
21
|
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
|