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,
|
type LbugConnectionHandle,
|
||||||
} from './lbug-config.js';
|
} from './lbug-config.js';
|
||||||
import {
|
import {
|
||||||
|
cleanQuarantinedMissingShadowWals,
|
||||||
finalizeLbugSidecarsAfterClose,
|
finalizeLbugSidecarsAfterClose,
|
||||||
guardWalQuarantine,
|
guardWalQuarantine,
|
||||||
isMissingShadowSidecarError,
|
isMissingShadowSidecarError,
|
||||||
|
|
@ -55,6 +56,7 @@ import {
|
||||||
quarantineWalForMissingShadow,
|
quarantineWalForMissingShadow,
|
||||||
renameFailureMessage,
|
renameFailureMessage,
|
||||||
shadowSidecarRecoveryMessage,
|
shadowSidecarRecoveryMessage,
|
||||||
|
sidecarPreflightDisabled,
|
||||||
} from './sidecar-recovery.js';
|
} from './sidecar-recovery.js';
|
||||||
|
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
|
|
@ -822,6 +824,30 @@ const doInitLbug = async (dbPath: string, readOnly: boolean = false) => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
const releaseInitLock = await acquireInitLock(dbPath);
|
const releaseInitLock = await acquireInitLock(dbPath);
|
||||||
try {
|
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
|
// Crash-recovery cleanup: if the main DB file is missing, stale sidecars
|
||||||
// from an interrupted run can block fresh opens indefinitely.
|
// from an interrupted run can block fresh opens indefinitely.
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export const isMissingFsError = (err: unknown): boolean =>
|
||||||
|
|
||||||
const missing = isMissingFsError;
|
const missing = isMissingFsError;
|
||||||
|
|
||||||
const sidecarPreflightDisabled = (): boolean =>
|
export const sidecarPreflightDisabled = (): boolean =>
|
||||||
/^(1|true|yes|on)$/i.test(process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT ?? '');
|
/^(1|true|yes|on)$/i.test(process.env.GITNEXUS_DISABLE_LBUG_SIDECAR_PREFLIGHT ?? '');
|
||||||
|
|
||||||
export const statIfExists = async (filePath: string): Promise<{ size: number } | null> => {
|
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 () => {}),
|
rename: vi.fn(async () => {}),
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -586,6 +587,7 @@ function makeFsMockWithWalSize(
|
||||||
rename: vi.fn(async () => {}),
|
rename: vi.fn(async () => {}),
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ const mockFsForInit = (dbPath: string) => {
|
||||||
unlink: vi.fn(async () => {}),
|
unlink: vi.fn(async () => {}),
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
@ -85,6 +86,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
||||||
unlink: unlinkMock,
|
unlink: unlinkMock,
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||||
|
|
@ -156,6 +158,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
||||||
unlink: unlinkMock,
|
unlink: unlinkMock,
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||||
|
|
@ -220,6 +223,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
||||||
unlink: unlinkMock,
|
unlink: unlinkMock,
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||||
|
|
@ -285,6 +289,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
||||||
unlink: unlinkMock,
|
unlink: unlinkMock,
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||||
|
|
@ -345,6 +350,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
||||||
unlink: unlinkMock,
|
unlink: unlinkMock,
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
vi.doMock('../../src/core/lbug/lbug-config.js', () => ({
|
||||||
|
|
@ -415,6 +421,7 @@ describe('lbug adapter CHECKPOINT lifecycle', () => {
|
||||||
unlink: unlinkMock,
|
unlink: unlinkMock,
|
||||||
mkdir: vi.fn(async () => {}),
|
mkdir: vi.fn(async () => {}),
|
||||||
open: makeOpenMock(),
|
open: makeOpenMock(),
|
||||||
|
readdir: vi.fn(async () => []),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const openLbugConnectionMock = vi.fn(async () => ({ db, conn }));
|
const openLbugConnectionMock = vi.fn(async () => ({ db, conn }));
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue