mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { runtimeRequire as _require } from "./runtimeRequire";
|
||||
import { existsSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createBetterSqliteAdapter } from "./betterSqliteAdapter";
|
||||
import { createBunSqliteAdapter, type BunSqliteDatabaseLike } from "./bunSqliteAdapter";
|
||||
import {
|
||||
@@ -11,6 +12,70 @@ import type { SqliteAdapter } from "./types";
|
||||
|
||||
type DriverLoader = (moduleName: string) => unknown;
|
||||
|
||||
type SpawnSyncLike = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { timeout: number; stdio: "ignore"; cwd: string; windowsHide: boolean }
|
||||
) => { status: number | null };
|
||||
|
||||
/** Returns whether better-sqlite3 may be loaded in this process. */
|
||||
export type DriverProbe = () => boolean;
|
||||
|
||||
/**
|
||||
* #10627 — Windows driver-hang guard.
|
||||
*
|
||||
* The sync cascade's try/catch only covers drivers that THROW on load
|
||||
* (ERR_DLOPEN_FAILED, "Module did not self-register", ...). On Windows, a
|
||||
* mismatched-ABI native addon can HANG inside DllMain (loader lock) instead of
|
||||
* throwing — a hang never reaches the catch, so the fallback to node:sqlite /
|
||||
* sql.js never runs and the first DB touch in a runtime stalls forever at ~0%
|
||||
* CPU (the exact #10627 symptom: every request hangs, 0 bytes, no logs).
|
||||
*
|
||||
* The probe answers "can better-sqlite3 load AND open a database?" by loading
|
||||
* it in a CHILD PROCESS with a bounded timeout, so a hang becomes a timed-out
|
||||
* probe (verdict "bad") instead of a process-level deadlock. The verdict is
|
||||
* cached per process — the child spawn happens at most once.
|
||||
*
|
||||
* On POSIX this is a no-op returning true: broken addons throw there, which
|
||||
* the existing cascade already handles, and we don't want to pay a subprocess
|
||||
* spawn on every Linux/CI boot.
|
||||
*/
|
||||
export function createBetterSqliteProbe(options: {
|
||||
platform?: string;
|
||||
execPath?: string;
|
||||
spawn?: SpawnSyncLike;
|
||||
timeoutMs?: number;
|
||||
}): DriverProbe {
|
||||
const {
|
||||
platform = process.platform,
|
||||
execPath = process.execPath,
|
||||
spawn = spawnSync as unknown as SpawnSyncLike,
|
||||
timeoutMs = 5_000,
|
||||
} = options;
|
||||
|
||||
let verdict: boolean | null = null;
|
||||
return () => {
|
||||
if (verdict !== null) return verdict;
|
||||
if (platform !== "win32") {
|
||||
verdict = true;
|
||||
return verdict;
|
||||
}
|
||||
try {
|
||||
const result = spawn(execPath, ["-e", "require('better-sqlite3')(':memory:')"], {
|
||||
timeout: timeoutMs,
|
||||
stdio: "ignore",
|
||||
cwd: process.cwd(),
|
||||
windowsHide: true,
|
||||
});
|
||||
// status === null means the child was killed by the timeout — a hang.
|
||||
verdict = result.status === 0;
|
||||
} catch {
|
||||
verdict = false;
|
||||
}
|
||||
return verdict;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The production loader for the sync driver cascade.
|
||||
*
|
||||
@@ -137,7 +202,12 @@ function getSqlJsPendingCache(): Map<string, Promise<SqliteAdapter>> {
|
||||
* Builds the synchronous driver cascade. Keeping the loader injectable makes
|
||||
* the real node:sqlite branch testable without changing the public adapter API.
|
||||
*/
|
||||
export function createSyncDriverFactory(load: DriverLoader) {
|
||||
export function createSyncDriverFactory(load: DriverLoader, betterSqliteProbe?: DriverProbe) {
|
||||
// #10627: when a probe is supplied, the better-sqlite3 branch is gated on it
|
||||
// so a Windows DllMain hang (which never throws, so never hits the catch)
|
||||
// cannot stall the request path. Default: no probe — existing callers/tests
|
||||
// keep the historical throw-only behavior.
|
||||
const mayLoadBetterSqlite = betterSqliteProbe ?? (() => true);
|
||||
return function tryOpenSync(
|
||||
filePath: string,
|
||||
options?: Record<string, unknown>
|
||||
@@ -164,7 +234,7 @@ export function createSyncDriverFactory(load: DriverLoader) {
|
||||
}
|
||||
|
||||
// better-sqlite3: rápido, nativo — skip em Bun
|
||||
if (!process.versions.bun) {
|
||||
if (!process.versions.bun && mayLoadBetterSqlite()) {
|
||||
try {
|
||||
const BetterSqlite = load("better-sqlite3") as {
|
||||
new (p: string, o?: object): import("better-sqlite3").Database;
|
||||
@@ -204,7 +274,10 @@ export function createSyncDriverFactory(load: DriverLoader) {
|
||||
};
|
||||
}
|
||||
|
||||
const openSyncDriver = createSyncDriverFactory(requireSqliteDriver);
|
||||
// Production wiring: the real probe (child-process, timed, cached) guards the
|
||||
// better-sqlite3 branch so a hang on Windows degrades to a failover instead of
|
||||
// a request-path deadlock (#10627).
|
||||
const openSyncDriver = createSyncDriverFactory(requireSqliteDriver, createBetterSqliteProbe({}));
|
||||
|
||||
/**
|
||||
* The installed-tarball smoke uses this paired marker to exercise the sql.js tier
|
||||
|
||||
19
src/proxy.ts
19
src/proxy.ts
@@ -1,6 +1,25 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { runAuthzPipeline } from "./server/authz/pipeline";
|
||||
|
||||
// #10627: the proxy runs in its own Next.js runtime and never executes
|
||||
// instrumentation-node.ts's startup warm-ups, so its FIRST request used to
|
||||
// trigger a cold `import("@/lib/db/settings")` → native SQLite driver load ON
|
||||
// the request path. If that addon hangs (see driverFactory's #10627 probe),
|
||||
// every proxied request stalled indefinitely with 0 bytes and no logs.
|
||||
// Warm the settings cache here at boot instead: a driver failure now surfaces
|
||||
// as a logged startup error, and real requests start with a hot cache.
|
||||
// Fire-and-forget — never blocks proxy initialization, never rejects the
|
||||
// module (mirrors the `void warmModelCatalogCache()` pattern in
|
||||
// instrumentation-node.ts).
|
||||
void import("./lib/db/readCache")
|
||||
.then(({ getCachedSettings }) => getCachedSettings())
|
||||
.catch((err: unknown) => {
|
||||
console.error(
|
||||
"[proxy] DB settings warm failed; requests will use default limits:",
|
||||
err instanceof Error ? err.message : err
|
||||
);
|
||||
});
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
return runAuthzPipeline(request, { enforce: true });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { runtimeRequire } from "../../../src/lib/db/adapters/runtimeRequire.ts";
|
||||
|
||||
const {
|
||||
createSyncDriverFactory,
|
||||
createBetterSqliteProbe,
|
||||
isPackBootForcedSqlJsSmoke,
|
||||
tryOpenSync,
|
||||
openDatabaseAsync,
|
||||
@@ -81,6 +82,55 @@ describe("driverFactory", () => {
|
||||
}
|
||||
);
|
||||
|
||||
test("rejected probe skips better-sqlite3 and falls through to node:sqlite", (t) => {
|
||||
const databasePath = createTempDatabasePath(t);
|
||||
const openWithoutBrokenAddon = createSyncDriverFactory(
|
||||
(moduleName: string) => {
|
||||
if (moduleName === "better-sqlite3") {
|
||||
throw new Error("better-sqlite3 must not load when the probe rejects it");
|
||||
}
|
||||
return require(moduleName);
|
||||
},
|
||||
() => false
|
||||
);
|
||||
|
||||
const adapter = openWithoutBrokenAddon(databasePath);
|
||||
assert.ok(adapter);
|
||||
assert.equal(adapter.driver, "node:sqlite");
|
||||
adapter.exec("CREATE TABLE items (value TEXT)");
|
||||
adapter.prepare("INSERT INTO items VALUES (?)").run("ok");
|
||||
assert.equal(
|
||||
(adapter.prepare("SELECT value FROM items").get() as { value: string }).value,
|
||||
"ok"
|
||||
);
|
||||
adapter.close();
|
||||
});
|
||||
|
||||
test("passed probe still prefers better-sqlite3 in the cascade", () => {
|
||||
let betterSqliteRequested = false;
|
||||
const openWithPassedProbe = createSyncDriverFactory(
|
||||
(moduleName: string) => {
|
||||
if (moduleName === "better-sqlite3") {
|
||||
betterSqliteRequested = true;
|
||||
return function FakeBetterSqlite() {
|
||||
return { close() {}, name: ":memory:", open: true };
|
||||
};
|
||||
}
|
||||
if (moduleName === "node:sqlite") {
|
||||
throw new Error("node:sqlite must not load when better-sqlite3 passes the probe");
|
||||
}
|
||||
throw new Error(`unexpected driver load: ${moduleName}`);
|
||||
},
|
||||
() => true
|
||||
);
|
||||
|
||||
const adapter = openWithPassedProbe(":memory:");
|
||||
assert.ok(adapter);
|
||||
assert.equal(adapter.driver, "better-sqlite3");
|
||||
assert.equal(betterSqliteRequested, true);
|
||||
adapter.close();
|
||||
});
|
||||
|
||||
test("prefers better-sqlite3 before node:sqlite in the driver cascade", () => {
|
||||
const fakeBetterSqlite = {
|
||||
close() {},
|
||||
@@ -337,6 +387,74 @@ describe("driverFactory", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// #10627 — the Windows driver-hang guard. On Windows a mismatched-ABI
|
||||
// better-sqlite3 addon can HANG inside DllMain instead of throwing, so the
|
||||
// cascade's try/catch never fires and the fallback never runs. The probe
|
||||
// loads the addon in a child process with a bounded timeout, turning a hang
|
||||
// into a cached "bad" verdict that skips the branch.
|
||||
test("probe: non-Windows platforms skip the child probe and report ok", () => {
|
||||
let spawned = 0;
|
||||
const probe = createBetterSqliteProbe({
|
||||
platform: "linux",
|
||||
execPath: "node",
|
||||
spawn: () => {
|
||||
spawned += 1;
|
||||
return { status: 0 };
|
||||
},
|
||||
});
|
||||
assert.equal(probe(), true);
|
||||
assert.equal(probe(), true);
|
||||
assert.equal(spawned, 0, "POSIX must not spawn a probe child process");
|
||||
});
|
||||
|
||||
test("probe: successful child probe is cached (spawned at most once)", () => {
|
||||
let spawned = 0;
|
||||
const probe = createBetterSqliteProbe({
|
||||
platform: "win32",
|
||||
execPath: "node",
|
||||
spawn: () => {
|
||||
spawned += 1;
|
||||
return { status: 0 };
|
||||
},
|
||||
});
|
||||
assert.equal(probe(), true);
|
||||
assert.equal(probe(), true);
|
||||
assert.equal(probe(), true);
|
||||
assert.equal(spawned, 1, "verdict must be cached per process");
|
||||
});
|
||||
|
||||
test("probe: non-zero child exit rejects better-sqlite3", () => {
|
||||
const probe = createBetterSqliteProbe({
|
||||
platform: "win32",
|
||||
execPath: "node",
|
||||
spawn: () => ({ status: 1 }),
|
||||
});
|
||||
assert.equal(probe(), false);
|
||||
assert.equal(probe(), false);
|
||||
});
|
||||
|
||||
test("probe: child spawn throw rejects better-sqlite3", () => {
|
||||
const probe = createBetterSqliteProbe({
|
||||
platform: "win32",
|
||||
execPath: "node",
|
||||
spawn: () => {
|
||||
throw new Error("spawn failed");
|
||||
},
|
||||
});
|
||||
assert.equal(probe(), false);
|
||||
});
|
||||
|
||||
test("probe: timed-out child (status null) rejects better-sqlite3 — the #10627 hang case", () => {
|
||||
// status === null is exactly what spawnSync returns when the child is
|
||||
// killed by the timeout — i.e. the DllMain hang that never throws.
|
||||
const probe = createBetterSqliteProbe({
|
||||
platform: "win32",
|
||||
execPath: "node",
|
||||
spawn: () => ({ status: null }),
|
||||
});
|
||||
assert.equal(probe(), false);
|
||||
});
|
||||
|
||||
test("retains the existing cascade when native drivers are unavailable", () => {
|
||||
const openWithoutNativeDrivers = createSyncDriverFactory(() => {
|
||||
throw new Error("forced driver load failure");
|
||||
|
||||
Reference in New Issue
Block a user