···11-// import { monitorPgPool } from '@christiangalsterer/node-postgres-prometheus-exporter';
11+import { monitorPgPool } from '@christiangalsterer/node-postgres-prometheus-exporter';
22import express from 'express';
33-import { Registry, collectDefaultMetrics } from 'prom-client';
33+import { Gauge, Registry, collectDefaultMetrics } from 'prom-client';
4455-import logger from './logger.js';
66-// import { pool } from './postgres.js';
55+import { pool } from './postgres.js';
66+import { Server } from 'http';
7788const register = new Registry();
99collectDefaultMetrics({ register });
10101111-// monitorPgPool(pool, register);
1111+export const concurrentPostgresInserts = new Gauge({
1212+ name: 'bluesky_concurrent_postgres_inserts',
1313+ help: 'Number of concurrent Postgres inserts',
1414+ registers: [register],
1515+});
1616+1717+monitorPgPool(pool, register);
12181319const app = express();
1420···2531 });
2632});
27333434+let metricsServer: Server;
3535+2836export const startMetricsServer = (port: number, host = '127.0.0.1') => {
2929- const server = app.listen(port, host, () => {
3737+ metricsServer = app.listen(port, host, () => {
3038 console.log(`Metrics server listening on port ${port}`);
3139 });
32403333- server.on('close', () => {
4141+ metricsServer.on('close', () => {
3442 console.log('Metrics server closed.');
3543 });
36443737- return server;
4545+ return metricsServer;
3846};
4747+4848+export function stopMetricsServer(): Promise<void> {
4949+ return new Promise((resolve, reject) => {
5050+ if (metricsServer) {
5151+ metricsServer.close((err) => {
5252+ if (err) {
5353+ console.error('Error shutting down metrics server:', err);
5454+ reject(err);
5555+ } else {
5656+ console.log('Metrics server shut down successfully.');
5757+ resolve();
5858+ }
5959+ });
6060+ } else {
6161+ resolve();
6262+ }
6363+ });
6464+}
+50
src/migrations/001-create.ts
···11+import 'dotenv/config';
22+import pg from 'pg';
33+44+const { Client } = pg;
55+66+const client = new Client({
77+ connectionString: process.env.DATABASE_URL!,
88+});
99+1010+await client.connect();
1111+1212+export async function createTables() {
1313+ await client.query(`
1414+ CREATE TABLE IF NOT EXISTS posts (
1515+ id BIGSERIAL PRIMARY KEY,
1616+ cid TEXT NOT NULL, -- ~64 characters
1717+ did TEXT NOT NULL, -- ~32 characters
1818+ rkey TEXT NOT NULL, -- ~13 characters
1919+ has_emojis BOOLEAN NOT NULL DEFAULT FALSE,
2020+ langs TEXT[] NOT NULL DEFAULT '{}',
2121+ text TEXT,
2222+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc')
2323+ );
2424+2525+ CREATE TABLE IF NOT EXISTS emojis (
2626+ id BIGSERIAL PRIMARY KEY,
2727+ post_id BIGINT DEFAULT NULL,
2828+ profile_id BIGINT DEFAULT NULL,
2929+ emoji TEXT NOT NULL,
3030+ lang TEXT NOT NULL, -- 2 or 5 characters
3131+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc')
3232+ );
3333+3434+ CREATE TABLE IF NOT EXISTS profiles (
3535+ id BIGSERIAL PRIMARY KEY,
3636+ cid TEXT NOT NULL, -- ~64 characters
3737+ did TEXT NOT NULL, -- ~32 characters
3838+ rkey TEXT NOT NULL, -- ~13 characters
3939+ description TEXT,
4040+ display_name TEXT,
4141+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc')
4242+ );
4343+ `);
4444+}
4545+4646+createTables()
4747+ .catch((e: unknown) => {
4848+ console.error(e);
4949+ })
5050+ .finally(() => void client.end());
+5-1
src/postgres.ts
···2121 dialect,
2222});
23232424-export { pool, db };
2424+async function closeDatabase() {
2525+ await db.destroy();
2626+}
2727+2828+export { closeDatabase, pool, db };
+159
src/postgresBatchQueue.ts
···11+// src/postgresBatchQueue.ts
22+33+import { Mutex } from 'async-mutex';
44+import { db, closeDatabase } from './postgres.js';
55+import { concurrentPostgresInserts } from './metrics.js';
66+import { PostData } from './types.js';
77+import { BATCH_SIZE, BATCH_TIMEOUT_MS, MAX_FLUSH_RETRIES } from './constants.js';
88+99+export class PostgresBatchQueue {
1010+ private queue: PostData[] = [];
1111+ private mutex = new Mutex();
1212+ private batchSize: number;
1313+ private batchTimeoutMs: number;
1414+ private batchTimer: NodeJS.Timeout | null = null;
1515+ private isShuttingDown = false;
1616+1717+ constructor(batchSize: number, batchTimeoutMs: number) {
1818+ this.batchSize = batchSize;
1919+ this.batchTimeoutMs = batchTimeoutMs;
2020+ }
2121+2222+ /**
2323+ * Adds a PostData item to the queue and triggers flush if necessary.
2424+ * @param data PostData to enqueue
2525+ */
2626+ public async enqueue(data: PostData): Promise<void> {
2727+ if (this.isShuttingDown) {
2828+ throw new Error('Cannot enqueue data, the queue is shutting down.');
2929+ }
3030+3131+ let shouldFlush = false;
3232+3333+ await this.mutex.runExclusive(() => {
3434+ this.queue.push(data);
3535+3636+ if (this.queue.length >= this.batchSize) {
3737+ shouldFlush = true;
3838+ } else if (!this.batchTimer) {
3939+ this.scheduleFlush();
4040+ }
4141+ });
4242+4343+ if (shouldFlush) {
4444+ await this.flushQueue();
4545+ }
4646+ }
4747+4848+ /**
4949+ * Schedules a flush after the specified timeout.
5050+ */
5151+ private scheduleFlush(): void {
5252+ this.batchTimer = setTimeout(() => {
5353+ this.flushQueue().catch((err) => {
5454+ console.error(`Scheduled flush error: ${(err as Error).message}`);
5555+ });
5656+ }, this.batchTimeoutMs);
5757+ }
5858+5959+ /**
6060+ * Flushes the current queue to PostgreSQL.
6161+ */
6262+ private async flushQueue(): Promise<void> {
6363+ // Clear the existing timer
6464+ if (this.batchTimer) {
6565+ clearTimeout(this.batchTimer);
6666+ this.batchTimer = null;
6767+ }
6868+6969+ let currentBatch: PostData[] = [];
7070+7171+ await this.mutex.runExclusive(() => {
7272+ if (this.queue.length === 0) {
7373+ return;
7474+ }
7575+ currentBatch = this.queue.splice(0, this.batchSize);
7676+ });
7777+7878+ if (currentBatch.length === 0) {
7979+ return;
8080+ }
8181+8282+ concurrentPostgresInserts.inc();
8383+8484+ try {
8585+ await this.attemptFlush(currentBatch);
8686+ process.stdout.write('.');
8787+ } catch (error) {
8888+ console.error(`Error flushing PostgreSQL batch: ${(error as Error).message}`);
8989+ // Re-add the failed batch back for retry
9090+ await this.mutex.runExclusive(() => {
9191+ this.queue = currentBatch.concat(this.queue);
9292+ });
9393+ } finally {
9494+ concurrentPostgresInserts.dec();
9595+ }
9696+ }
9797+9898+ /**
9999+ * Attempts to flush the batch with retry logic.
100100+ * @param batch The batch of PostData to flush
101101+ */
102102+ private async attemptFlush(batch: PostData[]): Promise<void> {
103103+ let attempt = 0;
104104+ let success = false;
105105+106106+ while (attempt < MAX_FLUSH_RETRIES && !success) {
107107+ try {
108108+ await db.transaction().execute(async (tx) => {
109109+ await tx
110110+ .insertInto('posts') // Ensure 'posts' is your table name
111111+ .values(
112112+ batch.map((post) => ({
113113+ cid: post.cid,
114114+ did: post.did,
115115+ rkey: post.rkey,
116116+ has_emojis: post.hasEmojis,
117117+ langs: post.langs,
118118+ text: post.post,
119119+ created_at: post.createdAt,
120120+ })),
121121+ )
122122+ .execute();
123123+ });
124124+125125+ success = true;
126126+ } catch (error) {
127127+ attempt++;
128128+ console.error(`Flush attempt ${attempt} failed: ${(error as Error).message}`);
129129+130130+ if (attempt < MAX_FLUSH_RETRIES) {
131131+ const backoffTime = 2 ** attempt * 1000; // Exponential backoff
132132+ console.log(`Retrying in ${backoffTime} ms...`);
133133+ await new Promise((resolve) => setTimeout(resolve, backoffTime));
134134+ } else {
135135+ console.error('Max retries reached. Re-queueing the batch.');
136136+ throw error; // Let the caller handle re-queueing
137137+ }
138138+ }
139139+ }
140140+ }
141141+142142+ /**
143143+ * Gracefully shuts down the queue by flushing remaining items.
144144+ */
145145+ public async shutdown(): Promise<void> {
146146+ this.isShuttingDown = true;
147147+ if (this.batchTimer) {
148148+ clearTimeout(this.batchTimer);
149149+ this.batchTimer = null;
150150+ }
151151+ await this.flushQueue();
152152+ console.log('Flushed all remaining items.');
153153+ await closeDatabase();
154154+ console.log('Database connections closed.');
155155+ }
156156+}
157157+158158+// Instantiate the queue
159159+export const postgresBatchQueue = new PostgresBatchQueue(BATCH_SIZE, BATCH_TIMEOUT_MS);
···11-import 'dotenv/config';
21import Database from 'libsql';
32import fs from 'node:fs/promises';
43import { performance } from 'node:perf_hooks';
5465import { PLC_DB_PATH, RELAY_URL, SQL_OUTPUT_FILE } from '../constants.js';
77-import logger from '../logger.js';
86import { DidAndPds } from '../types.js';
97108const didDb = new Database(PLC_DB_PATH);
-1
src/stages/stage2.ts
···11import { HEALTH_CHECK_FILE, PDS_HEALTH_CHECK_CONCURRENCY, SQL_OUTPUT_FILE } from "../constants.js";
22-import logger from "../logger.js";
32import fs from "node:fs/promises";
43import readline from "node:readline";
54import pLimit from "p-limit";