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:
co-authored by
Claude Fable 5
parent
567b4ad4be
commit
4efc6c70e2
+10
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user