mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix(ws): warm SSE auth import on LiveWS startup; relocate boot test to integration (#4063)
The live dashboard WebSocket sidecar lazily import()-ed the SSE auth module inside the connection handler, only on the API-key path. That cold import pulls in hundreds of transitive modules and takes ~7s under tsx, blocking the single-threaded event loop. The first API-key WebSocket connection therefore stalled the loop long enough that any connection arriving in that window — e.g. a same-origin cookie client — could not complete its handshake and timed out. This was deterministic, not an "env flake": the boot test fires an API-key connection immediately followed by a cookie connection, so the cookie connection always raced the cold import and timed out (reproduced 3/3 locally and red on every CI run; proven via instrumented probes — reversing the order or warming the module first makes both connections open in ~20ms). Fix: - Memoize the auth-module import and warm it once at startup (before listen), so connection handling never pays the cold-import cost. Real improvement: the first API-key client no longer stalls the event loop for concurrent clients. - Relocate the boot test from tests/unit/cli to tests/integration. It spawns a real subprocess + WS server + SQLite (~9-11s); under the unit suite's --test-concurrency=20 it contended for CPU and destabilized the shard. The serial integration runner is its correct home; it still guards #4004's cookie-parse fix on every PR via the integration CI job. - Bump the test's startup/overall timeouts to absorb the eager auth warm. Makes `npm run test:unit` deterministically green (the only remaining unit red). Validated: relocated test 3/3 green via the integration runner (was 3/3 red); typecheck:core + eslint clean; confirmed it no longer matches the test:unit glob and does match tests/integration/*.test.ts.
This commit is contained in:
committed by
GitHub
parent
e6675cccaf
commit
13c4316c16
@@ -108,6 +108,24 @@ function toWebHeaders(headers: import("http").IncomingMessage["headers"]): Heade
|
||||
return webHeaders;
|
||||
}
|
||||
|
||||
// Auth-module warmer. The SSE auth graph is large (hundreds of transitive
|
||||
// modules); a cold dynamic import takes several seconds and runs synchronously
|
||||
// enough to stall the single-threaded event loop. Loading it lazily inside the
|
||||
// connection handler meant the FIRST API-key WebSocket connection blocked the
|
||||
// loop long enough that any connection arriving in that window (e.g. a
|
||||
// same-origin cookie client) could not complete its handshake and timed out.
|
||||
// Memoize the import and warm it once during startup (before listen) so
|
||||
// connection handling never pays that cost. Kept as a dynamic import (not a
|
||||
// top-level static one) to preserve the sidecar's decoupling from the SSE auth
|
||||
// graph at module-load time.
|
||||
let authModulePromise: Promise<typeof import("../../sse/services/auth.ts")> | null = null;
|
||||
function loadAuthModule(): Promise<typeof import("../../sse/services/auth.ts")> {
|
||||
if (!authModulePromise) {
|
||||
authModulePromise = import("../../sse/services/auth.ts");
|
||||
}
|
||||
return authModulePromise;
|
||||
}
|
||||
|
||||
async function authorizeConnection(request: import("http").IncomingMessage): Promise<WsAuthResult> {
|
||||
const sessionId = randomUUID().slice(0, 8);
|
||||
|
||||
@@ -128,8 +146,8 @@ async function authorizeConnection(request: import("http").IncomingMessage): Pro
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate API key via the existing auth system.
|
||||
const { extractApiKey, isValidApiKey } = await import("../../sse/services/auth.ts");
|
||||
// Validate API key via the existing auth system (warmed at startup).
|
||||
const { extractApiKey, isValidApiKey } = await loadAuthModule();
|
||||
const apiKey = extractApiKey({ headers: { authorization: `Bearer ${token}` } } as any, {
|
||||
allowUrl: false,
|
||||
});
|
||||
@@ -448,6 +466,12 @@ export async function startLiveDashboardServer(
|
||||
const unsubscribe = subscribeToEventBus();
|
||||
await seedLatestCompressionRunFromDb();
|
||||
|
||||
// Warm the auth module before accepting clients so the first API-key connection
|
||||
// does not block the event loop on a cold import — which would starve concurrent
|
||||
// WebSocket handshakes (see loadAuthModule). A failed warm is non-fatal: the
|
||||
// handler retries the import lazily.
|
||||
await loadAuthModule().catch(() => {});
|
||||
|
||||
wss.on("connection", async (ws, request) => {
|
||||
const pendingMessages: string[] = [];
|
||||
let activeClientId: string | null = null;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// Integration test (relocated from tests/unit/cli): it spawns the real
|
||||
// start-ws-server.mjs subprocess, which boots a full WebSocket server + SQLite
|
||||
// and eagerly warms the SSE auth module (~7s under tsx). Running it in the unit
|
||||
// suite under --test-concurrency=20 made it flaky/red because the heavy subprocess
|
||||
// boot contended for CPU; it belongs in the serial (--test-concurrency=1)
|
||||
// integration runner. It still guards #4004's same-origin cookie-parse fix on
|
||||
// every PR via the integration CI job.
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { SignJWT } from "jose";
|
||||
@@ -34,9 +41,12 @@ function waitForStartup(
|
||||
getOutput: () => string
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Startup eagerly warms the SSE auth module (see liveServer.ts), which takes
|
||||
// several seconds under tsx, so "listening" can appear ~7s after spawn. 30s
|
||||
// leaves headroom for a loaded CI runner.
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error(`LiveWS startup timed out. Output:\n${getOutput()}`));
|
||||
}, 8_000);
|
||||
}, 30_000);
|
||||
|
||||
const onData = () => {
|
||||
const output = getOutput();
|
||||
@@ -69,7 +79,7 @@ function waitForStartup(
|
||||
|
||||
test(
|
||||
"LiveWS startup script boots on current Node and accepts API-key WebSocket clients",
|
||||
{ timeout: 15_000 },
|
||||
{ timeout: 45_000 },
|
||||
async () => {
|
||||
const port = await getFreePort();
|
||||
const apiKey = "test-live-ws-key";
|
||||
Reference in New Issue
Block a user