-
Notifications
You must be signed in to change notification settings - Fork 174
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
Introduce Deferred Parent Cache Expiry #569
Conversation
A few answers:
|
lib/identity_cache.rb
Outdated
Thread.current[:parent_records_for_cache_expiry].each(&:expire_primary_index) | ||
ensure | ||
Thread.current[:deferred_parent_expiration] = nil | ||
Thread.current[:parent_records_for_cache_expiry].clear |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree we just want to empty this vs trying to process it in the ensure block... all real processing we expect to be handled normally in the around yield this cleans up in both standard and exceptional cases
lib/identity_cache.rb
Outdated
@@ -191,6 +193,21 @@ def fetch_multi(*keys) | |||
result | |||
end | |||
|
|||
def with_deferred_parent_expiration | |||
raise NestedDeferredParentBlockError if Thread.current[:deferred_parent_expiration] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ergonomics of disallowing nested use make it unpredictable to compose. We could instead allow nested use with the outermost usage doing the actual invalidations.
def with_deferred_parent_expiration
if Thread.current[:parent_records_for_cache_expiry]
return yield
end
Thread.current[:parent_records_for_cache_expiry] = Set.new
result = yield
Thread.current[:parent_records_for_cache_expiry].each(&:expire_primary_index)
result
ensure
Thread.current[:parent_records_for_cache_expiry] = nil
end
Other suggestions:
- may not need a separate boolean with
:parent_records_for_cache_expiry
beingnil
before/after use - return value of
with_deferred_parent_expiration
can be that of theyield
block passthru. - since
Thread.current
is accessible by name from any method it would be good to use a prefix for the key, e.g.idc_parent_records_for_cache_expiry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, perhaps I'm misunderstanding but, in the code above, return yield
will lead to unintended side effects. I believe the code will do the following:
IdentityCache.with_deferred_expiration do
@parent.transaction do
@parent.children.destroy_all # Thread.current[:parent_records_for_cache_expiry] => [@parent]
IdentityCache.with_deferred_expiration do # The `ensure` block runs when we `return yield`, so Thread.current[:parent_records_for_cache_expiry] => `nil`
@unrelated_record.update # Thread.current[:parent_records_for_cache_expiry] => [<parent of @unrelated_record>]
end
end
end
I'm fairly sure ensure
always runs, even when returning early, which means that @parent
wouldn't be expired here, even though its associated children
records were destroyed and had it queued for expiry.
may not need a separate boolean with :parent_records_for_cache_expiry being nil before/after use
I believe we will need some "global" marker to inform the ParentModelExpiration
class when to alter its behaviour as this is not default behaviour and we don't want to alter behaviour in all cases.
return value of with_deferred_parent_expiration can be that of the yield block passthru.
To my shame, I missed this and it definitely needed fixing, thanks for flagging this! I have amended, rebased and pushed the changed behaviour.
since Thread.current is accessible by name from any method it would be good to use a prefix for the key, e.g. idc_parent_records_for_cache_expiry
Great suggestion, have prefixed the Thread.current
hash keys to prefix idc_
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right about early nil
ing with nested usage, we only want the ensure
behaviour on the outermost one. We can get that by using a nest level counter, incremented on each entry of with_deferred_parent_expiration
decremented for each exit and only do the nil
ing when zero.
Would we also need levels of Thread.current[:parent_records_for_cache_expiry].each(&:expire_primary_index)
as well for nesting to work properly? e.g.
Thread.current[:parent_records_for_cache_expiries] ||= []
Thread.current[:parent_records_for_cache_expiries] << Set.new
result = yield
Thread.current[:parent_records_for_cache_expiries].last.each(&:expire_primary_index)
result
ensure
Thread.current[:parent_records_for_cache_expiries].pop
This could have caused the cache to have been overwritten
82364c2
to
1aba576
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great!
The case in the tests models the situation this solves for. In summary, if a parent record has three children records which are all deleted, we don't want to expire the parent record cache three times. Once will do, at the end of the operation.
This PR introduces an "escape" for the expiration of a parent's cache. It is currently reliant upon the user of the gem to utilise this functionality safely. Therefore, it is strongly recommended to be used within a transaction.
To introduce the feature we are simply using a class method that wraps the existing code. If this feature has further use, we may choose to build upon this approach in two ways: