Skip to content

fix: use LRU eviction for cron schedule cache instead of FIFO#39703

Open
Bortlesboat wants to merge 2 commits intoopenclaw:mainfrom
Bortlesboat:fix/cron-cache-lru-eviction
Open

fix: use LRU eviction for cron schedule cache instead of FIFO#39703
Bortlesboat wants to merge 2 commits intoopenclaw:mainfrom
Bortlesboat:fix/cron-cache-lru-eviction

Conversation

@Bortlesboat
Copy link
Copy Markdown

Summary

  • On cache hit in resolveCachedCron(), delete and re-set the entry to move it to the end of Map iteration order
  • This gives true LRU eviction instead of FIFO, preventing frequently-accessed cron expressions (e.g. */5 * * * * heartbeats) from being evicted prematurely
  • Added test verifying that accessed entries survive eviction after cache fills

Test plan

  • Existing 22 tests pass
  • New LRU promotion test added and passing
  • pnpm build && pnpm check && pnpm test all green

Fixes #39679

🤖 Generated with Claude Code

On cache hit, delete and re-set the entry to move it to the end of
Map iteration order. This prevents frequently-accessed cron expressions
(e.g. heartbeat schedules) from being evicted prematurely while
rarely-used entries persist.

Fixes openclaw#39679

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR fixes the cron schedule cache eviction policy from FIFO to true LRU by adding a delete/set promotion on every cache hit in resolveCachedCron(), correctly leveraging JavaScript Map's insertion-order iteration.

  • src/cron/schedule.ts — The LRU promotion logic itself is correct: deleting and re-inserting on a hit moves the entry to the tail of Map iteration order, so the existing head-eviction logic now removes the least recently used entry rather than the oldest inserted entry.
  • src/cron/schedule.test.ts — The new test only asserts on getCronScheduleCacheSizeForTest() (always 512 after any eviction), so it passes identically under FIFO and LRU. The test does not actually verify that the promoted entry survives eviction; a regression that reverts the delete/set promotion would not be detected. A hasCronInCacheForTest helper and assertions checking the presence/absence of specific keys after eviction are needed to make this test meaningful.

Confidence Score: 3/5

  • The implementation is correct but the accompanying test does not actually validate the LRU behaviour it claims to cover.
  • The two-line production change (delete + set on hit) is a well-known correct pattern for LRU with a JavaScript Map and carries negligible risk. The score is reduced because the new test provides false confidence — it passes under both the old FIFO code and the new LRU code, meaning it would not catch a future regression.
  • src/cron/schedule.test.ts — the LRU promotion test needs stronger assertions to be meaningful.

Last reviewed commit: a2dd5c5

Comment on lines +147 to +171
it("promotes accessed entries to avoid premature LRU eviction", () => {
const nowMs = Date.parse("2026-03-01T00:00:00.000Z");

// Fill cache to capacity with unique expressions
for (let i = 0; i < 512; i++) {
computeNextRunAtMs(
{ kind: "cron", expr: `${i % 60} ${Math.floor(i / 60)} * * *`, tz: "UTC" },
nowMs,
);
}
expect(getCronScheduleCacheSizeForTest()).toBe(512);

// Access the very first entry so it gets promoted (LRU touch)
computeNextRunAtMs({ kind: "cron", expr: "0 0 * * *", tz: "UTC" }, nowMs);

// Insert a new entry — this should evict the second-oldest, not the first
computeNextRunAtMs({ kind: "cron", expr: "0 0 1 1 *", tz: "UTC" }, nowMs);
expect(getCronScheduleCacheSizeForTest()).toBe(512);

// The first entry should still be cached (was promoted).
// Insert another new entry — if FIFO, the first entry would now be evicted.
// With LRU, the third-oldest entry is evicted instead.
computeNextRunAtMs({ kind: "cron", expr: "0 0 2 1 *", tz: "UTC" }, nowMs);
expect(getCronScheduleCacheSizeForTest()).toBe(512);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test does not verify LRU promotion

The assertions in this test only check getCronScheduleCacheSizeForTest(), which always returns 512 regardless of whether eviction is FIFO or LRU. The cache size stays at 512 after every insertion in both cases, so this test would pass even with the old FIFO behaviour. The test does not actually validate that the promoted entry ("0 0 * * *") survived eviction.

To properly verify the LRU semantics, you need the ability to inspect whether a specific key is still in the cache. Since the only test-facing API currently only exposes the count, a new helper should be exported from schedule.ts:

// In schedule.ts
export function hasCronInCacheForTest(expr: string, tz: string): boolean {
  const key = [tz, expr].join("\0"); // same key format used internally
  return cronEvalCache.has(key);
}

Then the critical assertions become:

// After inserting the two new entries:
expect(hasCronInCacheForTest("0 0 * * *", "UTC")).toBe(true);   // promoted – must survive
expect(hasCronInCacheForTest("1 0 * * *", "UTC")).toBe(false);  // second-oldest – must be evicted

Without assertions like these, the test provides no regression coverage for the LRU promotion behaviour it claims to verify — a future revert of the delete/set promotion would not be caught.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/schedule.test.ts
Line: 147-171

Comment:
**Test does not verify LRU promotion**

The assertions in this test only check `getCronScheduleCacheSizeForTest()`, which always returns 512 regardless of whether eviction is FIFO or LRU. The cache size stays at 512 after every insertion in *both* cases, so this test would pass even with the old FIFO behaviour. The test does not actually validate that the promoted entry (`"0 0 * * *"`) survived eviction.

To properly verify the LRU semantics, you need the ability to inspect whether a specific key is still in the cache. Since the only test-facing API currently only exposes the count, a new helper should be exported from `schedule.ts`:

```ts
// In schedule.ts
export function hasCronInCacheForTest(expr: string, tz: string): boolean {
  const key = [tz, expr].join("\0"); // same key format used internally
  return cronEvalCache.has(key);
}
```

Then the critical assertions become:
```ts
// After inserting the two new entries:
expect(hasCronInCacheForTest("0 0 * * *", "UTC")).toBe(true);   // promoted – must survive
expect(hasCronInCacheForTest("1 0 * * *", "UTC")).toBe(false);  // second-oldest – must be evicted
```

Without assertions like these, the test provides no regression coverage for the LRU promotion behaviour it claims to verify — a future revert of the `delete`/`set` promotion would not be caught.

How can I resolve this? If you propose a fix, please make it concise.

Add hasCronInCacheForTest helper and assert specific cache entries
survive or get evicted. The test now fails under FIFO but passes
under LRU, directly verifying the promotion behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Cron schedule cache eviction is FIFO, not LRU — hot entries get evicted prematurely

1 participant