Skip to content

Commit

Permalink
Add expire methods by key values
Browse files Browse the repository at this point in the history
  • Loading branch information
Hector Mendoza Jacobo committed Jun 8, 2021
1 parent b7564e7 commit a2f57ea
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/identity_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class DerivedModelError < StandardError; end

class LockWaitTimeout < StandardError; end

class MissingKeyName < StandardError; end

mattr_accessor :cache_namespace
self.cache_namespace = "IDC:#{CACHE_VERSION}:"

Expand Down
27 changes: 27 additions & 0 deletions lib/identity_cache/cached/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ def expire(record)
end
end

def expire_by_key_value(key_values)
missing_keys = missing_keys(key_values)
unless missing_keys.empty?
raise MissingKeyName,
"#{model.name} attribute #{alias_name} expire_by_key_value - "\
"required fields: #{key_fields.join(", ")}. "\
"missing: #{missing_keys.join(", ")}"
end

key = cache_key_by_values(key_values)
IdentityCache.cache.delete(key)
end

def cache_key(index_key)
values_hash = IdentityCache.memcache_hash(unhashed_values_cache_key_string(index_key))
"#{model.rails_cache_key_namespace}#{cache_key_prefix}#{values_hash}"
Expand All @@ -59,6 +72,11 @@ def load_one_from_db(key)
unique ? results.first : results
end

# @abstract
def pack_values_into_hash(_values)
raise NotImplementedError
end

private

# @abstract
Expand Down Expand Up @@ -100,6 +118,11 @@ def new_cache_key(record)
cache_key_from_key_values(new_key_values)
end

def cache_key_by_values(key_values)
new_key_values = key_fields.map { |field| key_values[field] }
cache_key_from_key_values(new_key_values)
end

def old_cache_key(record)
old_key_values = key_fields.map do |field|
field_string = field.to_s
Expand All @@ -118,6 +141,10 @@ def old_cache_key(record)
def fetch_method_suffix
"#{alias_name}_by_#{key_fields.join("_and_")}"
end

def missing_keys(key_values)
key_fields - key_values.keys
end
end
end
end
11 changes: 11 additions & 0 deletions lib/identity_cache/cached/attribute_by_multi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ def build
raise_if_scoped
cached_attribute.fetch(key_values)
end

model.define_singleton_method(:"expire_#{fetch_method_suffix}") do |*key_values|
raise_if_scoped
cached_attribute.expire_by_key_value(cached_attribute.pack_values_into_hash(key_values))
end
end

def pack_values_into_hash(values)
key_fields.each_with_object({}).with_index do |(key_name, hash), index|
hash[key_name] = values[index]
end
end

private
Expand Down
18 changes: 17 additions & 1 deletion lib/identity_cache/cached/attribute_by_one.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# frozen_string_literal: true

module IdentityCache
module Cached
class AttributeByOne < Attribute
Expand All @@ -22,6 +21,18 @@ def build
raise_if_scoped
cached_attribute.fetch_multi(keys)
end

model.define_singleton_method(:"expire_#{fetch_method_suffix}") do |key|
raise_if_scoped
cached_attribute.expire_by_key_value(cached_attribute.pack_values_into_hash(key))
end

model.define_singleton_method(:"expire_multi_#{fetch_method_suffix}") do |keys|
raise_if_scoped
keys.each do |key|
cached_attribute.expire_by_key_value(cached_attribute.pack_values_into_hash(key))
end
end
end

def fetch_multi(keys)
Expand Down Expand Up @@ -62,6 +73,11 @@ def load_multi_from_db(keys)
def cache_encode(db_value)
db_value
end

def pack_values_into_hash(value)
{ key_field.to_sym => value }
end

alias_method :cache_decode, :cache_encode

private
Expand Down
8 changes: 8 additions & 0 deletions lib/identity_cache/query_api.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

module IdentityCache
module QueryAPI
extend ActiveSupport::Concern
Expand All @@ -19,6 +20,13 @@ def all_cached_associations # :nodoc:
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
end

# Expire the cache by key values
def expire_by_key_values(key_values) # :nodoc:
cache_indexes.each do |cached_attribute|
cached_attribute.expire_by_key_value(key_values)
end
end

private

def raise_if_scoped
Expand Down
9 changes: 9 additions & 0 deletions test/attribute_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ def test_attribute_values_are_fetched_and_returned_on_cache_misses
assert(fetch.has_been_called_with?(@name_attribute_key, {}))
end

def test_attribute_expire_by_value_of_by_one_key
assert_equal("foo", AssociatedRecord.fetch_name_by_id(1))
AssociatedRecord.expire_name_by_id(1)

assert_queries(1) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id(1))
end
end

def test_attribute_values_are_returned_on_cache_hits
assert_equal("foo", AssociatedRecord.fetch_name_by_id(1))

Expand Down
32 changes: 32 additions & 0 deletions test/expire_attribute_by_multi_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true
require "test_helper"

class ExpireAttributeByMultiTest < IdentityCache::TestCase
NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE

def setup
super
AssociatedRecord.cache_attribute(:name, by: [:id, :item_id])

@parent = Item.create!(title: "bob")
@record = @parent.associated_records.create!(name: "foo")
@name_attribute_key = "#{NAMESPACE}attr:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s.inspect)}"
IdentityCache.cache.clear
end

def test_expire_attribute_by_multi
assert_queries(1) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id_and_item_id(1, 1))
end

assert_queries(0) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id_and_item_id(1, 1))
end

AssociatedRecord.expire_name_by_id_and_item_id(1, 1)

assert_queries(1) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id_and_item_id(1, 1))
end
end
end
68 changes: 68 additions & 0 deletions test/expire_by_key_values_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true
require "test_helper"

class ExpireByKeyValuesTest < IdentityCache::TestCase
NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE

def setup
super
AssociatedRecord.cache_attribute(:name)
AssociatedRecord.cache_attribute(:name, by: :item_id)
AssociatedRecord.cache_attribute(:name, by: [:id, :item_id])

@parent = Item.create!(title: "bob")
@record = @parent.associated_records.create!(name: "foo")
@name_attribute_key = "#{NAMESPACE}attr:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s.inspect)}"
IdentityCache.cache.clear
end

def test_expire_by_key_values
assert_queries(3) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id(1))
assert_equal("foo", AssociatedRecord.fetch_name_by_item_id(1))
assert_equal("foo", AssociatedRecord.fetch_name_by_id_and_item_id(1, 1))
end

assert_queries(0) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id(1))
assert_equal("foo", AssociatedRecord.fetch_name_by_item_id(1))
assert_equal("foo", AssociatedRecord.fetch_name_by_id_and_item_id(1, 1))
end

expire_hash = {
id: 1,
item_id: 1,
}
AssociatedRecord.expire_by_key_values(expire_hash)

assert_queries(3) do
assert_equal("foo", AssociatedRecord.fetch_name_by_id(1))
assert_equal("foo", AssociatedRecord.fetch_name_by_item_id(1))
assert_equal("foo", AssociatedRecord.fetch_name_by_id_and_item_id(1, 1))
end
end

def test_expire_by_key_values_raises_exception_on_missing_key
missing_item_id_error_message =
"AssociatedRecord attribute name expire_by_key_value - required fields: item_id. missing: item_id"
missing_id_error_message =
"AssociatedRecord attribute name expire_by_key_value - required fields: id. missing: id"
missing_item_two_error_message =
"AssociatedRecord attribute name expire_by_key_value - required fields: id, item_id, item_two. missing: item_two"

AssociatedRecord.cache_attribute(:name, by: [:id, :item_id, :item_two])
missing_item_id_error = assert_raises(IdentityCache::MissingKeyName) do
AssociatedRecord.expire_by_key_values({ id: 1 })
end
missing_id_error = assert_raises(IdentityCache::MissingKeyName) do
AssociatedRecord.expire_by_key_values({ item_id: 1 })
end
missing_item_two_error = assert_raises(IdentityCache::MissingKeyName) do
AssociatedRecord.expire_by_key_values({ id: 1, item_id: 1 })
end

assert_equal(missing_item_id_error_message, missing_item_id_error.message)
assert_equal(missing_id_error_message, missing_id_error.message)
assert_equal(missing_item_two_error_message, missing_item_two_error.message)
end
end
20 changes: 20 additions & 0 deletions test/fetch_multi_by_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,24 @@ def test_fetch_multi_attribute_by_unique_cache_key_with_unknown_key

assert_equal({ 1 => "bob", 999 => nil }, Item.fetch_multi_title_by_id([1, 999]))
end

def test_expire_multi_attribute_by_key
Item.cache_attribute(:title, by: :id)

@bob.save!
@bertha.save!

# We hydrate the cache
Item.fetch_multi_title_by_id([1, 2])
# Expire the cache
Item.expire_multi_title_by_id([1, 2])

assert_queries(1) do
result = {
1 => "bob",
2 => "bertha",
}
assert_equal(result, Item.fetch_multi_title_by_id([1, 2]))
end
end
end

0 comments on commit a2f57ea

Please sign in to comment.