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:
Colby Mchenry
2026-07-17 07:48:34 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 04ab45c91f
commit 6e52295ceb
4 changed files with 118 additions and 6 deletions
+66
View File
@@ -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();
});
});