Spaces:
Running
fix: Prevent Supabase connection pool exhaustion in health endpoint
Browse files**Problem:**
Health endpoint caused connection timeouts in production by firing 10+
concurrent database queries immediately after initialization, exhausting
the connection pool.
**Root Cause:**
- Promise.all() executed 5 tables Γ 2 queries = ~10 concurrent connections
- 10-second timeout too aggressive for HuggingFace β Supabase latency
- Connection pool (max: 10) exhausted after slow 3.6s initialization
**Solution:**
1. **Serialize queries** - Replace Promise.all() with for...of loop
- Process tables sequentially to prevent pool exhaustion
- Reduces concurrent connections from 10+ to 1-2 at a time
- Trade-off: Slower response (~2-3s) but reliable
2. **Increase timeouts** - Change timeouts from 10s to 30s
- connectionTimeoutMillis: 10000 β 30000
- statement_timeout: 10000 β 30000
- Accounts for HuggingFace β Singapore Supabase latency
**Testing:**
β Local Supabase: 5 consecutive health checks, no timeouts
β Initialization: 1.2s (down from 3.6s in production)
β Health endpoint: All 5 tables queried successfully
β API tests: All 95 tests passing
**Impact:**
- Production health endpoint should now work reliably
- Slower response time (sequential) but stable
- Better connection pool management
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
|
@@ -11,46 +11,46 @@ export async function GET() {
|
|
| 11 |
|
| 12 |
const tables = ['users', 'conversations', 'messages', 'sessions', 'prompt_templates'];
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
}
|
| 42 |
-
} catch (error) {
|
| 43 |
-
// If column doesn't exist or other error, leave as null
|
| 44 |
-
console.error(`[Health] Error querying ${tableName}:`, error);
|
| 45 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
);
|
| 54 |
|
| 55 |
return NextResponse.json({
|
| 56 |
tables: tableMetrics,
|
|
|
|
| 11 |
|
| 12 |
const tables = ['users', 'conversations', 'messages', 'sessions', 'prompt_templates'];
|
| 13 |
|
| 14 |
+
// Process tables sequentially to avoid connection pool exhaustion
|
| 15 |
+
const tableMetrics = [];
|
| 16 |
+
for (const tableName of tables) {
|
| 17 |
+
// Get row count
|
| 18 |
+
const countResult = (await db.raw(`SELECT COUNT(*) as count FROM ${tableName}`)) as any;
|
| 19 |
+
const rowCount = countResult?.data?.[0]?.count || 0;
|
| 20 |
|
| 21 |
+
// Get last modified timestamp (if table has updated_at or created_at)
|
| 22 |
+
let lastModified = null;
|
| 23 |
+
try {
|
| 24 |
+
if (['conversations', 'prompt_templates'].includes(tableName)) {
|
| 25 |
+
// These tables have updated_at column
|
| 26 |
+
const latestResult = (await db.raw(
|
| 27 |
+
`SELECT updated_at FROM ${tableName} ORDER BY updated_at DESC LIMIT 1`
|
| 28 |
+
)) as any;
|
| 29 |
+
lastModified = latestResult?.data?.[0]?.updatedAt || null;
|
| 30 |
+
} else if (['users', 'sessions'].includes(tableName)) {
|
| 31 |
+
// These tables only have created_at
|
| 32 |
+
const latestResult = (await db.raw(
|
| 33 |
+
`SELECT created_at FROM ${tableName} ORDER BY created_at DESC LIMIT 1`
|
| 34 |
+
)) as any;
|
| 35 |
+
lastModified = latestResult?.data?.[0]?.createdAt || null;
|
| 36 |
+
} else if (tableName === 'messages') {
|
| 37 |
+
// Messages table uses timestamp
|
| 38 |
+
const latestResult = (await db.raw(
|
| 39 |
+
`SELECT timestamp FROM ${tableName} ORDER BY timestamp DESC LIMIT 1`
|
| 40 |
+
)) as any;
|
| 41 |
+
lastModified = latestResult?.data?.[0]?.timestamp || null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
+
} catch (error) {
|
| 44 |
+
// If column doesn't exist or other error, leave as null
|
| 45 |
+
console.error(`[Health] Error querying ${tableName}:`, error);
|
| 46 |
+
}
|
| 47 |
|
| 48 |
+
tableMetrics.push({
|
| 49 |
+
name: tableName,
|
| 50 |
+
rowCount,
|
| 51 |
+
lastModified,
|
| 52 |
+
});
|
| 53 |
+
}
|
|
|
|
| 54 |
|
| 55 |
return NextResponse.json({
|
| 56 |
tables: tableMetrics,
|
|
@@ -311,8 +311,8 @@ export class SupabaseDatabaseClient implements DatabaseClient {
|
|
| 311 |
connectionString: config.databaseUrl,
|
| 312 |
max: 10, // Max 10 connections in pool
|
| 313 |
idleTimeoutMillis: 30000, // Close idle connections after 30s
|
| 314 |
-
connectionTimeoutMillis:
|
| 315 |
-
statement_timeout:
|
| 316 |
});
|
| 317 |
|
| 318 |
this.ready = this.initialize(config.schemaSql);
|
|
|
|
| 311 |
connectionString: config.databaseUrl,
|
| 312 |
max: 10, // Max 10 connections in pool
|
| 313 |
idleTimeoutMillis: 30000, // Close idle connections after 30s
|
| 314 |
+
connectionTimeoutMillis: 30000, // Timeout if can't get connection in 30s
|
| 315 |
+
statement_timeout: 30000, // Timeout queries after 30s
|
| 316 |
});
|
| 317 |
|
| 318 |
this.ready = this.initialize(config.schemaSql);
|