mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
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:
+6
-2
@@ -25,9 +25,13 @@ const keepAliveEnabled = process.env.DATABASE_KEEPALIVE !== "false";
|
|||||||
// ones that go stale and wedge the app until a manual restart
|
// ones that go stale and wedge the app until a manual restart
|
||||||
// - 15s acquire/create timeouts: fail fast instead of hanging requests for
|
// - 15s acquire/create timeouts: fail fast instead of hanging requests for
|
||||||
// knex's default 60s during a database outage
|
// 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 = {
|
const pool = {
|
||||||
min: intFromEnv("DATABASE_POOL_MIN", 0),
|
min: poolMin,
|
||||||
max: intFromEnv("DATABASE_POOL_MAX", 10),
|
max: poolMax,
|
||||||
idleTimeoutMillis: intFromEnv("DATABASE_IDLE_TIMEOUT_MS", 30000),
|
idleTimeoutMillis: intFromEnv("DATABASE_IDLE_TIMEOUT_MS", 30000),
|
||||||
createTimeoutMillis: intFromEnv("DATABASE_CREATE_TIMEOUT_MS", 15000),
|
createTimeoutMillis: intFromEnv("DATABASE_CREATE_TIMEOUT_MS", 15000),
|
||||||
};
|
};
|
||||||
|
|||||||
+17
-2
@@ -24,14 +24,19 @@ async function start() {
|
|||||||
// Caps a health probe at 2s so a wedged dependency can not hang the
|
// 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.
|
// endpoint. A probe is healthy unless it throws, times out, or resolves false.
|
||||||
const probe = async (check: () => Promise<unknown>): Promise<boolean> => {
|
const probe = async (check: () => Promise<unknown>): Promise<boolean> => {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
try {
|
try {
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
check(),
|
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;
|
return result !== false;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
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
|
// 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.
|
// dead database); pass ?strict=1 to get 503 when any component is down.
|
||||||
app.get(base + "/healthcheck", async (req: any, res: any) => {
|
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 healthy = dbOk && redisOk;
|
||||||
const strict = req.query.strict === "1";
|
const strict = req.query.strict === "1";
|
||||||
res.status(strict && !healthy ? 503 : 200).json({
|
res.status(strict && !healthy ? 503 : 200).json({
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@
|
|||||||
<h1>This status page is temporarily unavailable</h1>
|
<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>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>
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -238,15 +238,15 @@ SMTP_SECURE=1
|
|||||||
|
|
||||||
### Database Configuration {#database-configuration}
|
### Database Configuration {#database-configuration}
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| 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_MIN` | Minimum pool connections (PostgreSQL/MySQL) | `0` |
|
||||||
| `DATABASE_POOL_MAX` | Maximum pool connections (PostgreSQL/MySQL) | `10` |
|
| `DATABASE_POOL_MAX` | Maximum pool connections (PostgreSQL/MySQL) | `10` |
|
||||||
| `DATABASE_ACQUIRE_TIMEOUT_MS` | Wait for a free connection before failing (PostgreSQL/MySQL) | `15000` |
|
| `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_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_IDLE_TIMEOUT_MS` | Idle time before a connection is closed (PostgreSQL/MySQL) | `30000` |
|
||||||
| `DATABASE_KEEPALIVE` | TCP keepalive on connections (PostgreSQL/MySQL) | `true` |
|
| `DATABASE_KEEPALIVE` | TCP keepalive on connections (PostgreSQL/MySQL) | `true` |
|
||||||
|
|
||||||
**Supported Databases**:
|
**Supported Databases**:
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ SMTP_SECURE=1
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# SQLite (default)
|
# SQLite (default)
|
||||||
DATABASE_URL=sqlite://./database/kener.db
|
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||||
|
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/kener
|
DATABASE_URL=postgresql://user:password@localhost:5432/kener
|
||||||
@@ -484,7 +484,7 @@ Create a `.env` file in the project root:
|
|||||||
```bash
|
```bash
|
||||||
# .env
|
# .env
|
||||||
KENER_SECRET_KEY=dev-secret-key
|
KENER_SECRET_KEY=dev-secret-key
|
||||||
DATABASE_URL=sqlite://./database/kener.db
|
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||||
|
|
||||||
# Custom variables
|
# Custom variables
|
||||||
API_KEY=test-key-123
|
API_KEY=test-key-123
|
||||||
|
|||||||
Reference in New Issue
Block a user