Skip to content

Commit a2dd5c5

Browse files
Bortlesboatclaude
andcommitted
fix: use LRU eviction for cron schedule cache instead of FIFO
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 #39679 Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent aedf3ee commit a2dd5c5

File tree

2 files changed

+29
-0
lines changed

2 files changed

+29
-0
lines changed

src/cron/schedule.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,32 @@ describe("cron schedule", () => {
144144
expect(getCronScheduleCacheSizeForTest()).toBe(2);
145145
});
146146

147+
it("promotes accessed entries to avoid premature LRU eviction", () => {
148+
const nowMs = Date.parse("2026-03-01T00:00:00.000Z");
149+
150+
// Fill cache to capacity with unique expressions
151+
for (let i = 0; i < 512; i++) {
152+
computeNextRunAtMs(
153+
{ kind: "cron", expr: `${i % 60} ${Math.floor(i / 60)} * * *`, tz: "UTC" },
154+
nowMs,
155+
);
156+
}
157+
expect(getCronScheduleCacheSizeForTest()).toBe(512);
158+
159+
// Access the very first entry so it gets promoted (LRU touch)
160+
computeNextRunAtMs({ kind: "cron", expr: "0 0 * * *", tz: "UTC" }, nowMs);
161+
162+
// Insert a new entry — this should evict the second-oldest, not the first
163+
computeNextRunAtMs({ kind: "cron", expr: "0 0 1 1 *", tz: "UTC" }, nowMs);
164+
expect(getCronScheduleCacheSizeForTest()).toBe(512);
165+
166+
// The first entry should still be cached (was promoted).
167+
// Insert another new entry — if FIFO, the first entry would now be evicted.
168+
// With LRU, the third-oldest entry is evicted instead.
169+
computeNextRunAtMs({ kind: "cron", expr: "0 0 2 1 *", tz: "UTC" }, nowMs);
170+
expect(getCronScheduleCacheSizeForTest()).toBe(512);
171+
});
172+
147173
describe("cron with specific seconds (6-field pattern)", () => {
148174
// Pattern: fire at exactly second 0 of minute 0 of hour 12 every day
149175
const dailyNoon = { kind: "cron" as const, expr: "0 0 12 * * *", tz: "UTC" };

src/cron/schedule.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ function resolveCachedCron(expr: string, timezone: string): Cron {
1717
const key = `${timezone}\u0000${expr}`;
1818
const cached = cronEvalCache.get(key);
1919
if (cached) {
20+
// Move to end of Map iteration order for LRU eviction
21+
cronEvalCache.delete(key);
22+
cronEvalCache.set(key, cached);
2023
return cached;
2124
}
2225
if (cronEvalCache.size >= CRON_EVAL_CACHE_MAX) {

0 commit comments

Comments
 (0)