├── .gitignore ├── .env ├── setup-otel.sh ├── apps ├── publisher │ ├── publisher.env │ └── index.js ├── subscriber │ ├── subscriber.env │ ├── transactions.schema.sql │ ├── save-transaction.js │ ├── get-transaction.js │ └── index.js └── instrumentation.js ├── packages ├── event-store │ ├── index.js │ ├── streams.js │ ├── event-store.schema.sql │ ├── event-store.js │ └── subscriptions │ │ ├── subscription.js │ │ └── stream.js └── db │ ├── index.js │ ├── query-runner.js │ ├── pool.js │ └── client.js ├── .idea └── .gitignore ├── setup-db.sh ├── docker ├── docker-entrypoint-initdb.d │ ├── transactions.schema.sql │ └── event-store.schema.sql └── pg.conf ├── package.json └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=localhost 2 | POSTGRES_PORT=5432 3 | POSTGRES_USER=postgres 4 | POSTGRES_PASSWORD=postgres 5 | POSTGRES_DB=test_app -------------------------------------------------------------------------------- /setup-otel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -p 3000:3000 \ 4 | -p 4317:4317 \ 5 | -p 4318:4318 \ 6 | --rm -ti grafana/otel-lgtm 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/publisher/publisher.env: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=localhost 2 | POSTGRES_PORT=5432 3 | POSTGRES_USER=postgres 4 | POSTGRES_PASSWORD=postgres 5 | POSTGRES_DB=test_app 6 | SERVICE_NAME=publisher -------------------------------------------------------------------------------- /apps/subscriber/subscriber.env: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=localhost 2 | POSTGRES_PORT=5432 3 | POSTGRES_USER=postgres 4 | POSTGRES_PASSWORD=postgres 5 | POSTGRES_DB=test_app 6 | SERVICE_NAME=subscriber -------------------------------------------------------------------------------- /packages/event-store/index.js: -------------------------------------------------------------------------------- 1 | export { createEventStore } from './event-store.js'; 2 | export { createStreamReducer } from './streams.js'; 3 | export { createSubscription } from './subscriptions/subscription.js'; 4 | -------------------------------------------------------------------------------- /packages/db/index.js: -------------------------------------------------------------------------------- 1 | export { createPool } from './pool.js'; 2 | export { createClient } from './client.js'; 3 | export { createQueryRunner } from './query-runner.js'; 4 | export { SQL } from 'sql-template-strings'; 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /packages/db/query-runner.js: -------------------------------------------------------------------------------- 1 | export const createQueryRunner = ({ client }) => { 2 | return { 3 | async query(query) { 4 | const { rows } = await client.query(query); 5 | return rows; 6 | }, 7 | async one(query) { 8 | const { rows } = await client.query(query); 9 | return rows.at(0); 10 | }, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /setup-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run db:copy-schema 4 | 5 | docker run --rm \ 6 | --name local_test \ 7 | --env-file .env \ 8 | -p 5432:5432 \ 9 | -v "$PWD/docker/docker-entrypoint-initdb.d":/docker-entrypoint-initdb.d \ 10 | -v "$PWD/docker/pg.conf":/etc/postgresql/postgresql.conf \ 11 | postgres \ 12 | -c 'config_file=/etc/postgresql/postgresql.conf' 13 | 14 | -------------------------------------------------------------------------------- /packages/event-store/streams.js: -------------------------------------------------------------------------------- 1 | const identity = (x) => x; 2 | 3 | export const createStreamReducer = 4 | ({ eventHandlers }) => 5 | ({ events = [] }) => { 6 | return events.reduce( 7 | (aggregate, event) => { 8 | const handler = eventHandlers[event.type] ?? identity; 9 | return handler(aggregate, event); 10 | }, 11 | { version: 0 }, 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/subscriber/transactions.schema.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM pg_create_logical_replication_slot('test_slot', 'pgoutput'); 3 | 4 | CREATE UNLOGGED TABLE IF NOT EXISTS transactions 5 | ( 6 | transaction_id varchar PRIMARY KEY, 7 | amount FLOAT NOT NULL, 8 | status TEXT NOT NULL, 9 | date TIMESTAMP WITH TIME ZONE NOT NULL, 10 | version INTEGER NOT NULL, 11 | authority TEXT 12 | ); 13 | -------------------------------------------------------------------------------- /docker/docker-entrypoint-initdb.d/transactions.schema.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM pg_create_logical_replication_slot('test_slot', 'pgoutput'); 3 | 4 | CREATE UNLOGGED TABLE IF NOT EXISTS transactions 5 | ( 6 | transaction_id varchar PRIMARY KEY, 7 | amount FLOAT NOT NULL, 8 | status TEXT NOT NULL, 9 | date TIMESTAMP WITH TIME ZONE NOT NULL, 10 | version INTEGER NOT NULL, 11 | authority TEXT 12 | ); 13 | -------------------------------------------------------------------------------- /packages/event-store/event-store.schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS transaction_events ( 2 | position BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, 3 | event_type VARCHAR NOT NULL, 4 | transaction_id varchar NOT NULL, 5 | payload JSONB, 6 | version INTEGER NOT NULL DEFAULT 1, 7 | created_at timestamp with time zone NOT NULL DEFAULT NOW(), 8 | UNIQUE (transaction_id, version) 9 | ); 10 | 11 | CREATE PUBLICATION test_publication FOR TABLE ONLY transaction_events WITH(publish='insert'); 12 | -------------------------------------------------------------------------------- /docker/docker-entrypoint-initdb.d/event-store.schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS transaction_events ( 2 | position BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, 3 | event_type VARCHAR NOT NULL, 4 | transaction_id varchar NOT NULL, 5 | payload JSONB, 6 | version INTEGER NOT NULL DEFAULT 1, 7 | created_at timestamp with time zone NOT NULL DEFAULT NOW(), 8 | UNIQUE (transaction_id, version) 9 | ); 10 | 11 | CREATE PUBLICATION test_publication FOR TABLE ONLY transaction_events WITH(publish='insert'); 12 | -------------------------------------------------------------------------------- /packages/db/pool.js: -------------------------------------------------------------------------------- 1 | import { createClient } from './client.js'; 2 | import { createQueryRunner } from './query-runner.js'; 3 | import pg from 'pg'; 4 | 5 | export const createPool = ({ signal, ...config }) => { 6 | const pool = new pg.Pool(config); 7 | 8 | signal.addEventListener( 9 | 'abort', 10 | () => { 11 | pool.end(); 12 | }, 13 | { once: true }, 14 | ); 15 | 16 | const queryRunner = createQueryRunner({ client: pool }); 17 | 18 | return { 19 | ...queryRunner, 20 | async withinTransaction(handlerOrOptions) { 21 | const pgClient = await pool.connect(); 22 | const db = createClient({ client: pgClient }); 23 | try { 24 | return await db.withinTransaction(handlerOrOptions); 25 | } finally { 26 | pgClient.release(); 27 | } 28 | }, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /apps/instrumentation.js: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import opentelemetry from '@opentelemetry/api'; 3 | import { 4 | PeriodicExportingMetricReader, 5 | MeterProvider, 6 | } from '@opentelemetry/sdk-metrics'; 7 | import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc'; 8 | import { Resource } from '@opentelemetry/resources'; 9 | import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; 10 | 11 | const resource = Resource.default().merge( 12 | new Resource({ 13 | [ATTR_SERVICE_NAME]: env.SERVICE_NAME, 14 | }), 15 | ); 16 | 17 | const metricReader = new PeriodicExportingMetricReader({ 18 | exporter: new OTLPMetricExporter(), 19 | // Default is 60000ms (60 seconds). Set to 10 seconds for demonstrative purposes only. 20 | exportIntervalMillis: 30_000, 21 | }); 22 | 23 | const myServiceMeterProvider = new MeterProvider({ 24 | resource: resource, 25 | readers: [metricReader], 26 | }); 27 | 28 | // Set this MeterProvider to be global to the app being instrumented. 29 | opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubsub-pg", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "license": "ISC", 7 | "author": "", 8 | "type": "module", 9 | "prettier": { 10 | "singleQuote": true 11 | }, 12 | "scripts": { 13 | "db:copy-schema": "cp **/**/*.schema.sql docker/docker-entrypoint-initdb.d/", 14 | "start:subscriber": "node --env-file='apps/subscriber/subscriber.env' ./apps/subscriber/index.js", 15 | "start:publisher": "node --env-file='apps/publisher/publisher.env' apps/publisher/index.js" 16 | }, 17 | "devDependencies": { 18 | "prettier": "^3.5.0" 19 | }, 20 | "dependencies": { 21 | "@faker-js/faker": "^9.5.0", 22 | "@opentelemetry/api": "^1.9.0", 23 | "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.1", 24 | "@opentelemetry/resources": "^1.30.1", 25 | "@opentelemetry/sdk-metrics": "^1.30.1", 26 | "@opentelemetry/semantic-conventions": "^1.30.0", 27 | "nanoid": "^5.1.0", 28 | "pg": "^8.13.2", 29 | "pg-logical-replication": "^2.0.7", 30 | "sql-template-strings": "^2.2.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/subscriber/save-transaction.js: -------------------------------------------------------------------------------- 1 | import { SQL } from '../../packages/db/index.js'; 2 | 3 | export const createSaveTransaction = 4 | ({ db }) => 5 | ({ transaction }) => { 6 | const { 7 | transactionId, 8 | status, 9 | version, 10 | date, 11 | authority = null, 12 | amount, 13 | } = transaction; 14 | return db.query(SQL` 15 | MERGE INTO transactions t 16 | USING (VALUES (${transactionId}, ${status}, ${version}::integer, ${authority}, ${date}::timestamp, ${amount}::float)) AS source(transaction_id, status, version, authority, date, amount) 17 | ON source.transaction_id = t.transaction_id 18 | WHEN MATCHED AND source.version > t.version THEN 19 | UPDATE SET 20 | version = source.version, 21 | status = source.status, 22 | authority = source.authority, 23 | date = source.date, 24 | amount = source.amount 25 | WHEN NOT MATCHED THEN 26 | INSERT (transaction_id, status, version, authority, date, amount) 27 | VALUES (source.transaction_id, source.status, source.version, source.authority, source.date, source.amount) 28 | `); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/subscriber/get-transaction.js: -------------------------------------------------------------------------------- 1 | import { createStreamReducer } from '../../packages/event-store/index.js'; 2 | 3 | const transactionFromEvents = createStreamReducer({ 4 | eventHandlers: { 5 | initiated(_, event) { 6 | return { 7 | transactionId: event.transactionId, 8 | status: 'initiated', 9 | amount: event.payload.amount, 10 | date: event.payload.date, 11 | version: 1, 12 | }; 13 | }, 14 | rejected(transaction, event) { 15 | return { 16 | ...transaction, 17 | status: 'rejected', 18 | authority: event.payload.authority, 19 | version: event.version, 20 | }; 21 | }, 22 | authorized(transaction, event) { 23 | return { 24 | ...transaction, 25 | status: 'authorized', 26 | authority: event.payload.authority, 27 | version: event.version, 28 | }; 29 | }, 30 | }, 31 | }); 32 | 33 | export const createGetTransaction = 34 | ({ eventStore }) => 35 | async ({ transactionId }) => { 36 | const events = await eventStore.getStream({ transactionId }); 37 | return transactionFromEvents({ events }); 38 | }; 39 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Pub/Sub with Postgres logical replication and NodeJS 2 | 3 | This repository shows how to set up a simple yet robust pub/sub system on top of [postgres logical replication](https://www.postgresql.org/docs/current/logical-replication.html) 4 | It was built for writing [this blog post](https://lorenzofox.dev/posts/pub-sub-pg-logical-replication/) 5 | 6 | ## Installation 7 | 8 | with NodeJs installed (version > 22 ); 9 | 10 | ```shell 11 | npm i 12 | ``` 13 | 14 | ### database 15 | 16 | Start the database with the proper configuration and publication: 17 | 18 | ```shell 19 | source setup-db.sh 20 | ``` 21 | 22 | ### telemetry 23 | 24 | You can also start an [Open Telemetry Stack](https://github.com/grafana/docker-otel-lgtm) to have some metrics and check how behaves your system 25 | 26 | ```shell 27 | source setup-otel.sh 28 | ``` 29 | 30 | ### subscriber process 31 | 32 | Modify the [subscription program](./apps/subscriber) and start the process 33 | 34 | ```shell 35 | npm run start:subscriber 36 | ``` 37 | 38 | ### publisher process 39 | 40 | Modify the [publisher program](./apps/publisher) and start the process 41 | 42 | ```shell 43 | npm run start:subscriber 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /packages/event-store/event-store.js: -------------------------------------------------------------------------------- 1 | import { SQL } from '../db/index.js'; 2 | import assert from 'node:assert'; 3 | 4 | export const createEventStore = ({ db }) => { 5 | return { 6 | getStream({ transactionId }) { 7 | return db.query( 8 | SQL`SELECT position, 9 | event_type as type, 10 | transaction_id as "transactionId", 11 | payload, 12 | version 13 | FROM transaction_events 14 | WHERE transaction_id = ${transactionId} 15 | ORDER BY version`, 16 | ); 17 | }, 18 | appendEvent({ events }) { 19 | assert(events.length > 0, 'at least one event should be provided'); 20 | 21 | const query = SQL`INSERT INTO transaction_events(event_type, transaction_id, payload, version) VALUES `; 22 | 23 | for (const { 24 | type, 25 | transactionId, 26 | payload = null, 27 | version = 1, 28 | } of events) { 29 | assert(type, 'type is required'); 30 | assert(transactionId, 'transactionId is required'); 31 | query.append(SQL`(${type}, ${transactionId}, ${payload}, ${version})`); 32 | } 33 | 34 | query.append(SQL`returning position`); 35 | 36 | return db.query(query); 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/db/client.js: -------------------------------------------------------------------------------- 1 | import { createQueryRunner } from './query-runner.js'; 2 | 3 | const getTransactionParameters = (handlerOrOptions) => { 4 | return typeof handlerOrOptions === 'function' 5 | ? { 6 | fn: handlerOrOptions, 7 | transactionIsolationLevel: 'READ COMMITTED', 8 | } 9 | : handlerOrOptions; 10 | }; 11 | 12 | export const createClient = ({ client }) => { 13 | const queryRunner = createQueryRunner({ client }); 14 | return { 15 | ...queryRunner, 16 | async withinTransaction(handlerOrOptions) { 17 | const { transactionIsolationLevel, fn } = 18 | getTransactionParameters(handlerOrOptions); 19 | try { 20 | await queryRunner.query( 21 | `BEGIN TRANSACTION ISOLATION LEVEL ${transactionIsolationLevel};`, 22 | ); 23 | const db = { 24 | ...queryRunner, 25 | withinTransaction(handlerOrOptions) { 26 | const { fn } = getTransactionParameters(handlerOrOptions); 27 | return fn({ db }); 28 | }, 29 | }; 30 | 31 | const result = await fn({ 32 | db, 33 | }); 34 | 35 | await queryRunner.query('COMMIT'); 36 | 37 | return result; 38 | } catch (error) { 39 | await queryRunner.query('ROLLBACK;'); 40 | throw error; 41 | } 42 | }, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/event-store/subscriptions/subscription.js: -------------------------------------------------------------------------------- 1 | import { LogicalReplicationStream } from './stream.js'; 2 | 3 | export const createSubscription = ({ 4 | subscriptionName, 5 | clientConfig, 6 | handler, 7 | }) => { 8 | const stream = new LogicalReplicationStream({ 9 | subscriptionName, 10 | publicationName: 'test_publication', 11 | slotName: 'test_slot', 12 | clientConfig, 13 | }); 14 | 15 | return { 16 | async listen() { 17 | for await (const transaction of groupByTransaction(stream)) { 18 | try { 19 | await handler({ transaction }); 20 | await stream.acknowledge(transaction.commitEndLsn); 21 | } catch (err) { 22 | console.error(err); 23 | } 24 | } 25 | }, 26 | }; 27 | }; 28 | 29 | async function* groupByTransaction(stream) { 30 | let currentTransaction = {}; 31 | for await (const { message } of stream) { 32 | if (message.tag === 'begin') { 33 | currentTransaction = { 34 | commitLsn: message.commitLsn, 35 | events: [], 36 | xid: message.xid, 37 | }; 38 | } 39 | 40 | if (message.tag === 'insert') { 41 | currentTransaction.events.push(message.new); 42 | } 43 | 44 | if (message.tag === 'commit') { 45 | const replicationLagMs = Date.now() - Number(message.commitTime / 1000n); 46 | currentTransaction.commitEndLsn = message.commitEndLsn; 47 | currentTransaction.replicationLagMs = replicationLagMs; 48 | yield currentTransaction; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/publisher/index.js: -------------------------------------------------------------------------------- 1 | import '../instrumentation.js'; 2 | import { env } from 'node:process'; 3 | import { setTimeout } from 'node:timers/promises'; 4 | import { createPool } from '../../packages/db/index.js'; 5 | import { createEventStore } from '../../packages/event-store/index.js'; 6 | import { nanoid } from 'nanoid'; 7 | import { faker } from '@faker-js/faker'; 8 | import opentelemetry from '@opentelemetry/api'; 9 | 10 | const { 11 | POSTGRES_HOST, 12 | POSTGRES_DB, 13 | POSTGRES_PASSWORD, 14 | POSTGRES_USER, 15 | POSTGRES_PORT, 16 | } = env; 17 | 18 | const clientConfig = { 19 | host: POSTGRES_HOST, 20 | port: Number(POSTGRES_PORT), 21 | password: POSTGRES_PASSWORD, 22 | database: POSTGRES_DB, 23 | user: POSTGRES_USER, 24 | }; 25 | 26 | const publisherMeter = opentelemetry.metrics.getMeter('publisher'); 27 | 28 | const eventCounter = publisherMeter.createCounter('publisher.events.counter'); 29 | 30 | (async () => { 31 | const signal = AbortSignal.timeout(1_000 * 60 * 20); 32 | const db = createPool({ 33 | ...clientConfig, 34 | signal, 35 | }); 36 | const eventStore = createEventStore({ db }); 37 | 38 | while (!signal.aborted) { 39 | await eventStore.appendEvent({ 40 | events: [ 41 | { 42 | type: 'initiated', 43 | version: 1, 44 | transactionId: nanoid(), 45 | payload: { 46 | amount: faker.finance.amount(), 47 | date: faker.date.recent(), 48 | }, 49 | }, 50 | ], 51 | }); 52 | await setTimeout(Math.ceil(Math.random() * 10)); 53 | eventCounter.add(1); 54 | } 55 | })(); 56 | -------------------------------------------------------------------------------- /packages/event-store/subscriptions/stream.js: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream'; 2 | import { 3 | LogicalReplicationService, 4 | PgoutputPlugin, 5 | } from 'pg-logical-replication'; 6 | 7 | export class LogicalReplicationStream extends Readable { 8 | #source; 9 | #lastLsn; 10 | #decoder; 11 | #slotName; 12 | 13 | constructor( 14 | { 15 | highWaterMark = 200, 16 | publicationName, 17 | clientConfig, 18 | slotName, 19 | onError = console.error, 20 | } = { 21 | highWaterMark: 200, 22 | }, 23 | ) { 24 | super({ highWaterMark, objectMode: true }); 25 | 26 | const replicationService = (this.#source = new LogicalReplicationService( 27 | clientConfig, 28 | { 29 | acknowledge: { auto: false, timeoutSeconds: 0 }, 30 | }, 31 | )); 32 | 33 | this.#slotName = slotName; 34 | this.#decoder = new PgoutputPlugin({ 35 | protoVersion: 4, 36 | binary: true, 37 | publicationNames: [publicationName], 38 | }); 39 | 40 | this.#source.on('data', async (lsn, message) => { 41 | if (!this.push({ lsn, message })) { 42 | await replicationService.stop(); 43 | } 44 | }); 45 | 46 | this.#source.on('error', onError); 47 | 48 | this.#source.on('heartbeat', async (lsn, timestamp, shouldRespond) => { 49 | if (shouldRespond) await replicationService.acknowledge(lsn); 50 | }); 51 | } 52 | 53 | _read() { 54 | if (this.#source.isStop()) { 55 | this.#source.subscribe(this.#decoder, this.#slotName, this.#lastLsn); 56 | } 57 | } 58 | 59 | async acknowledge(lsn) { 60 | this.#lastLsn = lsn; 61 | await this.#source.acknowledge(lsn); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/subscriber/index.js: -------------------------------------------------------------------------------- 1 | import '../instrumentation.js'; 2 | import { env } from 'node:process'; 3 | import { createPool } from '../../packages/db/index.js'; 4 | import opentelemetry, { ValueType } from '@opentelemetry/api'; 5 | import { 6 | createEventStore, 7 | createSubscription, 8 | } from '../../packages/event-store/index.js'; 9 | import { createGetTransaction } from './get-transaction.js'; 10 | import { createSaveTransaction } from './save-transaction.js'; 11 | 12 | const { 13 | POSTGRES_HOST, 14 | POSTGRES_DB, 15 | POSTGRES_PASSWORD, 16 | POSTGRES_USER, 17 | POSTGRES_PORT, 18 | } = env; 19 | 20 | const clientConfig = { 21 | host: POSTGRES_HOST, 22 | port: Number(POSTGRES_PORT), 23 | password: POSTGRES_PASSWORD, 24 | database: POSTGRES_DB, 25 | user: POSTGRES_USER, 26 | }; 27 | 28 | const subscriptionMeter = opentelemetry.metrics.getMeter('subscription'); 29 | 30 | const lag = subscriptionMeter.createHistogram('subscription.events.lag', { 31 | description: 32 | 'duration between the commit time of a transaction and the processing by the subscription', 33 | unit: 'ms', 34 | valueType: ValueType.INT, 35 | }); 36 | 37 | (async () => { 38 | const signal = AbortSignal.timeout(1_000 * 60 * 25); 39 | const db = createPool({ 40 | ...clientConfig, 41 | signal, 42 | }); 43 | const eventStore = createEventStore({ db }); 44 | 45 | const getTransaction = createGetTransaction({ eventStore }); 46 | const saveTransaction = createSaveTransaction({ db }); 47 | 48 | const subscription = createSubscription({ 49 | subscriptionName: 'test_slot', 50 | clientConfig, 51 | handler: transactionHandler, 52 | }); 53 | 54 | await subscription.listen(); 55 | 56 | async function transactionHandler({ transaction: dbTransaction }) { 57 | const { events } = dbTransaction; 58 | await Promise.all( 59 | events.map(({ transaction_id: transactionId }) => 60 | getTransaction({ 61 | transactionId, 62 | }).then((transaction) => saveTransaction({ transaction })), 63 | ), 64 | ); 65 | 66 | lag.record(dbTransaction.replicationLagMs); 67 | } 68 | })(); 69 | -------------------------------------------------------------------------------- /docker/pg.conf: -------------------------------------------------------------------------------- 1 | # ----------------------------- 2 | # PostgreSQL configuration file 3 | # ----------------------------- 4 | # 5 | # This file consists of lines of the form: 6 | # 7 | # name = value 8 | # 9 | # (The "=" is optional.) Whitespace may be used. Comments are introduced with 10 | # "#" anywhere on a line. The complete list of parameter names and allowed 11 | # values can be found in the PostgreSQL documentation. 12 | # 13 | # The commented-out settings shown in this file represent the default values. 14 | # Re-commenting a setting is NOT sufficient to revert it to the default value; 15 | # you need to reload the server. 16 | # 17 | # This file is read on server startup and when the server receives a SIGHUP 18 | # signal. If you edit the file on a running system, you have to SIGHUP the 19 | # server for the changes to take effect, run "pg_ctl reload", or execute 20 | # "SELECT pg_reload_conf()". Some parameters, which are marked below, 21 | # require a server shutdown and restart to take effect. 22 | # 23 | # Any parameter can also be given as a command-line option to the server, e.g., 24 | # "postgres -c log_connections=on". Some parameters can be changed at run time 25 | # with the "SET" SQL command. 26 | # 27 | # Memory units: B = bytes Time units: us = microseconds 28 | # kB = kilobytes ms = milliseconds 29 | # MB = megabytes s = seconds 30 | # GB = gigabytes min = minutes 31 | # TB = terabytes h = hours 32 | # d = days 33 | 34 | 35 | #------------------------------------------------------------------------------ 36 | # FILE LOCATIONS 37 | #------------------------------------------------------------------------------ 38 | 39 | # The default values of these variables are driven from the -D command-line 40 | # option or PGDATA environment variable, represented here as ConfigDir. 41 | 42 | #data_directory = 'ConfigDir' # use data in another directory 43 | # (change requires restart) 44 | #hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file 45 | # (change requires restart) 46 | #ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file 47 | # (change requires restart) 48 | 49 | # If external_pid_file is not explicitly set, no extra PID file is written. 50 | #external_pid_file = '' # write an extra PID file 51 | # (change requires restart) 52 | 53 | 54 | #------------------------------------------------------------------------------ 55 | # CONNECTIONS AND AUTHENTICATION 56 | #------------------------------------------------------------------------------ 57 | 58 | # - Connection Settings - 59 | 60 | listen_addresses = '*' 61 | # comma-separated list of addresses; 62 | # defaults to 'localhost'; use '*' for all 63 | # (change requires restart) 64 | #port = 5432 # (change requires restart) 65 | #max_connections = 100 # (change requires restart) 66 | #reserved_connections = 0 # (change requires restart) 67 | #superuser_reserved_connections = 3 # (change requires restart) 68 | #unix_socket_directories = '/tmp' # comma-separated list of directories 69 | # (change requires restart) 70 | #unix_socket_group = '' # (change requires restart) 71 | #unix_socket_permissions = 0777 # begin with 0 to use octal notation 72 | # (change requires restart) 73 | #bonjour = off # advertise server via Bonjour 74 | # (change requires restart) 75 | #bonjour_name = '' # defaults to the computer name 76 | # (change requires restart) 77 | 78 | # - TCP settings - 79 | # see "man tcp" for details 80 | 81 | #tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; 82 | # 0 selects the system default 83 | #tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; 84 | # 0 selects the system default 85 | #tcp_keepalives_count = 0 # TCP_KEEPCNT; 86 | # 0 selects the system default 87 | #tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; 88 | # 0 selects the system default 89 | 90 | #client_connection_check_interval = 0 # time between checks for client 91 | # disconnection while running queries; 92 | # 0 for never 93 | 94 | # - Authentication - 95 | 96 | #authentication_timeout = 1min # 1s-600s 97 | #password_encryption = scram-sha-256 # scram-sha-256 or md5 98 | #scram_iterations = 4096 99 | 100 | # GSSAPI using Kerberos 101 | #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab' 102 | #krb_caseins_users = off 103 | #gss_accept_delegation = off 104 | 105 | # - SSL - 106 | 107 | #ssl = off 108 | #ssl_ca_file = '' 109 | #ssl_cert_file = 'server.crt' 110 | #ssl_crl_file = '' 111 | #ssl_crl_dir = '' 112 | #ssl_key_file = 'server.key' 113 | #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers 114 | #ssl_prefer_server_ciphers = on 115 | #ssl_ecdh_curve = 'prime256v1' 116 | #ssl_min_protocol_version = 'TLSv1.2' 117 | #ssl_max_protocol_version = '' 118 | #ssl_dh_params_file = '' 119 | #ssl_passphrase_command = '' 120 | #ssl_passphrase_command_supports_reload = off 121 | 122 | 123 | #------------------------------------------------------------------------------ 124 | # RESOURCE USAGE (except WAL) 125 | #------------------------------------------------------------------------------ 126 | 127 | # - Memory - 128 | 129 | #shared_buffers = 128MB # min 128kB 130 | # (change requires restart) 131 | #huge_pages = try # on, off, or try 132 | # (change requires restart) 133 | #huge_page_size = 0 # zero for system default 134 | # (change requires restart) 135 | #temp_buffers = 8MB # min 800kB 136 | #max_prepared_transactions = 0 # zero disables the feature 137 | # (change requires restart) 138 | # Caution: it is not advisable to set max_prepared_transactions nonzero unless 139 | # you actively intend to use prepared transactions. 140 | #work_mem = 4MB # min 64kB 141 | #hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem 142 | #maintenance_work_mem = 64MB # min 64kB 143 | #autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem 144 | #logical_decoding_work_mem = 64MB # min 64kB 145 | #max_stack_depth = 2MB # min 100kB 146 | #shared_memory_type = mmap # the default is the first option 147 | # supported by the operating system: 148 | # mmap 149 | # sysv 150 | # windows 151 | # (change requires restart) 152 | #dynamic_shared_memory_type = posix # the default is usually the first option 153 | # supported by the operating system: 154 | # posix 155 | # sysv 156 | # windows 157 | # mmap 158 | # (change requires restart) 159 | #min_dynamic_shared_memory = 0MB # (change requires restart) 160 | #vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring; 161 | # 0 to disable vacuum buffer access strategy; 162 | # range 128kB to 16GB 163 | 164 | # SLRU buffers (change requires restart) 165 | #commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto) 166 | #multixact_offset_buffers = 16 # memory for pg_multixact/offsets 167 | #multixact_member_buffers = 32 # memory for pg_multixact/members 168 | #notify_buffers = 16 # memory for pg_notify 169 | #serializable_buffers = 32 # memory for pg_serial 170 | #subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto) 171 | #transaction_buffers = 0 # memory for pg_xact (0 = auto) 172 | 173 | # - Disk - 174 | 175 | #temp_file_limit = -1 # limits per-process temp file space 176 | # in kilobytes, or -1 for no limit 177 | 178 | #max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated 179 | # for NOTIFY / LISTEN queue 180 | 181 | # - Kernel Resources - 182 | 183 | #max_files_per_process = 1000 # min 64 184 | # (change requires restart) 185 | 186 | # - Cost-Based Vacuum Delay - 187 | 188 | #vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) 189 | #vacuum_cost_page_hit = 1 # 0-10000 credits 190 | #vacuum_cost_page_miss = 2 # 0-10000 credits 191 | #vacuum_cost_page_dirty = 20 # 0-10000 credits 192 | #vacuum_cost_limit = 200 # 1-10000 credits 193 | 194 | # - Background Writer - 195 | 196 | #bgwriter_delay = 200ms # 10-10000ms between rounds 197 | #bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables 198 | #bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round 199 | #bgwriter_flush_after = 0 # measured in pages, 0 disables 200 | 201 | # - Asynchronous Behavior - 202 | 203 | #backend_flush_after = 0 # measured in pages, 0 disables 204 | #effective_io_concurrency = 1 # 1-1000; 0 disables prefetching 205 | #maintenance_io_concurrency = 10 # 1-1000; 0 disables prefetching 206 | #io_combine_limit = 128kB # usually 1-32 blocks (depends on OS) 207 | #max_worker_processes = 8 # (change requires restart) 208 | #max_parallel_workers_per_gather = 2 # limited by max_parallel_workers 209 | #max_parallel_maintenance_workers = 2 # limited by max_parallel_workers 210 | #max_parallel_workers = 8 # number of max_worker_processes that 211 | # can be used in parallel operations 212 | #parallel_leader_participation = on 213 | 214 | 215 | #------------------------------------------------------------------------------ 216 | # WRITE-AHEAD LOG 217 | #------------------------------------------------------------------------------ 218 | 219 | # - Settings - 220 | 221 | wal_level = logical # minimal, replica, or logical 222 | # (change requires restart) 223 | #fsync = on # flush data to disk for crash safety 224 | # (turning this off can cause 225 | # unrecoverable data corruption) 226 | #synchronous_commit = on # synchronization level; 227 | # off, local, remote_write, remote_apply, or on 228 | #wal_sync_method = fsync # the default is the first option 229 | # supported by the operating system: 230 | # open_datasync 231 | # fdatasync (default on Linux and FreeBSD) 232 | # fsync 233 | # fsync_writethrough 234 | # open_sync 235 | #full_page_writes = on # recover from partial page writes 236 | #wal_log_hints = off # also do full page writes of non-critical updates 237 | # (change requires restart) 238 | #wal_compression = off # enables compression of full-page writes; 239 | # off, pglz, lz4, zstd, or on 240 | #wal_init_zero = on # zero-fill new WAL files 241 | #wal_recycle = on # recycle WAL files 242 | #wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers 243 | # (change requires restart) 244 | #wal_writer_delay = 200ms # 1-10000 milliseconds 245 | #wal_writer_flush_after = 1MB # measured in pages, 0 disables 246 | #wal_skip_threshold = 2MB 247 | 248 | #commit_delay = 0 # range 0-100000, in microseconds 249 | #commit_siblings = 5 # range 1-1000 250 | 251 | # - Checkpoints - 252 | 253 | #checkpoint_timeout = 5min # range 30s-1d 254 | #checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0 255 | #checkpoint_flush_after = 0 # measured in pages, 0 disables 256 | #checkpoint_warning = 30s # 0 disables 257 | #max_wal_size = 1GB 258 | #min_wal_size = 80MB 259 | 260 | # - Prefetching during recovery - 261 | 262 | #recovery_prefetch = try # prefetch pages referenced in the WAL? 263 | #wal_decode_buffer_size = 512kB # lookahead window used for prefetching 264 | # (change requires restart) 265 | 266 | # - Archiving - 267 | 268 | #archive_mode = off # enables archiving; off, on, or always 269 | # (change requires restart) 270 | #archive_library = '' # library to use to archive a WAL file 271 | # (empty string indicates archive_command should 272 | # be used) 273 | #archive_command = '' # command to use to archive a WAL file 274 | # placeholders: %p = path of file to archive 275 | # %f = file name only 276 | # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' 277 | #archive_timeout = 0 # force a WAL file switch after this 278 | # number of seconds; 0 disables 279 | 280 | # - Archive Recovery - 281 | 282 | # These are only used in recovery mode. 283 | 284 | #restore_command = '' # command to use to restore an archived WAL file 285 | # placeholders: %p = path of file to restore 286 | # %f = file name only 287 | # e.g. 'cp /mnt/server/archivedir/%f %p' 288 | #archive_cleanup_command = '' # command to execute at every restartpoint 289 | #recovery_end_command = '' # command to execute at completion of recovery 290 | 291 | # - Recovery Target - 292 | 293 | # Set these only when performing a targeted recovery. 294 | 295 | #recovery_target = '' # 'immediate' to end recovery as soon as a 296 | # consistent state is reached 297 | # (change requires restart) 298 | #recovery_target_name = '' # the named restore point to which recovery will proceed 299 | # (change requires restart) 300 | #recovery_target_time = '' # the time stamp up to which recovery will proceed 301 | # (change requires restart) 302 | #recovery_target_xid = '' # the transaction ID up to which recovery will proceed 303 | # (change requires restart) 304 | #recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed 305 | # (change requires restart) 306 | #recovery_target_inclusive = on # Specifies whether to stop: 307 | # just after the specified recovery target (on) 308 | # just before the recovery target (off) 309 | # (change requires restart) 310 | #recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID 311 | # (change requires restart) 312 | #recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' 313 | # (change requires restart) 314 | 315 | # - WAL Summarization - 316 | 317 | #summarize_wal = off # run WAL summarizer process? 318 | #wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never 319 | 320 | 321 | #------------------------------------------------------------------------------ 322 | # REPLICATION 323 | #------------------------------------------------------------------------------ 324 | 325 | # - Sending Servers - 326 | 327 | # Set these on the primary and on any standby that will send replication data. 328 | 329 | #max_wal_senders = 10 # max number of walsender processes 330 | # (change requires restart) 331 | #max_replication_slots = 10 # max number of replication slots 332 | # (change requires restart) 333 | #wal_keep_size = 0 # in megabytes; 0 disables 334 | #max_slot_wal_keep_size = -1 # in megabytes; -1 disables 335 | #wal_sender_timeout = 60s # in milliseconds; 0 disables 336 | #track_commit_timestamp = off # collect timestamp of transaction commit 337 | # (change requires restart) 338 | 339 | # - Primary Server - 340 | 341 | # These settings are ignored on a standby server. 342 | 343 | #synchronous_standby_names = '' # standby servers that provide sync rep 344 | # method to choose sync standbys, number of sync standbys, 345 | # and comma-separated list of application_name 346 | # from standby(s); '*' = all 347 | #synchronized_standby_slots = '' # streaming replication standby server slot 348 | # names that logical walsender processes will wait for 349 | 350 | # - Standby Servers - 351 | 352 | # These settings are ignored on a primary server. 353 | 354 | #primary_conninfo = '' # connection string to sending server 355 | #primary_slot_name = '' # replication slot on sending server 356 | #hot_standby = on # "off" disallows queries during recovery 357 | # (change requires restart) 358 | #max_standby_archive_delay = 30s # max delay before canceling queries 359 | # when reading WAL from archive; 360 | # -1 allows indefinite delay 361 | #max_standby_streaming_delay = 30s # max delay before canceling queries 362 | # when reading streaming WAL; 363 | # -1 allows indefinite delay 364 | #wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name 365 | # is not set 366 | #wal_receiver_status_interval = 10s # send replies at least this often 367 | # 0 disables 368 | #hot_standby_feedback = off # send info from standby to prevent 369 | # query conflicts 370 | #wal_receiver_timeout = 60s # time that receiver waits for 371 | # communication from primary 372 | # in milliseconds; 0 disables 373 | #wal_retrieve_retry_interval = 5s # time to wait before retrying to 374 | # retrieve WAL after a failed attempt 375 | #recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery 376 | #sync_replication_slots = off # enables slot synchronization on the physical standby from the primary 377 | 378 | # - Subscribers - 379 | 380 | # These settings are ignored on a publisher. 381 | 382 | #max_logical_replication_workers = 4 # taken from max_worker_processes 383 | # (change requires restart) 384 | #max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers 385 | #max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers 386 | 387 | 388 | #------------------------------------------------------------------------------ 389 | # QUERY TUNING 390 | #------------------------------------------------------------------------------ 391 | 392 | # - Planner Method Configuration - 393 | 394 | #enable_async_append = on 395 | #enable_bitmapscan = on 396 | #enable_gathermerge = on 397 | #enable_hashagg = on 398 | #enable_hashjoin = on 399 | #enable_incremental_sort = on 400 | #enable_indexscan = on 401 | #enable_indexonlyscan = on 402 | #enable_material = on 403 | #enable_memoize = on 404 | #enable_mergejoin = on 405 | #enable_nestloop = on 406 | #enable_parallel_append = on 407 | #enable_parallel_hash = on 408 | #enable_partition_pruning = on 409 | #enable_partitionwise_join = off 410 | #enable_partitionwise_aggregate = off 411 | #enable_presorted_aggregate = on 412 | #enable_seqscan = on 413 | #enable_sort = on 414 | #enable_tidscan = on 415 | #enable_group_by_reordering = on 416 | 417 | # - Planner Cost Constants - 418 | 419 | #seq_page_cost = 1.0 # measured on an arbitrary scale 420 | #random_page_cost = 4.0 # same scale as above 421 | #cpu_tuple_cost = 0.01 # same scale as above 422 | #cpu_index_tuple_cost = 0.005 # same scale as above 423 | #cpu_operator_cost = 0.0025 # same scale as above 424 | #parallel_setup_cost = 1000.0 # same scale as above 425 | #parallel_tuple_cost = 0.1 # same scale as above 426 | #min_parallel_table_scan_size = 8MB 427 | #min_parallel_index_scan_size = 512kB 428 | #effective_cache_size = 4GB 429 | 430 | #jit_above_cost = 100000 # perform JIT compilation if available 431 | # and query more expensive than this; 432 | # -1 disables 433 | #jit_inline_above_cost = 500000 # inline small functions if query is 434 | # more expensive than this; -1 disables 435 | #jit_optimize_above_cost = 500000 # use expensive JIT optimizations if 436 | # query is more expensive than this; 437 | # -1 disables 438 | 439 | # - Genetic Query Optimizer - 440 | 441 | #geqo = on 442 | #geqo_threshold = 12 443 | #geqo_effort = 5 # range 1-10 444 | #geqo_pool_size = 0 # selects default based on effort 445 | #geqo_generations = 0 # selects default based on effort 446 | #geqo_selection_bias = 2.0 # range 1.5-2.0 447 | #geqo_seed = 0.0 # range 0.0-1.0 448 | 449 | # - Other Planner Options - 450 | 451 | #default_statistics_target = 100 # range 1-10000 452 | #constraint_exclusion = partition # on, off, or partition 453 | #cursor_tuple_fraction = 0.1 # range 0.0-1.0 454 | #from_collapse_limit = 8 455 | #jit = on # allow JIT compilation 456 | #join_collapse_limit = 8 # 1 disables collapsing of explicit 457 | # JOIN clauses 458 | #plan_cache_mode = auto # auto, force_generic_plan or 459 | # force_custom_plan 460 | #recursive_worktable_factor = 10.0 # range 0.001-1000000 461 | 462 | 463 | #------------------------------------------------------------------------------ 464 | # REPORTING AND LOGGING 465 | #------------------------------------------------------------------------------ 466 | 467 | # - Where to Log - 468 | 469 | #log_destination = 'stderr' # Valid values are combinations of 470 | # stderr, csvlog, jsonlog, syslog, and 471 | # eventlog, depending on platform. 472 | # csvlog and jsonlog require 473 | # logging_collector to be on. 474 | 475 | # This is used when logging to stderr: 476 | #logging_collector = off # Enable capturing of stderr, jsonlog, 477 | # and csvlog into log files. Required 478 | # to be on for csvlogs and jsonlogs. 479 | # (change requires restart) 480 | 481 | # These are only used if logging_collector is on: 482 | #log_directory = 'log' # directory where log files are written, 483 | # can be absolute or relative to PGDATA 484 | #log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, 485 | # can include strftime() escapes 486 | #log_file_mode = 0600 # creation mode for log files, 487 | # begin with 0 to use octal notation 488 | #log_rotation_age = 1d # Automatic rotation of logfiles will 489 | # happen after that time. 0 disables. 490 | #log_rotation_size = 10MB # Automatic rotation of logfiles will 491 | # happen after that much log output. 492 | # 0 disables. 493 | #log_truncate_on_rotation = off # If on, an existing log file with the 494 | # same name as the new log file will be 495 | # truncated rather than appended to. 496 | # But such truncation only occurs on 497 | # time-driven rotation, not on restarts 498 | # or size-driven rotation. Default is 499 | # off, meaning append to existing files 500 | # in all cases. 501 | 502 | # These are relevant when logging to syslog: 503 | #syslog_facility = 'LOCAL0' 504 | #syslog_ident = 'postgres' 505 | #syslog_sequence_numbers = on 506 | #syslog_split_messages = on 507 | 508 | # This is only relevant when logging to eventlog (Windows): 509 | # (change requires restart) 510 | #event_source = 'PostgreSQL' 511 | 512 | # - When to Log - 513 | 514 | #log_min_messages = warning # values in order of decreasing detail: 515 | # debug5 516 | # debug4 517 | # debug3 518 | # debug2 519 | # debug1 520 | # info 521 | # notice 522 | # warning 523 | # error 524 | # log 525 | # fatal 526 | # panic 527 | 528 | #log_min_error_statement = error # values in order of decreasing detail: 529 | # debug5 530 | # debug4 531 | # debug3 532 | # debug2 533 | # debug1 534 | # info 535 | # notice 536 | # warning 537 | # error 538 | # log 539 | # fatal 540 | # panic (effectively off) 541 | 542 | #log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements 543 | # and their durations, > 0 logs only 544 | # statements running at least this number 545 | # of milliseconds 546 | 547 | #log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements 548 | # and their durations, > 0 logs only a sample of 549 | # statements running at least this number 550 | # of milliseconds; 551 | # sample fraction is determined by log_statement_sample_rate 552 | 553 | #log_statement_sample_rate = 1.0 # fraction of logged statements exceeding 554 | # log_min_duration_sample to be logged; 555 | # 1.0 logs all such statements, 0.0 never logs 556 | 557 | 558 | #log_transaction_sample_rate = 0.0 # fraction of transactions whose statements 559 | # are logged regardless of their duration; 1.0 logs all 560 | # statements from all transactions, 0.0 never logs 561 | 562 | #log_startup_progress_interval = 10s # Time between progress updates for 563 | # long-running startup operations. 564 | # 0 disables the feature, > 0 indicates 565 | # the interval in milliseconds. 566 | 567 | # - What to Log - 568 | 569 | #debug_print_parse = off 570 | #debug_print_rewritten = off 571 | #debug_print_plan = off 572 | #debug_pretty_print = on 573 | #log_autovacuum_min_duration = 10min # log autovacuum activity; 574 | # -1 disables, 0 logs all actions and 575 | # their durations, > 0 logs only 576 | # actions running at least this number 577 | # of milliseconds. 578 | #log_checkpoints = on 579 | #log_connections = off 580 | #log_disconnections = off 581 | #log_duration = off 582 | #log_error_verbosity = default # terse, default, or verbose messages 583 | #log_hostname = off 584 | #log_line_prefix = '%m [%p] ' # special values: 585 | # %a = application name 586 | # %u = user name 587 | # %d = database name 588 | # %r = remote host and port 589 | # %h = remote host 590 | # %b = backend type 591 | # %p = process ID 592 | # %P = process ID of parallel group leader 593 | # %t = timestamp without milliseconds 594 | # %m = timestamp with milliseconds 595 | # %n = timestamp with milliseconds (as a Unix epoch) 596 | # %Q = query ID (0 if none or not computed) 597 | # %i = command tag 598 | # %e = SQL state 599 | # %c = session ID 600 | # %l = session line number 601 | # %s = session start timestamp 602 | # %v = virtual transaction ID 603 | # %x = transaction ID (0 if none) 604 | # %q = stop here in non-session 605 | # processes 606 | # %% = '%' 607 | # e.g. '<%u%%%d> ' 608 | #log_lock_waits = off # log lock waits >= deadlock_timeout 609 | #log_recovery_conflict_waits = off # log standby recovery conflict waits 610 | # >= deadlock_timeout 611 | #log_parameter_max_length = -1 # when logging statements, limit logged 612 | # bind-parameter values to N bytes; 613 | # -1 means print in full, 0 disables 614 | #log_parameter_max_length_on_error = 0 # when logging an error, limit logged 615 | # bind-parameter values to N bytes; 616 | # -1 means print in full, 0 disables 617 | #log_statement = 'none' # none, ddl, mod, all 618 | #log_replication_commands = off 619 | #log_temp_files = -1 # log temporary files equal or larger 620 | # than the specified size in kilobytes; 621 | # -1 disables, 0 logs all temp files 622 | #log_timezone = 'GMT' 623 | 624 | # - Process Title - 625 | 626 | #cluster_name = '' # added to process titles if nonempty 627 | # (change requires restart) 628 | #update_process_title = on 629 | 630 | 631 | #------------------------------------------------------------------------------ 632 | # STATISTICS 633 | #------------------------------------------------------------------------------ 634 | 635 | # - Cumulative Query and Index Statistics - 636 | 637 | #track_activities = on 638 | #track_activity_query_size = 1024 # (change requires restart) 639 | #track_counts = on 640 | #track_io_timing = off 641 | #track_wal_io_timing = off 642 | #track_functions = none # none, pl, all 643 | #stats_fetch_consistency = cache # cache, none, snapshot 644 | 645 | 646 | # - Monitoring - 647 | 648 | #compute_query_id = auto 649 | #log_statement_stats = off 650 | #log_parser_stats = off 651 | #log_planner_stats = off 652 | #log_executor_stats = off 653 | 654 | 655 | #------------------------------------------------------------------------------ 656 | # AUTOVACUUM 657 | #------------------------------------------------------------------------------ 658 | 659 | #autovacuum = on # Enable autovacuum subprocess? 'on' 660 | # requires track_counts to also be on. 661 | #autovacuum_max_workers = 3 # max number of autovacuum subprocesses 662 | # (change requires restart) 663 | #autovacuum_naptime = 1min # time between autovacuum runs 664 | #autovacuum_vacuum_threshold = 50 # min number of row updates before 665 | # vacuum 666 | #autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts 667 | # before vacuum; -1 disables insert 668 | # vacuums 669 | #autovacuum_analyze_threshold = 50 # min number of row updates before 670 | # analyze 671 | #autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum 672 | #autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of inserts over table 673 | # size before insert vacuum 674 | #autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze 675 | #autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum 676 | # (change requires restart) 677 | #autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age 678 | # before forced vacuum 679 | # (change requires restart) 680 | #autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for 681 | # autovacuum, in milliseconds; 682 | # -1 means use vacuum_cost_delay 683 | #autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for 684 | # autovacuum, -1 means use 685 | # vacuum_cost_limit 686 | 687 | 688 | #------------------------------------------------------------------------------ 689 | # CLIENT CONNECTION DEFAULTS 690 | #------------------------------------------------------------------------------ 691 | 692 | # - Statement Behavior - 693 | 694 | #client_min_messages = notice # values in order of decreasing detail: 695 | # debug5 696 | # debug4 697 | # debug3 698 | # debug2 699 | # debug1 700 | # log 701 | # notice 702 | # warning 703 | # error 704 | #search_path = '"$user", public' # schema names 705 | #row_security = on 706 | #default_table_access_method = 'heap' 707 | #default_tablespace = '' # a tablespace name, '' uses the default 708 | #default_toast_compression = 'pglz' # 'pglz' or 'lz4' 709 | #temp_tablespaces = '' # a list of tablespace names, '' uses 710 | # only default tablespace 711 | #check_function_bodies = on 712 | #default_transaction_isolation = 'read committed' 713 | #default_transaction_read_only = off 714 | #default_transaction_deferrable = off 715 | #session_replication_role = 'origin' 716 | #statement_timeout = 0 # in milliseconds, 0 is disabled 717 | #transaction_timeout = 0 # in milliseconds, 0 is disabled 718 | #lock_timeout = 0 # in milliseconds, 0 is disabled 719 | #idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled 720 | #idle_session_timeout = 0 # in milliseconds, 0 is disabled 721 | #vacuum_freeze_table_age = 150000000 722 | #vacuum_freeze_min_age = 50000000 723 | #vacuum_failsafe_age = 1600000000 724 | #vacuum_multixact_freeze_table_age = 150000000 725 | #vacuum_multixact_freeze_min_age = 5000000 726 | #vacuum_multixact_failsafe_age = 1600000000 727 | #bytea_output = 'hex' # hex, escape 728 | #xmlbinary = 'base64' 729 | #xmloption = 'content' 730 | #gin_pending_list_limit = 4MB 731 | #createrole_self_grant = '' # set and/or inherit 732 | #event_triggers = on 733 | 734 | # - Locale and Formatting - 735 | 736 | #datestyle = 'iso, mdy' 737 | #intervalstyle = 'postgres' 738 | #timezone = 'GMT' 739 | #timezone_abbreviations = 'Default' # Select the set of available time zone 740 | # abbreviations. Currently, there are 741 | # Default 742 | # Australia (historical usage) 743 | # India 744 | # You can create your own file in 745 | # share/timezonesets/. 746 | #extra_float_digits = 1 # min -15, max 3; any value >0 actually 747 | # selects precise output mode 748 | #client_encoding = sql_ascii # actually, defaults to database 749 | # encoding 750 | 751 | # These settings are initialized by initdb, but they can be changed. 752 | #lc_messages = '' # locale for system error message 753 | # strings 754 | #lc_monetary = 'C' # locale for monetary formatting 755 | #lc_numeric = 'C' # locale for number formatting 756 | #lc_time = 'C' # locale for time formatting 757 | 758 | #icu_validation_level = warning # report ICU locale validation 759 | # errors at the given level 760 | 761 | # default configuration for text search 762 | #default_text_search_config = 'pg_catalog.simple' 763 | 764 | # - Shared Library Preloading - 765 | 766 | #local_preload_libraries = '' 767 | #session_preload_libraries = '' 768 | #shared_preload_libraries = '' # (change requires restart) 769 | #jit_provider = 'llvmjit' # JIT library to use 770 | 771 | # - Other Defaults - 772 | 773 | #dynamic_library_path = '$libdir' 774 | #extension_destdir = '' # prepend path when loading extensions 775 | # and shared objects (added by Debian) 776 | #gin_fuzzy_search_limit = 0 777 | 778 | 779 | #------------------------------------------------------------------------------ 780 | # LOCK MANAGEMENT 781 | #------------------------------------------------------------------------------ 782 | 783 | #deadlock_timeout = 1s 784 | #max_locks_per_transaction = 64 # min 10 785 | # (change requires restart) 786 | #max_pred_locks_per_transaction = 64 # min 10 787 | # (change requires restart) 788 | #max_pred_locks_per_relation = -2 # negative values mean 789 | # (max_pred_locks_per_transaction 790 | # / -max_pred_locks_per_relation) - 1 791 | #max_pred_locks_per_page = 2 # min 0 792 | 793 | 794 | #------------------------------------------------------------------------------ 795 | # VERSION AND PLATFORM COMPATIBILITY 796 | #------------------------------------------------------------------------------ 797 | 798 | # - Previous PostgreSQL Versions - 799 | 800 | #array_nulls = on 801 | #backslash_quote = safe_encoding # on, off, or safe_encoding 802 | #escape_string_warning = on 803 | #lo_compat_privileges = off 804 | #quote_all_identifiers = off 805 | #standard_conforming_strings = on 806 | #synchronize_seqscans = on 807 | 808 | # - Other Platforms and Clients - 809 | 810 | #transform_null_equals = off 811 | #allow_alter_system = on 812 | 813 | 814 | #------------------------------------------------------------------------------ 815 | # ERROR HANDLING 816 | #------------------------------------------------------------------------------ 817 | 818 | #exit_on_error = off # terminate session on any error? 819 | #restart_after_crash = on # reinitialize after backend crash? 820 | #data_sync_retry = off # retry or panic on failure to fsync 821 | # data? 822 | # (change requires restart) 823 | #recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+) 824 | 825 | 826 | #------------------------------------------------------------------------------ 827 | # CONFIG FILE INCLUDES 828 | #------------------------------------------------------------------------------ 829 | 830 | # These options allow settings to be loaded from files other than the 831 | # default postgresql.conf. Note that these are directives, not variable 832 | # assignments, so they can usefully be given more than once. 833 | 834 | #include_dir = '...' # include files ending in '.conf' from 835 | # a directory, e.g., 'conf.d' 836 | #include_if_exists = '...' # include file only if it exists 837 | #include = '...' # include file 838 | 839 | 840 | #------------------------------------------------------------------------------ 841 | # CUSTOMIZED OPTIONS 842 | #------------------------------------------------------------------------------ 843 | 844 | # Add settings for extensions here 845 | --------------------------------------------------------------------------------