Skip to content

Commit 7857655

Browse files
feat: Add multihost support for native js driver
1 parent c78b302 commit 7857655

File tree

10 files changed

+1143
-70
lines changed

10 files changed

+1143
-70
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ dist
1010
/.eslintcache
1111
.vscode/
1212
manually-test-on-heroku.js
13+
.history

docs/pages/apis/client.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Every field of the `config` object is entirely optional. A `Client` instance wil
1212
type Config = {
1313
user?: string, // default process.env.PGUSER || process.env.USER
1414
password?: string or function, //default process.env.PGPASSWORD
15-
host?: string, // default process.env.PGHOST
16-
port?: number, // default process.env.PGPORT
15+
host?: string | string[], // default process.env.PGHOST; array enables multi-host failover
16+
port?: number | number[], // default process.env.PGPORT; one value or one per host
1717
database?: string, // default process.env.PGDATABASE || user
1818
connectionString?: string, // e.g. postgres://user:password@host:5432/database
1919
ssl?: any, // passed directly to node.TLSSocket, supports all tls.connect options
@@ -29,7 +29,8 @@ type Config = {
2929
idle_in_transaction_session_timeout?: number, // number of milliseconds before terminating any session with an open idle transaction, default is no timeout
3030
client_encoding?: string, // specifies the character set encoding that the database uses for sending data to the client
3131
fallback_application_name?: string, // provide an application name to use if application_name is not set
32-
options?: string // command-line options to be sent to the server
32+
options?: string, // command-line options to be sent to the server
33+
targetSessionAttrs?: 'any' | 'read-write' | 'read-only' | 'primary' | 'standby' | 'prefer-standby', // default 'any'; requires host to be an array
3334
}
3435
```
3536

docs/pages/features/connecting.mdx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,62 @@ client = new Client({
129129
})
130130
```
131131

132+
## Multiple hosts
133+
134+
node-postgres supports connecting to multiple PostgreSQL hosts. Pass arrays to `host` and `port` to enable automatic failover — the client tries each host in order and uses the first one it can reach.
135+
136+
```js
137+
import { Client } from 'pg'
138+
139+
const client = new Client({
140+
host: ['primary.db.com', 'replica1.db.com', 'replica2.db.com'],
141+
port: 5432, // single port reused for all hosts
142+
database: 'mydb',
143+
user: 'dbuser',
144+
password: 'secretpassword',
145+
})
146+
147+
await client.connect() // tries hosts left to right until one succeeds
148+
```
149+
150+
You can also specify a different port for each host:
151+
152+
```js
153+
const client = new Client({
154+
host: ['host-a.db.com', 'host-b.db.com'],
155+
port: [5432, 5433],
156+
database: 'mydb',
157+
})
158+
```
159+
160+
Port rules (same as libpq):
161+
- **one port** — reused for every host
162+
- **one port per host** — each port is paired with the corresponding host by index
163+
- any other combination throws at construction time
164+
165+
### target_session_attrs
166+
167+
Use `targetSessionAttrs` to control which host is accepted based on its role. This mirrors the [libpq `target_session_attrs`](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS) option.
168+
169+
```js
170+
const client = new Client({
171+
host: ['primary.db.com', 'replica.db.com'],
172+
port: 5432,
173+
targetSessionAttrs: 'read-write', // only connect to a writable primary
174+
})
175+
```
176+
177+
| Value | Accepted server |
178+
|---|---|
179+
| `any` (default) | any server |
180+
| `read-write` | server where `transaction_read_only = off` |
181+
| `read-only` | server where `transaction_read_only = on` |
182+
| `primary` | server that is not in hot standby |
183+
| `standby` | server that is in hot standby |
184+
| `prefer-standby` | standby if available, otherwise any |
185+
186+
When all hosts are exhausted without finding a matching server, the client emits an error.
187+
132188
## Connection URI
133189

134190
You can initialize both a pool and a client with a connection string URI as well. This is common in environments like Heroku where the database connection string is supplied to your application dyno through an environment variable. Connection string parsing brought to you by [pg-connection-string](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string).

packages/pg/lib/client.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class Client extends EventEmitter {
8181
keepAlive: c.keepAlive || false,
8282
keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0,
8383
encoding: this.connectionParameters.client_encoding || 'utf8',
84+
targetSessionAttrs: c.targetSessionAttrs || this.connectionParameters.targetSessionAttrs || null,
85+
trustParameterStatus: c.trustParameterStatus || false,
8486
})
8587
this._queryQueue = []
8688
this.binary = c.binary || defaults.binary
@@ -155,7 +157,7 @@ class Client extends EventEmitter {
155157
}
156158
}
157159

158-
if (this.host && this.host.indexOf('/') === 0) {
160+
if (!Array.isArray(this.host) && this.host && this.host.indexOf('/') === 0) {
159161
con.connect(this.host + '/.s.PGSQL.' + this.port)
160162
} else {
161163
con.connect(this.port, this.host)
@@ -542,7 +544,7 @@ class Client extends EventEmitter {
542544
if (client.activeQuery === query) {
543545
const con = this.connection
544546

545-
if (this.host && this.host.indexOf('/') === 0) {
547+
if (!Array.isArray(this.host) && this.host && this.host.indexOf('/') === 0) {
546548
con.connect(this.host + '/.s.PGSQL.' + this.port)
547549
} else {
548550
con.connect(this.port, this.host)

packages/pg/lib/connection-parameters.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,16 @@ class ConnectionParameters {
6767
this.database = this.user
6868
}
6969

70-
this.port = parseInt(val('port', config), 10)
70+
const rawPort = val('port', config)
71+
this.port = Array.isArray(rawPort) ? rawPort.map((p) => parseInt(p, 10)) : parseInt(rawPort, 10)
7172
this.host = val('host', config)
7273

74+
const hosts = Array.isArray(this.host) ? this.host : [this.host]
75+
const ports = Array.isArray(this.port) ? this.port : [this.port]
76+
if (ports.length !== 1 && ports.length !== hosts.length) {
77+
throw new Error(`ports must have either 1 entry or the same number of entries as hosts (${hosts.length})`)
78+
}
79+
7380
// "hiding" the password so it doesn't show up in stack traces
7481
// or if the client is console.logged
7582
Object.defineProperty(this, 'password', {
@@ -111,6 +118,17 @@ class ConnectionParameters {
111118
this.idle_in_transaction_session_timeout = val('idle_in_transaction_session_timeout', config, false)
112119
this.query_timeout = val('query_timeout', config, false)
113120

121+
this.targetSessionAttrs = val('targetSessionAttrs', config)
122+
123+
const validTargetSessionAttrs = ['any', 'read-write', 'read-only', 'primary', 'standby', 'prefer-standby']
124+
if (this.targetSessionAttrs && !validTargetSessionAttrs.includes(this.targetSessionAttrs)) {
125+
throw new Error(
126+
`invalid targetSessionAttrs value: "${this.targetSessionAttrs}". Must be one of: ${validTargetSessionAttrs.join(
127+
', '
128+
)}`
129+
)
130+
}
131+
114132
if (config.connectionTimeoutMillis === undefined) {
115133
this.connect_timeout = process.env.PGCONNECT_TIMEOUT || 0
116134
} else {

0 commit comments

Comments
 (0)