Skip to content

[BCN] Fix cache expiry race that discards refreshed entries - #4252

Open
leolambo wants to merge 1 commit into
bitpay:masterfrom
leolambo:cacheExpiryFix
Open

leolambo wants to merge 1 commit into
bitpay:masterfrom
leolambo:cacheExpiryFix

Conversation

@leolambo

Copy link
Copy Markdown
Contributor

Description

CacheStorage.getGlobal and getForWallet fire an unawaited remove keyed on _id when they read an expired entry. setGlobal's upsert refreshes the same document in place, so a delete that lands late removes the entry that was just written. Around each expiry a hot key can end up missing on every request, and each miss re-runs the underlying lookup (RPC fee estimates today, plus any other getGlobalOrRefresh consumer). Surfaced while reviewing cache usage in the wallet stats work (#4250).

Changelog

  • Await the expiry delete and match on the entry's created stamp, so the delete only removes the version that was actually read as expired. A concurrently refreshed entry keeps its _id but gets a new created, and the stale delete no-ops against it.
  • Add Cache Model integration tests pinning the expiry-delete contract.

Testing Notes

Covered by the new cases in the Cache Model integration suite. Fresh entries and normal expiry behave the same as before.


Checklist

  • I have read CONTRIBUTING.md and verified that this PR follows the guidelines and requirements outlined in it.
  • I have added the appropriate package tag(s) (e.g. BWC if modifying the bitcore-wallet-client package, CLI if modifying the bitcore-cli package, etc.)
  • I have verified that this is not an existing PR (open or closed)

getGlobal and getForWallet fired an unawaited remove keyed on _id when
they found an expired entry. setGlobal's upsert refreshes the same
document in place, so that delete could land after a concurrent (or
the same request's own) refresh and silently discard the new entry,
leaving hot keys perpetually cache-missing around every expiry. Await
the delete and match on the stale created stamp so it can only ever
remove the version that was actually observed expired.

@kajoseph kajoseph left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice catch! While we're at it, might as well change the deprecated .remove to .deleteOne

} else {
// cache miss
this.collection.remove({ _id: found._id });
// cache miss - only delete the version seen expiring; a refresh reuses the _id

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rm comment above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants