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,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
class RedisMemo::Memoizable
|
5
|
+
require_relative 'memoizable/dependency'
|
6
|
+
require_relative 'memoizable/invalidation'
|
7
|
+
|
8
|
+
attr_accessor :props
|
9
|
+
attr_reader :depends_on
|
10
|
+
|
11
|
+
def initialize(**props, &depends_on)
|
12
|
+
@props = props
|
13
|
+
@depends_on = depends_on
|
14
|
+
@cache_key = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def extra_props(**args)
|
18
|
+
instance = dup
|
19
|
+
instance.props = props.dup.merge(**args)
|
20
|
+
instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_key
|
24
|
+
@cache_key ||= [
|
25
|
+
self.class.name,
|
26
|
+
RedisMemo.checksum(
|
27
|
+
RedisMemo.deep_sort_hash(@props).to_json,
|
28
|
+
),
|
29
|
+
].join(':')
|
30
|
+
end
|
31
|
+
|
32
|
+
# Calculate the checksums for all memoizable groups in one Redis round trip
|
33
|
+
def self.checksums(instances_groups)
|
34
|
+
dependents_cache_keys = []
|
35
|
+
cache_key_groups = instances_groups.map do |instances|
|
36
|
+
cache_keys = instances.map(&:cache_key)
|
37
|
+
dependents_cache_keys += cache_keys
|
38
|
+
cache_keys
|
39
|
+
end
|
40
|
+
|
41
|
+
dependents_cache_keys.uniq!
|
42
|
+
dependents_versions = find_or_create_versions(dependents_cache_keys)
|
43
|
+
version_hash = dependents_cache_keys.zip(dependents_versions).to_h
|
44
|
+
|
45
|
+
cache_key_groups.map do |cache_keys|
|
46
|
+
RedisMemo.checksum(version_hash.slice(*cache_keys).to_json)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.invalidate(instances)
|
51
|
+
instances.each do |instance|
|
52
|
+
cache_key = instance.cache_key
|
53
|
+
RedisMemo::Memoizable::Invalidation.bump_version_later(
|
54
|
+
cache_key,
|
55
|
+
SecureRandom.uuid,
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def self.find_or_create_versions(keys)
|
65
|
+
need_to_bump_versions = false
|
66
|
+
|
67
|
+
# Must check the local pending_memo_versions first in order to generate
|
68
|
+
# memo checksums. The pending_memo_versions are the expected versions that
|
69
|
+
# would be used if a transaction commited. With checksums consistent of
|
70
|
+
# pending versions, the method results would only be visible after a
|
71
|
+
# transaction commited (we bump the pending_memo_versions on redis as an
|
72
|
+
# after_commit callback)
|
73
|
+
if RedisMemo::AfterCommit.in_transaction?
|
74
|
+
memo_versions = RedisMemo::AfterCommit.pending_memo_versions.slice(*keys)
|
75
|
+
else
|
76
|
+
memo_versions = {}
|
77
|
+
end
|
78
|
+
|
79
|
+
keys_to_fetch = keys
|
80
|
+
keys_to_fetch -= memo_versions.keys unless memo_versions.empty?
|
81
|
+
|
82
|
+
cached_versions =
|
83
|
+
if keys_to_fetch.empty?
|
84
|
+
{}
|
85
|
+
else
|
86
|
+
RedisMemo::Cache.read_multi(*keys_to_fetch, raise_error: true)
|
87
|
+
end
|
88
|
+
memo_versions.merge!(cached_versions) unless cached_versions.empty?
|
89
|
+
|
90
|
+
versions = keys.map do |key|
|
91
|
+
version = memo_versions[key]
|
92
|
+
if version.nil?
|
93
|
+
# If a version does not exist, we assume it's because the version has
|
94
|
+
# expired due to TTL or it's evicted by a cache eviction policy. In
|
95
|
+
# this case, we will create a new version and use it for memoizing the
|
96
|
+
# cached result.
|
97
|
+
need_to_bump_versions = true
|
98
|
+
|
99
|
+
new_version = SecureRandom.uuid
|
100
|
+
RedisMemo::Memoizable::Invalidation.bump_version_later(
|
101
|
+
key,
|
102
|
+
new_version,
|
103
|
+
previous_version: '',
|
104
|
+
)
|
105
|
+
new_version
|
106
|
+
else
|
107
|
+
version
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Flush out the versions to Redis (async) if we created new versions
|
112
|
+
RedisMemo::Memoizable::Invalidation.drain_invalidation_queue if need_to_bump_versions
|
113
|
+
|
114
|
+
versions
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A Directed Acyclic Graph (DAG) of Memoizables
|
4
|
+
class RedisMemo::Memoizable::Dependency
|
5
|
+
attr_accessor :nodes
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@nodes = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def memos
|
12
|
+
@nodes.values
|
13
|
+
end
|
14
|
+
|
15
|
+
def depends_on(memo_or_model, **conditions)
|
16
|
+
if !memo_or_model.is_a?(RedisMemo::Memoizable)
|
17
|
+
[
|
18
|
+
memo_or_model.redis_memo_class_memoizable,
|
19
|
+
RedisMemo::MemoizeRecords.create_memo(memo_or_model, **conditions),
|
20
|
+
].each do |memo|
|
21
|
+
nodes[memo.cache_key] = memo
|
22
|
+
end
|
23
|
+
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
memo = memo_or_model
|
28
|
+
return if nodes.include?(memo.cache_key)
|
29
|
+
nodes[memo.cache_key] = memo
|
30
|
+
|
31
|
+
if memo.depends_on
|
32
|
+
# Extract dependencies from the current memoizable and recurse
|
33
|
+
instance_exec(&memo.depends_on)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../after_commit'
|
3
|
+
require_relative '../cache'
|
4
|
+
|
5
|
+
module RedisMemo::Memoizable::Invalidation
|
6
|
+
# This is a thread safe data structure
|
7
|
+
#
|
8
|
+
# Handle transient network errors during cache invalidation
|
9
|
+
# Each item in the queue is a tuple:
|
10
|
+
#
|
11
|
+
# [key, version (a UUID), previous_version (a UUID)]
|
12
|
+
#
|
13
|
+
# When an invalidation call arrives at Redis, we only bump to the specified
|
14
|
+
# version (so the cached results using that version will become visible) if
|
15
|
+
# the actual and expected previous_version on Redis match, to ensure eventual
|
16
|
+
# consistency: If the versions mismatch, we will use a new version that has
|
17
|
+
# not been associated with any cached_results.
|
18
|
+
#
|
19
|
+
# - No invalid cached results will be read
|
20
|
+
#
|
21
|
+
# - New memoized calculations will write back the fresh_results using the
|
22
|
+
# new version as part of their checksums.
|
23
|
+
#
|
24
|
+
# Note: Cached data is not guaranteed to be consistent by design. Between the
|
25
|
+
# moment we should invalidate a version and the moment we actually
|
26
|
+
# invalidated a version, we would serve out-dated cached results, as if the
|
27
|
+
# operations that triggered the invalidation has not yet happened.
|
28
|
+
@@invalidation_queue = Queue.new
|
29
|
+
|
30
|
+
def self.bump_version_later(key, version, previous_version: nil)
|
31
|
+
RedisMemo::DefaultOptions.logger&.info("[received] Bump memo key #{key}")
|
32
|
+
|
33
|
+
if RedisMemo::AfterCommit.in_transaction?
|
34
|
+
previous_version ||= RedisMemo::AfterCommit.pending_memo_versions[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
local_cache = RedisMemo::Cache.local_cache
|
38
|
+
if previous_version.nil? && local_cache&.include?(key)
|
39
|
+
previous_version = local_cache[key]
|
40
|
+
elsif RedisMemo::AfterCommit.in_transaction?
|
41
|
+
# Fill an expected previous version so the later calculation results
|
42
|
+
# based on this version can still be rolled out if this version
|
43
|
+
# does not change
|
44
|
+
previous_version ||= RedisMemo::Cache.read_multi(key)[key]
|
45
|
+
end
|
46
|
+
|
47
|
+
local_cache&.send(:[]=, key, version)
|
48
|
+
if RedisMemo::AfterCommit.in_transaction?
|
49
|
+
RedisMemo::AfterCommit.bump_memo_version_after_commit(
|
50
|
+
key,
|
51
|
+
version,
|
52
|
+
previous_version: previous_version,
|
53
|
+
)
|
54
|
+
else
|
55
|
+
@@invalidation_queue << [key, version, previous_version]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.drain_invalidation_queue
|
60
|
+
async_handler = RedisMemo::DefaultOptions.async
|
61
|
+
if async_handler.nil?
|
62
|
+
drain_invalidation_queue_now
|
63
|
+
else
|
64
|
+
async_handler.call do
|
65
|
+
drain_invalidation_queue_now
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
LUA_BUMP_VERSION = <<~LUA
|
71
|
+
local key = KEYS[1]
|
72
|
+
local expected_prev_version,
|
73
|
+
desired_new_version,
|
74
|
+
version_on_mismatch,
|
75
|
+
ttl = unpack(ARGV)
|
76
|
+
|
77
|
+
local actual_prev_version = redis.call('get', key)
|
78
|
+
local new_version = version_on_mismatch
|
79
|
+
local px = {}
|
80
|
+
|
81
|
+
if (not actual_prev_version and expected_prev_version == '') or expected_prev_version == actual_prev_version then
|
82
|
+
new_version = desired_new_version
|
83
|
+
end
|
84
|
+
|
85
|
+
if ttl ~= '' then
|
86
|
+
px = {'px', ttl}
|
87
|
+
end
|
88
|
+
|
89
|
+
return redis.call('set', key, new_version, unpack(px))
|
90
|
+
LUA
|
91
|
+
|
92
|
+
def self.bump_version(cache_key, version, previous_version:)
|
93
|
+
RedisMemo::Tracer.trace('redis_memo.memoizable.bump_version', nil) do
|
94
|
+
ttl = RedisMemo::DefaultOptions.expires_in
|
95
|
+
ttl = (ttl * 1000.0).to_i if ttl
|
96
|
+
RedisMemo::Cache.redis.eval(
|
97
|
+
LUA_BUMP_VERSION,
|
98
|
+
keys: [cache_key],
|
99
|
+
argv: [previous_version, version, SecureRandom.uuid, ttl],
|
100
|
+
)
|
101
|
+
end
|
102
|
+
RedisMemo::DefaultOptions.logger&.info("[performed] Bump memo key #{cache_key}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.drain_invalidation_queue_now
|
106
|
+
retry_queue = []
|
107
|
+
until @@invalidation_queue.empty?
|
108
|
+
tuple = @@invalidation_queue.pop
|
109
|
+
begin
|
110
|
+
bump_version(tuple[0], tuple[1], previous_version: tuple[2])
|
111
|
+
rescue SignalException, Redis::BaseConnectionError
|
112
|
+
retry_queue << tuple
|
113
|
+
end
|
114
|
+
end
|
115
|
+
ensure
|
116
|
+
retry_queue.each { |t| @@invalidation_queue << t }
|
117
|
+
end
|
118
|
+
|
119
|
+
at_exit do
|
120
|
+
# The best effort
|
121
|
+
drain_invalidation_queue_now
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'batch'
|
3
|
+
require_relative 'future'
|
4
|
+
require_relative 'memoizable'
|
5
|
+
require_relative 'middleware'
|
6
|
+
require_relative 'options'
|
7
|
+
|
8
|
+
module RedisMemo::MemoizeMethod
|
9
|
+
def memoize_method(method_name, **options, &depends_on)
|
10
|
+
method_name_without_memo = :"_redis_memo_#{method_name}_without_memo"
|
11
|
+
method_name_with_memo = :"_redis_memo_#{method_name}_with_memo"
|
12
|
+
|
13
|
+
alias_method method_name_without_memo, method_name
|
14
|
+
|
15
|
+
define_method method_name_with_memo do |*args|
|
16
|
+
return send(method_name_without_memo, *args) if RedisMemo.without_memo?
|
17
|
+
|
18
|
+
future = RedisMemo::Future.new(
|
19
|
+
self,
|
20
|
+
RedisMemo::MemoizeMethod.method_id(self, method_name),
|
21
|
+
args,
|
22
|
+
depends_on,
|
23
|
+
options,
|
24
|
+
method_name_without_memo,
|
25
|
+
)
|
26
|
+
|
27
|
+
if RedisMemo::Batch.current
|
28
|
+
RedisMemo::Batch.current << future
|
29
|
+
return future
|
30
|
+
end
|
31
|
+
|
32
|
+
future.execute
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method method_name, method_name_with_memo
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.method_id(ref, method_name)
|
39
|
+
is_class_method = ref.class == Class
|
40
|
+
class_name = is_class_method ? ref.name : ref.class.name
|
41
|
+
|
42
|
+
"#{class_name}#{is_class_method ? '::' : '#'}#{method_name}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.method_cache_keys(future_contexts)
|
46
|
+
memos = Array.new(future_contexts.size)
|
47
|
+
future_contexts.each_with_index do |(ref, _, method_args, depends_on), i|
|
48
|
+
if depends_on
|
49
|
+
dependency = RedisMemo::Memoizable::Dependency.new
|
50
|
+
|
51
|
+
# Resolve the dependency recursively
|
52
|
+
dependency.instance_exec(ref, *method_args, &depends_on)
|
53
|
+
|
54
|
+
memos[i] = dependency.memos
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
j = 0
|
59
|
+
memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
|
60
|
+
method_cache_key_versions = Array.new(future_contexts.size)
|
61
|
+
future_contexts.each_with_index do |(_, method_id, method_args, _), i|
|
62
|
+
if memos[i]
|
63
|
+
method_cache_key_versions[i] = [method_id, memo_checksums[j]]
|
64
|
+
j += 1
|
65
|
+
else
|
66
|
+
ordered_method_args = method_args.map do |arg|
|
67
|
+
arg.is_a?(Hash) ? RedisMemo.deep_sort_hash(arg) : arg
|
68
|
+
end
|
69
|
+
|
70
|
+
method_cache_key_versions[i] = [
|
71
|
+
method_id,
|
72
|
+
RedisMemo.checksum(ordered_method_args.to_json),
|
73
|
+
]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
method_cache_key_versions.map do |method_id, method_cache_key_version|
|
78
|
+
# Example:
|
79
|
+
#
|
80
|
+
# RedisMemo:MyModel#slow_calculation:<global cache version>:<local
|
81
|
+
# cache version>
|
82
|
+
#
|
83
|
+
[
|
84
|
+
RedisMemo.name,
|
85
|
+
method_id,
|
86
|
+
RedisMemo::DefaultOptions.global_cache_key_version,
|
87
|
+
method_cache_key_version,
|
88
|
+
].join(':')
|
89
|
+
end
|
90
|
+
rescue RedisMemo::Cache::Rescuable
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'memoize_method'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Automatically invalidate memoizable when modifying ActiveRecords objects.
|
6
|
+
# You still need to invalidate memos when you are using SQL queries to perform
|
7
|
+
# update / delete (does not trigger record callbacks)
|
8
|
+
#
|
9
|
+
module RedisMemo::MemoizeRecords
|
10
|
+
require_relative 'memoize_records/cached_select'
|
11
|
+
require_relative 'memoize_records/invalidation'
|
12
|
+
require_relative 'memoize_records/model_callback'
|
13
|
+
|
14
|
+
# TODO: MemoizeRecords -> MemoizeQuery
|
15
|
+
def memoize_records
|
16
|
+
RedisMemo::MemoizeRecords.using_active_record!(self)
|
17
|
+
|
18
|
+
memoize_table_column(primary_key.to_sym, editable: false)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Only editable columns will be used to create memos that are invalidatable
|
22
|
+
# after each record save
|
23
|
+
def memoize_table_column(*raw_columns, editable: true)
|
24
|
+
RedisMemo::MemoizeRecords.using_active_record!(self)
|
25
|
+
|
26
|
+
columns = raw_columns.map(&:to_sym).sort
|
27
|
+
|
28
|
+
RedisMemo::MemoizeRecords.memoized_columns(self, editable_only: true) << columns if editable
|
29
|
+
RedisMemo::MemoizeRecords.memoized_columns(self, editable_only: false) << columns
|
30
|
+
|
31
|
+
RedisMemo::MemoizeRecords::ModelCallback.install(self)
|
32
|
+
RedisMemo::MemoizeRecords::Invalidation.install(self)
|
33
|
+
|
34
|
+
if ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] != 'true'
|
35
|
+
RedisMemo::MemoizeRecords::CachedSelect.install(ActiveRecord::Base.connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
columns.each do |column|
|
39
|
+
unless self.columns_hash.include?(column.to_s)
|
40
|
+
raise(
|
41
|
+
RedisMemo::ArgumentError,
|
42
|
+
"'#{self.name}' does not contain column '#{column}'",
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
47
|
+
# no-opts: models with memoize_table_column decleared might be loaded in
|
48
|
+
# rake tasks that are used to create databases
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.using_active_record!(model_class)
|
52
|
+
unless model_class.respond_to?(:<) && model_class < ActiveRecord::Base
|
53
|
+
raise RedisMemo::ArgumentError, "'#{model_class.name}' does not use ActiveRecord"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
|
58
|
+
|
59
|
+
def self.memoized_columns(model_or_table, editable_only: false)
|
60
|
+
table = model_or_table.is_a?(Class) ? model_or_table.table_name : model_or_table
|
61
|
+
@@memoized_columns[table.to_sym][editable_only ? 1 : 0]
|
62
|
+
end
|
63
|
+
|
64
|
+
# extra_props are considered as AND conditions on the model class
|
65
|
+
def self.create_memo(model_class, **extra_props)
|
66
|
+
RedisMemo::MemoizeRecords.using_active_record!(model_class)
|
67
|
+
|
68
|
+
keys = extra_props.keys.sort
|
69
|
+
if !keys.empty? && !RedisMemo::MemoizeRecords.memoized_columns(model_class).include?(keys)
|
70
|
+
raise(
|
71
|
+
RedisMemo::ArgumentError,
|
72
|
+
"'#{model_class.name}' has not memoized columns: #{keys}",
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
extra_props.each do |key, values|
|
77
|
+
# The data type is ensured by the database, thus we don't need to cast
|
78
|
+
# types here for better performance
|
79
|
+
column_name = key.to_s
|
80
|
+
values = [values] unless values.is_a?(Enumerable)
|
81
|
+
extra_props[key] =
|
82
|
+
if model_class.defined_enums.include?(column_name)
|
83
|
+
enum_mapping = model_class.defined_enums[column_name]
|
84
|
+
values.map do |value|
|
85
|
+
# Assume a value is a converted enum if it does not exist in the
|
86
|
+
# enum mapping
|
87
|
+
(enum_mapping[value.to_s] || value).to_s
|
88
|
+
end
|
89
|
+
else
|
90
|
+
values.map(&:to_s)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
RedisMemo::Memoizable.new(
|
95
|
+
__redis_memo_memoize_records_model_class_name__: model_class.name,
|
96
|
+
**extra_props,
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.invalidate_all(model_class)
|
101
|
+
RedisMemo::Tracer.trace(
|
102
|
+
'redis_memo.memoizable.invalidate_all',
|
103
|
+
model_class.name,
|
104
|
+
) do
|
105
|
+
RedisMemo::Memoizable.invalidate([model_class.redis_memo_class_memoizable])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.invalidate(record)
|
110
|
+
# Invalidate memos with current values
|
111
|
+
memos_to_invalidate = memoized_columns(record.class).map do |columns|
|
112
|
+
props = {}
|
113
|
+
columns.each do |column|
|
114
|
+
props[column] = record.send(column)
|
115
|
+
end
|
116
|
+
|
117
|
+
RedisMemo::MemoizeRecords.create_memo(record.class, **props)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Create memos with previous values if
|
121
|
+
# - there are saved changes
|
122
|
+
# - this is not creating a new record
|
123
|
+
if !record.saved_changes.empty? && !record.saved_changes.include?(record.class.primary_key)
|
124
|
+
previous_values = {}
|
125
|
+
record.saved_changes.each do |column, (previous_value, _)|
|
126
|
+
previous_values[column.to_sym] = previous_value
|
127
|
+
end
|
128
|
+
|
129
|
+
memoized_columns(record.class, editable_only: true).each do |columns|
|
130
|
+
props = previous_values.slice(*columns)
|
131
|
+
next if props.empty?
|
132
|
+
|
133
|
+
# Fill the column values that have not changed
|
134
|
+
columns.each do |column|
|
135
|
+
next if props.include?(column)
|
136
|
+
|
137
|
+
props[column] = record.send(column)
|
138
|
+
end
|
139
|
+
|
140
|
+
memos_to_invalidate << RedisMemo::MemoizeRecords.create_memo(record.class, **props)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
RedisMemo::Memoizable.invalidate(memos_to_invalidate)
|
145
|
+
end
|
146
|
+
end
|