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
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
|