From 071b1201690be467f171cd6432d859261b381c34 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 21 Jun 2026 06:02:59 +0000 Subject: [PATCH] fix(lbug): lock queryImporters against the WAL driver (#2264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01JBJomjoTdBV2eveDVq4JMm --- gitnexus/src/core/lbug/lbug-adapter.ts | 38 +++++++++++-------- .../lbug-conn-serialization.test.ts | 7 ++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index fadedb645..39c91b7a0 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -2003,7 +2003,8 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME; * exports. */ export const queryImporters = async (targetFilePath: string): Promise => { - 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 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); - } + }); }; /** diff --git a/gitnexus/test/integration/lbug-conn-serialization.test.ts b/gitnexus/test/integration/lbug-conn-serialization.test.ts index d23507f6c..89fbf1a01 100644 --- a/gitnexus/test/integration/lbug-conn-serialization.test.ts +++ b/gitnexus/test/integration/lbug-conn-serialization.test.ts @@ -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([]); + }); }); });