fix(scale): kernel-scale hardening — OOM-safe pass skipping + watchdog-safe index recreate (#1323)

Two hazards found by running today's full stack against the Linux kernel
(70,129 files) in the cg1212 repro container:

1. The parallel-synthesis fallback retried a worker-failed pass on the MAIN
   thread. At multi-million-node scale a worker failure is usually a memory
   ceiling, so the retry would OOM the process and take the whole index with
   it. Above 1.5M nodes a failed pass is now skipped with a clear stderr
   message (its synthesized edges are absent; the index completes). Below
   that, the main-thread retry stays — small-scale worker crashes are
   transient and the retry keeps coverage.

2. endBulkEdgeLoad rebuilt all four edge indexes in one synchronous span —
   measured 79s at kernel scale, past the #850 liveness watchdog's 60s
   stall window. A daemon-triggered re-index would have been SIGKILLed right
   after doing the work. Now async with an event-loop yield between builds,
   keeping each stall to a single index (~20s at kernel scale).

Validation: full Linux kernel index to completion in the repro container —
2,048,674 nodes / 6,405,964 edges, EXIT 0, zero passes skipped, on a 2-CPU
VM (worst case: pool disabled, sequential resolution + synthesis) in ~27min.
Phase walls: parse 6.0m, resolution 19.5m (incl. synthesis 6.3m, recreate
79s), maintenance 74s. Suite green (2444).

Also adds docs/design/native-extraction-kernel.md — the spike-validated
design for the native extraction kernel (Rust parse+walk over dubbo's Java:
202ms rayon / 1.07s single-thread vs 4.7s for the current wasm pipeline).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-16 19:09:01 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 567b4ad4be
commit 4efc6c70e2
5 changed files with 119 additions and 4 deletions
+10 -1
View File
@@ -185,14 +185,23 @@ export class DatabaseConnection {
* Leave bulk-edge-load mode: recreate the dropped indexes in one pass each
* over the (now fully loaded) edges table — far cheaper than maintaining
* them per-insert. DDL is extracted from schema.sql so it cannot drift.
*
* Async with a yield BETWEEN the four CREATE INDEX statements: each build is
* a synchronous scan of the whole edges table (~20s apiece at Linux-kernel
* scale, 79s total measured), and running them back-to-back is a single
* event-loop stall longer than the #850 liveness watchdog's 60s window — a
* daemon-triggered re-index would be SIGKILLed right after doing the work.
* One yield per statement keeps every stall to a single index build, which
* stays inside the window.
*/
endBulkEdgeLoad(): void {
async endBulkEdgeLoad(): Promise<void> {
const schemaPath = path.join(__dirname, 'schema.sql');
const schema = fs.readFileSync(schemaPath, 'utf-8');
for (const idx of DatabaseConnection.BULK_EDGE_INDEX_NAMES) {
const m = schema.match(new RegExp(`CREATE INDEX IF NOT EXISTS ${idx}\\b[^;]*;`));
if (!m) throw new Error(`schema.sql: edge index ${idx} not found for bulk-load recreation`);
this.db.exec(m[0]);
await new Promise((resolve) => setImmediate(resolve));
}
}
+19 -1
View File
@@ -3666,6 +3666,15 @@ export async function synthesizeCallbackEdges(
else markPass(SYNTH_PASSES[i]!.name, 0);
}
// Above this node count, a pass that OOM-killed its worker must NOT be
// retried on the main thread — the retry would OOM the whole process and
// take the index with it (the #1212 failure class). Below it, a worker
// failure is more likely a transient crash than a memory ceiling, and the
// main-thread retry keeps coverage. Skipping loses only that pass's
// synthesized edges; the index still completes.
const MAIN_RETRY_MAX_NODES = 1_500_000;
const graphNodes = queries.getNodeAndEdgeCount().nodes;
if (pool && gatedIn.length > 1) {
await Promise.all(
gatedIn.map(async (i) => {
@@ -3674,7 +3683,16 @@ export async function synthesizeCallbackEdges(
const out = await pool.runSynthPass(pass.name);
passEdges[i] = out.edges;
markPass(pass.name, out.ms);
} catch {
} catch (err) {
if (graphNodes > MAIN_RETRY_MAX_NODES) {
// Worker died at a scale where the main-thread retry is a process
// OOM risk: skip the pass, keep the index alive, and say so.
console.error(
`[synthesis] pass '${pass.name}' failed on a worker at ${graphNodes} nodes — skipped (edges from this pass are absent): ${err instanceof Error ? err.message : String(err)}`
);
markPass(`${pass.name} (skipped at scale)`, 0);
return;
}
// Worker-side failure (crash, OOM, unknown pass after a version
// mismatch): retry this one pass on the main thread.
await runPassOnMain(i);
+2 -2
View File
@@ -1339,7 +1339,7 @@ export class ReferenceResolver {
// disables entirely. bulkEdgeLoad hooks (when provided) bracket the batch
// loop with drop/recreate of the non-unique edge indexes on big runs —
// see DatabaseConnection.beginBulkEdgeLoad.
parallel?: { dbPath: string; bulkEdgeLoad?: { begin: () => void; end: () => void } }
parallel?: { dbPath: string; bulkEdgeLoad?: { begin: () => void; end: () => void | Promise<void> } }
): Promise<ResolutionResult> {
// Resolution runs on the indexer's MAIN thread, and the #850 liveness
// watchdog SIGKILLs a process whose event loop stalls past its window (60s
@@ -1583,7 +1583,7 @@ export class ReferenceResolver {
// DatabaseConnection open (schema.sql re-applies IF NOT EXISTS).
if (bulkEdgesActive) {
const tIdx = Date.now();
parallel!.bulkEdgeLoad!.end();
await parallel!.bulkEdgeLoad!.end();
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] edge-index-recreate: ${Date.now() - tIdx}ms`);
}
}