Skip to content

Commit 1c1050e

Browse files
fix(server): release completed PR sync sweep data from the read cache (#25285)
1 parent 210b2e7 commit 1c1050e

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

‎apps/server/src/pullRequest/PullRequestReadCache.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as NodeV8 from "node:v8";
2+
import * as NodeVM from "node:vm";
13
import { assert, it } from "@effect/vitest";
24
import * as NodeServices from "@effect/platform-node/NodeServices";
35
import { PullRequestOperationError } from "@t3tools/contracts";
@@ -8,6 +10,9 @@ import * as FileSystem from "effect/FileSystem";
810
import * as KeyValueStore from "effect/persistence/KeyValueStore";
911
import * as PullRequestReadCache from "./PullRequestReadCache.ts";
1012

13+
NodeV8.setFlagsFromString("--expose-gc");
14+
const collectGarbage = NodeVM.runInNewContext("gc") as () => void;
15+
1116
const cacheLayer = (directory: string) =>
1217
PullRequestReadCache.make.pipe(Effect.provide(KeyValueStore.layerFileSystem(directory)));
1318

@@ -147,6 +152,32 @@ it.layer(NodeServices.layer)("PR filesystem cache", (it) => {
147152
}).pipe(Effect.provide(KeyValueStore.layerMemory)),
148153
);
149154

155+
it.effect("does not keep a caller's data alive after its reads and invalidations", () =>
156+
Effect.gen(function* () {
157+
const cache = yield* PullRequestReadCache.make;
158+
const sweeps: Array<WeakRef<object>> = [];
159+
// Like a sync sweep: an Effect.fn made per run that closes over that run's data.
160+
const sweep = Effect.fn("sweep")(function* (index: number) {
161+
const groups = new Map<string, number>(
162+
Array.from({ length: 1_000 }, (_, id) => [`group-${id}`, id]),
163+
);
164+
sweeps.push(new WeakRef(groups));
165+
const read = Effect.fn("read")(function* (pr: string) {
166+
const size = groups.size;
167+
yield* cache.invalidate(pr);
168+
const answer = yield* cache.get(`${pr}-${index}`, Effect.succeed("answer"), [pr]);
169+
return size + answer.length;
170+
});
171+
yield* Effect.forEach(["pr-1", "pr-2"], read, { discard: true });
172+
});
173+
for (let index = 0; index < 5; index++) yield* sweep(index);
174+
// WeakRef targets stay alive until the current job ends, so yield before collecting.
175+
yield* Effect.yieldNow;
176+
collectGarbage();
177+
assert.strictEqual(sweeps.filter((sweepData) => sweepData.deref() !== undefined).length, 0);
178+
}).pipe(Effect.provide(KeyValueStore.layerMemory)),
179+
);
180+
150181
it.effect("cancels abandoned reads without blocking invalidation", () =>
151182
Effect.gen(function* () {
152183
const cache = yield* PullRequestReadCache.make;

‎apps/server/src/pullRequest/PullRequestReadCache.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as Persistable from "effect/persistence/Persistable";
2121
import * as PersistedCache from "effect/persistence/PersistedCache";
2222
import * as Persistence from "effect/persistence/Persistence";
2323
import * as ServerConfig from "../config.ts";
24+
import { detachStackFrame } from "../git/detachStackFrame.ts";
2425
import { forkParked } from "../serverActivation.ts";
2526

2627
const CONCURRENT_READS = 512;
@@ -124,6 +125,8 @@ export const make = Effect.gen(function* () {
124125
}),
125126
{ capacity: CONCURRENT_READS, timeToLive: () => Duration.zero },
126127
);
128+
// A cache entry keeps the fiber that filled it, whose lazy stack frames would keep the caller's
129+
// data (e.g. a whole sync sweep) alive until eviction.
127130
return PullRequestReadCache.of({
128131
get: Effect.fn("PullRequestReadCache.get")(function* (key, lookup, scopes = []) {
129132
if (!enabled) return yield* lookup;
@@ -151,7 +154,7 @@ export const make = Effect.gen(function* () {
151154
}),
152155
lock.withPermits(1),
153156
);
154-
}),
157+
}, detachStackFrame),
155158
invalidate: Effect.fn("PullRequestReadCache.invalidate")(
156159
function* (scope) {
157160
const now = clock.currentTimeMillisUnsafe();
@@ -170,6 +173,7 @@ export const make = Effect.gen(function* () {
170173
}),
171174
Effect.uninterruptible,
172175
lock.withPermits(CONCURRENT_READS),
176+
detachStackFrame,
173177
),
174178
});
175179
});

0 commit comments

Comments
 (0)