Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

fixes to db connections going stale

nekomimi.pet 09397d47 e395d9eb

verified
Changed files
+67 -5
apps
hosting-service
src
main-app
src
lib
routes
+3 -1
apps/hosting-service/src/index.ts
··· 4 import { createLogger, initializeGrafanaExporters } from '@wisp/observability'; 5 import { mkdirSync, existsSync } from 'fs'; 6 import { backfillCache } from './lib/backfill'; 7 - import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode } from './lib/db'; 8 9 // Initialize Grafana exporters if configured 10 initializeGrafanaExporters({ ··· 94 console.log('\n๐Ÿ›‘ Shutting down...'); 95 firehose.stop(); 96 stopDomainCacheCleanup(); 97 server.close(); 98 process.exit(0); 99 }); ··· 102 console.log('\n๐Ÿ›‘ Shutting down...'); 103 firehose.stop(); 104 stopDomainCacheCleanup(); 105 server.close(); 106 process.exit(0); 107 });
··· 4 import { createLogger, initializeGrafanaExporters } from '@wisp/observability'; 5 import { mkdirSync, existsSync } from 'fs'; 6 import { backfillCache } from './lib/backfill'; 7 + import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode, closeDatabase } from './lib/db'; 8 9 // Initialize Grafana exporters if configured 10 initializeGrafanaExporters({ ··· 94 console.log('\n๐Ÿ›‘ Shutting down...'); 95 firehose.stop(); 96 stopDomainCacheCleanup(); 97 + await closeDatabase(); 98 server.close(); 99 process.exit(0); 100 }); ··· 103 console.log('\n๐Ÿ›‘ Shutting down...'); 104 firehose.stop(); 105 stopDomainCacheCleanup(); 106 + await closeDatabase(); 107 server.close(); 108 process.exit(0); 109 });
+32 -1
apps/hosting-service/src/lib/db.ts
··· 183 return hashNum & 0x7FFFFFFFFFFFFFFFn; 184 } 185 186 /** 187 * Acquire a distributed lock using PostgreSQL advisory locks 188 * Returns true if lock was acquired, false if already held by another instance ··· 193 194 try { 195 const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`; 196 - return result[0]?.acquired === true; 197 } catch (err) { 198 console.error('Failed to acquire lock', { key, error: err }); 199 return false; ··· 208 209 try { 210 await sql`SELECT pg_advisory_unlock(${Number(lockId)})`; 211 } catch (err) { 212 console.error('Failed to release lock', { key, error: err }); 213 } 214 } 215
··· 183 return hashNum & 0x7FFFFFFFFFFFFFFFn; 184 } 185 186 + // Track active locks for cleanup on shutdown 187 + const activeLocks = new Set<string>(); 188 + 189 /** 190 * Acquire a distributed lock using PostgreSQL advisory locks 191 * Returns true if lock was acquired, false if already held by another instance ··· 196 197 try { 198 const result = await sql`SELECT pg_try_advisory_lock(${Number(lockId)}) as acquired`; 199 + const acquired = result[0]?.acquired === true; 200 + if (acquired) { 201 + activeLocks.add(key); 202 + } 203 + return acquired; 204 } catch (err) { 205 console.error('Failed to acquire lock', { key, error: err }); 206 return false; ··· 215 216 try { 217 await sql`SELECT pg_advisory_unlock(${Number(lockId)})`; 218 + activeLocks.delete(key); 219 } catch (err) { 220 console.error('Failed to release lock', { key, error: err }); 221 + // Still remove from tracking even if unlock fails 222 + activeLocks.delete(key); 223 + } 224 + } 225 + 226 + /** 227 + * Close all database connections 228 + * Call this during graceful shutdown 229 + */ 230 + export async function closeDatabase(): Promise<void> { 231 + try { 232 + // Release all active advisory locks before closing connections 233 + if (activeLocks.size > 0) { 234 + console.log(`[DB] Releasing ${activeLocks.size} active advisory locks before shutdown`); 235 + for (const key of activeLocks) { 236 + await releaseLock(key); 237 + } 238 + } 239 + 240 + await sql.end({ timeout: 5 }); 241 + console.log('[DB] Database connections closed'); 242 + } catch (err) { 243 + console.error('[DB] Error closing database connections:', err); 244 } 245 } 246
+16 -1
apps/main-app/src/index.ts
··· 12 cleanupExpiredSessions, 13 rotateKeysIfNeeded 14 } from './lib/oauth-client' 15 - import { getCookieSecret } from './lib/db' 16 import { authRoutes } from './routes/auth' 17 import { wispRoutes } from './routes/wisp' 18 import { domainRoutes } from './routes/domain' ··· 205 console.log( 206 `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}` 207 )
··· 12 cleanupExpiredSessions, 13 rotateKeysIfNeeded 14 } from './lib/oauth-client' 15 + import { getCookieSecret, closeDatabase } from './lib/db' 16 import { authRoutes } from './routes/auth' 17 import { wispRoutes } from './routes/wisp' 18 import { domainRoutes } from './routes/domain' ··· 205 console.log( 206 `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}` 207 ) 208 + 209 + // Graceful shutdown 210 + process.on('SIGINT', async () => { 211 + console.log('\n๐Ÿ›‘ Shutting down...') 212 + dnsVerifier.stop() 213 + await closeDatabase() 214 + process.exit(0) 215 + }) 216 + 217 + process.on('SIGTERM', async () => { 218 + console.log('\n๐Ÿ›‘ Shutting down...') 219 + dnsVerifier.stop() 220 + await closeDatabase() 221 + process.exit(0) 222 + })
+13
apps/main-app/src/lib/db.ts
··· 526 console.log('[CookieSecret] Generated new cookie signing secret'); 527 return secret; 528 };
··· 526 console.log('[CookieSecret] Generated new cookie signing secret'); 527 return secret; 528 }; 529 + 530 + /** 531 + * Close database connection 532 + * Call this during graceful shutdown 533 + */ 534 + export const closeDatabase = async (): Promise<void> => { 535 + try { 536 + await db.end(); 537 + console.log('[DB] Database connection closed'); 538 + } catch (err) { 539 + console.error('[DB] Error closing database connection:', err); 540 + } 541 + };
+3 -2
apps/main-app/src/routes/wisp.ts
··· 39 40 const logger = createLogger('main-app') 41 42 - function isValidSiteName(siteName: string): boolean { 43 if (!siteName || typeof siteName !== 'string') return false; 44 45 // Length check (AT Protocol rkey limit) ··· 184 } 185 186 // Use webkitRelativePath when available (directory uploads), fallback to name for regular file uploads 187 - const filePath = (file as any).webkitRelativePath || file.name; 188 189 updateJobProgress(jobId, { 190 filesProcessed: i + 1,
··· 39 40 const logger = createLogger('main-app') 41 42 + export function isValidSiteName(siteName: string): boolean { 43 if (!siteName || typeof siteName !== 'string') return false; 44 45 // Length check (AT Protocol rkey limit) ··· 184 } 185 186 // Use webkitRelativePath when available (directory uploads), fallback to name for regular file uploads 187 + const webkitPath = 'webkitRelativePath' in file ? String(file.webkitRelativePath) : ''; 188 + const filePath = webkitPath || file.name; 189 190 updateJobProgress(jobId, { 191 filesProcessed: i + 1,