fix(lbug): lock queryImporters against the WAL driver (#2264)

queryImporters issued a raw conn.query on the singleton connection inside the
importer-BFS loop of incremental --pdg writeback, while the WAL-checkpoint driver
could fire a concurrent CHECKPOINT — the same double-free class. Wrap the read
(query + getAll + drain) in withConnLock.

Extends lbug-conn-serialization.test.ts with a routing assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBJomjoTdBV2eveDVq4JMm
This commit is contained in:
Gergo Magyar 2026-06-21 06:02:59 +00:00
parent 775059912d
commit 071b120169
2 changed files with 29 additions and 16 deletions

View file

@ -2003,7 +2003,8 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME;
* exports.
*/
export const queryImporters = async (targetFilePath: string): Promise<string[]> => {
if (!conn) {
const c = conn;
if (!c) {
throw new Error('LadybugDB not initialized. Call initLbug first.');
}
const escaped = targetFilePath.replace(/'/g, "''");
@ -2012,22 +2013,27 @@ export const queryImporters = async (targetFilePath: string): Promise<string[]>
WHERE r.type = 'IMPORTS' AND b.filePath = '${escaped}'
RETURN DISTINCT a.filePath AS importer
`;
let queryResult: lbug.QueryResult | lbug.QueryResult[] | undefined;
try {
queryResult = await conn.query(cypher);
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
const rows = await result.getAll();
const out: string[] = [];
for (const row of rows) {
const v = (row as { importer?: unknown }).importer;
if (typeof v === 'string' && v.length > 0) out.push(v);
// Runs inside the connection lock: queryImporters is called in the importer-BFS
// loop during incremental --pdg writeback while the WAL driver is live, so an
// unlocked conn.query here could race a concurrent CHECKPOINT on the singleton.
return withConnLock(async () => {
let queryResult: lbug.QueryResult | lbug.QueryResult[] | undefined;
try {
queryResult = await c.query(cypher);
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
const rows = await result.getAll();
const out: string[] = [];
for (const row of rows) {
const v = (row as { importer?: unknown }).importer;
if (typeof v === 'string' && v.length > 0) out.push(v);
}
return out;
} catch {
return [];
} finally {
if (queryResult) await closeQueryResults(queryResult);
}
return out;
} catch {
return [];
} finally {
if (queryResult) await closeQueryResults(queryResult);
}
});
};
/**

View file

@ -41,5 +41,12 @@ withTestLbugDB('conn-serialization', () => {
expect(lockSpy).toHaveBeenCalled();
expect(result).toMatchObject({ nodesDeleted: 0 });
});
it('U2: queryImporters routes through withConnLock', async () => {
const { queryImporters } = await import('../../src/core/lbug/lbug-adapter.js');
const importers = await queryImporters('any/path.ts');
expect(lockSpy).toHaveBeenCalled();
expect(importers).toEqual([]);
});
});
});