diigo-cache: pace diigo calls only; cache hits don't wait
page() ran the 500 ms pacing gate before the cache check, so every hit waited too: a fill rerun skipping 166 cached pages took ~80 s and looked like a start from scratch. The gate now sits after the hit check (re-checking storage once through it, in case a request ahead had just etched the page). Regression test: the fixed-clock test cache records its sleeps; a miss sleeps once, three hits add none (failed before: 4 sleeps). Generated-by: Claude Opus 5.5
This commit is contained in:
1 parent
f5d57f73d3
commit
fd16c7db53
3 files changed
+43
-8
No files matched your search
@@ -285,8 +285,15 @@ abstract class PagedCache extends DurableObject<CacheEnv> {
|
|||||||
if (!init) throw new Error("not wired: this cache id was never set up");
|
if (!init) throw new Error("not wired: this cache id was never set up");
|
||||||
this.ctx.storage.sql.exec(SCHEMA_V1);
|
this.ctx.storage.sql.exec(SCHEMA_V1);
|
||||||
|
|
||||||
// one diigo call path at a time, min spacing MS_PER_CALL (webapp's
|
const hit = await this.#pageFromStorage(pageNum, count);
|
||||||
// Gate pattern; per activation)
|
if (hit) {
|
||||||
|
console.log({ event: "cache", kind: this.kind, page: pageNum, outcome: "hit", items: hit.items.length });
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a miss calls diigo: one call path at a time, min spacing
|
||||||
|
// MS_PER_CALL (webapp's Gate pattern; per activation). Hits above
|
||||||
|
// never wait.
|
||||||
let before;
|
let before;
|
||||||
do {
|
do {
|
||||||
before = this.#step;
|
before = this.#step;
|
||||||
@@ -294,11 +301,11 @@ abstract class PagedCache extends DurableObject<CacheEnv> {
|
|||||||
} while (before !== this.#step);
|
} while (before !== this.#step);
|
||||||
this.#step += 1;
|
this.#step += 1;
|
||||||
this.#pause = this.#clock.sleep(MS_PER_CALL);
|
this.#pause = this.#clock.sleep(MS_PER_CALL);
|
||||||
|
// a request ahead of us at the gate may have etched this page
|
||||||
const hit = await this.#pageFromStorage(pageNum, count);
|
const late = await this.#pageFromStorage(pageNum, count);
|
||||||
if (hit) {
|
if (late) {
|
||||||
console.log({ event: "cache", kind: this.kind, page: pageNum, outcome: "hit", items: hit.items.length });
|
console.log({ event: "cache", kind: this.kind, page: pageNum, outcome: "hit", items: late.items.length });
|
||||||
return hit;
|
return late;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPC stubs (the app and each pipelined step) are disposed once the
|
// RPC stubs (the app and each pipelined step) are disposed once the
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const WEBAPP_ID = "fake-webapp-id";
|
|||||||
type TestEnv = {
|
type TestEnv = {
|
||||||
CACHE_DO: DurableObjectNamespace<import("../src/index.js").DiigoCache>;
|
CACHE_DO: DurableObjectNamespace<import("../src/index.js").DiigoCache>;
|
||||||
PUBLIC_DO: DurableObjectNamespace<import("../src/index.js").PublicCache>;
|
PUBLIC_DO: DurableObjectNamespace<import("../src/index.js").PublicCache>;
|
||||||
|
CLOCKED_DO: DurableObjectNamespace<import("./test-worker.js").FixedClockCache>;
|
||||||
WEBAPP: { calls(): Promise<number> };
|
WEBAPP: { calls(): Promise<number> };
|
||||||
};
|
};
|
||||||
const testEnv = env as unknown as TestEnv;
|
const testEnv = env as unknown as TestEnv;
|
||||||
@@ -109,6 +110,18 @@ describe("page size", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("pacing", () => {
|
||||||
|
it("paces diigo calls only: a cache hit doesn't wait", async () => {
|
||||||
|
const stub = testEnv.CLOCKED_DO.get(testEnv.CLOCKED_DO.newUniqueId());
|
||||||
|
await stub.setup({ webappId: WEBAPP_ID });
|
||||||
|
await stub.page(1, PAGE_SIZE);
|
||||||
|
const afterMiss = (await stub.sleeps()).length;
|
||||||
|
expect(afterMiss).toBeGreaterThan(0);
|
||||||
|
for (let i = 0; i < 3; i += 1) expect((await stub.page(1, PAGE_SIZE)).cached).toBe(true);
|
||||||
|
expect(await stub.sleeps()).toHaveLength(afterMiss);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("eviction", () => {
|
describe("eviction", () => {
|
||||||
it("a cache miss leaves no undisposed RPC stubs to block eviction", async () => {
|
it("a cache miss leaves no undisposed RPC stubs to block eviction", async () => {
|
||||||
const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId());
|
const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId());
|
||||||
|
|||||||
@@ -12,8 +12,23 @@ export { default } from "../src/index.js";
|
|||||||
/** 2026-10-07T12:00:00Z */
|
/** 2026-10-07T12:00:00Z */
|
||||||
export const FIXED_NOW = Date.UTC(2026, 9, 7, 12, 0, 0);
|
export const FIXED_NOW = Date.UTC(2026, 9, 7, 12, 0, 0);
|
||||||
|
|
||||||
|
/** a fixed clock that records each sleep it is asked for */
|
||||||
export class FixedClockCache extends DiigoCacheLogic {
|
export class FixedClockCache extends DiigoCacheLogic {
|
||||||
|
#sleeps: number[] = [];
|
||||||
|
|
||||||
constructor(ctx: DurableObjectState, env: CacheEnv) {
|
constructor(ctx: DurableObjectState, env: CacheEnv) {
|
||||||
super(ctx, env, { now: () => FIXED_NOW, sleep: async () => {} });
|
const sleeps: number[] = [];
|
||||||
|
super(ctx, env, {
|
||||||
|
now: () => FIXED_NOW,
|
||||||
|
sleep: async (ms) => {
|
||||||
|
sleeps.push(ms);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.#sleeps = sleeps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the sleeps so far (test hook) */
|
||||||
|
async sleeps(): Promise<number[]> {
|
||||||
|
return [...this.#sleeps];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user