Files
codegraph/__tests__/index-store-failure.test.ts
Colby Mchenry c3b1348065 fix(indexing): reject indexAll after a failed store instead of spinning (#1773) (#2079)
The in-order commit window waits for the commit cursor to advance before
dispatching more parses. After a store fails, flushOrdered returns at once and
the cursor never moves, so the backpressure loop spun on microtasks forever:
indexAll never settled, a core stayed pinned, and no timer in the process
could fire. It reproduces whenever a store error (a closed database, disk
full, an I/O error) arrives with more files left than the window holds.

In the test suite, which runs from source with a window of one, a test that
timed out mid-index and closed its graph in afterEach wedged the whole worker,
so a loaded Windows run never produced a report.

Stop waiting once a store has failed or the run was aborted, and destroy the
parse pool on that error path so its worker threads do not outlive the index.
The regression test runs the scenario in a child process under a hard timeout.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:14:10 +00:00

58 lines
2.6 KiB
TypeScript

/**
* A store failure during indexAll must reject it, not hang the process (#1773).
*
* The in-order commit window waited for the commit cursor to advance, but after
* a failed store the cursor never moves: flushOrdered returns at once, so the
* backpressure loop spun on microtasks forever. The process pinned a core and
* no timer could fire, so a test that closed its graph mid-index (after a
* timeout) wedged the whole vitest worker. Runs in a child process under a hard
* timeout: a regression would otherwise hang this worker the same way.
*/
import { describe, it, expect } from 'vitest';
import { spawnSync } from 'child_process';
import * as path from 'path';
import { WASM_RUNTIME_FLAGS } from '../src/extraction/wasm-runtime-flags';
const DIST = path.resolve(__dirname, '../dist');
const script = `
const fs = require('fs'); const os = require('os'); const path = require('path');
const { default: CodeGraph } = require(path.join(process.argv[1], 'index.js'));
const { ExtractionOrchestrator } = require(path.join(process.argv[1], 'extraction/index.js'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-store-failure-'));
// More files than the commit window holds, so the failed cursor is waited on.
for (let i = 0; i < 12; i++) fs.writeFileSync(path.join(dir, 'f' + i + '.ts'), 'export function f' + i + '() { return ' + i + '; }\\n');
const store = ExtractionOrchestrator.prototype.storeExtractionResult;
let calls = 0;
ExtractionOrchestrator.prototype.storeExtractionResult = function (...args) {
if (calls++ === 0) throw new Error('injected store failure');
return store.apply(this, args);
};
const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.ts'], exclude: [] } });
cg.indexAll().then(
() => process.stdout.write('resolved\\n'),
(err) => process.stdout.write('rejected: ' + err.message + '\\n'),
).finally(() => {
try { cg.close(); } catch {}
fs.rmSync(dir, { recursive: true, force: true });
});
`;
describe('indexAll after a failed store (#1773)', () => {
it('rejects with the store error instead of spinning', () => {
const child = spawnSync(process.execPath, [...WASM_RUNTIME_FLAGS, '-e', script, DIST], {
encoding: 'utf8',
timeout: 60_000,
env: {
...process.env,
// The main-thread store path, with the smallest pooled commit window.
CODEGRAPH_NO_STORE_WORKER: '1',
CODEGRAPH_PARSE_WORKERS: '1',
CODEGRAPH_TELEMETRY: '0',
},
});
expect(child.signal, `indexAll did not settle: ${child.stderr}`).toBeNull();
expect(child.stdout.trim()).toBe('rejected: injected store failure');
}, 90_000);
});