mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
feat(database): ✨ Implement read replicas for database
This commit is contained in:
parent
c75306c58b
commit
bc0943c569
|
|
@ -10,6 +10,14 @@ username = "versia"
|
||||||
password = "mycoolpassword"
|
password = "mycoolpassword"
|
||||||
database = "versia"
|
database = "versia"
|
||||||
|
|
||||||
|
# Add any eventual read-only database replicas here
|
||||||
|
# [[database.replicas]]
|
||||||
|
# host = "other-host"
|
||||||
|
# port = 5432
|
||||||
|
# username = "versia"
|
||||||
|
# password = "mycoolpassword2"
|
||||||
|
# database = "replica1"
|
||||||
|
|
||||||
[redis.queue]
|
[redis.queue]
|
||||||
# Redis instance for storing the federation queue
|
# Redis instance for storing the federation queue
|
||||||
# Required for federation
|
# Required for federation
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,39 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"default": "versia"
|
"default": "versia"
|
||||||
|
},
|
||||||
|
"replicas": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"default": 5432
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"default": "versia"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["host", "username"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["username"],
|
"required": ["username"],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import chalk from "chalk";
|
||||||
|
import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import { withReplicas } from "drizzle-orm/pg-core";
|
||||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
import { Client } from "pg";
|
import { Pool } from "pg";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema";
|
||||||
|
|
||||||
export const client = new Client({
|
const primaryDb = new Pool({
|
||||||
host: config.database.host,
|
host: config.database.host,
|
||||||
port: Number(config.database.port),
|
port: Number(config.database.port),
|
||||||
user: config.database.username,
|
user: config.database.username,
|
||||||
|
|
@ -13,11 +15,35 @@ export const client = new Client({
|
||||||
database: config.database.database,
|
database: config.database.database,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const replicas =
|
||||||
|
config.database.replicas?.map(
|
||||||
|
(replica) =>
|
||||||
|
new Pool({
|
||||||
|
host: replica.host,
|
||||||
|
port: Number(replica.port),
|
||||||
|
user: replica.username,
|
||||||
|
password: replica.password,
|
||||||
|
database: replica.database,
|
||||||
|
}),
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
export const db =
|
||||||
|
(replicas.length ?? 0) > 0
|
||||||
|
? withReplicas(
|
||||||
|
drizzle(primaryDb, { schema }),
|
||||||
|
replicas.map((r) => drizzle(r, { schema })) as [
|
||||||
|
NodePgDatabase<typeof schema>,
|
||||||
|
...NodePgDatabase<typeof schema>[],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: drizzle(primaryDb, { schema });
|
||||||
|
|
||||||
export const setupDatabase = async (info = true) => {
|
export const setupDatabase = async (info = true) => {
|
||||||
const logger = getLogger("database");
|
const logger = getLogger("database");
|
||||||
|
|
||||||
|
for (const dbPool of [primaryDb, ...replicas]) {
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await dbPool.connect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
if (
|
||||||
(e as Error).message ===
|
(e as Error).message ===
|
||||||
|
|
@ -27,11 +53,17 @@ export const setupDatabase = async (info = true) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.fatal`${e}`;
|
logger.fatal`${e}`;
|
||||||
logger.fatal`Failed to connect to database. Please check your configuration.`;
|
logger.fatal`Failed to connect to database ${chalk.bold(
|
||||||
|
// Index of the database in the array
|
||||||
|
replicas.indexOf(dbPool) === -1
|
||||||
|
? "primary"
|
||||||
|
: `replica-${replicas.indexOf(dbPool)}`,
|
||||||
|
)}. Please check your configuration.`;
|
||||||
|
|
||||||
// Hang until Ctrl+C is pressed
|
// Hang until Ctrl+C is pressed
|
||||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate the database
|
// Migrate the database
|
||||||
info && logger.info`Migrating database...`;
|
info && logger.info`Migrating database...`;
|
||||||
|
|
@ -50,5 +82,3 @@ export const setupDatabase = async (info = true) => {
|
||||||
|
|
||||||
info && logger.info`Database migrated`;
|
info && logger.info`Database migrated`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const db = drizzle(client, { schema });
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,22 @@ export const configValidator = z.object({
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
password: z.string().default(""),
|
password: z.string().default(""),
|
||||||
database: z.string().min(1).default("versia"),
|
database: z.string().min(1).default("versia"),
|
||||||
|
replicas: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
host: z.string().min(1),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(2 ** 16 - 1)
|
||||||
|
.default(5432),
|
||||||
|
username: z.string().min(1),
|
||||||
|
password: z.string().default(""),
|
||||||
|
database: z.string().min(1).default("versia"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
redis: z.object({
|
redis: z.object({
|
||||||
queue: z
|
queue: z
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue