Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add expire methods by key values #495

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ jobs:
run: bundle exec rake test
- name: Run rubocop
if: matrix.entry.rubocop
run: bundle exec rubocop
run: bundle exec rubocop --debug
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
22 changes: 22 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't just need the name of the key, so MissingKey seems like a more appropriate name, but then why not just use KeyError?

"#{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 Down Expand Up @@ -100,6 +113,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 +136,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
70 changes: 70 additions & 0 deletions test/expire_by_key_values_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true
require "test_helper"

class ExpireByKeyValuesTest < IdentityCache::TestCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I believe that these feature based unit test files are an anti-pattern (#498). I just hadn't gotten around to actually refactoring the tests around the code

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")
parent.associated_records.create!(name: "foo")
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

key_values = {
Copy link

@Larochelle Larochelle Jun 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be this hash that crashes the cop?

undefined method `loc' for nil:NilClass
/opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/rubocop-1.16.1/lib/rubocop/cop/layout/hash_alignment.rb:221:in `autocorrect_incompatible_with_other_cops?'
/opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/rubocop-1.16.1/lib/rubocop/cop/layout/hash_alignment.rb:206:in `on_hash'

Or maybe because ({ item_id: 1 }) does not need the {}?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rubocop failure was a bug and the fix hasn't been released yet. I opened #496 to fix that.

id: 1,
item_id: 1,
}

AssociatedRecord.cache_indexes.each do |index|
index.expire_by_key_value(key_values)
end

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
by_id_index, by_item_id_index, by_multi_index = AssociatedRecord.cache_indexes
missing_id_error_message =
"AssociatedRecord attribute name expire_by_key_value - required fields: id. missing: id"
missing_item_id_error_message =
"AssociatedRecord attribute name expire_by_key_value - required fields: item_id. missing: item_id"
missing_id_key_in_multi_error_message =
"AssociatedRecord attribute name expire_by_key_value - required fields: id, item_id. missing: id"

missing_id_error = assert_raises(IdentityCache::MissingKeyName) do
by_id_index.expire_by_key_value({ item_id: 1 })
end
missing_item_id_error = assert_raises(IdentityCache::MissingKeyName) do
by_item_id_index.expire_by_key_value({ id: 1 })
end
missing_id_in_multi_error = assert_raises(IdentityCache::MissingKeyName) do
by_multi_index.expire_by_key_value({ item_id: 1 })
end

assert_equal(missing_id_error_message, missing_id_error.message)
assert_equal(missing_item_id_error_message, missing_item_id_error.message)
assert_equal(missing_id_key_in_multi_error_message, missing_id_in_multi_error.message)
end
end