mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix: clarify PGLite data-dir lock contention (#2658)
This commit is contained in:
+31
-16
@@ -108,6 +108,35 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function formatLockTimestamp(value: unknown): string {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? new Date(value).toISOString()
|
||||
: 'unknown time';
|
||||
}
|
||||
|
||||
function pgliteLockTimeoutError(lockDir: string): Error {
|
||||
const lockPath = join(lockDir, LOCK_FILE);
|
||||
try {
|
||||
const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
||||
const pid = String(lockData.pid ?? 'unknown');
|
||||
const command = String(lockData.command ?? 'unknown');
|
||||
const serveHint = command.includes('gbrain serve')
|
||||
? ' The holder looks like `gbrain serve`, so this is probably serve↔sync contention from an MCP/HTTP server; stop that server/client and rerun the command.'
|
||||
: '';
|
||||
|
||||
return new Error(
|
||||
`GBrain: Timed out waiting for PGLite data-dir lock. Process ${pid} has held it since ${formatLockTimestamp(lockData.acquired_at)} (command: ${command}). ` +
|
||||
`Lock directory: ${lockDir}. If that process is dead, remove the lock directory and try again. ` +
|
||||
`This is a PGLite data-dir lock, not the \`gbrain-sync:*\` advisory lock; \`gbrain sync --break-lock\` will not clear a live PGLite holder.` +
|
||||
serveHint,
|
||||
);
|
||||
} catch {
|
||||
return new Error(
|
||||
`GBrain: Timed out waiting for PGLite lock. Remove ${lockDir} and try again.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to acquire an exclusive lock on the PGLite data directory.
|
||||
* Returns { acquired: true } if the lock was obtained, { acquired: false } otherwise.
|
||||
@@ -177,28 +206,14 @@ export async function acquireLock(dataDir: string | undefined, opts?: { timeoutM
|
||||
// mkdir failed — someone else grabbed it between our check and mkdir
|
||||
// This is fine, we'll retry
|
||||
if (Date.now() - startTime >= timeoutMs) {
|
||||
// Timeout — report which process holds the lock
|
||||
const lockPath = join(lockDir, LOCK_FILE);
|
||||
try {
|
||||
const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
||||
throw new Error(
|
||||
`GBrain: Timed out waiting for PGLite lock. Process ${lockData.pid} has held it since ${new Date(lockData.acquired_at).toISOString()} (command: ${lockData.command}). ` +
|
||||
`If that process is dead, remove ${lockDir} and try again.`
|
||||
);
|
||||
} catch (readErr) {
|
||||
if (readErr instanceof Error && readErr.message.startsWith('GBrain')) throw readErr;
|
||||
throw new Error(
|
||||
`GBrain: Timed out waiting for PGLite lock. Remove ${lockDir} and try again.`
|
||||
);
|
||||
}
|
||||
throw pgliteLockTimeoutError(lockDir);
|
||||
}
|
||||
// Brief wait before retry
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Should not reach here, but just in case
|
||||
throw new Error(`GBrain: Timed out waiting for PGLite lock.`);
|
||||
throw pgliteLockTimeoutError(lockDir);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('pglite-lock #2058 heartbeat + steal-grace', () => {
|
||||
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeHolder(fields: { pid: number; acquiredAgoMs: number; refreshedAgoMs: number }) {
|
||||
function writeHolder(fields: { pid: number; acquiredAgoMs: number; refreshedAgoMs: number; command?: string }) {
|
||||
const lockDir = join(TEST_DIR, '.gbrain-lock');
|
||||
mkdirSync(lockDir, { recursive: true });
|
||||
const now = Date.now();
|
||||
@@ -117,7 +117,7 @@ describe('pglite-lock #2058 heartbeat + steal-grace', () => {
|
||||
pid: fields.pid,
|
||||
acquired_at: now - fields.acquiredAgoMs,
|
||||
refreshed_at: now - fields.refreshedAgoMs,
|
||||
command: 'test holder',
|
||||
command: fields.command ?? 'test holder',
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -146,6 +146,26 @@ describe('pglite-lock #2058 heartbeat + steal-grace', () => {
|
||||
expect(existsSync(join(TEST_DIR, '.gbrain-lock'))).toBe(true);
|
||||
});
|
||||
|
||||
test('explains live gbrain serve contention is not a sync advisory lock', async () => {
|
||||
writeHolder({
|
||||
pid: process.pid,
|
||||
acquiredAgoMs: 60_000,
|
||||
refreshedAgoMs: 0,
|
||||
command: 'bun /Users/master/.bun/bin/gbrain serve',
|
||||
});
|
||||
|
||||
let message = '';
|
||||
try {
|
||||
await acquireLock(TEST_DIR, { timeoutMs: 100 });
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
expect(message).toContain('serve↔sync contention');
|
||||
expect(message).toContain('not the `gbrain-sync:*` advisory lock');
|
||||
expect(message).toContain('`gbrain sync --break-lock` will not clear a live PGLite holder');
|
||||
expect(existsSync(join(TEST_DIR, '.gbrain-lock'))).toBe(true);
|
||||
});
|
||||
|
||||
test('[REGRESSION] releaseLock does NOT remove a lock that was stolen + re-acquired by another process', async () => {
|
||||
// We acquire, then simulate a steal: another process reaped us past grace
|
||||
// and now owns the lock (different pid + acquired_at). Our releaseLock must
|
||||
|
||||
Reference in New Issue
Block a user