fix(db): delete orphaned WAL/SHM files alongside empty gsd.db (#2478)
syncProjectRootToWorktree deleted empty gsd.db but left companion -wal and -shm files on disk. On Node 24, node:sqlite attempts WAL recovery from orphaned files, triggering a synchronous CPU spin loop (227% CPU, 1.4GB RSS). Now deletes gsd.db-wal and gsd.db-shm when the main DB is deleted or already missing.
This commit is contained in:
parent
b0697f24f6
commit
e4987f5337
2 changed files with 99 additions and 2 deletions
|
|
@ -314,10 +314,28 @@ export function syncProjectRootToWorktree(
|
|||
// openDatabase re-creates it, causing "no such table" failures (#2815).
|
||||
try {
|
||||
const wtDb = join(wtGsd, "gsd.db");
|
||||
let deleteSidecars = false;
|
||||
if (existsSync(wtDb)) {
|
||||
const size = statSync(wtDb).size;
|
||||
if (size === 0) {
|
||||
unlinkSync(wtDb);
|
||||
deleteSidecars = true;
|
||||
}
|
||||
} else {
|
||||
// Main DB already missing — sidecars are orphaned from a previous
|
||||
// partial cleanup and must still be removed.
|
||||
deleteSidecars = true;
|
||||
}
|
||||
// Always clean up WAL/SHM sidecar files when the main DB was deleted
|
||||
// or is already missing. Orphaned WAL/SHM files cause SQLite WAL
|
||||
// recovery on next open, which triggers a CPU spin on Node 24's
|
||||
// node:sqlite DatabaseSync implementation (#2478).
|
||||
if (deleteSidecars) {
|
||||
for (const suffix of ["-wal", "-shm"]) {
|
||||
const f = wtDb + suffix;
|
||||
if (existsSync(f)) {
|
||||
unlinkSync(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -100,8 +100,87 @@ describe('worktree-db-respawn-truncation (#2815)', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── 3. Milestone artifacts still synced when DB is preserved ────────
|
||||
console.log('\n=== 3. milestone artifacts still synced even when DB preserved ===');
|
||||
// ─── 3. WAL/SHM sidecar files cleaned up when empty DB is deleted (#2478) ──
|
||||
console.log('\n=== 3. orphaned WAL/SHM cleaned up alongside empty gsd.db (#2478) ===');
|
||||
{
|
||||
const mainBase = createBase('main');
|
||||
const wtBase = createBase('wt');
|
||||
|
||||
try {
|
||||
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
||||
mkdirSync(m001Dir, { recursive: true });
|
||||
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
||||
|
||||
// Create an empty (0-byte) gsd.db plus orphaned WAL and SHM files —
|
||||
// this is the exact state that causes Node 24 node:sqlite CPU spin (#2478).
|
||||
const wtGsd = join(wtBase, '.gsd');
|
||||
writeFileSync(join(wtGsd, 'gsd.db'), '');
|
||||
writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(605672, 0xAA));
|
||||
writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(32768, 0xBB));
|
||||
|
||||
assert.ok(existsSync(join(wtGsd, 'gsd.db')), 'gsd.db exists before sync');
|
||||
assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'gsd.db-wal exists before sync');
|
||||
assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'gsd.db-shm exists before sync');
|
||||
|
||||
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
||||
|
||||
assert.ok(
|
||||
!existsSync(join(wtGsd, 'gsd.db')),
|
||||
'#2478: empty gsd.db must be deleted',
|
||||
);
|
||||
assert.ok(
|
||||
!existsSync(join(wtGsd, 'gsd.db-wal')),
|
||||
'#2478: orphaned gsd.db-wal must be deleted alongside gsd.db',
|
||||
);
|
||||
assert.ok(
|
||||
!existsSync(join(wtGsd, 'gsd.db-shm')),
|
||||
'#2478: orphaned gsd.db-shm must be deleted alongside gsd.db',
|
||||
);
|
||||
} finally {
|
||||
cleanup(mainBase);
|
||||
cleanup(wtBase);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 4. Orphaned WAL/SHM cleaned up even when gsd.db already missing (#2478) ──
|
||||
console.log('\n=== 4. orphaned WAL/SHM cleaned up even without gsd.db (#2478) ===');
|
||||
{
|
||||
const mainBase = createBase('main');
|
||||
const wtBase = createBase('wt');
|
||||
|
||||
try {
|
||||
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
||||
mkdirSync(m001Dir, { recursive: true });
|
||||
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
||||
|
||||
// Orphaned WAL/SHM with NO gsd.db at all — can happen from a previous
|
||||
// partial cleanup. These must still be cleaned up.
|
||||
const wtGsd = join(wtBase, '.gsd');
|
||||
writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(1024, 0xAA));
|
||||
writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(1024, 0xBB));
|
||||
|
||||
assert.ok(!existsSync(join(wtGsd, 'gsd.db')), 'gsd.db does not exist');
|
||||
assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'orphaned gsd.db-wal exists');
|
||||
assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'orphaned gsd.db-shm exists');
|
||||
|
||||
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
||||
|
||||
assert.ok(
|
||||
!existsSync(join(wtGsd, 'gsd.db-wal')),
|
||||
'#2478: orphaned gsd.db-wal must be deleted even without main db file',
|
||||
);
|
||||
assert.ok(
|
||||
!existsSync(join(wtGsd, 'gsd.db-shm')),
|
||||
'#2478: orphaned gsd.db-shm must be deleted even without main db file',
|
||||
);
|
||||
} finally {
|
||||
cleanup(mainBase);
|
||||
cleanup(wtBase);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 5. Milestone artifacts still synced when DB is preserved ────────
|
||||
console.log('\n=== 5. milestone artifacts still synced even when DB preserved ===');
|
||||
{
|
||||
const mainBase = createBase('main');
|
||||
const wtBase = createBase('wt');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue