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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user