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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'memoize_method'
3
+
4
+ module RedisMemo::MemoizeQuery
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RedisMemo::MemoizeRecords::ModelCallback
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::MemoizeRecords.invalidate(record)
15
+ RedisMemo::MemoizeQuery.invalidate(record)
16
16
  end
17
17
 
18
18
  def after_destroy(record)
19
- RedisMemo::MemoizeRecords.invalidate(record)
19
+ RedisMemo::MemoizeQuery.invalidate(record)
20
20
  end
21
21
  end
@@ -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
- @redis = redis
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(&blk)
26
- if blk.nil?
27
- return @redis_client if @redis_client.is_a?(RedisMemo::Redis)
25
+ def redis
26
+ @redis_client ||= RedisMemo::Redis.new(redis_config)
27
+ end
28
28
 
29
- if @redis.respond_to?(:call)
30
- @redis_client = RedisMemo::Redis.new(@redis.call)
31
- elsif @redis
32
- @redis_client = RedisMemo::Redis.new(@redis)
33
- else
34
- @redis_client = RedisMemo::Redis.new
35
- end
36
- else
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 :redis_error_handler
80
+ attr_accessor :connection_pool
80
81
  attr_accessor :expires_in
81
- attr_accessor :cache_validation_sampler
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