Skip to content

Commit 0a35737

Browse files
r1tsuuDanRibbens
andauthored
feat(db-postgres): support read replicas (#12728)
Adds support for read replicas https://orm.drizzle.team/docs/read-replicas that can be used to offload read-heavy traffic. To use (both `db-postgres` and `db-vercel-postgres` are supported): ```ts import { postgresAdapter } from '@payloadcms/db-postgres' database: postgresAdapter({ pool: { connectionString: process.env.POSTGRES_URL, }, readReplicas: [process.env.POSTGRES_REPLICA_URL], }) ``` --------- Co-authored-by: Dan Ribbens <[email protected]>
1 parent 865a9cd commit 0a35737

File tree

10 files changed

+122
-13
lines changed

10 files changed

+122
-13
lines changed

docs/database/postgres.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default buildConfig({
8080
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
8181
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
8282
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
83+
| `readReplicas` | An array of DB read replicas connection strings, can be used to offload read-heavy traffic. |
8384

8485
## Access to Drizzle
8586

packages/db-postgres/src/connect.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
2-
import type { Connect, Migration, Payload } from 'payload'
2+
import type { Connect, Migration } from 'payload'
33

44
import { pushDevSchema } from '@payloadcms/drizzle'
55
import { drizzle } from 'drizzle-orm/node-postgres'
6+
import { withReplicas } from 'drizzle-orm/pg-core'
67

78
import type { PostgresAdapter } from './types.js'
89

910
const connectWithReconnect = async function ({
1011
adapter,
11-
payload,
12+
pool,
1213
reconnect = false,
1314
}: {
1415
adapter: PostgresAdapter
15-
payload: Payload
16+
pool: PostgresAdapter['pool']
1617
reconnect?: boolean
1718
}) {
1819
let result
1920

2021
if (!reconnect) {
21-
result = await adapter.pool.connect()
22+
result = await pool.connect()
2223
} else {
2324
try {
24-
result = await adapter.pool.connect()
25+
result = await pool.connect()
2526
} catch (ignore) {
2627
setTimeout(() => {
27-
payload.logger.info('Reconnecting to postgres')
28-
void connectWithReconnect({ adapter, payload, reconnect: true })
28+
adapter.payload.logger.info('Reconnecting to postgres')
29+
void connectWithReconnect({ adapter, pool, reconnect: true })
2930
}, 1000)
3031
}
3132
}
@@ -35,7 +36,7 @@ const connectWithReconnect = async function ({
3536
result.prependListener('error', (err) => {
3637
try {
3738
if (err.code === 'ECONNRESET') {
38-
void connectWithReconnect({ adapter, payload, reconnect: true })
39+
void connectWithReconnect({ adapter, pool, reconnect: true })
3940
}
4041
} catch (ignore) {
4142
// swallow error
@@ -54,12 +55,29 @@ export const connect: Connect = async function connect(
5455
try {
5556
if (!this.pool) {
5657
this.pool = new this.pg.Pool(this.poolOptions)
57-
await connectWithReconnect({ adapter: this, payload: this.payload })
58+
await connectWithReconnect({ adapter: this, pool: this.pool })
5859
}
5960

6061
const logger = this.logger || false
6162
this.drizzle = drizzle({ client: this.pool, logger, schema: this.schema })
6263

64+
if (this.readReplicaOptions) {
65+
const readReplicas = this.readReplicaOptions.map((connectionString) => {
66+
const options = {
67+
...this.poolOptions,
68+
connectionString,
69+
}
70+
const pool = new this.pg.Pool(options)
71+
void connectWithReconnect({
72+
adapter: this,
73+
pool,
74+
})
75+
return drizzle({ client: pool, logger, schema: this.schema })
76+
})
77+
const myReplicas = withReplicas(this.drizzle, readReplicas as any)
78+
this.drizzle = myReplicas
79+
}
80+
6381
if (!hotReload) {
6482
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
6583
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)

packages/db-postgres/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
139139
prodMigrations: args.prodMigrations,
140140
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
141141
push: args.push,
142+
readReplicaOptions: args.readReplicas,
142143
relations: {},
143144
relationshipsSuffix: args.relationshipsSuffix || '_rels',
144145
schema: {},

packages/db-postgres/src/types.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import type {
33
GenericEnum,
44
MigrateDownArgs,
55
MigrateUpArgs,
6-
PostgresDB,
76
PostgresSchemaHook,
87
} from '@payloadcms/drizzle/postgres'
98
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
10-
import type { DrizzleConfig } from 'drizzle-orm'
9+
import type { DrizzleConfig, ExtractTablesWithRelations } from 'drizzle-orm'
1110
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
12-
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
11+
import type {
12+
PgDatabase,
13+
PgQueryResultHKT,
14+
PgSchema,
15+
PgTableFn,
16+
PgTransactionConfig,
17+
PgWithReplicas,
18+
} from 'drizzle-orm/pg-core'
1319
import type { Pool, PoolConfig } from 'pg'
1420

1521
type PgDependency = typeof import('pg')
@@ -55,6 +61,7 @@ export type Args = {
5561
up: (args: MigrateUpArgs) => Promise<void>
5662
}[]
5763
push?: boolean
64+
readReplicas?: string[]
5865
relationshipsSuffix?: string
5966
/**
6067
* The schema name to use for the database
@@ -74,7 +81,16 @@ type ResolveSchemaType<T> = 'schema' extends keyof T
7481
? T['schema']
7582
: GeneratedDatabaseSchema['schemaUntyped']
7683

77-
type Drizzle = NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
84+
type Drizzle =
85+
| NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
86+
| PgWithReplicas<
87+
PgDatabase<
88+
PgQueryResultHKT,
89+
Record<string, unknown>,
90+
ExtractTablesWithRelations<Record<string, unknown>>
91+
>
92+
>
93+
7894
export type PostgresAdapter = {
7995
drizzle: Drizzle
8096
pg: PgDependency

packages/db-vercel-postgres/src/connect.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Connect, Migration } from 'payload'
44
import { pushDevSchema } from '@payloadcms/drizzle'
55
import { sql, VercelPool } from '@vercel/postgres'
66
import { drizzle } from 'drizzle-orm/node-postgres'
7+
import { withReplicas } from 'drizzle-orm/pg-core'
78
import pg from 'pg'
89

910
import type { VercelPostgresAdapter } from './types.js'
@@ -46,6 +47,19 @@ export const connect: Connect = async function connect(
4647
schema: this.schema,
4748
})
4849

50+
if (this.readReplicaOptions) {
51+
const readReplicas = this.readReplicaOptions.map((connectionString) => {
52+
const options = {
53+
...this.poolOptions,
54+
connectionString,
55+
}
56+
const pool = new VercelPool(options)
57+
return drizzle({ client: pool, logger, schema: this.schema })
58+
})
59+
const myReplicas = withReplicas(this.drizzle, readReplicas as any)
60+
this.drizzle = myReplicas
61+
}
62+
4963
if (!hotReload) {
5064
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
5165
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)

packages/db-vercel-postgres/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
174174
find,
175175
findGlobal,
176176
findGlobalVersions,
177+
readReplicaOptions: args.readReplicas,
177178
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
178179
findOne,
179180
findVersions,

packages/db-vercel-postgres/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export type Args = {
6464
up: (args: MigrateUpArgs) => Promise<void>
6565
}[]
6666
push?: boolean
67+
readReplicas?: string[]
6768
relationshipsSuffix?: string
6869
/**
6970
* The schema name to use for the database

packages/drizzle/src/postgres/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export type BasePostgresAdapter = {
159159
up: (args: MigrateUpArgs) => Promise<void>
160160
}[]
161161
push: boolean
162+
readReplicaOptions?: string[]
162163
rejectInitializing: () => void
163164
relations: Record<string, GenericRelation>
164165
relationshipsSuffix?: string
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright Broadcom, Inc. All Rights Reserved.
2+
# SPDX-License-Identifier: APACHE-2.0
3+
4+
services:
5+
postgresql-master:
6+
image: docker.io/bitnami/postgresql:17
7+
ports:
8+
- '5433:5432'
9+
volumes:
10+
- 'postgresql_master_data:/bitnami/postgresql'
11+
environment:
12+
- POSTGRESQL_REPLICATION_MODE=master
13+
- POSTGRESQL_REPLICATION_USER=repl_user
14+
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
15+
- POSTGRESQL_USERNAME=postgres
16+
- POSTGRESQL_PASSWORD=my_password
17+
- POSTGRESQL_DATABASE=my_database
18+
- ALLOW_EMPTY_PASSWORD=yes
19+
postgresql-slave:
20+
image: docker.io/bitnami/postgresql:17
21+
ports:
22+
- '5434:5432'
23+
depends_on:
24+
- postgresql-master
25+
environment:
26+
- POSTGRESQL_REPLICATION_MODE=slave
27+
- POSTGRESQL_REPLICATION_USER=repl_user
28+
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
29+
- POSTGRESQL_MASTER_HOST=postgresql-master
30+
- POSTGRESQL_PASSWORD=my_password
31+
- POSTGRESQL_MASTER_PORT_NUMBER=5432
32+
- ALLOW_EMPTY_PASSWORD=yes
33+
34+
volumes:
35+
postgresql_master_data:
36+
driver: local

test/generateDatabaseAdapter.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ export const allDatabaseAdapters = {
4747
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
4848
},
4949
})`,
50+
'postgres-read-replica': `
51+
import { postgresAdapter } from '@payloadcms/db-postgres'
52+
53+
export const databaseAdapter = postgresAdapter({
54+
pool: {
55+
connectionString: process.env.POSTGRES_URL,
56+
},
57+
readReplicas: [process.env.POSTGRES_REPLICA_URL],
58+
})
59+
`,
60+
'vercel-postgres-read-replica': `
61+
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
62+
63+
export const databaseAdapter = vercelPostgresAdapter({
64+
pool: {
65+
connectionString: process.env.POSTGRES_URL,
66+
},
67+
readReplicas: [process.env.POSTGRES_REPLICA_URL],
68+
})
69+
`,
5070
sqlite: `
5171
import { sqliteAdapter } from '@payloadcms/db-sqlite'
5272

0 commit comments

Comments
 (0)