pg-logical-replication
Version:
PostgreSQL Location Replication client - logical WAL replication streaming
163 lines (162 loc) • 6.06 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogicalReplicationService = void 0;
const eventemitter2_1 = __importDefault(require("eventemitter2"));
const pg_1 = require("pg");
class LogicalReplicationService extends eventemitter2_1.default {
clientConfig;
config;
constructor(clientConfig, config) {
super();
this.clientConfig = clientConfig;
this.config = {
acknowledge: {
auto: true,
timeoutSeconds: 10,
...(config?.acknowledge || {}),
},
};
}
_lastLsn = null;
lastLsn() {
return this._lastLsn || '0/00000000';
}
_client = null;
_connection = null;
async client() {
await this.stop();
this._client = new pg_1.Client({
...this.clientConfig,
replication: 'database',
});
await this._client.connect();
// @ts-ignore
this._connection = this._client.connection;
this._client.on('error', (e) => this.emit('error', e));
return [this._client, this._connection];
}
_stop = true;
isStop() {
return this._stop;
}
async stop() {
this._stop = true;
this._connection?.removeAllListeners();
this._connection = null;
this._client?.removeAllListeners();
await this._client?.end();
this._client = null;
this.checkStandbyStatus(false);
return this;
}
/**
* subscribe
* @param plugin One of [TestDecodingPlugin, ]
* @param slotName
* @param uptoLsn
*/
async subscribe(plugin, slotName, uptoLsn) {
try {
const [client, connection] = await this.client();
this._lastLsn = uptoLsn || this._lastLsn;
// check replicationStart
connection.once('replicationStart', () => {
this._stop = false;
this.emit('start');
this.checkStandbyStatus(true);
});
connection.on('copyData', ({ chunk: buffer }) => {
if (buffer[0] != 0x77 && buffer[0] != 0x6b) {
console.warn('Unknown message', buffer[0]);
return;
}
const lsn = buffer.readUInt32BE(1).toString(16).toUpperCase() + '/' + buffer.readUInt32BE(5).toString(16).toUpperCase();
if (buffer[0] == 0x77) {
// XLogData
this.emit('data', lsn, plugin.parse(buffer.subarray(25)));
this._acknowledge(lsn);
}
else if (buffer[0] == 0x6b) {
// Primary keepalive message
const timestamp = Math.floor(buffer.readUInt32BE(9) * 4294967.296 + buffer.readUInt32BE(13) / 1000 + 946080000000);
const shouldRespond = !!buffer.readInt8(17);
this.emit('heartbeat', lsn, timestamp, shouldRespond);
}
this._lastLsn = lsn;
});
return plugin.start(client, slotName, this._lastLsn || '0/00000000');
}
catch (e) {
await this.stop();
this.emit('error', e);
throw e;
}
}
async _acknowledge(lsn) {
if (!this.config.acknowledge.auto)
return;
this.emit('acknowledge', lsn);
await this.acknowledge(lsn);
}
lastStandbyStatusUpdatedTime = 0;
checkStandbyStatusTimer = null;
checkStandbyStatus(enable) {
if (this.checkStandbyStatusTimer) {
clearInterval(this.checkStandbyStatusTimer);
this.checkStandbyStatusTimer = null;
}
if (this.config.acknowledge.timeoutSeconds > 0 && enable)
this.checkStandbyStatusTimer = setInterval(async () => {
if (this._stop)
return;
if (this._lastLsn &&
Date.now() - this.lastStandbyStatusUpdatedTime > this.config.acknowledge.timeoutSeconds * 1000)
await this.acknowledge(this._lastLsn);
}, 1000);
}
/**
* @param lsn
*/
async acknowledge(lsn) {
if (this._stop)
return false;
this.lastStandbyStatusUpdatedTime = Date.now();
const slice = lsn.split('/');
let [upperWAL, lowerWAL] = [parseInt(slice[0], 16), parseInt(slice[1], 16)];
// Timestamp as microseconds since midnight 2000-01-01
const now = Date.now() - 946080000000;
const upperTimestamp = Math.floor(now / 4294967.296);
const lowerTimestamp = Math.floor(now - upperTimestamp * 4294967.296);
if (lowerWAL === 4294967295) {
// [0xff, 0xff, 0xff, 0xff]
upperWAL = upperWAL + 1;
lowerWAL = 0;
}
else {
lowerWAL = lowerWAL + 1;
}
const response = Buffer.alloc(34);
response.fill(0x72); // 'r'
// Last WAL Byte + 1 received and written to disk locally
response.writeUInt32BE(upperWAL, 1);
response.writeUInt32BE(lowerWAL, 5);
// Last WAL Byte + 1 flushed to disk in the standby
response.writeUInt32BE(upperWAL, 9);
response.writeUInt32BE(lowerWAL, 13);
// Last WAL Byte + 1 applied in the standby
response.writeUInt32BE(upperWAL, 17);
response.writeUInt32BE(lowerWAL, 21);
// Timestamp as microseconds since midnight 2000-01-01
response.writeUInt32BE(upperTimestamp, 25);
response.writeUInt32BE(lowerTimestamp, 29);
// If 1, requests server to respond immediately - can be used to verify connectivity
response.writeInt8(0, 33);
// @ts-ignore
this._connection?.sendCopyFromChunk(response);
return true;
}
}
exports.LogicalReplicationService = LogicalReplicationService;