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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1316982523363ce87b0ada30b82b8200559a07b2f1a6bea54168b40b2afd397e
|
4
|
+
data.tar.gz: 1278e1ff6b7cc2275416c7af9b8668a13128c105a4cde74640deccd0a4380373
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 121069c8de985a8977234776ad15fd209c1c0db523c361a5caf9181ed7120533350af16c354d7f76dfc03168230248e37c64b37eb8ce9f6af16cfdc9160448f3
|
7
|
+
data.tar.gz: 133d18912a95eb599554b9cacb5c5932471772930165249381baaa8aeb403a3a6b21c6e17711a11eda56a1fa6dbf347396c7fe513cec3b53ec6f0c592d5b7d9a
|
data/lib/redis-memo.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'redis_memo'
|
data/lib/redis_memo.rb
CHANGED
@@ -1,6 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'digest'
|
4
|
+
require 'json'
|
4
5
|
|
5
6
|
module RedisMemo
|
7
|
+
require 'redis_memo/memoize_method'
|
8
|
+
require 'redis_memo/memoize_records'
|
9
|
+
|
10
|
+
DefaultOptions = RedisMemo::Options.new
|
11
|
+
|
12
|
+
def self.configure(&blk)
|
13
|
+
blk.call(DefaultOptions)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.batch(&blk)
|
17
|
+
RedisMemo::Batch.open
|
18
|
+
blk.call
|
19
|
+
RedisMemo::Batch.execute
|
20
|
+
ensure
|
21
|
+
RedisMemo::Batch.close
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.checksum(serialized)
|
25
|
+
Digest::SHA1.base64digest(serialized)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.deep_sort_hash(orig_hash)
|
29
|
+
{}.tap do |new_hash|
|
30
|
+
orig_hash.sort.each do |k, v|
|
31
|
+
new_hash[k] = v.is_a?(Hash) ? deep_sort_hash(v) : v
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
|
37
|
+
|
38
|
+
def self.without_memo?
|
39
|
+
Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.without_memo
|
43
|
+
prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
|
44
|
+
Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
|
45
|
+
yield
|
46
|
+
ensure
|
47
|
+
Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
|
48
|
+
end
|
49
|
+
|
50
|
+
class ArgumentError < ::ArgumentError; end
|
51
|
+
class RuntimeError < ::RuntimeError; end
|
6
52
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# TODO: -> RedisMemo::Memoizable::AfterCommit
|
3
|
+
|
4
|
+
class RedisMemo::AfterCommit
|
5
|
+
# We assume there's only one ActiveRecord DB connection used for opening
|
6
|
+
# transactions
|
7
|
+
@@callback_added = false
|
8
|
+
@@pending_memo_versions = {}
|
9
|
+
@@previous_memo_versions = {}
|
10
|
+
|
11
|
+
def self.connection
|
12
|
+
ActiveRecord::Base.connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.pending_memo_versions
|
16
|
+
# In DB transactions, the pending memo version should be used immediately
|
17
|
+
# as part of method checksums. method_cache_keys made of
|
18
|
+
# pending_memo_versions are not referencable until we bump the
|
19
|
+
# pending_memo_versions after commiting the current transaction
|
20
|
+
@@pending_memo_versions
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.bump_memo_version_after_commit(key, version, previous_version:)
|
24
|
+
@@pending_memo_versions[key] = version
|
25
|
+
@@previous_memo_versions[key] = previous_version
|
26
|
+
|
27
|
+
reset_after_transaction
|
28
|
+
end
|
29
|
+
|
30
|
+
# https://github.com/Envek/after_commit_everywhere/blob/be8602f9fbb8e40b0fc8a04a47e4c2bc6b560ad5/lib/after_commit_everywhere.rb#L93
|
31
|
+
# Helper method to determine whether we're currently in transaction or not
|
32
|
+
def self.in_transaction?
|
33
|
+
# service transactions (tests and database_cleaner) are not joinable
|
34
|
+
connection.transaction_open? && connection.current_transaction.joinable?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.after_commit(&blk)
|
40
|
+
connection.add_transaction_record(
|
41
|
+
RedisMemo::AfterCommit::Callback.new(connection, committed: blk),
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.after_rollback(&blk)
|
46
|
+
connection.add_transaction_record(
|
47
|
+
RedisMemo::AfterCommit::Callback.new(connection, rolledback: blk),
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.reset_after_transaction
|
52
|
+
return if @@callback_added
|
53
|
+
@@callback_added = true
|
54
|
+
|
55
|
+
after_commit do
|
56
|
+
reset(commited: true)
|
57
|
+
end
|
58
|
+
|
59
|
+
after_rollback do
|
60
|
+
reset(commited: false)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.reset(commited:)
|
65
|
+
if commited
|
66
|
+
@@pending_memo_versions.each do |key, version|
|
67
|
+
invalidation_queue =
|
68
|
+
RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue)
|
69
|
+
invalidation_queue << [key, version, @@previous_memo_versions[key]]
|
70
|
+
end
|
71
|
+
|
72
|
+
RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
|
73
|
+
else
|
74
|
+
@@pending_memo_versions.each_key do |key|
|
75
|
+
RedisMemo::Cache.local_cache&.delete(key)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
@@callback_added = false
|
79
|
+
@@pending_memo_versions.clear
|
80
|
+
@@previous_memo_versions.clear
|
81
|
+
end
|
82
|
+
|
83
|
+
# https://github.com/Envek/after_commit_everywhere/blob/master/lib/after_commit_everywhere/wrap.rb
|
84
|
+
class Callback
|
85
|
+
def initialize(connection, committed: nil, rolledback: nil)
|
86
|
+
@connection = connection
|
87
|
+
@committed = committed
|
88
|
+
@rolledback = rolledback
|
89
|
+
end
|
90
|
+
|
91
|
+
def has_transactional_callbacks?
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
def trigger_transactional_callbacks?
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def committed!(*)
|
100
|
+
@committed&.call
|
101
|
+
end
|
102
|
+
|
103
|
+
# Required for +transaction(requires_new: true)+
|
104
|
+
def add_to_transaction(*)
|
105
|
+
@connection.add_transaction_record(self)
|
106
|
+
end
|
107
|
+
|
108
|
+
def before_committed!(*); end
|
109
|
+
|
110
|
+
def rolledback!(*)
|
111
|
+
@rolledback&.call
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'cache'
|
3
|
+
require_relative 'tracer'
|
4
|
+
|
5
|
+
class RedisMemo::Batch
|
6
|
+
THREAD_KEY = :__redis_memo_current_batch__
|
7
|
+
|
8
|
+
def self.open
|
9
|
+
if current
|
10
|
+
raise RedisMemo::RuntimeError, 'Batch can not be nested'
|
11
|
+
end
|
12
|
+
|
13
|
+
Thread.current[THREAD_KEY] = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.close
|
17
|
+
if current
|
18
|
+
futures = current
|
19
|
+
Thread.current[THREAD_KEY] = nil
|
20
|
+
futures
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.current
|
25
|
+
Thread.current[THREAD_KEY]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.execute
|
29
|
+
futures = close
|
30
|
+
return unless futures
|
31
|
+
|
32
|
+
cached_results = {}
|
33
|
+
method_cache_keys = nil
|
34
|
+
|
35
|
+
RedisMemo::Tracer.trace('redis_memo.cache.batch.read', nil) do
|
36
|
+
method_cache_keys = RedisMemo::MemoizeMethod.method_cache_keys(
|
37
|
+
futures.map(&:context),
|
38
|
+
)
|
39
|
+
|
40
|
+
if method_cache_keys
|
41
|
+
cached_results = RedisMemo::Cache.read_multi(*method_cache_keys)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
RedisMemo::Tracer.trace('redis_memo.cache.batch.execute', nil) do
|
46
|
+
results = Array.new(futures.size)
|
47
|
+
futures.each_with_index do |future, i|
|
48
|
+
future.method_cache_key = method_cache_keys ? method_cache_keys[i] : ''
|
49
|
+
results[i] = future.execute(cached_results)
|
50
|
+
end
|
51
|
+
results
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'options'
|
3
|
+
require_relative 'redis'
|
4
|
+
|
5
|
+
class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
|
6
|
+
class Rescuable < Exception; end
|
7
|
+
|
8
|
+
THREAD_KEY_LOCAL_CACHE = :__redis_memo_cache_local_cache__
|
9
|
+
THREAD_KEY_RAISE_ERROR = :__redis_memo_cache_raise_error__
|
10
|
+
|
11
|
+
@@redis = nil
|
12
|
+
@@redis_store = nil
|
13
|
+
|
14
|
+
@@redis_store_error_handler = proc do |method:, exception:, returning:|
|
15
|
+
RedisMemo::DefaultOptions.redis_error_handler&.call(exception, method)
|
16
|
+
RedisMemo::DefaultOptions.logger&.warn(exception.full_message)
|
17
|
+
|
18
|
+
if Thread.current[THREAD_KEY_RAISE_ERROR]
|
19
|
+
raise RedisMemo::Cache::Rescuable
|
20
|
+
else
|
21
|
+
returning
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.redis
|
26
|
+
@@redis ||= RedisMemo::DefaultOptions.redis
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.redis_store
|
30
|
+
@@redis_store ||= new(
|
31
|
+
compress: RedisMemo::DefaultOptions.compress,
|
32
|
+
compress_threshold: RedisMemo::DefaultOptions.compress_threshold,
|
33
|
+
error_handler: @@redis_store_error_handler,
|
34
|
+
expires_in: RedisMemo::DefaultOptions.expires_in,
|
35
|
+
redis: redis,
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# We use our own local_cache instead of the one from RedisCacheStore, because
|
40
|
+
# the local_cache in RedisCacheStore stores a dumped
|
41
|
+
# ActiveSupport::Cache::Entry object -- which is slower comparing to a simple
|
42
|
+
# hash storing object references
|
43
|
+
def self.local_cache
|
44
|
+
Thread.current[THREAD_KEY_LOCAL_CACHE]
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
def with_local_cache(&blk)
|
49
|
+
Thread.current[THREAD_KEY_LOCAL_CACHE] = {}
|
50
|
+
blk.call
|
51
|
+
ensure
|
52
|
+
Thread.current[THREAD_KEY_LOCAL_CACHE] = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# RedisCacheStore doesn't read from the local cache before reading from redis
|
56
|
+
def read_multi(*keys, raise_error: false)
|
57
|
+
return {} if keys.empty?
|
58
|
+
|
59
|
+
Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
|
60
|
+
|
61
|
+
local_entries = local_cache&.slice(*keys) || {}
|
62
|
+
|
63
|
+
keys_to_fetch = keys
|
64
|
+
keys_to_fetch -= local_entries.keys unless local_entries.empty?
|
65
|
+
return local_entries if keys_to_fetch.empty?
|
66
|
+
|
67
|
+
remote_entries = redis_store.read_multi(*keys_to_fetch)
|
68
|
+
local_cache&.merge!(remote_entries)
|
69
|
+
|
70
|
+
if local_entries.empty?
|
71
|
+
remote_entries
|
72
|
+
else
|
73
|
+
local_entries.merge!(remote_entries)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def write(key, value, disable_async: false, raise_error: false, **options)
|
78
|
+
Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
|
79
|
+
|
80
|
+
if local_cache
|
81
|
+
local_cache[key] = value
|
82
|
+
end
|
83
|
+
|
84
|
+
async = RedisMemo::DefaultOptions.async
|
85
|
+
if async.nil? || disable_async
|
86
|
+
redis_store.write(key, value, **options)
|
87
|
+
else
|
88
|
+
async.call do
|
89
|
+
Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
|
90
|
+
redis_store.write(key, value, **options)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'cache'
|
3
|
+
require_relative 'tracer'
|
4
|
+
|
5
|
+
class RedisMemo::Future
|
6
|
+
attr_writer :method_cache_key
|
7
|
+
|
8
|
+
def initialize(
|
9
|
+
ref,
|
10
|
+
method_id,
|
11
|
+
method_args,
|
12
|
+
depends_on,
|
13
|
+
cache_options,
|
14
|
+
method_name_without_memo
|
15
|
+
)
|
16
|
+
@ref = ref
|
17
|
+
@method_id = method_id
|
18
|
+
@method_args = method_args
|
19
|
+
@depends_on = depends_on
|
20
|
+
@cache_options = cache_options
|
21
|
+
@method_name_without_memo = method_name_without_memo
|
22
|
+
@method_cache_key = nil
|
23
|
+
@cache_hit = false
|
24
|
+
@cached_result = nil
|
25
|
+
@computed_cached_result = false
|
26
|
+
@fresh_result = nil
|
27
|
+
@computed_fresh_result = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def context
|
31
|
+
[@ref, @method_id, @method_args, @depends_on]
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_cache_key
|
35
|
+
@method_cache_key ||=
|
36
|
+
RedisMemo::MemoizeMethod.method_cache_keys([context])&.first || ''
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute(cached_results=nil)
|
40
|
+
if RedisMemo::Batch.current
|
41
|
+
raise RedisMemo::RuntimeError, 'Cannot execute future when a batch is still open'
|
42
|
+
end
|
43
|
+
|
44
|
+
if cache_hit?(cached_results)
|
45
|
+
validate_cache_result =
|
46
|
+
RedisMemo::DefaultOptions.cache_validation_sampler&.call(@method_id)
|
47
|
+
|
48
|
+
if validate_cache_result && cached_result != fresh_result
|
49
|
+
RedisMemo::DefaultOptions.cache_out_of_date_handler&.call(
|
50
|
+
@ref,
|
51
|
+
@method_id,
|
52
|
+
@method_args,
|
53
|
+
cached_result,
|
54
|
+
fresh_result,
|
55
|
+
)
|
56
|
+
return fresh_result
|
57
|
+
end
|
58
|
+
|
59
|
+
return cached_result
|
60
|
+
end
|
61
|
+
|
62
|
+
fresh_result
|
63
|
+
end
|
64
|
+
|
65
|
+
def result
|
66
|
+
unless @computed_cached_result
|
67
|
+
raise RedisMemo::RuntimeError, 'Future has not been executed'
|
68
|
+
end
|
69
|
+
|
70
|
+
@fresh_result || @cached_result
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def cache_hit?(cached_results=nil)
|
76
|
+
cached_result(cached_results)
|
77
|
+
|
78
|
+
@cache_hit
|
79
|
+
end
|
80
|
+
|
81
|
+
def cached_result(cached_results=nil)
|
82
|
+
return @cached_result if @computed_cached_result
|
83
|
+
|
84
|
+
@cache_hit = false
|
85
|
+
RedisMemo::Tracer.trace('redis_memo.cache.read', @method_id) do
|
86
|
+
# Calculate the method_cache_key now, or use the method_cache_key obtained
|
87
|
+
# from batching previously
|
88
|
+
if !method_cache_key.empty?
|
89
|
+
cached_results ||= RedisMemo::Cache.read_multi(method_cache_key)
|
90
|
+
@cache_hit = cached_results.include?(method_cache_key)
|
91
|
+
@cached_result = cached_results[method_cache_key]
|
92
|
+
end
|
93
|
+
RedisMemo::Tracer.set_tag(cache_hit: @cache_hit)
|
94
|
+
end
|
95
|
+
|
96
|
+
@computed_cached_result = true
|
97
|
+
@cached_result
|
98
|
+
end
|
99
|
+
|
100
|
+
def fresh_result
|
101
|
+
return @fresh_result if @computed_fresh_result
|
102
|
+
|
103
|
+
RedisMemo::Tracer.trace('redis_memo.cache.write', @method_id) do
|
104
|
+
# cache miss
|
105
|
+
@fresh_result = @ref.send(@method_name_without_memo, *@method_args)
|
106
|
+
if @cache_options.include?(:expires_in) && @cache_options[:expires_in].respond_to?(:call)
|
107
|
+
@cache_options[:expires_in] = @cache_options[:expires_in].call(@fresh_result)
|
108
|
+
end
|
109
|
+
|
110
|
+
if !method_cache_key.empty? && (
|
111
|
+
# Write back fresh result if cache miss
|
112
|
+
!@cache_hit || (
|
113
|
+
# or cached result is out of date (sampled to validate the cache
|
114
|
+
# result)
|
115
|
+
@cache_hit && @cached_result != @fresh_result
|
116
|
+
)
|
117
|
+
)
|
118
|
+
RedisMemo::Cache.write(method_cache_key, @fresh_result, **@cache_options)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
@computed_fresh_result = true
|
123
|
+
@fresh_result
|
124
|
+
end
|
125
|
+
end
|