Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ SENTRY_AUTH_TOKEN=
# The DFO APIs and the MCP servers are mirrored 1:1 under this site's domain
# (/api/search|datasets|graph/* -> dfo-apis, /api/studio/* -> dfo-ai, and
# /mcp -> dfo-ai's combined MCP server at /mcp/). /api/* and /mcp have a
# per-IP rate limit shared through Redis. Without REDIS_URL the proxy runs
# without limits and logs one warning at startup.
REDIS_URL=
# per-IP rate limit, counted in the website database (table limit_counter).
# RATE_LIMIT_WINDOW_S=60
# RATE_LIMIT_API=120
# RATE_LIMIT_PIN=10
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,5 @@ CLAUDE.md
*.pem
npm-debug.log*

# local redis (rate-limit dev runs)
dump.rdb
/.claude
/.idea
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Around it:
frontend contract, the storage/Slack/email sinks and their config, and
the local dev loop.
- `docs/analytics.md` — usage events (the `event` table and what each
surface records), Sentry, and the guest Studio limit.
surface records), Sentry, and the Studio prompt allowance.

## Repo map

Expand Down
61 changes: 46 additions & 15 deletions db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
IpAddressTypes,
type DriverOptions,
} from '@google-cloud/cloud-sql-connector';
import { Pool } from 'pg';
import { Pool, type PoolConfig } from 'pg';

/**
* The env var contract the migrate entrypoint and dfo-infra's Terraform
Expand Down Expand Up @@ -214,21 +214,52 @@ export function getCloudSqlRuntimeStream(): DriverOptions['stream'] {
* import or build.
* - URL: `connectionString` may be `undefined` here (no env at build); pg
* only fails to connect with it later.
*
* `sizing` overrides the pool size and adds timeouts, for a pool that must
* give up fast (src/lib/db/index.ts's counter pool).
*
* Every pool gets an `'error'` listener. pg-pool emits `'error'` when an
* idle client fails (Cloud SQL or a proxy dropping the connection), and an
* emitter with no listener throws, which would crash the server. pg-pool
* has already removed the dead client by then, so the listener only logs.
* The next query opens a fresh connection.
*/
export function buildRuntimePool(env: EnvLike = process.env): Pool {
if (isCloudSqlConfigured(env)) {
return new Pool({
stream: () => getCloudSqlRuntimeStream()(),
user: env.WEBSITE_DB_USER,
database: env.WEBSITE_DB_NAME,
max: 5,
idleTimeoutMillis: 30_000,
});
}
export function buildRuntimePool(
env: EnvLike = process.env,
sizing: Pick<
PoolConfig,
'max' | 'connectionTimeoutMillis' | 'query_timeout' | 'statement_timeout'
> = {},
): Pool {
const pool = isCloudSqlConfigured(env)
? new Pool({
stream: () => getCloudSqlRuntimeStream()(),
user: env.WEBSITE_DB_USER,
database: env.WEBSITE_DB_NAME,
max: 5,
idleTimeoutMillis: 30_000,
...sizing,
})
: new Pool({
connectionString: env.DATABASE_URL,
max: 5,
idleTimeoutMillis: 30_000,
...sizing,
});

pool.on('error', (error) => {
console.error('idle database client failed:', error);
});

return pool;
}

return new Pool({
connectionString: env.DATABASE_URL,
max: 5,
idleTimeoutMillis: 30_000,
/** A small, bounded pool for rate and allowance counters. */
export function buildCounterPool(env: EnvLike = process.env): Pool {
return buildRuntimePool(env, {
max: 2,
connectionTimeoutMillis: 2_000,
query_timeout: 500,
statement_timeout: 400,
});
}
7 changes: 7 additions & 0 deletions db/migrations/0017_limit_counter.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE "limit_counter" (
"key" text PRIMARY KEY NOT NULL,
"window_start" timestamp with time zone NOT NULL,
"count" integer NOT NULL
);
--> statement-breakpoint
CREATE INDEX "limit_counter_window_start_idx" ON "limit_counter" USING btree ("window_start");
Loading
Loading