redis-memo 0.0.0.beta → 0.0.0.beta.5
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 +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
|