Skip to content

Commit 0903979

Browse files
committed
feat: support db read replicas
Signed-off-by: Ricardo Arturo Cabral Mejía <[email protected]>
1 parent b24b028 commit 0903979

File tree

13 files changed

+135
-60
lines changed

13 files changed

+135
-60
lines changed

CONFIGURATION.md

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,37 @@
44

55
The following environment variables can be set:
66

7-
| Name | Description | Default |
8-
|-----------------------|--------------------------------|------------------------|
9-
| RELAY_PORT | Relay's server port | 8008 |
10-
| WORKER_COUNT | Number of workers override | No. of available CPUs |
11-
| DB_HOST | PostgresSQL Hostname | |
12-
| DB_PORT | PostgreSQL Port | 5432 |
13-
| DB_USER | PostgreSQL Username | nostr_ts_relay |
14-
| DB_PASSWORD | PostgreSQL Password | nostr_ts_relay |
15-
| DB_NAME | PostgreSQL Database name | nostr_ts_relay |
16-
| DB_MIN_POOL_SIZE | Min. connections per worker | 16 |
17-
| DB_MAX_POOL_SIZE | Max. connections per worker | 32 |
18-
| DB_ACQUIRE_CONNECTION_TIMEOUT | New connection timeout (ms) | 60000 |
19-
| TOR_HOST | Tor Hostname | |
20-
| TOR_CONTROL_PORT | Tor control Port | 9051 |
21-
| TOR_PASSWORD | Tor control password | nostr_ts_relay |
22-
| HIDDEN_SERVICE_PORT | Tor hidden service port | 80 |
23-
| REDIS_HOST | | |
24-
| REDIS_PORT | Redis Port | 6379 |
25-
| REDIS_USER | Redis User | default |
26-
| REDIS_PASSWORD | Redis Password | nostr_ts_relay |
27-
| NOSTR_CONFIG_DIR | Configuration directory | <project_root>/.nostr/ |
28-
| DEBUG | Debugging filter | |
7+
| Name | Description | Default |
8+
|----------------------------------|--------------------------------|------------------------|
9+
| RELAY_PORT | Relay's server port | 8008 |
10+
| WORKER_COUNT | Number of workers override | No. of available CPUs |
11+
| DB_HOST | PostgresSQL Hostname | |
12+
| DB_PORT | PostgreSQL Port | 5432 |
13+
| DB_USER | PostgreSQL Username | nostr_ts_relay |
14+
| DB_PASSWORD | PostgreSQL Password | nostr_ts_relay |
15+
| DB_NAME | PostgreSQL Database name | nostr_ts_relay |
16+
| DB_MIN_POOL_SIZE | Min. connections per worker | 16 |
17+
| DB_MAX_POOL_SIZE | Max. connections per worker | 32 |
18+
| DB_ACQUIRE_CONNECTION_TIMEOUT | New connection timeout (ms) | 60000 |
19+
| READ_REPLICA_ENABLED | Read Replica (RR) Toggle | 'false' |
20+
| RR_DB_HOST | PostgresSQL Hostname (RR) | |
21+
| RR_DB_PORT | PostgreSQL Port (RR) | 5432 |
22+
| RR_DB_USER | PostgreSQL Username (RR) | nostr_ts_relay |
23+
| RR_DB_PASSWORD | PostgreSQL Password (RR) | nostr_ts_relay |
24+
| RR_DB_NAME | PostgreSQL Database name (RR) | nostr_ts_relay |
25+
| RR_DB_MIN_POOL_SIZE | Min. connections per worker (RR) | 16 |
26+
| RR_DB_MAX_POOL_SIZE | Max. connections per worker (RR) | 32 |
27+
| RR_DB_ACQUIRE_CONNECTION_TIMEOUT | New connection timeout (ms) (RR) | 60000 |
28+
| TOR_HOST | Tor Hostname | |
29+
| TOR_CONTROL_PORT | Tor control Port | 9051 |
30+
| TOR_PASSWORD | Tor control password | nostr_ts_relay |
31+
| HIDDEN_SERVICE_PORT | Tor hidden service port | 80 |
32+
| REDIS_HOST | | |
33+
| REDIS_PORT | Redis Port | 6379 |
34+
| REDIS_USER | Redis User | default |
35+
| REDIS_PASSWORD | Redis Password | nostr_ts_relay |
36+
| NOSTR_CONFIG_DIR | Configuration directory | <project_root>/.nostr/ |
37+
| DEBUG | Debugging filter | |
2938

