Skip to content

fix(ledger): expire the widened last-mark memo so a still-feedless window self-heals#4281

Merged
TaprootFreak merged 2 commits into
developfrom
fix/ledger-widened-mark-cache-ttl
Jul 19, 2026
Merged

fix(ledger): expire the widened last-mark memo so a still-feedless window self-heals#4281
TaprootFreak merged 2 commits into
developfrom
fix/ledger-widened-mark-cache-ttl

Conversation

@TaprootFreak

Copy link
Copy Markdown
Collaborator

Problem

getMarkAtWidened (#4275) memoizes its per-window preload on the process-lifetime LedgerMarkService with no TTL — only a rejected load is evicted. A successful preload whose immutable LedgerMarkCache lacks 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 via CUTOVER_SNAPSHOT_LOG_ID_KEY), so each retry re-reads the same stale-empty cache → bookReceivedOwedOpening fail-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 sibling latestMarks bridge, 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 widenedCaches the same self-healing TTL as latestMarks (WIDENED_MARK_TTL_MS = LATEST_MARK_TTL_MS, 5 min): carry a loadedAt and 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.

…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.
@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

Verification (money-path PR-completion process):

  • CI green — Build and test (13m), Analyze (js-ts ×2 / actions / python), CodeQL, review all pass.
  • 2 independent reviews (fix logic · test/edge-cases): 0 blocking findings. Two nits, both addressed in 130d3fd25:
    • guard the rejected-preload eviction by loadedAt — a widened window is now replaced after the TTL, so the unconditional delete(key) became newly able to drop a fresher entry a retry installed (net effect only an extra read, never a misbooking; tightened anyway).
    • make the Date.now spy restore fail-safe via a describe-level afterEach (the jest config has no restoreMocks).

Confirmed: no financial-correctness change; the happy / transient-failure / self-heal paths are unchanged; the TTL mirrors the existing latestMarks bridge. Ready for review.

@TaprootFreak
TaprootFreak marked this pull request as ready for review July 19, 2026 11:58
@TaprootFreak
TaprootFreak merged commit 10ad1b5 into develop Jul 19, 2026
7 checks passed
@TaprootFreak
TaprootFreak deleted the fix/ledger-widened-mark-cache-ttl branch July 19, 2026 12:41
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.

1 participant