redis-memo 0.0.0.alpha → 0.0.0.beta
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 +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: []
|