3039
# Settings
3140

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ NIPs with a relay-specific implementation are listed here.
6363
## Requirements
6464

6565
### Standalone setup
66-
- PostgreSQL 15.0
66+
- PostgreSQL 14.0
6767
- Redis
6868
- Node v18
6969
- Typescript

docker-compose.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ services:
55
environment:
66
RELAY_PORT: 8008
77
NOSTR_CONFIG_DIR: /home/node/
8+
# Master
89
DB_HOST: db
910
DB_PORT: 5432
1011
DB_USER: nostr_ts_relay
@@ -13,6 +14,17 @@ services:
1314
DB_MIN_POOL_SIZE: 16
1415
DB_MAX_POOL_SIZE: 64
1516
DB_ACQUIRE_CONNECTION_TIMEOUT: 60000
17+
# Read Replica
18+
READ_REPLICA_ENABLED: 'false'
19+
RR_DB_HOST: db
20+
RR_DB_PORT: 5432
21+
RR_DB_USER: nostr_ts_relay
22+
RR_DB_PASSWORD: nostr_ts_relay
23+
RR_DB_NAME: nostr_ts_relay
24+
RR_DB_MIN_POOL_SIZE: 16
25+
RR_DB_MAX_POOL_SIZE: 64
26+
RR_DB_ACQUIRE_CONNECTION_TIMEOUT: 60000
27+
# Redis
1628
REDIS_HOST: cache
1729
REDIS_PORT: 6379
1830
REDIS_USER: default
@@ -42,7 +54,7 @@ services:
4254
default:
4355
ipv4_address: 10.10.10.2
4456
db:
45-
image: postgres:14
57+
image: postgres
4658
container_name: db
4759
environment:
4860
POSTGRES_DB: nostr_ts_relay

src/database/client.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import 'pg-query-stream'
33
import knex, { Knex } from 'knex'
44
import { createLogger } from '../factories/logger-factory'
55

