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 <noreply@anthropic.com>
This commit is contained in:
Raj Nandan Sharma
2026-06-06 21:24:03 +05:30
parent 638393efac
commit 508b08f8f3
4 changed files with 35 additions and 16 deletions
+6 -2
View File
@@ -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),
};
+17 -2
View File
@@ -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<unknown>): Promise<boolean> => {
let timer: ReturnType<typeof setTimeout> | 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({
+1 -1
View File
@@ -67,7 +67,7 @@
<h1>This status page is temporarily unavailable</h1>
<p>We are having trouble serving this page right now. It usually resolves on its own.</p>
<p>This page will retry automatically in 30 seconds.</p>
<div class="code">%sveltekit.status% · %sveltekit.error.message%</div>
<div class="code">%sveltekit.status%</div>
</div>
</body>
</html>
@@ -239,8 +239,8 @@ SMTP_SECURE=1
### Database Configuration {#database-configuration}
| Variable | Description | Default |
| :---------------------------- | :----------------------------------------------------------- | :----------------------------- |
| `DATABASE_URL` | Full database connection string | `sqlite://./database/kener.db` |
| :---------------------------- | :----------------------------------------------------------- | :------------------------------------ |
| `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` |
@@ -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