fix(ledger): expire the widened last-mark memo so a still-feedless window self-heals#4281
Merged
Merged
Conversation
…ndow self-heals getMarkAtWidened (#4275) memoized its per-window preload on the process-lifetime LedgerMarkService with no TTL — only a rejected load was evicted. A *successful* preload whose immutable LedgerMarkCache lacks the asset (a still-feedless/delisted mark) was therefore cached forever. Because the (from,to) key is byte-stable across every 5-min cutover cron retry (the snapshot date is pinned), each retry re-read the same stale-empty cache -> fail-loud -> the cutover wedged until a process restart, defeating the "retry once the feed is back" recovery #4275 documents (asymmetry vs the TTL'd latestMarks bridge). Give widenedCaches the same self-healing TTL as latestMarks: a stale entry (incl. a successful-but-empty one) expires after WIDENED_MARK_TTL_MS so a later retry re-reads and can pick up a backfilled historical mark; within one run (seconds) the memo still dedupes several feedless rows sharing the window. Liveness-only fix — no financial-correctness impact (fail-loud stays safe). Follows up #4275 (#4270).
- guard the rejected-preload eviction by loadedAt so a late-rejecting stale load can never delete a fresher entry a later retry installed (a widened window now gets replaced after the TTL, so unconditional delete-by-key was newly unsafe); net effect was only an extra read, never a misbooking, but tighten it anyway. - make the Date.now spy restore fail-safe via a describe-level afterEach so a failing assertion cannot leak the mock (the jest config has no restoreMocks). No behaviour change on the happy/transient-failure/self-heal paths.
Collaborator
Author
|
Verification (money-path PR-completion process):
Confirmed: no financial-correctness change; the happy / transient-failure / self-heal paths are unchanged; the TTL mirrors the existing |
TaprootFreak
marked this pull request as ready for review
July 19, 2026 11:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
getMarkAtWidened(#4275) memoizes its per-window preload on the process-lifetimeLedgerMarkServicewith no TTL — only a rejected load is evicted. A successful preload whose immutableLedgerMarkCachelacks the asset (a still-feedless/delisted mark) is cached forever. The(from,to)key is byte-stable across every 5-min cutover cron retry (the snapshot date is pinned viaCUTOVER_SNAPSHOT_LOG_ID_KEY), so each retry re-reads the same stale-empty cache →bookReceivedOwedOpeningfail-loud → the whole cutover aborts and wedges until a process restart, defeating the "retry once the feed is back" recovery #4275 documents. Asymmetric vs the siblinglatestMarksbridge, which already carries a 5-min TTL and self-heals.Impact: liveness/recovery only — no financial-correctness risk (fail-loud never books a wrong value). 0 such rows today (defensive for a future delisting, e.g. the DeFiChain sunset) and it self-heals on any deploy/restart — but the recovery gap shouldn't ship silently in the actively-watched cutover money-path.
Fix
Give
widenedCachesthe same self-healing TTL aslatestMarks(WIDENED_MARK_TTL_MS = LATEST_MARK_TTL_MS, 5 min): carry aloadedAtand expire a stale entry (including a successful-but-empty one) so a later retry re-reads and can pick up a backfilled historical mark. Within one run (seconds) the memo still dedupes several feedless rows sharing the window; the happy path and the transient-failure eviction are unchanged.Test
ledger-mark.service.spec: a still-empty window is reused within the TTL (single read), then after the TTL a later call re-reads and picks up the now-restored mark — asserting the exact wedge is gone.Follows up #4275 (#4270). Found by a post-merge money-path review of #4275.