redis-memo 0.0.0.alpha → 0.0.0.beta
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.rb +48 -2
- data/lib/redis_memo/after_commit.rb +114 -0
- data/lib/redis_memo/batch.rb +54 -0
- data/lib/redis_memo/cache.rb +95 -0
- data/lib/redis_memo/future.rb +125 -0
- data/lib/redis_memo/memoizable.rb +116 -0
- data/lib/redis_memo/memoizable/dependency.rb +36 -0
- data/lib/redis_memo/memoizable/invalidation.rb +123 -0
- data/lib/redis_memo/memoize_method.rb +93 -0
- data/lib/redis_memo/memoize_method.rbi +10 -0
- data/lib/redis_memo/memoize_records.rb +146 -0
- data/lib/redis_memo/memoize_records/cached_select.rb +499 -0
- data/lib/redis_memo/memoize_records/invalidation.rb +85 -0
- data/lib/redis_memo/memoize_records/model_callback.rb +21 -0
- data/lib/redis_memo/middleware.rb +18 -0
- data/lib/redis_memo/options.rb +88 -0
- data/lib/redis_memo/redis.rb +67 -0
- data/lib/redis_memo/tracer.rb +23 -0
- metadata +71 -11
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::MemoizeRecords::Invalidation
|
4
|
+
def self.install(model_class)
|
5
|
+
var_name = :@@__redis_memo_memoize_records_invalidation_installed__
|
6
|
+
return if model_class.class_variable_defined?(var_name)
|
7
|
+
|
8
|
+
model_class.class_eval do
|
9
|
+
# A memory-persistent memoizable used for invalidating all queries of a
|
10
|
+
# particular model
|
11
|
+
def self.redis_memo_class_memoizable
|
12
|
+
@redis_memo_class_memoizable ||= RedisMemo::MemoizeRecords.create_memo(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
%i(delete decrement! increment!).each do |method_name|
|
16
|
+
alias_method :"without_redis_memo_invalidation_#{method_name}", method_name
|
17
|
+
|
18
|
+
define_method method_name do |*args|
|
19
|
+
result = send(:"without_redis_memo_invalidation_#{method_name}", *args)
|
20
|
+
|
21
|
+
RedisMemo::MemoizeRecords.invalidate(self)
|
22
|
+
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Methods that won't trigger model callbacks
|
29
|
+
# https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
|
30
|
+
%i(
|
31
|
+
import
|
32
|
+
decrement_counter
|
33
|
+
delete_all delete_by
|
34
|
+
increment_counter
|
35
|
+
insert insert! insert_all insert_all!
|
36
|
+
touch_all
|
37
|
+
update_column update_columns update_all update_counters
|
38
|
+
upsert upsert_all
|
39
|
+
).each do |method_name|
|
40
|
+
# Example: Model.update_all
|
41
|
+
rewrite_bulk_update_method(
|
42
|
+
model_class,
|
43
|
+
model_class,
|
44
|
+
method_name,
|
45
|
+
class_method: true,
|
46
|
+
)
|
47
|
+
|
48
|
+
# Example: Model.where(...).update_all
|
49
|
+
rewrite_bulk_update_method(
|
50
|
+
model_class,
|
51
|
+
model_class.const_get(:ActiveRecord_Relation),
|
52
|
+
method_name,
|
53
|
+
class_method: false,
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
model_class.class_variable_set(var_name, true)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
#
|
63
|
+
# There’s no good way to perform fine-grind cache invalidation when operations
|
64
|
+
# are bulk update operations such as import, update_all, and destroy_all:
|
65
|
+
# Performing fine-grind cache invalidation would require the applications to
|
66
|
+
# fetch additional data from the database, which might lead to performance
|
67
|
+
# degradation. Thus we simply invalidate all existing cached records after each
|
68
|
+
# bulk_updates.
|
69
|
+
#
|
70
|
+
def self.rewrite_bulk_update_method(model_class, klass, method_name, class_method:)
|
71
|
+
methods = class_method ? :methods : :instance_methods
|
72
|
+
return unless klass.send(methods).include?(method_name)
|
73
|
+
|
74
|
+
klass = klass.singleton_class if class_method
|
75
|
+
klass.class_eval do
|
76
|
+
alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
|
77
|
+
|
78
|
+
define_method method_name do |*args|
|
79
|
+
result = send(:"#{method_name}_without_redis_memo_invalidation", *args)
|
80
|
+
RedisMemo::MemoizeRecords.invalidate_all(model_class)
|
81
|
+
result
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::MemoizeRecords::ModelCallback
|
4
|
+
def self.install(model_class)
|
5
|
+
var_name = :@@__redis_memo_memoize_record_after_save_callback_installed__
|
6
|
+
return if model_class.class_variable_defined?(var_name)
|
7
|
+
|
8
|
+
model_class.after_save(new)
|
9
|
+
model_class.after_destroy(new)
|
10
|
+
|
11
|
+
model_class.class_variable_set(var_name, true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_save(record)
|
15
|
+
RedisMemo::MemoizeRecords.invalidate(record)
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_destroy(record)
|
19
|
+
RedisMemo::MemoizeRecords.invalidate(record)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::Middleware
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
result = nil
|
10
|
+
|
11
|
+
RedisMemo::Cache.with_local_cache do
|
12
|
+
result = @app.call(env)
|
13
|
+
end
|
14
|
+
RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisMemo::Options
|
4
|
+
def initialize(
|
5
|
+
async: nil,
|
6
|
+
compress: nil,
|
7
|
+
compress_threshold: nil,
|
8
|
+
redis: nil,
|
9
|
+
redis_error_handler: nil,
|
10
|
+
tracer: nil,
|
11
|
+
global_cache_key_version: nil,
|
12
|
+
expires_in: nil
|
13
|
+
)
|
14
|
+
@compress = compress.nil? ? true : compress
|
15
|
+
@compress_threshold = compress_threshold || 1.kilobyte
|
16
|
+
@redis = redis
|
17
|
+
@redis_client = nil
|
18
|
+
@redis_error_handler = redis_error_handler
|
19
|
+
@tracer = tracer
|
20
|
+
@logger = logger
|
21
|
+
@global_cache_key_version = global_cache_key_version
|
22
|
+
@expires_in = expires_in
|
23
|
+
end
|
24
|
+
|
25
|
+
def redis(&blk)
|
26
|
+
if blk.nil?
|
27
|
+
return @redis_client if @redis_client.is_a?(RedisMemo::Redis)
|
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
|
39
|
+
end
|
40
|
+
|
41
|
+
def tracer(&blk)
|
42
|
+
if blk.nil?
|
43
|
+
return @tracer if @tracer.respond_to?(:trace)
|
44
|
+
|
45
|
+
@tracer&.call
|
46
|
+
else
|
47
|
+
@tracer = blk
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def logger(&blk)
|
52
|
+
if blk.nil?
|
53
|
+
return @logger if @logger.respond_to?(:warn)
|
54
|
+
|
55
|
+
@logger&.call
|
56
|
+
else
|
57
|
+
@logger = blk
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def global_cache_key_version(&blk)
|
62
|
+
# this method takes a block to be consistent with the inline memo_method
|
63
|
+
# API
|
64
|
+
if blk.nil?
|
65
|
+
if !@global_cache_key_version.respond_to?(:call)
|
66
|
+
return @global_cache_key_version
|
67
|
+
end
|
68
|
+
|
69
|
+
@global_cache_key_version&.call
|
70
|
+
else
|
71
|
+
# save the global cache_key_version eagerly
|
72
|
+
@global_cache_key_version = blk
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_accessor :async
|
77
|
+
attr_accessor :compress
|
78
|
+
attr_accessor :compress_threshold
|
79
|
+
attr_accessor :redis_error_handler
|
80
|
+
attr_accessor :expires_in
|
81
|
+
attr_accessor :cache_validation_sampler
|
82
|
+
attr_accessor :cache_out_of_date_handler
|
83
|
+
|
84
|
+
attr_writer :global_cache_key_version
|
85
|
+
attr_writer :redis
|
86
|
+
attr_writer :tracer
|
87
|
+
attr_writer :logger
|
88
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'redis'
|
3
|
+
require 'redis/distributed'
|
4
|
+
|
5
|
+
require_relative 'options'
|
6
|
+
|
7
|
+
# Redis::Distributed does not support reading from multiple read replicas. This
|
8
|
+
# class adds this functionality
|
9
|
+
class RedisMemo::Redis < Redis::Distributed
|
10
|
+
def initialize(options={})
|
11
|
+
clients =
|
12
|
+
if options.is_a?(Array)
|
13
|
+
options.map do |option|
|
14
|
+
if option.is_a?(Array)
|
15
|
+
RedisMemo::Redis::WithReplicas.new(option)
|
16
|
+
else
|
17
|
+
option[:logger] ||= RedisMemo::DefaultOptions.logger
|
18
|
+
::Redis.new(option)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
22
|
+
options[:logger] ||= RedisMemo::DefaultOptions.logger
|
23
|
+
[::Redis.new(options)]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Pass in our own hash ring to use the clients with multi-read-replica
|
27
|
+
# support
|
28
|
+
hash_ring = Redis::HashRing.new(clients)
|
29
|
+
|
30
|
+
super([], ring: hash_ring)
|
31
|
+
end
|
32
|
+
|
33
|
+
class WithReplicas < ::Redis
|
34
|
+
def initialize(options)
|
35
|
+
primary_option = options.shift
|
36
|
+
@replicas = options.map do |option|
|
37
|
+
option[:logger] ||= RedisMemo::DefaultOptions.logger
|
38
|
+
::Redis.new(option)
|
39
|
+
end
|
40
|
+
|
41
|
+
primary_option[:logger] ||= RedisMemo::DefaultOptions.logger
|
42
|
+
super(primary_option)
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :get_primary, :get
|
46
|
+
alias_method :mget_primary, :mget
|
47
|
+
alias_method :mapped_mget_primary, :mapped_mget
|
48
|
+
|
49
|
+
def get(key)
|
50
|
+
return get_primary(key) if @replicas.empty?
|
51
|
+
|
52
|
+
@replicas.sample(1).first.get(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def mget(*keys, &blk)
|
56
|
+
return mget_primary(*keys, &blk) if @replicas.empty?
|
57
|
+
|
58
|
+
@replicas.sample(1).first.mget(*keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
def mapped_mget(*keys)
|
62
|
+
return mapped_mget_primary(*keys) if @replicas.empty?
|
63
|
+
|
64
|
+
@replicas.sample(1).first.mapped_mget(*keys)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'options'
|
3
|
+
|
4
|
+
class RedisMemo::Tracer
|
5
|
+
def self.trace(span_name, method_id, &blk)
|
6
|
+
tracer = RedisMemo::DefaultOptions.tracer
|
7
|
+
return blk.call if tracer.nil?
|
8
|
+
|
9
|
+
tracer.trace(span_name, resource: method_id, service: 'redis_memo') do
|
10
|
+
blk.call
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.set_tag(cache_hit:)
|
15
|
+
tracer = RedisMemo::DefaultOptions.tracer
|
16
|
+
return if tracer.nil? || !tracer.respond_to?(:active_span)
|
17
|
+
|
18
|
+
active_span = tracer.active_span
|
19
|
+
return if !active_span.respond_to?(:set_tag)
|
20
|
+
|
21
|
+
active_span.set_tag('cache_hit', cache_hit)
|
22
|
+
end
|
23
|
+
end
|
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.0.0.
|
4
|
+
version: 0.0.0.beta
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chan Zuckerberg Initiative
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
10
10
|
cert_chain: []
|
11
11
|
date: 2020-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: redis
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -25,35 +39,63 @@ dependencies:
|
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '4'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codecov
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
30
58
|
requirements:
|
31
59
|
- - ">="
|
32
60
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0
|
61
|
+
version: '0'
|
34
62
|
type: :development
|
35
63
|
prerelease: false
|
36
64
|
version_requirements: !ruby/object:Gem::Requirement
|
37
65
|
requirements:
|
38
66
|
- - ">="
|
39
67
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0
|
68
|
+
version: '0'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
70
|
+
name: database_cleaner-active_record
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
44
72
|
requirements:
|
45
73
|
- - ">="
|
46
74
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0
|
75
|
+
version: '0'
|
48
76
|
type: :development
|
49
77
|
prerelease: false
|
50
78
|
version_requirements: !ruby/object:Gem::Requirement
|
51
79
|
requirements:
|
52
80
|
- - ">="
|
53
81
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0
|
82
|
+
version: '0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
84
|
+
name: pg
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
58
100
|
requirements:
|
59
101
|
- - ">="
|
@@ -100,7 +142,25 @@ executables: []
|
|
100
142
|
extensions: []
|
101
143
|
extra_rdoc_files: []
|
102
144
|
files:
|
145
|
+
- lib/redis-memo.rb
|
103
146
|
- lib/redis_memo.rb
|
147
|
+
- lib/redis_memo/after_commit.rb
|
148
|
+
- lib/redis_memo/batch.rb
|
149
|
+
- lib/redis_memo/cache.rb
|
150
|
+
- lib/redis_memo/future.rb
|
151
|
+
- lib/redis_memo/memoizable.rb
|
152
|
+
- lib/redis_memo/memoizable/dependency.rb
|
153
|
+
- lib/redis_memo/memoizable/invalidation.rb
|
154
|
+
- lib/redis_memo/memoize_method.rb
|
155
|
+
- lib/redis_memo/memoize_method.rbi
|
156
|
+
- lib/redis_memo/memoize_records.rb
|
157
|
+
- lib/redis_memo/memoize_records/cached_select.rb
|
158
|
+
- lib/redis_memo/memoize_records/invalidation.rb
|
159
|
+
- lib/redis_memo/memoize_records/model_callback.rb
|
160
|
+
- lib/redis_memo/middleware.rb
|
161
|
+
- lib/redis_memo/options.rb
|
162
|
+
- lib/redis_memo/redis.rb
|
163
|
+
- lib/redis_memo/tracer.rb
|
104
164
|
homepage: https://github.com/chanzuckerberg/redis-memo
|
105
165
|
licenses:
|
106
166
|
- MIT
|
@@ -120,9 +180,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
180
|
- !ruby/object:Gem::Version
|
121
181
|
version: 1.3.1
|
122
182
|
requirements: []
|
123
|
-
|
124
|
-
rubygems_version: 2.7.6.2
|
183
|
+
rubygems_version: 3.0.8
|
125
184
|
signing_key:
|
126
185
|
specification_version: 4
|
127
|
-
summary: Redis
|
186
|
+
summary: A Redis-based version-addressable caching system. Memoize pure functions,
|
187
|
+
aggregated database queries, and 3rd party API calls.
|
128
188
|
test_files: []
|