mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(lbug): reclaim missing-shadow WAL quarantine files on write-path init (#2638)
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
This commit is contained in:
parent
9538be957d
commit
cdbdf219dc
5 changed files with 173 additions and 1 deletions
|
|
@ -46,6 +46,7 @@ import {
|
|||
type LbugConnectionHandle,
|
||||
} from './lbug-config.js';
|
||||
import {
|
||||
cleanQuarantinedMissingShadowWals,
|
||||
finalizeLbugSidecarsAfterClose,
|
||||
guardWalQuarantine,
|
||||
isMissingShadowSidecarError,
|
||||
|
|
@ -55,6 +56,7 @@ import {
|
|||
quarantineWalForMissingShadow,
|
||||
renameFailureMessage,
|
||||
shadowSidecarRecoveryMessage,
|
||||
sidecarPreflightDisabled,
|
||||
} from './sidecar-recovery.js';
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
|
|
@ -822,6 +824,30 @@ const doInitLbug = async (dbPath: string, readOnly: boolean = false) => {
|
|||
// -------------------------------------------------------------------------
|
||||
const releaseInitLock = await acquireInitLock(dbPath);
|
||||
try {
|
||||
// Reclaim missing-shadow WAL quarantines from a PRIOR crash (#2637).
|
||||
// LadybugDB renames an unrecoverable WAL aside as
|
||||
// `${dbPath}.wal.missing-shadow.<ts>-<rand>` (quarantineWalForMissingShadow)
|
||||
// instead of deleting it. Once quarantined it is permanently detached from
|
||||
// the live store and never reopened, so reclaiming it is safe regardless of
|
||||
// whether the main DB file exists this run — unlike the orphan-sidecar
|
||||
// cleanup below, this must NOT be gated on "main DB missing": a quarantine
|
||||
// event and a healthy main DB are independent facts. Never let a reclaim
|
||||
// failure (e.g. a transient EBUSY from an antivirus scan) block DB startup.
|
||||
if (!sidecarPreflightDisabled()) {
|
||||
try {
|
||||
const reclaimed = await cleanQuarantinedMissingShadowWals(dbPath);
|
||||
for (const file of reclaimed) {
|
||||
logger.warn(
|
||||
`GitNexus: reclaimed quarantined WAL ${path.basename(file)} from a prior crash`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`GitNexus: failed to reclaim missing-shadow WAL quarantines: ${summarizeError(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Crash-recovery cleanup: if the main DB file is missing, stale sidecars
|
||||
// from an interrupted run can block fresh opens indefinitely.
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export const isMissingFsError = (err: unknown): boolean =>
|
|||
|
||||
const missing = isMissingFsError;
|
||||
|
||||
const sidecarPreflightDisabled = (): boolean =>
|
||||
export const sidecarPreflightDisabled = (): boolean =>
|
||||
/^(1|true|yes|on)$/i.test(process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT ?? '');
|
||||
|
||||
export const statIfExists = async (filePath: string): Promise<{ size: number } | null> => {
|
||||
|
|
|
|||
|
|
@ -328,3 +328,140 @@ describe('init lock — single-process ownership contract', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Missing-shadow WAL quarantine reclaim (issue #2637)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const plantMissingShadowQuarantine = async (dbPath: string): Promise<string> => {
|
||||
const quarantinePath = `${dbPath}.wal.missing-shadow.${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2)}`;
|
||||
await fs.writeFile(quarantinePath, 'stale-quarantined-wal-bytes');
|
||||
return quarantinePath;
|
||||
};
|
||||
|
||||
describe('missing-shadow quarantine reclaim — native integration (issue #2637)', () => {
|
||||
itLbugReopen(
|
||||
'reclaims a pre-existing missing-shadow WAL quarantine file on write-path init when the main DB is present',
|
||||
async () => {
|
||||
const tmp = await createTempDir('gitnexus-lbug-quarantine-');
|
||||
const dbPath = path.join(tmp.dbPath, 'lbug');
|
||||
|
||||
try {
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// Create a real DB first, then close it.
|
||||
await adapter.initLbug(dbPath);
|
||||
await adapter.closeLbug();
|
||||
|
||||
// Plant a quarantine file left over from an earlier crash.
|
||||
const quarantinePath = await plantMissingShadowQuarantine(dbPath);
|
||||
await expect(fs.access(quarantinePath)).resolves.toBeUndefined();
|
||||
|
||||
// Re-init with the main DB present — reclaim must fire unconditionally.
|
||||
await adapter.initLbug(dbPath);
|
||||
|
||||
const rows = await adapter.executeQuery('RETURN 1 AS ok');
|
||||
expect(rows).toEqual([{ ok: 1 }]);
|
||||
|
||||
await expect(fs.access(quarantinePath)).rejects.toThrow();
|
||||
|
||||
await adapter.closeLbug();
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itLbugReopen(
|
||||
'reclaims a pre-existing missing-shadow WAL quarantine file when the main DB is ALSO missing (crash-recovery path)',
|
||||
async () => {
|
||||
const tmp = await createTempDir('gitnexus-lbug-quarantine-');
|
||||
const dbPath = path.join(tmp.dbPath, 'lbug');
|
||||
const shadowPath = `${dbPath}.shadow`;
|
||||
const walCheckpointPath = `${dbPath}.wal.checkpoint`;
|
||||
|
||||
try {
|
||||
// No main DB file — plant the quarantine file alongside the #1618
|
||||
// orphan sidecars to prove both cleanup blocks coexist correctly.
|
||||
const quarantinePath = await plantMissingShadowQuarantine(dbPath);
|
||||
await fs.writeFile(shadowPath, 'stale-shadow-data');
|
||||
await fs.writeFile(walCheckpointPath, 'stale-wal-checkpoint-data');
|
||||
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
await adapter.initLbug(dbPath);
|
||||
|
||||
const rows = await adapter.executeQuery('RETURN 1 AS ok');
|
||||
expect(rows).toEqual([{ ok: 1 }]);
|
||||
|
||||
await expect(fs.access(quarantinePath)).rejects.toThrow();
|
||||
await expect(fs.access(shadowPath)).rejects.toThrow();
|
||||
await expect(fs.access(walCheckpointPath)).rejects.toThrow();
|
||||
|
||||
await adapter.closeLbug();
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itLbugReopen(
|
||||
'leaves a missing-shadow quarantine file untouched when GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT=1',
|
||||
async () => {
|
||||
const tmp = await createTempDir('gitnexus-lbug-quarantine-');
|
||||
const dbPath = path.join(tmp.dbPath, 'lbug');
|
||||
const previousEnv = process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT;
|
||||
|
||||
try {
|
||||
const quarantinePath = await plantMissingShadowQuarantine(dbPath);
|
||||
|
||||
process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT = '1';
|
||||
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
await adapter.initLbug(dbPath);
|
||||
|
||||
const rows = await adapter.executeQuery('RETURN 1 AS ok');
|
||||
expect(rows).toEqual([{ ok: 1 }]);
|
||||
|
||||
// Reclaim was suppressed — the quarantine file survives.
|
||||
await expect(fs.access(quarantinePath)).resolves.toBeUndefined();
|
||||
|
||||
await adapter.closeLbug();
|
||||
} finally {
|
||||
if (previousEnv === undefined) {
|
||||
delete process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT;
|
||||
} else {
|
||||
process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT = previousEnv;
|
||||
}
|
||||
await tmp.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itLbugReopen(
|
||||
'does not touch a .dirty-recovery parked sidecar (isolation from the missing-shadow family)',
|
||||
async () => {
|
||||
const tmp = await createTempDir('gitnexus-lbug-quarantine-');
|
||||
const dbPath = path.join(tmp.dbPath, 'lbug');
|
||||
const dirtyRecoveryPath = `${dbPath}.wal.dirty-recovery`;
|
||||
|
||||
try {
|
||||
await fs.writeFile(dirtyRecoveryPath, 'parked-from-an-interrupted-dirty-recovery-rebuild');
|
||||
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
await adapter.initLbug(dbPath);
|
||||
|
||||
const rows = await adapter.executeQuery('RETURN 1 AS ok');
|
||||
expect(rows).toEqual([{ ok: 1 }]);
|
||||
|
||||
// Different sidecar family, different lifecycle — must survive.
|
||||
await expect(fs.access(dirtyRecoveryPath)).resolves.toBeUndefined();
|
||||
|
||||
await adapter.closeLbug();
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ function makeFsMock(dbPath: string) {
|
|||
rename: vi.fn(async () => {}),
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -586,6 +587,7 @@ function makeFsMockWithWalSize(
|
|||
rename: vi.fn(async () => {}),
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const mockFsForInit = (dbPath: string) => {
|
|||
unlink: vi.fn(async () => {}),
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
|
@ -85,6 +86,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
|||
unlink: unlinkMock,
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||
|
|
@ -156,6 +158,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
|||
unlink: unlinkMock,
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||
|
|
@ -220,6 +223,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
|||
unlink: unlinkMock,
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||
|
|
@ -285,6 +289,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
|||
unlink: unlinkMock,
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||
|
|
@ -345,6 +350,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
|||
unlink: unlinkMock,
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||
|
|
@ -415,6 +421,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
|||
unlink: unlinkMock,
|
||||
mkdir: vi.fn(async () => {}),
|
||||
open: makeOpenMock(),
|
||||
readdir: vi.fn(async () => []),
|
||||
},
|
||||
}));
|
||||
const openLbugConnectionMock = vi.fn(async () => ({ db, conn }));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue