+3
-1
apps/hosting-service/src/index.ts
+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
+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
+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
+13
apps/main-app/src/lib/db.ts
···
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
+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,