Gracefully start and exit the database

This commit is contained in:
Alex Gleason 2024-09-12 13:03:23 -05:00
parent d67f2a27ea
commit fc912f185e
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
6 changed files with 74 additions and 6 deletions

37
src/DittoExit.ts Normal file
View file

@ -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<unknown>> = [];
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<unknown>): void {
this.tasks.push(task);
this.console.debug(`Added cleanup task #${this.tasks.length}`);
}
private static async cleanup(): Promise<void> {
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<void> {
this.console.debug(signal);
await this.cleanup();
this.console.debug('Exiting gracefully.');
Deno.exit(0);
}
}

View file

@ -6,6 +6,7 @@ export interface DittoDatabase {
readonly kysely: Kysely<DittoTables>; readonly kysely: Kysely<DittoTables>;
readonly poolSize: number; readonly poolSize: number;
readonly availableConnections: number; readonly availableConnections: number;
readonly waitReady: Promise<void>;
} }
export interface DittoDatabaseOpts { export interface DittoDatabaseOpts {

View file

@ -8,9 +8,11 @@ import { KyselyLogger } from '@/db/KyselyLogger.ts';
export class DittoPglite { export class DittoPglite {
static create(databaseUrl: string): DittoDatabase { static create(databaseUrl: string): DittoDatabase {
const pglite = new PGlite(databaseUrl);
const kysely = new Kysely<DittoTables>({ const kysely = new Kysely<DittoTables>({
dialect: new PgliteDialect({ dialect: new PgliteDialect({
database: new PGlite(databaseUrl), database: pglite,
}), }),
log: KyselyLogger, log: KyselyLogger,
}); });
@ -19,6 +21,7 @@ export class DittoPglite {
kysely, kysely,
poolSize: 1, poolSize: 1,
availableConnections: 1, availableConnections: 1,
waitReady: pglite.waitReady,
}; };
} }
} }

View file

@ -48,6 +48,7 @@ export class DittoPostgres {
get availableConnections() { get availableConnections() {
return pg.connections.idle; return pg.connections.idle;
}, },
waitReady: Promise.resolve(),
}; };
} }
} }

View file

@ -5,5 +5,16 @@ import '@/sentry.ts';
import '@/nostr-wasm.ts'; import '@/nostr-wasm.ts';
import app from '@/app.ts'; import app from '@/app.ts';
import { Conf } from '@/config.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,
);

View file

@ -2,6 +2,7 @@
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoDatabase } from '@/db/DittoDatabase.ts'; import { DittoDatabase } from '@/db/DittoDatabase.ts';
import { DittoDB } from '@/db/DittoDB.ts'; import { DittoDB } from '@/db/DittoDB.ts';
import { DittoExit } from '@/DittoExit.ts';
import { AdminStore } from '@/storages/AdminStore.ts'; import { AdminStore } from '@/storages/AdminStore.ts';
import { EventsDB } from '@/storages/EventsDB.ts'; import { EventsDB } from '@/storages/EventsDB.ts';
import { SearchStore } from '@/storages/search-store.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 { getRelays } from '@/utils/outbox.ts';
import { seedZapSplits } from '@/utils/zap-split.ts'; import { seedZapSplits } from '@/utils/zap-split.ts';
DittoExit.add(() => Storages.close());
export class Storages { export class Storages {
private static _db: Promise<EventsDB> | undefined; private static _db: Promise<EventsDB> | undefined;
private static _database: DittoDatabase | undefined; private static _database: Promise<DittoDatabase> | undefined;
private static _admin: Promise<AdminStore> | undefined; private static _admin: Promise<AdminStore> | undefined;
private static _client: Promise<NPool> | undefined; private static _client: Promise<NPool> | undefined;
private static _pubsub: Promise<InternalRelay> | undefined; private static _pubsub: Promise<InternalRelay> | undefined;
@ -20,8 +23,12 @@ export class Storages {
public static async database(): Promise<DittoDatabase> { public static async database(): Promise<DittoDatabase> {
if (!this._database) { if (!this._database) {
this._database = DittoDB.create(Conf.databaseUrl, { poolSize: Conf.pg.poolSize }); this._database = (async () => {
await DittoDB.migrate(this._database.kysely); const db = DittoDB.create(Conf.databaseUrl, { poolSize: Conf.pg.poolSize });
await db.waitReady;
await DittoDB.migrate(db.kysely);
return db;
})();
} }
return this._database; return this._database;
} }
@ -35,7 +42,7 @@ export class Storages {
public static async db(): Promise<EventsDB> { public static async db(): Promise<EventsDB> {
if (!this._db) { if (!this._db) {
this._db = (async () => { 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 }); const store = new EventsDB({ kysely, pubkey: Conf.pubkey, timeout: Conf.db.timeouts.default });
await seedZapSplits(store); await seedZapSplits(store);
return store; return store;
@ -118,4 +125,12 @@ export class Storages {
} }
return this._search; return this._search;
} }
/** Close the database connection, if one has been opened. */
public static async close(): Promise<void> {
if (this._database) {
const { kysely } = await this._database;
await kysely.destroy();
}
}
} }