Files
OmniRoute/src/lib/db/probeUtils.ts
backryun 752aac65d6 fix(ci): repair release-root regressions — pack dedup, web-session syntax, uc-image ids (#12423)
Three regressions inherited by every PR rebased onto release/v3.8.51, caught and documented with the exact failing output.

The one that mattered most: src/shared/providers/webSessionCredentials.ts did not parse. The UC merge (#11513) inserted the uc: entry inside maxai.storageKeys and lost the array's closing ], plus the entry's }, leaving `ERROR: Expected "]" but found ":"` at line 351. That module is imported by the provider API routes, bulk-web-session, autoCombo's virtualFactory, keepaliveThreshold and dashboard components, so the break was live on the tip and flooded unrelated catalog tests with transform failures. That was my conflict resolution, not the contributor's code — thank you for catching it and for tracing it to the root commit rather than patching around the symptom.

Also fixed: the duplicate bin/cli/utils/volatileEnvPath.mjs entry in PACK_ARTIFACT_REQUIRED_PATHS (findMissingArtifactPaths reported it twice), and UC image models made prefix-addressable without letting them claim historical bare model ids belonging to other providers.

Reconciled on merge: #12394 landed the busy_timeout/probe work first, so src/lib/db/core.ts takes the tip's side. probeUtils.ts is the union of both rather than either side — this PR's message regex is wider (SQLite also reports "database table is locked", "database schema is locked" and "database is busy"), while #12394 added the driver code/errcode path that keeps a transient lock from being classified as corruption and renaming the database away. Taking either alone would have dropped the other half; this PR's own ENOENT test is what surfaced it.

Verified: 76/76 across uc-image, probe-9541-repro, web-session-contract, pack-artifact-policy, bulk-web-session-import and exclusive-connection-leases, and every changed .ts file parses.

Thanks @backryun.
2026-09-02 03:57:46 -03:00

116 lines
4.3 KiB
TypeScript

/**
* Probe-retry utilities for the SQLite corruption-probe path in getDbInstance().
*
* Transient probe errors (SQLITE_BUSY, ENOENT, SQLITE_PROTOCOL, SQLITE_IOERR)
* should be retried with backoff instead of immediately renaming the DB away
* and creating an empty one (data loss under concurrent load, #9541).
*/
import fs from "node:fs";
import path from "node:path";
/**
* Identifies transient SQLite/OS probe errors that should be retried instead of
* triggering the corruption-rename path.
*
* Transient errors are conditions that can self-resolve within milliseconds:
* - SQLITE_BUSY: database is locked by another connection
* - SQLITE_PROTOCOL: locking protocol violation
* - SQLITE_IOERR: disk I/O error (can be transient under load)
* - ENOENT: file disappeared (race with another process/worker deleting it)
*
* Fatal errors (native load failures, OOM, module-not-found) are NOT transient.
*/
export function isTransientProbeError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
// #12423 widened the message side: SQLite also reports "database table is
// locked", "database schema is locked" and "database is busy" for the same
// transient contention that "database is locked" covers.
if (
/SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT|database(?: table| schema)? is (?:locked|busy)/i.test(
message
)
) {
return true;
}
// The real drivers do not put the result-code name in the message: both
// report plain "database is locked" for SQLITE_BUSY. better-sqlite3 carries
// the name in `code`, node:sqlite the numeric primary code in `errcode`
// (5 BUSY, 10 IOERR, 15 PROTOCOL; extended codes live in the high bits).
// Without this, a transient lock during the probe was classified as
// corruption and the database was renamed away.
if (typeof error !== "object" || error === null) return false;
const { code, errcode } = error as { code?: unknown; errcode?: unknown };
if (typeof code === "string" && /^SQLITE_(BUSY|PROTOCOL|IOERR)/.test(code)) return true;
return typeof errcode === "number" && [5, 10, 15].includes(errcode & 0xff);
}
/**
* Synchronous sleep that blocks the event loop for `ms` milliseconds.
* Only used in the transient-probe-error retry path where we are already in
* a synchronous context (better-sqlite3). Uses `Atomics.wait` which yields to
* the OS scheduler during the wait, falling back to a busy-wait on runtimes
* where Atomics.wait is restricted.
*/
function syncSleep(ms: number): void {
if (typeof SharedArrayBuffer !== "undefined" && typeof Atomics !== "undefined") {
try {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
return;
} catch {
// Atomics.wait may throw on restricted runtimes — fall through to busy-wait
}
}
const deadline = Date.now() + ms;
while (Date.now() < deadline) {
/* busy-wait */
}
}
/**
* Type for openSqliteDatabase callback — avoids importing the full SQLite adapter type.
*/
type OpenDbFn = (
filePath: string,
options?: Record<string, unknown>
) => {
driver: string;
open: boolean;
close(): void;
};
/**
* Retries opening a SQLite database probe when the initial attempt fails with
* a transient error. Uses exponential backoff (500ms, 1000ms, 2000ms).
*
* @param sqliteFile - Path to the SQLite database file
* @param openDb - Function to open the database (normally openSqliteDatabase)
* @param closeDb - Function to safely close the probe adapter
* @returns true if the retry succeeded (transient condition resolved)
* false if all retries were exhausted or error is non-transient
*/
export function retryProbeIfTransient(
sqliteFile: string,
probeError: unknown,
openDb: OpenDbFn,
closeDb: (adapter: { driver: string; open: boolean; close(): void } | null | undefined) => void
): boolean {
if (!isTransientProbeError(probeError)) return false;
const retryDelays = [500, 1000, 2000];
for (let i = 0; i < retryDelays.length; i++) {
syncSleep(retryDelays[i]);
try {
const retryAdapter = openDb(sqliteFile, { readonly: true });
closeDb(retryAdapter);
return true;
} catch {
// Retry failed, try next delay
}
}
console.warn(
`[DB] All ${retryDelays.length} transient probe retries exhausted — declaring corruption`
);
return false;
}