Skip to content

Commit fd72782

Browse files
committed
fix(antigravity): throttle the lazy expiry scan (CI ubuntu timeout fix)
Every observe/apply ran an O(sessions) expiry scan; at the 10,240 cap that is O(n^2), and the worst-case fixed-key test exceeded the ubuntu CI test timeout. The 60s state-store sweeper is already the periodic expiry authority, so lazy scans are throttled to one per 30s. The worst-case test now runs in ~50ms and carries a 30s budget guard.
1 parent c1098af commit fd72782

2 files changed

Lines changed: 38 additions & 2 deletions

File tree

src/adapters/google-antigravity-replay.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,22 @@ function deleteExpiredReplaySessions(now: number): void {
294294
for (const [key, entry] of replayCache) if (entry.expiresAtMs <= now) deleteReplaySession(key);
295295
}
296296

297+
/**
298+
* The lazy per-call expiry scan is O(sessions); at the 10,240-session cap
299+
* every observe/apply would rescan the whole map — O(n²) under load. The 60s
300+
* state-store sweeper is already the periodic expiry authority, so lazy scans
301+
* are throttled to at most one per interval (expired entries may linger a few
302+
* extra seconds; TTL is fuzzy at that scale by design).
303+
*/
304+
const LAZY_SWEEP_INTERVAL_MS = 30_000;
305+
let lastLazySweepAt = Number.NEGATIVE_INFINITY;
306+
307+
function deleteExpiredReplaySessionsThrottled(now: number): void {
308+
if (now - lastLazySweepAt < LAZY_SWEEP_INTERVAL_MS) return;
309+
lastLazySweepAt = now;
310+
deleteExpiredReplaySessions(now);
311+
}
312+
297313
export function sweepExpiredAntigravityReplay(now = Date.now()): number {
298314
const before = replayCache.size;
299315
deleteExpiredReplaySessions(now);
@@ -349,7 +365,7 @@ export function antigravityUsesReplayCache(model: string): boolean {
349365
export function observeAntigravityReplay(model: string, sessionId: string, parts: unknown[]): void {
350366
if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return;
351367
const now = Date.now();
352-
deleteExpiredReplaySessions(now);
368+
deleteExpiredReplaySessionsThrottled(now);
353369
const key = replayKey(model, sessionId);
354370
const existing = replayCache.get(key);
355371
const entry = existing ?? {
@@ -403,7 +419,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
403419
export function applyAntigravityReplay(model: string, sessionId: string, contents: unknown[]): unknown[] {
404420
if (!antigravityUsesReplayCache(model) || !Array.isArray(contents)) return contents;
405421
const now = Date.now();
406-
deleteExpiredReplaySessions(now);
422+
deleteExpiredReplaySessionsThrottled(now);
407423
const entry = replayCache.get(replayKey(model, sessionId));
408424
if (!entry) {
409425
return contents;
@@ -478,6 +494,7 @@ export function setAntigravityReplayLimitsForTests(limits?: Partial<ReplayLimits
478494

479495
/** Test seam. */
480496
export function __resetAntigravityReplayCache(): void {
497+
lastLazySweepAt = Number.NEGATIVE_INFINITY;
481498
replayCache.clear();
482499
replayBytes = 0;
483500
replayOldestSessionKey = undefined;

tests/google-antigravity-replay.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
evictOldestAntigravityReplayForBudget,
1515
observeAntigravityReplay,
1616
setAntigravityReplayLimitsForTests,
17+
sweepExpiredAntigravityReplay,
1718
} from "../src/adapters/google-antigravity-replay";
1819
import { sanitizeAntigravityClaudeSignatures } from "../src/adapters/google-antigravity-wire";
1920

@@ -340,6 +341,24 @@ describe("antigravity replay fixed-size key identities", () => {
340341
// keys never scale with input length, all within the 64 MiB global cap.
341342
expect(metrics.totalBytes).toBeLessThan(64 * 1024 * 1024);
342343
expect(metrics.totalBytes).toBe(10_240 * (64 + 64 + SIG.length));
344+
}, 30_000);
345+
346+
test("lazy expiry scan is throttled; the sweeper remains authoritative", () => {
347+
observeAntigravityReplay(MODEL, "s-1", [fcPart("f", {}, "sig-1234567890abcdef")]);
348+
const originalNow = Date.now;
349+
try {
350+
// An expired session is NOT re-scanned within the 30s lazy interval.
351+
Date.now = () => originalNow() + 1000;
352+
observeAntigravityReplay(MODEL, "s-2", [fcPart("f", {}, "sig-1234567890abcdef")]);
353+
expect(antigravityReplayMetrics().sessions).toBe(2);
354+
// The periodic sweeper still removes expired sessions on its own pass.
355+
Date.now = () => originalNow() + 60 * 60 * 1000 + 1000;
356+
const removed = sweepExpiredAntigravityReplay(Date.now());
357+
expect(removed).toBe(2);
358+
expect(antigravityReplayMetrics().sessions).toBe(0);
359+
} finally {
360+
Date.now = originalNow;
361+
}
343362
});
344363

345364
test("length-prefixed components are unambiguous across separator content", () => {

0 commit comments

Comments
 (0)