fix(resolution): WAL containment for the pooled superphase — writer backpressure at pool-idle boundaries (#1332)
At kernel scale the pooled resolution/synthesis superphase grew a 22GB WAL on a 4.6GB DB (cg1212, §7a.1): autocheckpointing is deferred for the run, and the valve's timer-driven passive checkpoints stay perpetually partial against the pool's continuous reads — no mechanism ever completed a backfill, so the WAL accreted the whole phase's write volume, blowing disk and feeding page-cache pressure into the 8-core/7GB container OOM. The valve's writer-side backpressure() hard-cap backstop existed but was wired only into the PARSE orchestrator. Thread it into the resolution batch loop at the double-buffer's one pool-idle boundary (batch settled, next not yet fanned out), after the edge-index recreate, and through the synthesis insert loops. Parked there, the backfill completes; readers re-enter at SQLite's backfilled mark and the next persist commit wraps the WAL. Dubbo validation, same build: valve@16MB peak WAL 251MB (floor = the single-transaction edge-index recreate) vs defaults 914MB; dumps byte-identical (441,270 rows); wall unchanged (11s). Suite 2,479 green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
04ab45c91f
commit
6e52295ceb
@@ -294,3 +294,69 @@ describe('sync WAL deferral end-to-end (#1248)', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolution-phase WAL backpressure plumbing (§7a.1)', () => {
|
||||
// The valve's timer-driven passive checkpoints stay perpetually partial
|
||||
// against the resolver pool's continuous reads, so during resolution the
|
||||
// writer-side backpressure() hook is the ONLY mechanism that can complete
|
||||
// a backfill and let the WAL wrap — a kernel-scale run without it grew a
|
||||
// 22GB WAL on a 4.6GB DB. These pin that the batch loop (a) calls the hook
|
||||
// at the pool-idle boundary and (b) actually parks on a returned promise.
|
||||
|
||||
async function seedPendingRefs(cg: CodeGraph): Promise<void> {
|
||||
const raw = (cg as unknown as { db: DatabaseConnection }).db.getDb();
|
||||
const node = raw.prepare("SELECT id, file_path FROM nodes WHERE kind = 'function' LIMIT 1").get() as
|
||||
| { id: string; file_path: string }
|
||||
| undefined;
|
||||
expect(node).toBeDefined();
|
||||
const ins = raw.prepare(
|
||||
"INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, file_path, language, status) VALUES (?, ?, 'calls', 1, 0, ?, 'typescript', 'pending')"
|
||||
);
|
||||
ins.run(node!.id, 'helper0', node!.file_path);
|
||||
ins.run(node!.id, 'helper1', node!.file_path);
|
||||
}
|
||||
|
||||
it('calls the backpressure hook once per settled batch', async () => {
|
||||
writeFixtureProject();
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
await cg.indexAll();
|
||||
await seedPendingRefs(cg);
|
||||
|
||||
let calls = 0;
|
||||
const result = await cg.resolveReferencesBatched(undefined, undefined, () => {
|
||||
calls++;
|
||||
return null; // under the hard cap — loop must proceed without waiting
|
||||
});
|
||||
expect(result.stats.total).toBeGreaterThan(0);
|
||||
expect(calls).toBeGreaterThanOrEqual(1);
|
||||
await cg.close();
|
||||
});
|
||||
|
||||
it('parks the batch loop on a backpressure promise until it resolves', async () => {
|
||||
writeFixtureProject();
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
await cg.indexAll();
|
||||
await seedPendingRefs(cg);
|
||||
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((r) => { release = r; });
|
||||
let hookHit = false;
|
||||
const done = cg
|
||||
.resolveReferencesBatched(undefined, undefined, () => {
|
||||
if (hookHit) return null; // park only on the first boundary
|
||||
hookHit = true;
|
||||
return gate;
|
||||
})
|
||||
.then(() => true);
|
||||
|
||||
// Give the loop ample turns: it must reach the hook and then be parked.
|
||||
for (let i = 0; i < 50; i++) await new Promise((r) => setImmediate(r));
|
||||
expect(hookHit).toBe(true);
|
||||
const settledEarly = await Promise.race([done, Promise.resolve(false)]);
|
||||
expect(settledEarly).toBe(false); // still parked on the gate
|
||||
|
||||
release();
|
||||
expect(await done).toBe(true);
|
||||
await cg.close();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user