mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(lbug): lock deleteNodesForFile count query on the singleton path (#2264)
The per-table count read used a raw targetConn.query while the sibling DETACH DELETE already routed through the locked queryAndDrain — an asymmetry that left the count racing the WAL-checkpoint driver during incremental --pdg writeback. Gate the count through withConnLock when targetConn === conn (the singleton), matching queryAndDrain; per-query/temp connections stay lock-free. Test asserts the count loop takes the lock once per filePath-bearing node table. 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:
parent
071b120169
commit
f5fb6fc384
2 changed files with 20 additions and 5 deletions
|
|
@ -1947,12 +1947,15 @@ export const deleteNodesForFile = async (
|
|||
if (tableName === 'Community' || tableName === 'Process') continue;
|
||||
|
||||
try {
|
||||
// First count how many we'll delete
|
||||
// First count how many we'll delete. On the singleton connection this
|
||||
// count runs inside withConnLock (incremental --pdg writeback executes
|
||||
// while the WAL driver is live); per-query/temp connections skip the
|
||||
// lock, matching queryAndDrain's `targetConn === conn` gate — the sibling
|
||||
// DETACH DELETE below already routes through it. (#2264)
|
||||
const tn = escapeTableName(tableName);
|
||||
const countResult = await targetConn!.query(
|
||||
`MATCH (n:${tn}) WHERE n.filePath = '${escapedPath}' RETURN count(n) AS cnt`,
|
||||
);
|
||||
const rows = await readQueryRows(countResult);
|
||||
const countCypher = `MATCH (n:${tn}) WHERE n.filePath = '${escapedPath}' RETURN count(n) AS cnt`;
|
||||
const runCount = async () => readQueryRows(await targetConn!.query(countCypher));
|
||||
const rows = targetConn === conn ? await withConnLock(runCount) : await runCount();
|
||||
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
||||
|
||||
if (count > 0) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
import { NODE_TABLES } from '../../src/core/lbug/schema.js';
|
||||
|
||||
// Spy `withConnLock` while preserving its real behavior (call-through). The
|
||||
// adapter imports this module, so the spy observes every lock acquisition.
|
||||
|
|
@ -48,5 +49,16 @@ withTestLbugDB('conn-serialization', () => {
|
|||
expect(lockSpy).toHaveBeenCalled();
|
||||
expect(importers).toEqual([]);
|
||||
});
|
||||
|
||||
it('U3: deleteNodesForFile (singleton) locks every per-table count query', async () => {
|
||||
const { deleteNodesForFile } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
const result = await deleteNodesForFile('any/path.ts');
|
||||
// One locked count per filePath-bearing node table (Community/Process are
|
||||
// skipped), proving the count read — not just the already-locked DELETE —
|
||||
// now serializes. Baseline (count unlocked) would show ~1 lock call.
|
||||
const filePathTables = NODE_TABLES.filter((t) => t !== 'Community' && t !== 'Process');
|
||||
expect(lockSpy.mock.calls.length).toBeGreaterThanOrEqual(filePathTables.length);
|
||||
expect(result).toMatchObject({ deletedNodes: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue