From 508b08f8f3e546c9b297d147d7c1fd4f24db6712 Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Sat, 6 Jun 2026 21:24:03 +0530 Subject: [PATCH] fix(database): clamp pool bounds, guard redis probe, harden error page Address review feedback on #744: - clamp DATABASE_POOL_MAX to >= 1 and DATABASE_POOL_MIN to <= max so bad env values can not produce a pool that fails every acquire - healthcheck redis probe checks client status before PING so commands are not queued indefinitely while redis is down (maxRetriesPerRequest is null) - probe() clears its timeout timer once the check settles - error.html shows only the status code, not the error message - docs: correct SQLite default to kener.sqlite.db to match knexfile Co-Authored-By: Claude Opus 4.8 --- knexfile.ts | 8 +++++-- scripts/main.ts | 19 ++++++++++++++-- src/error.html | 2 +- .../content/v4/setup/environment-variables.md | 22 +++++++++---------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/knexfile.ts b/knexfile.ts index a0df63c5..93528caf 100644 --- a/knexfile.ts +++ b/knexfile.ts @@ -25,9 +25,13 @@ const keepAliveEnabled = process.env.DATABASE_KEEPALIVE !== "false"; // ones that go stale and wedge the app until a manual restart // - 15s acquire/create timeouts: fail fast instead of hanging requests for // knex's default 60s during a database outage +// Tarn requires max >= 1 and min <= max; clamp so a bad env value can not +// produce a pool that fails every acquire +const poolMax = Math.max(1, intFromEnv("DATABASE_POOL_MAX", 10)); +const poolMin = Math.min(intFromEnv("DATABASE_POOL_MIN", 0), poolMax); const pool = { - min: intFromEnv("DATABASE_POOL_MIN", 0), - max: intFromEnv("DATABASE_POOL_MAX", 10), + min: poolMin, + max: poolMax, idleTimeoutMillis: intFromEnv("DATABASE_IDLE_TIMEOUT_MS", 30000), createTimeoutMillis: intFromEnv("DATABASE_CREATE_TIMEOUT_MS", 15000), }; diff --git a/scripts/main.ts b/scripts/main.ts index ecc0094a..67d35410 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -24,14 +24,19 @@ async function start() { // Caps a health probe at 2s so a wedged dependency can not hang the // endpoint. A probe is healthy unless it throws, times out, or resolves false. const probe = async (check: () => Promise): Promise => { + let timer: ReturnType | undefined; try { const result = await Promise.race([ check(), - new Promise((_, reject) => setTimeout(() => reject(new Error("health probe timeout")), 2000)), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("health probe timeout")), 2000); + }), ]); return result !== false; } catch { return false; + } finally { + clearTimeout(timer); } }; @@ -39,7 +44,17 @@ async function start() { // not bounce the app while a dependency is down (a restart can not fix a // dead database); pass ?strict=1 to get 503 when any component is down. app.get(base + "/healthcheck", async (req: any, res: any) => { - const [dbOk, redisOk] = await Promise.all([probe(() => dbInstance.ping()), probe(() => redisConnection().ping())]); + const [dbOk, redisOk] = await Promise.all([ + probe(() => dbInstance.ping()), + // Guard on status before PING: the shared ioredis client has + // maxRetriesPerRequest null, so commands sent while disconnected would + // queue forever and accumulate across healthcheck polls + probe(async () => { + const redis = redisConnection(); + if (redis.status !== "ready") return false; + return await redis.ping(); + }), + ]); const healthy = dbOk && redisOk; const strict = req.query.strict === "1"; res.status(strict && !healthy ? 503 : 200).json({ diff --git a/src/error.html b/src/error.html index c282ff16..4a1cf847 100644 --- a/src/error.html +++ b/src/error.html @@ -67,7 +67,7 @@

This status page is temporarily unavailable

We are having trouble serving this page right now. It usually resolves on its own.

This page will retry automatically in 30 seconds.

-
%sveltekit.status% ยท %sveltekit.error.message%
+
%sveltekit.status%
diff --git a/src/routes/(docs)/docs/content/v4/setup/environment-variables.md b/src/routes/(docs)/docs/content/v4/setup/environment-variables.md index d4b51b1f..fb081c49 100644 --- a/src/routes/(docs)/docs/content/v4/setup/environment-variables.md +++ b/src/routes/(docs)/docs/content/v4/setup/environment-variables.md @@ -238,15 +238,15 @@ SMTP_SECURE=1 ### Database Configuration {#database-configuration} -| Variable | Description | Default | -| :---------------------------- | :----------------------------------------------------------- | :----------------------------- | -| `DATABASE_URL` | Full database connection string | `sqlite://./database/kener.db` | -| `DATABASE_POOL_MIN` | Minimum pool connections (PostgreSQL/MySQL) | `0` | -| `DATABASE_POOL_MAX` | Maximum pool connections (PostgreSQL/MySQL) | `10` | -| `DATABASE_ACQUIRE_TIMEOUT_MS` | Wait for a free connection before failing (PostgreSQL/MySQL) | `15000` | -| `DATABASE_CREATE_TIMEOUT_MS` | Wait for a new connection before failing (PostgreSQL/MySQL) | `15000` | -| `DATABASE_IDLE_TIMEOUT_MS` | Idle time before a connection is closed (PostgreSQL/MySQL) | `30000` | -| `DATABASE_KEEPALIVE` | TCP keepalive on connections (PostgreSQL/MySQL) | `true` | +| Variable | Description | Default | +| :---------------------------- | :----------------------------------------------------------- | :------------------------------------ | +| `DATABASE_URL` | Full database connection string | `sqlite://./database/kener.sqlite.db` | +| `DATABASE_POOL_MIN` | Minimum pool connections (PostgreSQL/MySQL) | `0` | +| `DATABASE_POOL_MAX` | Maximum pool connections (PostgreSQL/MySQL) | `10` | +| `DATABASE_ACQUIRE_TIMEOUT_MS` | Wait for a free connection before failing (PostgreSQL/MySQL) | `15000` | +| `DATABASE_CREATE_TIMEOUT_MS` | Wait for a new connection before failing (PostgreSQL/MySQL) | `15000` | +| `DATABASE_IDLE_TIMEOUT_MS` | Idle time before a connection is closed (PostgreSQL/MySQL) | `30000` | +| `DATABASE_KEEPALIVE` | TCP keepalive on connections (PostgreSQL/MySQL) | `true` | **Supported Databases**: @@ -258,7 +258,7 @@ SMTP_SECURE=1 ```bash # SQLite (default) -DATABASE_URL=sqlite://./database/kener.db +DATABASE_URL=sqlite://./database/kener.sqlite.db # PostgreSQL DATABASE_URL=postgresql://user:password@localhost:5432/kener @@ -484,7 +484,7 @@ Create a `.env` file in the project root: ```bash # .env KENER_SECRET_KEY=dev-secret-key -DATABASE_URL=sqlite://./database/kener.db +DATABASE_URL=sqlite://./database/kener.sqlite.db # Custom variables API_KEY=test-key-123