From fc912f185e9da4a943320f3ca00cbeb6c4dcb9fa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 12 Sep 2024 13:03:23 -0500 Subject: [PATCH] Gracefully start and exit the database --- src/DittoExit.ts | 37 ++++++++++++++++++++++++++++++++ src/db/DittoDatabase.ts | 1 + src/db/adapters/DittoPglite.ts | 5 ++++- src/db/adapters/DittoPostgres.ts | 1 + src/server.ts | 13 ++++++++++- src/storages.ts | 23 ++++++++++++++++---- 6 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/DittoExit.ts diff --git a/src/DittoExit.ts b/src/DittoExit.ts new file mode 100644 index 00000000..36201fc7 --- /dev/null +++ b/src/DittoExit.ts @@ -0,0 +1,37 @@ +import { Stickynotes } from '@soapbox/stickynotes'; + +/** + * Add cleanup tasks to this module, + * then they will automatically be called (and the program exited) after SIGINT. + */ +export class DittoExit { + private static tasks: Array<() => Promise> = []; + private static console = new Stickynotes('ditto:exit'); + + static { + Deno.addSignalListener('SIGINT', () => this.finish('SIGINT')); + Deno.addSignalListener('SIGTERM', () => this.finish('SIGTERM')); + Deno.addSignalListener('SIGHUP', () => this.finish('SIGHUP')); + Deno.addSignalListener('SIGQUIT', () => this.finish('SIGQUIT')); + Deno.addSignalListener('SIGABRT', () => this.finish('SIGABRT')); + } + + static add(task: () => Promise): void { + this.tasks.push(task); + this.console.debug(`Added cleanup task #${this.tasks.length}`); + } + + private static async cleanup(): Promise { + this.console.debug(`Running ${this.tasks.length} cleanup tasks...`); + await Promise.allSettled( + this.tasks.map((task) => task()), + ); + } + + private static async finish(signal: Deno.Signal): Promise { + this.console.debug(signal); + await this.cleanup(); + this.console.debug('Exiting gracefully.'); + Deno.exit(0); + } +} diff --git a/src/db/DittoDatabase.ts b/src/db/DittoDatabase.ts index 530d9391..93c71a90 100644 --- a/src/db/DittoDatabase.ts +++ b/src/db/DittoDatabase.ts @@ -6,6 +6,7 @@ export interface DittoDatabase { readonly kysely: Kysely; readonly poolSize: number; readonly availableConnections: number; + readonly waitReady: Promise; } export interface DittoDatabaseOpts { diff --git a/src/db/adapters/DittoPglite.ts b/src/db/adapters/DittoPglite.ts index 4ec7d8a5..9b425c4b 100644 --- a/src/db/adapters/DittoPglite.ts +++ b/src/db/adapters/DittoPglite.ts @@ -8,9 +8,11 @@ import { KyselyLogger } from '@/db/KyselyLogger.ts'; export class DittoPglite { static create(databaseUrl: string): DittoDatabase { + const pglite = new PGlite(databaseUrl); + const kysely = new Kysely({ dialect: new PgliteDialect({ - database: new PGlite(databaseUrl), + database: pglite, }), log: KyselyLogger, }); @@ -19,6 +21,7 @@ export class DittoPglite { kysely, poolSize: 1, availableConnections: 1, + waitReady: pglite.waitReady, }; } } diff --git a/src/db/adapters/DittoPostgres.ts b/src/db/adapters/DittoPostgres.ts index f1a5bcc9..0300c3e0 100644 --- a/src/db/adapters/DittoPostgres.ts +++ b/src/db/adapters/DittoPostgres.ts @@ -48,6 +48,7 @@ export class DittoPostgres { get availableConnections() { return pg.connections.idle; }, + waitReady: Promise.resolve(), }; } } diff --git a/src/server.ts b/src/server.ts index f7a33dc0..bfa240c9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,5 +5,16 @@ import '@/sentry.ts'; import '@/nostr-wasm.ts'; import app from '@/app.ts'; import { Conf } from '@/config.ts'; +import { DittoExit } from '@/DittoExit.ts'; -Deno.serve({ port: Conf.port }, app.fetch); +const ac = new AbortController(); +// deno-lint-ignore require-await +DittoExit.add(async () => ac.abort()); + +Deno.serve( + { + port: Conf.port, + signal: ac.signal, + }, + app.fetch, +); diff --git a/src/storages.ts b/src/storages.ts index 60a0cebc..fb66c55e 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -2,6 +2,7 @@ import { Conf } from '@/config.ts'; import { DittoDatabase } from '@/db/DittoDatabase.ts'; import { DittoDB } from '@/db/DittoDB.ts'; +import { DittoExit } from '@/DittoExit.ts'; import { AdminStore } from '@/storages/AdminStore.ts'; import { EventsDB } from '@/storages/EventsDB.ts'; import { SearchStore } from '@/storages/search-store.ts'; @@ -10,9 +11,11 @@ import { NPool, NRelay1 } from '@nostrify/nostrify'; import { getRelays } from '@/utils/outbox.ts'; import { seedZapSplits } from '@/utils/zap-split.ts'; +DittoExit.add(() => Storages.close()); + export class Storages { private static _db: Promise | undefined; - private static _database: DittoDatabase | undefined; + private static _database: Promise | undefined; private static _admin: Promise | undefined; private static _client: Promise | undefined; private static _pubsub: Promise | undefined; @@ -20,8 +23,12 @@ export class Storages { public static async database(): Promise { if (!this._database) { - this._database = DittoDB.create(Conf.databaseUrl, { poolSize: Conf.pg.poolSize }); - await DittoDB.migrate(this._database.kysely); + this._database = (async () => { + const db = DittoDB.create(Conf.databaseUrl, { poolSize: Conf.pg.poolSize }); + await db.waitReady; + await DittoDB.migrate(db.kysely); + return db; + })(); } return this._database; } @@ -35,7 +42,7 @@ export class Storages { public static async db(): Promise { if (!this._db) { this._db = (async () => { - const { kysely } = await this.database(); + const kysely = await this.kysely(); const store = new EventsDB({ kysely, pubkey: Conf.pubkey, timeout: Conf.db.timeouts.default }); await seedZapSplits(store); return store; @@ -118,4 +125,12 @@ export class Storages { } return this._search; } + + /** Close the database connection, if one has been opened. */ + public static async close(): Promise { + if (this._database) { + const { kysely } = await this._database; + await kysely.destroy(); + } + } }