Skip to content

Commit

Permalink
Introduce .with_deferred_parent_expiration
Browse files Browse the repository at this point in the history
  • Loading branch information
Stivaros committed Jun 7, 2024
1 parent db5bbb3 commit e5847d2
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 0 deletions.
10 changes: 10 additions & 0 deletions lib/identity_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@ def fetch_multi(*keys)
result
end

def with_deferred_parent_expiration
# Bonus: Do this in one external call (if possible)
Thread.current[:deferred_parent_expiration] = true
Thread.current[:parent_records_for_cache_expiry] = Set.new

yield

Thread.current[:parent_records_for_cache_expiry].each(&:expire_primary_index)
end

def with_fetch_read_only_records(value = true)
old_value = Thread.current[:identity_cache_fetch_read_only_records]
Thread.current[:identity_cache_fetch_read_only_records] = value
Expand Down
4 changes: 4 additions & 0 deletions lib/identity_cache/parent_model_expiration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def expire_parent_caches
add_parents_to_cache_expiry_set(parents_to_expire)
parents_to_expire.select! { |parent| parent.class.primary_cache_index_enabled }
parents_to_expire.reduce(true) do |all_expired, parent|
if Thread.current[:deferred_parent_expiration]
Thread.current[:parent_records_for_cache_expiry] << parent
next parent
end
parent.expire_primary_index && all_expired
end
end
Expand Down
78 changes: 78 additions & 0 deletions test/index_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,84 @@ def test_unique_cache_index_with_non_id_primary_key
assert_equal(123, KeyedRecord.fetch_by_value("a").id)
end

def test_with_deferred_parent_expiration
# TODO: Delete these notes
# Done: Create a record (`Item`) with associated records (`AssociatedRecord`) outside of "recorded space"
# Done: Start recording externals
# Done: Open defer block
# Done: Open tx
# Done: Delete all of the associated records
# Done: Inside defer block (outside tx) assert that no external calls have been made yet
# Done: Outside defer block (outside tx) assert that:
# - External calls have now been made
# - `Item` was expired only once
# - `AssociatedRecord` was expired once each

Item.send(:cache_has_many, :associated_records, embed: true)

@parent = Item.create!(title: "bob")
@records = @parent.associated_records.create!([{ name: "foo" }, { name: "bar" }, { name: "baz" }])

@memcached_spy = Spy.on(backend, :write).and_call_through

expected_item_expiration_count = Array(@parent).count
expected_associated_record_expiration_count = @records.count

IdentityCache.with_deferred_parent_expiration do
@parent.transaction do
@parent.associated_records.destroy_all
end
assert_equal(expected_associated_record_expiration_count, @memcached_spy.calls.count)
end

expired_cache_keys = @memcached_spy.calls.map(&:args).map(&:first)
item_expiration_count = expired_cache_keys.count { _1.include?("Item") }
associated_record_expiration_count = expired_cache_keys.count { _1.include?("AssociatedRecord") }

assert_operator(@memcached_spy.calls.count, :>, 0)
assert_equal(expected_item_expiration_count, item_expiration_count)
assert_equal(expected_associated_record_expiration_count, associated_record_expiration_count)
end

def test_deep_association_with_deferred_parent_expiration
# OPTIONAL / EXTRA CREDIT: What happens when nested association does the same thing? Prod/PV/IIV case?

AssociatedRecord.send(:has_many, :deeply_associated_records, dependent: :destroy)
Item.send(:cache_has_many, :associated_records, embed: true)

@parent = Item.create!(title: "bob")
@records = @parent.associated_records.create!([{ name: "foo" }, { name: "bar" }, { name: "baz" }])
@records.each do
_1.deeply_associated_records.create!([
{ name: "a", item: @parent },
{ name: "b", item: @parent },
{ name: "c", item: @parent },
])
end

@memcached_spy = Spy.on(backend, :write).and_call_through

expected_item_expiration_count = Array(@parent).count
expected_associated_record_expiration_count = @records.count
expected_deeply_associated_record_expiration_count = @records.flat_map(&:deeply_associated_records).count

IdentityCache.with_deferred_parent_expiration do
@parent.transaction do
@parent.associated_records.destroy_all
end
end

expired_cache_keys = @memcached_spy.calls.map(&:args).map(&:first)
item_expiration_count = expired_cache_keys.count { _1.include?("Item") }
associated_record_expiration_count = expired_cache_keys.count { _1.include?(":AssociatedRecord:") }
deeply_associated_record_expiration_count = expired_cache_keys.count { _1.include?("DeeplyAssociatedRecord") }

assert_operator(@memcached_spy.calls.count, :>, 0)
assert_equal(expected_item_expiration_count, item_expiration_count)
assert_equal(expected_associated_record_expiration_count, associated_record_expiration_count)
assert_equal(expected_deeply_associated_record_expiration_count, deeply_associated_record_expiration_count)
end

private

def cache_key(unique: false)
Expand Down

0 comments on commit e5847d2

Please sign in to comment.