6-
const debug = createLogger('database-client')
7-
8-
const createDbConfig = (): Knex.Config => ({
6+
const getMasterConfig = (): Knex.Config => ({
97
client: 'pg',
108
connection: {
119
host: process.env.DB_HOST,
@@ -28,14 +26,55 @@ const createDbConfig = (): Knex.Config => ({
2826
: 60000,
2927
})
3028

31-
let client: Knex
29+
const getReadReplicaConfig = (): Knex.Config => ({
30+
client: 'pg',
31+
connection: {
32+
host: process.env.RR_DB_HOST,
33+
port: Number(process.env.RR_DB_PORT),
34+
user: process.env.RR_DB_USER,
35+
password: process.env.RR_DB_PASSWORD,
36+
database: process.env.RR_DB_NAME,
37+
},
38+
pool: {
39+
min: process.env.RR_DB_MIN_POOL_SIZE ? Number(process.env.RR_DB_MIN_POOL_SIZE) : 0,
40+
max: process.env.RR_DB_MAX_POOL_SIZE ? Number(process.env.RR_DB_MAX_POOL_SIZE) : 3,
41+
idleTimeoutMillis: 60000,
42+
propagateCreateError: false,
43+
acquireTimeoutMillis: process.env.RR_DB_ACQUIRE_CONNECTION_TIMEOUT
44+
? Number(process.env.RR_DB_ACQUIRE_CONNECTION_TIMEOUT)
45+
: 60000,
46+
},
47+
acquireConnectionTimeout: process.env.RR_DB_ACQUIRE_CONNECTION_TIMEOUT
48+
? Number(process.env.RR_DB_ACQUIRE_CONNECTION_TIMEOUT)
49+
: 60000,
50+
})
51+
52+
let writeClient: Knex
53+
54+
export const getMasterDbClient = () => {
55+
const debug = createLogger('database-client:get-db-client')
56+
if (!writeClient) {
57+
const config = getMasterConfig()
58+
debug('config: %o', config)
59+
writeClient = knex(config)
60+
}
61+
62+
return writeClient
63+
}
64+
65+
let readClient: Knex
66+
67+
export const getReadReplicaDbClient = () => {
68+
if (process.env.READ_REPLICA_ENABLED !== 'true') {
69+
return getMasterDbClient()
70+
}
3271

33-
export const getDbClient = () => {
34-
if (!client) {
35-
const config = createDbConfig()
72+
const debug = createLogger('database-client:get-read-replica-db-client')
73+
if (!readClient) {
74+
const config = getReadReplicaConfig()
3675
debug('config: %o', config)
37-
client = knex(config)
76+
readClient = knex(config)
3877
}
3978

40-
return client
79+
return readClient
4180
}

src/factories/worker-factory.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ import http from 'http'
22
import process from 'process'
33
import { WebSocketServer } from 'ws'
44

5+
import { getMasterDbClient, getReadReplicaDbClient } from '../database/client'
56
import { AppWorker } from '../app/worker'
67
import { createSettings } from '../factories/settings-factory'
78
import { EventRepository } from '../repositories/event-repository'
8-
import { getDbClient } from '../database/client'
99
import { webSocketAdapterFactory } from './websocket-adapter-factory'
1010
import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter'
1111

1212
export const workerFactory = (): AppWorker => {
13-
const dbClient = getDbClient()
14-
const eventRepository = new EventRepository(dbClient)
13+
const dbClient = getMasterDbClient()
14+
const readReplicaDbClient = getReadReplicaDbClient()
15+
const eventRepository = new EventRepository(dbClient, readReplicaDbClient)
1516

1617
// deepcode ignore HttpToHttps: we use proxies
1718
const server = http.createServer()

src/repositories/event-repository.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,20 @@ const groupByLengthSpec = groupBy(
5353
const debug = createLogger('event-repository')
5454

5555
export class EventRepository implements IEventRepository {
56-
public constructor(private readonly dbClient: DatabaseClient) { }
56+
public constructor(
57+
private readonly masterDbClient: DatabaseClient,
58+
private readonly readReplicaDbClient: DatabaseClient,
59+
) { }
5760

5861
public findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]> {
5962
debug('querying for %o', filters)
6063
if (!Array.isArray(filters) || !filters.length) {
6164
throw new Error('Filters cannot be empty')
6265
}
6366
const queries = filters.map((currentFilter) => {
64-
const builder = this.dbClient<DBEvent>('events')
67+
const builder = this.readReplicaDbClient<DBEvent>('events')
6568

66-
forEachObjIndexed((tableFields: string[], filterName: string) => {
69+
forEachObjIndexed((tableFields: string[], filterName: string | number) => {
6770
builder.andWhere((bd) => {
6871
cond([
6972
[isEmpty, () => void bd.whereRaw('1 = 0')],
@@ -179,7 +182,7 @@ export class EventRepository implements IEventRepository {
179182
),
180183
})(event)
181184

182-
return this.dbClient('events')
185+
return this.masterDbClient('events')
183186
.insert(row)
184187
.onConflict()
185188
.ignore()
@@ -211,12 +214,12 @@ export class EventRepository implements IEventRepository {
211214
),
212215
})(event)
213216

214-
const query = this.dbClient('events')
217+
const query = this.masterDbClient('events')
215218
.insert(row)
216219
// NIP-16: Replaceable Events
217220
// NIP-33: Parameterized Replaceable Events
218221
.onConflict(
219-
this.dbClient.raw(
222+
this.masterDbClient.raw(
220223
'(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)'
221224
)
222225
)
@@ -233,7 +236,7 @@ export class EventRepository implements IEventRepository {
233236
public insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise<number> {
234237
debug('inserting stubs for %s: %o', pubkey, eventIdsToDelete)
235238
const date = new Date()
236-
return this.dbClient('events').insert(
239+
return this.masterDbClient('events').insert(
237240
eventIdsToDelete.map(
238241
applySpec({
239242
event_id: pipe(identity, toBuffer),
@@ -256,12 +259,12 @@ export class EventRepository implements IEventRepository {
256259
public deleteByPubkeyAndIds(pubkey: string, eventIdsToDelete: EventId[]): Promise<number> {
257260
debug('deleting events from %s: %o', pubkey, eventIdsToDelete)
258261

259-
return this.dbClient('events')
262+
return this.masterDbClient('events')
260263
.where('event_pubkey', toBuffer(pubkey))
261264
.whereIn('event_id', map(toBuffer)(eventIdsToDelete))
262265
.whereNull('deleted_at')
263266
.update({
264-
deleted_at: this.dbClient.raw('now()'),
267+
deleted_at: this.masterDbClient.raw('now()'),
265268
})
266269
}
267270
}

test/integration/features/shared.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { CacheClient } from '../../../src/@types/cache'
1818
import { DatabaseClient } from '../../../src/@types/base'
1919
import { Event } from '../../../src/@types/event'
2020
import { getCacheClient } from '../../../src/cache/client'
21-
import { getDbClient } from '../../../src/database/client'
21+
import { getMasterDbClient } from '../../../src/database/client'
2222
import { SettingsStatic } from '../../../src/utils/settings'
2323
import { workerFactory } from '../../../src/factories/worker-factory'
2424

@@ -34,7 +34,7 @@ export const streams = new WeakMap<WebSocket, Observable<unknown>>()
3434
BeforeAll({ timeout: 1000 }, async function () {
3535
process.env.RELAY_PORT = '18808'
3636
cacheClient = getCacheClient()
37-
dbClient = getDbClient()
37+
dbClient = getMasterDbClient()
3838
await dbClient.raw('SELECT 1=1')
3939
await cacheClient.connect()
4040
await cacheClient.ping()
@@ -72,7 +72,7 @@ After(async function () {
7272
}
7373
this.parameters.clients = {}
7474

75-
const dbClient = getDbClient()
75+
const dbClient = getMasterDbClient()
7676

7777
await dbClient('events')
7878
.where({

test/unit/factories/worker-factory.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import { AppWorker } from '../../../src/app/worker'
77
import { workerFactory } from '../../../src/factories/worker-factory'
88

99
describe('workerFactory', () => {
10-
let getDbClientStub: Sinon.SinonStub
10+
let getMasterDbClientStub: Sinon.SinonStub
11+
let getReadReplicaDbClientStub: Sinon.SinonStub
1112

1213
beforeEach(() => {
13-
getDbClientStub = Sinon.stub(databaseClientModule, 'getDbClient')
14+
getMasterDbClientStub = Sinon.stub(databaseClientModule, 'getMasterDbClient')
15+
getReadReplicaDbClientStub = Sinon.stub(databaseClientModule, 'getReadReplicaDbClient')
1416
})
1517

1618
afterEach(() => {
17-
getDbClientStub.restore()
19+
getMasterDbClientStub.restore()
20+
getReadReplicaDbClientStub.restore()
1821
})
1922

2023
it('returns an AppWorker', () => {

test/unit/handlers/event-strategies/default-event-strategy.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ describe('DefaultEventStrategy', () => {
3939
webSocket = {
4040
emit: webSocketEmitStub,
4141
} as any
42-
const client: DatabaseClient = {} as any
43-
eventRepository = new EventRepository(client)
42+
const masterClient: DatabaseClient = {} as any
43+
const readReplicaClient: DatabaseClient = {} as any
44+
eventRepository = new EventRepository(masterClient, readReplicaClient)
4445

4546
strategy = new DefaultEventStrategy(webSocket, eventRepository)
4647
})

test/unit/handlers/event-strategies/delete-event-strategy.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ describe('DeleteEventStrategy', () => {
4949
webSocket = {
5050
emit: webSocketEmitStub,
5151
} as any
52-
const client: DatabaseClient = {} as any
53-
eventRepository = new EventRepository(client)
52+
const masterClient: DatabaseClient = {} as any
53+
const readReplicaClient: DatabaseClient = {} as any
54+
eventRepository = new EventRepository(masterClient, readReplicaClient)
5455

5556
strategy = new DeleteEventStrategy(webSocket, eventRepository)
5657
})

0 commit comments

Comments
 (0)