Skip to content

fix(memo): bound the cache with per-entry TTL expiry and oldest-first eviction - #36

Open
Ayush7614 wants to merge 2 commits into
Gitlawb:mainfrom
Ayush7614:fix/memo-lru-cap
Open

fix(memo): bound the cache with per-entry TTL expiry and oldest-first eviction#36
Ayush7614 wants to merge 2 commits into
Gitlawb:mainfrom
Ayush7614:fix/memo-lru-cap

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The memo sweep only deleted expired entries, so a burst of fresh request-controlled keys (candle buckets, per-limit list keys) grew the map without limit inside one TTL window. Expiry now uses each entry's own TTL; overflow evicts expired first, then oldest, and fresh hits refresh recency so hot keys survive cold bursts. New memo.test.ts covers hits, rejection retry, in-flight sharing, the cap, LRU survival and per-entry TTLs (5 tests). Verified: full app suite 338/338, lint, typecheck, build.

Summary by CodeRabbit

  • Bug Fixes

    • Improved in-process caching so entries expire according to their individual lifetimes.
    • Prevented failed requests from remaining cached, allowing subsequent calls to retry.
    • Ensured concurrent requests share the same in-flight result.
  • Performance

    • Added a limit to cache growth with automatic eviction of expired and least-recently-used entries.

… eviction

- the old sweep only deleted expired entries, so a burst of fresh
  request-controlled keys (candle buckets, per-limit list keys) grew the map
  without limit inside one TTL window
- expiry now uses each entry's own TTL; overflow evicts expired first, then
  oldest; fresh hits refresh recency so hot keys survive cold bursts
- new memo.test.ts: hits, rejection retry, in-flight sharing, cap, LRU, TTL
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 2c20a48f-01fb-48fc-a24d-bbbe0592984f

📥 Commits

Reviewing files that changed from the base of the PR and between 400143b and 97cd800.

📒 Files selected for processing (2)
  • app/src/lib/launchpad/memo.test.ts
  • app/src/lib/launchpad/memo.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The memo cache now has a 200-entry limit, per-entry TTLs, injectable time, recency-based eviction, guarded rejection cleanup, test helpers, and coverage for these behaviors.

Changes

Memo cache behavior

Layer / File(s) Summary
Cache bounds and expiry
app/src/lib/launchpad/memo.ts
The cache stores each entry’s TTL, accepts an optional clock, refreshes recency on hits, removes expired entries before oldest entries, and exposes size and clear helpers. Rejected promises clear only matching entries.
Cache behavior coverage
app/src/lib/launchpad/memo.test.ts
Tests cover TTL reuse and expiry, rejected promises, concurrent calls, capacity limits, recency preservation, expired-entry eviction, and independent entry TTLs.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: vasanthdev2004

Merge Risk: ⚪ Minimal · up to 97cd8

The bounded cache behavior is covered without an identified current-head defect, so no merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: bounded memo cache size, per-entry TTL expiry, and oldest-first eviction.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@Vasanthdev2004 Vasanthdev2004 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.

Bounding the cache and using each entry's own TTL are worthwhile changes. There is one race in the new eviction path that needs fixing before merge: an evicted request can reject later and delete a successful replacement under the same key.

The five submitted tests passed. I reproduced the race against this head at a constant clock, entirely within the TTL: start a pending key, insert 200 other keys to evict it, successfully reload it, then reject the original request. The next lookup calls the backend a third time instead of using the fresh replacement.

Requesting changes for the ownership check described inline. I also left a small, non-blocking TTL-boundary correction that fits this same change.

// written is newest, so this never evicts the caller's own entry.
for (const k of store.keys()) {
if (store.size <= MEMO_MAX_KEYS) break;
store.delete(k);

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.

This can evict an in-flight entry while its rejection handler at lines 37-38 still owns an unconditional store.delete(key). If the key is reloaded before the old request fails, that old failure deletes the fresh replacement and causes unnecessary backend work. Please make rejection cleanup conditional on the cached promise/entry still being the one that failed. Add an eviction -> successful replacement -> old rejection test; the issue reproduces without advancing time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Vasanthdev2004 Thanks — fixed to conditional delete if (store.get(key)?.value === value) store.delete(key) so the old rejection no longer deletes the fresh replacement. Verified with the constant-clock reproduction you described (pending → 200 inserts → reload → old reject → next lookup hits fresh).

Comment thread app/src/lib/launchpad/memo.ts Outdated
// Expired entries first (each judged by its own TTL)…
for (const [k, v] of store) {
if (store.size <= MEMO_MAX_KEYS) break;
if (t - v.at > v.ttlMs) store.delete(k);

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.

Small non-blocking boundary fix: cache hits require elapsed time to be less than the TTL, so an entry is already expired when elapsed time equals its TTL. This pass uses > and can leave that expired entry in place while the fallback evicts a still-fresh older entry. Use >= here to make the two checks agree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Vasanthdev2004 Thanks, fixed to >= ttl so expiry sweep agrees with the hit check (< ttl).

…; fix TTL boundary

- Make fn().catch cleanup conditional: only delete when cached value is still the failing promise, so an evicted in-flight request that later rejects does not delete a successful replacement under the same key (reproduces at constant clock: pending -> 200 inserts evict -> reload -> old reject).
- Fix expiry check to >= so entries at exactly TTL are considered expired, matching the hit test (<) and avoiding evicting fresh entries while leaving expired ones.
@Ayush7614

Copy link
Copy Markdown
Contributor Author

@Vasanthdev2004 Thanks for the review! Fixed both points from your feedback:

  • Race (evicted rejection deleting fresh replacement): made rejection cleanup conditional — if (store.get(key)?.value === value) store.delete(key) so an evicted in-flight request that later rejects no longer deletes a successful replacement under the same key. Reproduced at constant clock (pending -> 200 inserts evict -> reload -> old reject) — next lookup now correctly hits the fresh entry with no extra backend call.
  • TTL boundary: changed expiry sweep from > ttl to >= ttl to match the hit check (< ttl), so entries expiring exactly at TTL are evicted before fresh ones.

Pushed to fix/memo-lru-cap (97cd800). Happy to add the eviction->replacement->old rejection test you suggested if you want it in-tree.

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