mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
* fix(lbug): robust Windows lock acquisition for CI integration tests
LadybugDB's `new Database()` raises `Could not set lock on file` from
local_file_system.cpp synchronously inside the constructor — before any
query is issued, so `withLbugDb`'s query-time retry never sees it. On
Windows CI this surfaces as flaky integration tests due to AV-scanner
holds, libuv handle-release lag, and stale `.wal` sidecars from aborted
prior runs.
This change closes the gap at *open time*:
- `openLbugConnection` now wraps `new lbug.Database()` in a bounded
busy-retry (5x100ms back-off) inside `lbug-config.ts`. Errors that
exhaust the budget are tagged via `LBUG_OPEN_RETRY_EXHAUSTED` so
`withLbugDb`'s outer 3x retry skips re-retrying a freshly-exhausted
path (eliminates the 3x5=15-attempt / ~6s tail latency).
- For recognized test fixtures only (immediate-parent dir matches a
known prefix AND resolves under `os.tmpdir()`), one final stale-
sidecar sweep removes `.wal`/`.lock` and retries once. Production
paths never enter this branch.
- `safeClose` on Windows runs a bounded `fs.open` probe to absorb
native handle-release lag; logs a warning if the probe exhausts so
operators can spot AV interference.
- `isDbBusyError` is now defined in `lbug-config.ts` as the single
source of truth, re-exported from `lbug-adapter.ts` for compatibility.
- New tests cover open-time retry (happy/retry/exhaust/non-busy/tag),
stale-sidecar sweep (test-fixture-only, production-rejection,
preserves-original-error), `isTestFixturePath` direct unit suite
(accept/reject/traversal/nested/trailing-sep), and
`waitForWindowsHandleRelease` (openable/ENOENT/no-leak).
- The two new test files are added to vitest's existing serialized
`lbug-db` project (already `fileParallelism: false`).
Closes the chronic Windows CI flake on lbug-touching integration tests
while preserving the existing single-writable-Database-per-process
LadybugDB contract. No public API surface changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(lbug): drop isDbBusyError re-export, import from lbug-config directly
The re-export from lbug-adapter.ts was a transitional convenience — with
the matcher now living in lbug-config.ts, having two import paths for the
same symbol invites future drift. Updated the two real consumers
(lbug-lock-retry.test.ts, lbug-open-retry.test.ts) to import from
lbug-config directly, removed the re-export equality test (now vacuous),
and refreshed the explanatory comment so it no longer references a
re-export pattern that doesn't exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(lbug): silence benign LadybugDB v0.16.1 schema-init lock warnings on Windows
doInitLbug logs "⚠️ Schema creation warning: ... Could not set lock on
file" on every CREATE NODE TABLE call after the first init on a given
dbPath, on Windows. The lock is internal to LadybugDB v0.16.1 and is
resolved before the table is created — same tolerance pattern as the
existing "already exists" filter. Genuine cross-process lock contention
still surfaces on the next operation through withLbugDb's retry, so
filtering at the schema-init catch only suppresses noise, not signal.
Also extend the safeClose Windows handle-release probe to cover the
.wal sidecar (the previous Database's WAL handle was the slowest to
release, surfacing as the schema-query lock contention) and switch the
probe back to 'r+' so it actually detects exclusive locks.
Test loop in lbug-close-handle-release.test.ts simplified to 10 plain
iterations now that the underlying noise is filtered upstream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(lbug): isDbBusyError review fixes
- Drop redundant `could not set lock` term — already subsumed by `lock`.
- Document the intentionally-broad matcher: graph-DB lock-shaped errors
("deadlock", "unlock failed", "lock contention", "could not open lock
file") are all treated as transient. If a non-transient surfaces,
tighten the matcher rather than raise the retry budget.
- Add positive test cases covering those lock-shaped strings so the
intent is visible and a future tightening would deliberately break
these.
- Fix the open-retry back-off comment: max sleep is 100+200+300+400 =
1000ms (no sleep after the final attempt), not 1.5s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
/**
|
|
* Integration Tests: lbug-adapter busy/lock retry logic
|
|
*
|
|
* Tests isDbBusyError() detection and withLbugDb() retry behaviour
|
|
* using the real LadybugDB via withTestLbugDB lifecycle.
|
|
*
|
|
* Follows existing lbug integration test patterns (lbug-core-adapter,
|
|
* lbug-pool, lbug-pool-stability).
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
|
|
|
// ─── isDbBusyError ────────────────────────────────────────────────────────
|
|
|
|
// Pure-function tests — no DB needed, but grouped here for cohesion
|
|
// with the retry logic they guard.
|
|
import { isDbBusyError } from '../../src/core/lbug/lbug-config.js';
|
|
|
|
describe('isDbBusyError', () => {
|
|
it('returns true for "busy" errors (case-insensitive)', () => {
|
|
expect(isDbBusyError(new Error('Database is BUSY'))).toBe(true);
|
|
expect(isDbBusyError(new Error('busy'))).toBe(true);
|
|
expect(isDbBusyError('resource busy')).toBe(true);
|
|
});
|
|
|
|
it('returns true for "lock" errors', () => {
|
|
expect(isDbBusyError(new Error('Could not set lock on file'))).toBe(true);
|
|
expect(isDbBusyError(new Error('database is locked'))).toBe(true);
|
|
expect(isDbBusyError(new Error('LOCK'))).toBe(true);
|
|
});
|
|
|
|
it('returns true for "already in use" errors', () => {
|
|
expect(isDbBusyError(new Error('file already in use by another process'))).toBe(true);
|
|
expect(isDbBusyError('already in use')).toBe(true);
|
|
});
|
|
|
|
it('returns true for "could not set lock" errors', () => {
|
|
expect(isDbBusyError(new Error('Could not set lock on the database file'))).toBe(true);
|
|
});
|
|
|
|
it('returns false for unrelated errors', () => {
|
|
expect(isDbBusyError(new Error('Table not found'))).toBe(false);
|
|
expect(isDbBusyError(new Error('Connection refused'))).toBe(false);
|
|
expect(isDbBusyError(new Error('Syntax error in Cypher query'))).toBe(false);
|
|
expect(isDbBusyError(null)).toBe(false);
|
|
expect(isDbBusyError(undefined)).toBe(false);
|
|
});
|
|
|
|
// Documented behavior for lock-shaped strings: the matcher is intentionally
|
|
// broad because in graph-DB contexts these are all transient. If LadybugDB
|
|
// ever surfaces a non-transient lock-shaped error (e.g., a recovery-time
|
|
// "lock file missing"), tighten the matcher and add a negative test here
|
|
// rather than raising the retry budget.
|
|
it('treats other lock-shaped errors as transient (current intentional behavior)', () => {
|
|
expect(isDbBusyError(new Error('deadlock detected'))).toBe(true);
|
|
expect(isDbBusyError(new Error('unlock failed'))).toBe(true);
|
|
expect(isDbBusyError(new Error('lock contention'))).toBe(true);
|
|
expect(isDbBusyError(new Error('Could not open lock file'))).toBe(true);
|
|
});
|
|
|
|
it('handles non-Error values gracefully', () => {
|
|
expect(isDbBusyError('BUSY error')).toBe(true);
|
|
expect(isDbBusyError(42)).toBe(false);
|
|
expect(isDbBusyError({ message: 'locked' })).toBe(false); // plain object not Error
|
|
});
|
|
});
|
|
|
|
// ─── withLbugDb retry integration tests ───────────────────────────────────
|
|
|
|
withTestLbugDB('lock-retry', (handle) => {
|
|
describe('withLbugDb retry behaviour', () => {
|
|
it('returns the operation result on success', async () => {
|
|
const { withLbugDb } = await import('../../src/core/lbug/lbug-adapter.js');
|
|
const result = await withLbugDb(handle.dbPath, async () => 'ok');
|
|
expect(result).toBe('ok');
|
|
});
|
|
|
|
it('retries on BUSY error and succeeds on later attempt', async () => {
|
|
const { withLbugDb } = await import('../../src/core/lbug/lbug-adapter.js');
|
|
let callCount = 0;
|
|
const result = await withLbugDb(handle.dbPath, async () => {
|
|
callCount++;
|
|
if (callCount === 1) throw new Error('database is BUSY');
|
|
return 'recovered';
|
|
});
|
|
|
|
expect(result).toBe('recovered');
|
|
expect(callCount).toBe(2);
|
|
});
|
|
|
|
it('propagates non-BUSY errors immediately without retrying', async () => {
|
|
const { withLbugDb } = await import('../../src/core/lbug/lbug-adapter.js');
|
|
let callCount = 0;
|
|
await expect(
|
|
withLbugDb(handle.dbPath, async () => {
|
|
callCount++;
|
|
throw new Error('Syntax error in Cypher');
|
|
}),
|
|
).rejects.toThrow('Syntax error in Cypher');
|
|
|
|
expect(callCount).toBe(1); // no retry for non-BUSY errors
|
|
});
|
|
|
|
it('throws after max retry attempts', async () => {
|
|
const { withLbugDb } = await import('../../src/core/lbug/lbug-adapter.js');
|
|
let callCount = 0;
|
|
await expect(
|
|
withLbugDb(handle.dbPath, async () => {
|
|
callCount++;
|
|
throw new Error('Could not set lock');
|
|
}),
|
|
).rejects.toThrow('Could not set lock');
|
|
|
|
// DB_LOCK_RETRY_ATTEMPTS = 3 (default in the implementation)
|
|
expect(callCount).toBe(3);
|
|
});
|
|
});
|
|
});
|