fix: use LRU eviction for cron schedule cache instead of FIFO#39703
fix: use LRU eviction for cron schedule cache instead of FIFO#39703Bortlesboat wants to merge 2 commits intoopenclaw:mainfrom
Conversation
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 SummaryThis PR fixes the cron schedule cache eviction policy from FIFO to true LRU by adding a
Confidence Score: 3/5
Last reviewed commit: a2dd5c5 |
| 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); | ||
| }); |
There was a problem hiding this 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:
// 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 evictedWithout 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.
Summary
resolveCachedCron(), delete and re-set the entry to move it to the end of Map iteration order*/5 * * * *heartbeats) from being evicted prematurelyTest plan
pnpm build && pnpm check && pnpm testall greenFixes #39679
🤖 Generated with Claude Code