mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
The CLI readiness budget is configurable through `OMNIROUTE_READY_TIMEOUT_MS` or `omniroute serve --ready-timeout <ms>` (default unchanged at 60s). The timeout warning prints the budget it actually used and suggests a larger value, for slow cold starts such as Windows (#13369). Documented in `ENVIRONMENT.md` and `TROUBLESHOOTING.md`, with 11 resolver cases. Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green. Thanks @KooshaPari!
This commit is contained in:
committed by
GitHub
parent
1738beb0fe
commit
fc111dc196
@@ -13,6 +13,7 @@ const OMNIROUTE_ENV_VARS = [
|
||||
"OMNIROUTE_API_KEY",
|
||||
"OMNIROUTE_BASE_URL",
|
||||
"OMNIROUTE_HTTP_TIMEOUT_MS",
|
||||
"OMNIROUTE_READY_TIMEOUT_MS",
|
||||
];
|
||||
|
||||
const ENV_DEFAULTS = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer, resolveReadyTimeoutMs } from "../utils/pid.mjs";
|
||||
import {
|
||||
ServerSupervisor,
|
||||
detectMitmCrash,
|
||||
@@ -58,6 +58,11 @@ export function registerServe(program) {
|
||||
.option("--max-restarts <n>", t("serve.max_restarts"), parseInt, 2)
|
||||
.option("--tray", t("serve.tray") || "Start in the system tray (desktop only)")
|
||||
.option("--no-tray", t("serve.no_tray") || "Disable system tray icon")
|
||||
.option(
|
||||
"--ready-timeout <ms>",
|
||||
t("serve.ready_timeout") ||
|
||||
"Readiness probe timeout in ms (also OMNIROUTE_READY_TIMEOUT_MS, default 60000)"
|
||||
)
|
||||
.option(
|
||||
"--tls-cert <path>",
|
||||
t("serve.tls_cert") ||
|
||||
@@ -452,7 +457,8 @@ async function runWithSupervisor(
|
||||
});
|
||||
|
||||
if (!showLog) {
|
||||
waitForServer(dashboardPort, 60000).then(async (up) => {
|
||||
const readyTimeoutMs = resolveReadyTimeoutMs({ timeoutMs: opts.readyTimeout });
|
||||
waitForServer(dashboardPort, readyTimeoutMs).then(async (up) => {
|
||||
if (up) {
|
||||
if (useTray) {
|
||||
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
|
||||
@@ -489,10 +495,15 @@ async function runWithSupervisor(
|
||||
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
|
||||
// plus whatever stdout/stderr the child buffered instead of going silent.
|
||||
export function reportReadinessTimeout(dashboardPort, supervisor) {
|
||||
const readyTimeoutMs = resolveReadyTimeoutMs();
|
||||
const seconds = Math.round(readyTimeoutMs / 1000);
|
||||
console.error(
|
||||
`\n\x1b[33m⚠ Server did not respond within 60s.\x1b[0m It may still be starting, or may` +
|
||||
`\n\x1b[33m⚠ Server did not respond within ${seconds}s.\x1b[0m It may still be starting, or may` +
|
||||
` have failed silently.`
|
||||
);
|
||||
console.error(
|
||||
` Tip: set OMNIROUTE_READY_TIMEOUT_MS=${readyTimeoutMs * 2} or --ready-timeout ${readyTimeoutMs * 2} for slower cold starts.`
|
||||
);
|
||||
console.error(` Try: curl -I http://localhost:${dashboardPort}/api/monitoring/health`);
|
||||
console.error(` Or: rerun with \x1b[36m--log\x1b[0m to see live server output.\n`);
|
||||
|
||||
|
||||
@@ -66,13 +66,30 @@ export function sleep(ms) {
|
||||
// #2460: Default raised from 15s to 60s so Windows users (slower Next.js
|
||||
// cold start due to filesystem watchers, antivirus, etc.) get a working
|
||||
// "server ready" signal instead of a phantom timeout while the server is
|
||||
// still booting. TCP fallback marks the server as ready when the port
|
||||
// still booting. #13369: Made configurable via OMNIROUTE_READY_TIMEOUT_MS
|
||||
// so operators on slow cold starts (e.g. 6+ min Windows boots) can raise
|
||||
// the budget instead of hitting the warning on every start.
|
||||
//
|
||||
// TCP fallback marks the server as ready when the port
|
||||
// has been listening for >= 3s consecutively AND the health route is
|
||||
// actively rejecting/resetting connections fast (route not mounted yet,
|
||||
// but the HTTP server is clearly alive and responsive) — never for a
|
||||
// socket that merely accepts TCP and then hangs without ever completing
|
||||
// a single request (#6800: that's a still-booting/CPU-bound process, not
|
||||
// a "route not mounted" gap, and must NOT be reported as ready).
|
||||
const DEFAULT_READY_TIMEOUT_MS = 60_000;
|
||||
|
||||
export function resolveReadyTimeoutMs(overrides = {}) {
|
||||
if (typeof overrides.timeoutMs === "number" && overrides.timeoutMs > 0) {
|
||||
return overrides.timeoutMs;
|
||||
}
|
||||
const envValue = Number.parseInt(
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS || "",
|
||||
10
|
||||
);
|
||||
return Number.isFinite(envValue) && envValue > 0 ? envValue : DEFAULT_READY_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
export async function waitForServer(port, timeout = 60000) {
|
||||
const start = Date.now();
|
||||
let tcpListeningSince = null;
|
||||
|
||||
@@ -739,6 +739,33 @@ Issues specific to the v3.8.0 release and their current workarounds. If a fix la
|
||||
|
||||
---
|
||||
|
||||
## Slow Startup / Readiness Timeout
|
||||
|
||||
If the CLI prints `⚠ Server did not respond within 60s` but the server is
|
||||
actually working, the readiness probe budget is too short for your environment.
|
||||
|
||||
This commonly happens on Windows (antivirus, filesystem watchers) or containers
|
||||
with heavy startup workloads.
|
||||
|
||||
**Fix — raise the budget:**
|
||||
|
||||
```bash
|
||||
# Via env var (persists across starts):
|
||||
export OMNIROUTE_READY_TIMEOUT_MS=180000 # 3 minutes
|
||||
omniroute serve
|
||||
|
||||
# Via CLI flag (one-off):
|
||||
omniroute serve --ready-timeout 180000
|
||||
```
|
||||
|
||||
The default is 60 000 ms (60 s). The warning is informational only; the server
|
||||
continues starting in the background and will be reachable once boot completes.
|
||||
|
||||
See [`docs/reference/ENVIRONMENT.md`](../reference/ENVIRONMENT.md) for full
|
||||
details on `OMNIROUTE_READY_TIMEOUT_MS`.
|
||||
|
||||
---
|
||||
|
||||
## Still Stuck?
|
||||
|
||||
- **GitHub Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
|
||||
@@ -504,6 +504,7 @@ detection above).
|
||||
| `OMNIROUTE_SHOW_LOG` | _(unset)_ | `bin/cli/runtime/processSupervisor.mjs` | Set to `1` to forward server stdout/stderr to the terminal in supervised mode. Equivalent to `--log` flag on `omniroute serve`. |
|
||||
| `OMNIROUTE_CLI_TOKEN` | _(unset)_ | `bin/cli/api.mjs` | Machine-auth token injected as `x-omniroute-cli-token` header. Auto-generated in task 8.12. |
|
||||
| `OMNIROUTE_HTTP_TIMEOUT_MS` | `30000` | `bin/cli/api.mjs` | Per-attempt HTTP timeout (ms) for CLI → server requests. |
|
||||
| `OMNIROUTE_READY_TIMEOUT_MS` | `60000` | `bin/cli/utils/pid.mjs` | Maximum time (ms) the CLI waits for the server health endpoint before printing a timeout warning. Useful for slow cold starts (e.g. Windows). Also settable via `--ready-timeout`. |
|
||||
| `OMNIROUTE_VERBOSE` | `0` | `bin/cli/api.mjs` | Set to `1` to print retry/backoff diagnostics to stderr during CLI commands. |
|
||||
| `OMNIROUTE_PLUGIN_PATH` | _(unset)_ | `bin/cli/plugins.mjs` | Custom directory for CLI plugin discovery (`omniroute-cmd-*` packages). Defaults to `~/.omniroute/plugins/` when unset. CLI-only — it never reaches the server-side plugin scanner, which is pointed by `OMNIROUTE_PLUGINS_DIR` (section 2). |
|
||||
|
||||
|
||||
71
tests/unit/cli/ready-timeout.test.ts
Normal file
71
tests/unit/cli/ready-timeout.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { test, describe, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveReadyTimeoutMs } from "../../../bin/cli/utils/pid.mjs";
|
||||
|
||||
describe("resolveReadyTimeoutMs", () => {
|
||||
const savedEnv = process.env.OMNIROUTE_READY_TIMEOUT_MS;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset env between tests
|
||||
if (savedEnv === undefined) {
|
||||
delete process.env.OMNIROUTE_READY_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = savedEnv;
|
||||
}
|
||||
});
|
||||
|
||||
test("returns 60000 by default (no env, no override)", () => {
|
||||
delete process.env.OMNIROUTE_READY_TIMEOUT_MS;
|
||||
assert.equal(resolveReadyTimeoutMs(), 60_000);
|
||||
});
|
||||
|
||||
test("honours explicit override when provided", () => {
|
||||
delete process.env.OMNIROUTE_READY_TIMEOUT_MS;
|
||||
assert.equal(resolveReadyTimeoutMs({ timeoutMs: 120_000 }), 120_000);
|
||||
});
|
||||
|
||||
test("explicit override wins over env var", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "300000";
|
||||
assert.equal(resolveReadyTimeoutMs({ timeoutMs: 90_000 }), 90_000);
|
||||
});
|
||||
|
||||
test("reads OMNIROUTE_READY_TIMEOUT_MS env var", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "180000";
|
||||
assert.equal(resolveReadyTimeoutMs(), 180_000);
|
||||
});
|
||||
|
||||
test("falls back to default when env var is non-numeric", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "not-a-number";
|
||||
assert.equal(resolveReadyTimeoutMs(), 60_000);
|
||||
});
|
||||
|
||||
test("falls back to default when env var is zero", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "0";
|
||||
assert.equal(resolveReadyTimeoutMs(), 60_000);
|
||||
});
|
||||
|
||||
test("falls back to default when env var is negative", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "-5000";
|
||||
assert.equal(resolveReadyTimeoutMs(), 60_000);
|
||||
});
|
||||
|
||||
test("falls back to default when override is zero", () => {
|
||||
delete process.env.OMNIROUTE_READY_TIMEOUT_MS;
|
||||
assert.equal(resolveReadyTimeoutMs({ timeoutMs: 0 }), 60_000);
|
||||
});
|
||||
|
||||
test("falls back to default when override is negative", () => {
|
||||
delete process.env.OMNIROUTE_READY_TIMEOUT_MS;
|
||||
assert.equal(resolveReadyTimeoutMs({ timeoutMs: -1 }), 60_000);
|
||||
});
|
||||
|
||||
test("accepts fractional seconds as milliseconds", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "65536";
|
||||
assert.equal(resolveReadyTimeoutMs(), 65_536);
|
||||
});
|
||||
|
||||
test("handles empty string env var as unset", () => {
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS = "";
|
||||
assert.equal(resolveReadyTimeoutMs(), 60_000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user