2022-10-24 16:11:25 +00:00
|
|
|
require('require-yaml');
|
|
|
|
require('colors');
|
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
2022-10-21 14:36:49 +00:00
|
|
|
const ZongJi = require('./zongji');
|
|
|
|
const mysql = require('mysql2/promise');
|
2022-10-23 19:46:07 +00:00
|
|
|
const amqp = require('amqplib');
|
2022-10-21 14:36:49 +00:00
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
const allEvents = [
|
|
|
|
'writerows',
|
|
|
|
'updaterows',
|
|
|
|
'deleterows'
|
|
|
|
];
|
2022-10-21 14:36:49 +00:00
|
|
|
|
2022-10-24 06:01:43 +00:00
|
|
|
module.exports = class MyCDC {
|
2022-10-24 16:11:25 +00:00
|
|
|
constructor() {
|
2022-10-21 14:37:35 +00:00
|
|
|
this.running = false;
|
|
|
|
this.filename = null;
|
|
|
|
this.position = null;
|
|
|
|
this.schemaMap = new Map();
|
2022-10-24 16:11:25 +00:00
|
|
|
this.queues = {};
|
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
async start() {
|
|
|
|
const defaultConfig = require('./config.yml');
|
|
|
|
const config = this.config = Object.assign({}, defaultConfig);
|
|
|
|
const localPath = path.join(__dirname, 'config.local.yml');
|
|
|
|
if (fs.existsSync(localPath)) {
|
|
|
|
const localConfig = require(localPath);
|
|
|
|
Object.assign(config, localConfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
const queues = config.queues;
|
|
|
|
for (const queueName in queues) {
|
|
|
|
const includeSchema = queues[queueName].includeSchema;
|
|
|
|
for (const schemaName in includeSchema) {
|
|
|
|
let tableMap = this.schemaMap.get(schemaName);
|
|
|
|
if (!tableMap) {
|
|
|
|
tableMap = new Map();
|
|
|
|
this.schemaMap.set(schemaName, tableMap);
|
|
|
|
}
|
|
|
|
|
|
|
|
const schema = includeSchema[schemaName];
|
|
|
|
for (const tableName in schema) {
|
|
|
|
const table = schema[tableName];
|
|
|
|
//if (typeof table !== 'object') continue;
|
|
|
|
|
|
|
|
let tableInfo = tableMap.get(tableName);
|
|
|
|
if (!tableInfo) {
|
|
|
|
tableInfo = {
|
|
|
|
queues: new Map(),
|
|
|
|
events: new Map(),
|
|
|
|
columns: new Map(),
|
|
|
|
fk: 'id'
|
|
|
|
};
|
|
|
|
tableMap.set(tableName, tableInfo);
|
|
|
|
}
|
|
|
|
tableInfo.queues.set(queueName, table);
|
|
|
|
|
|
|
|
const events = table.events || allEvents;
|
|
|
|
for (const event of events) {
|
|
|
|
let eventInfo = tableInfo.events.get(event);
|
|
|
|
if (!eventInfo) {
|
|
|
|
eventInfo = [];
|
|
|
|
tableInfo.events.set(event, eventInfo);
|
|
|
|
}
|
|
|
|
eventInfo.push(queueName);
|
|
|
|
}
|
|
|
|
|
|
|
|
const columns = table.columns;
|
|
|
|
for (const column of columns) {
|
|
|
|
let columnInfo = tableInfo.columns.get(column);
|
|
|
|
if (!columnInfo) {
|
|
|
|
columnInfo = [];
|
|
|
|
tableInfo.columns.set(column, columnInfo);
|
|
|
|
}
|
|
|
|
columnInfo.push(queueName);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (table.id)
|
|
|
|
tableInfo.id = table.id;
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-24 16:11:25 +00:00
|
|
|
|
|
|
|
this.schemaMap.set(schemaName, tableMap);
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-24 16:11:25 +00:00
|
|
|
|
|
|
|
const includeSchema = {};
|
|
|
|
for (const [schemaName, tableMap] of this.schemaMap)
|
|
|
|
includeSchema[schemaName] = Array.from(tableMap.keys());
|
2022-10-21 14:36:49 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
this.opts = {
|
2022-10-24 16:11:25 +00:00
|
|
|
includeEvents: [
|
|
|
|
'rotate',
|
|
|
|
'tablemap',
|
|
|
|
'writerows',
|
|
|
|
'updaterows',
|
|
|
|
'deleterows'
|
|
|
|
],
|
2022-10-21 14:37:35 +00:00
|
|
|
includeSchema
|
|
|
|
};
|
2022-10-23 19:46:07 +00:00
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
if (config.testMode)
|
2022-10-23 19:46:07 +00:00
|
|
|
console.log('Test mode enabled, just logging queries to console.');
|
|
|
|
|
|
|
|
console.log('Starting process.');
|
|
|
|
await this.init();
|
|
|
|
console.log('Process started.');
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
console.log('Stopping process.');
|
|
|
|
await this.end();
|
|
|
|
console.log('Process stopped.');
|
|
|
|
}
|
|
|
|
|
|
|
|
async init() {
|
2022-10-24 16:11:25 +00:00
|
|
|
const config = this.config;
|
|
|
|
this.debug('MyCDC', 'Initializing.');
|
2022-10-23 19:46:07 +00:00
|
|
|
this.onErrorListener = err => this.onError(err);
|
|
|
|
|
|
|
|
// DB connection
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
this.db = await mysql.createConnection(config.db);
|
2022-10-23 19:46:07 +00:00
|
|
|
this.db.on('error', this.onErrorListener);
|
|
|
|
|
|
|
|
// RabbitMQ
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
this.publisher = await amqp.connect(config.amqp);
|
2022-10-23 19:46:07 +00:00
|
|
|
this.channel = await this.publisher.createChannel();
|
2022-10-24 16:11:25 +00:00
|
|
|
|
|
|
|
for (const queueName in config.queues) {
|
|
|
|
await this.channel.assertQueue(queueName, {
|
|
|
|
durable: true
|
|
|
|
});
|
|
|
|
}
|
2022-10-23 19:46:07 +00:00
|
|
|
|
|
|
|
// Zongji
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
const zongji = new ZongJi(config.db);
|
2022-10-23 19:46:07 +00:00
|
|
|
this.zongji = zongji;
|
|
|
|
|
|
|
|
this.onBinlogListener = evt => this.onBinlog(evt);
|
|
|
|
zongji.on('binlog', this.onBinlogListener);
|
|
|
|
|
2022-10-21 14:37:35 +00:00
|
|
|
const [res] = await this.db.query(
|
|
|
|
'SELECT `logName`, `position` FROM `binlogQueue` WHERE code = ?',
|
2022-10-25 11:20:22 +00:00
|
|
|
[config.code]
|
2022-10-21 14:37:35 +00:00
|
|
|
);
|
|
|
|
if (res.length) {
|
|
|
|
const [row] = res;
|
|
|
|
this.filename = row.logName;
|
|
|
|
this.position = row.position;
|
2022-10-23 19:46:07 +00:00
|
|
|
Object.assign(this.opts, {
|
2022-10-21 14:37:35 +00:00
|
|
|
filename: this.filename,
|
|
|
|
position: this.position
|
|
|
|
});
|
|
|
|
} else
|
2022-10-23 19:46:07 +00:00
|
|
|
this.opts.startAtEnd = true;
|
2022-10-21 14:36:49 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
this.debug('Zongji', 'Starting.');
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
const onReady = () => {
|
|
|
|
zongji.off('error', onError);
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
const onError = err => {
|
|
|
|
this.zongji = null;
|
|
|
|
zongji.off('ready', onReady);
|
|
|
|
zongji.off('binlog', this.onBinlogListener);
|
|
|
|
reject(err);
|
|
|
|
}
|
2022-10-21 14:36:49 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
zongji.once('ready', onReady);
|
|
|
|
zongji.once('error', onError);
|
|
|
|
zongji.start(this.opts);
|
|
|
|
});
|
|
|
|
this.debug('Zongji', 'Started.');
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
this.zongji.on('error', this.onErrorListener);
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
this.flushInterval = setInterval(
|
2022-10-25 11:20:22 +00:00
|
|
|
() => this.flushQueue(), config.flushInterval * 1000);
|
2022-10-23 19:46:07 +00:00
|
|
|
this.pingInterval = setInterval(
|
2022-10-24 16:11:25 +00:00
|
|
|
() => this.connectionPing(), config.pingInterval * 1000);
|
2022-10-23 19:46:07 +00:00
|
|
|
|
|
|
|
// Summary
|
|
|
|
|
|
|
|
this.running = true;
|
2022-10-24 16:11:25 +00:00
|
|
|
this.debug('MyCDC', 'Initialized.');
|
2022-10-21 14:37:35 +00:00
|
|
|
}
|
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
async end(silent) {
|
|
|
|
const zongji = this.zongji;
|
|
|
|
if (!zongji) return;
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
this.debug('MyCDC', 'Ending.');
|
2022-10-23 19:46:07 +00:00
|
|
|
|
|
|
|
// Zongji
|
|
|
|
|
2022-10-21 14:37:35 +00:00
|
|
|
clearInterval(this.flushInterval);
|
|
|
|
clearInterval(this.pingInterval);
|
2022-10-23 19:46:07 +00:00
|
|
|
zongji.off('binlog', this.onBinlogListener);
|
|
|
|
zongji.off('error', this.onErrorListener);
|
|
|
|
this.zongji = null;
|
|
|
|
this.running = false;
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
this.debug('Zongji', 'Stopping.');
|
|
|
|
// FIXME: Cannot call Zongji.stop(), it doesn't wait to end connection
|
|
|
|
zongji.connection.destroy(() => {
|
|
|
|
console.log('zongji.connection.destroy');
|
|
|
|
});
|
|
|
|
await new Promise(resolve => {
|
|
|
|
zongji.ctrlConnection.query('KILL ' + zongji.connection.threadId,
|
|
|
|
err => {
|
|
|
|
if (err && !silent)
|
|
|
|
console.error(err);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
zongji.ctrlConnection.destroy(() => {
|
|
|
|
console.log('zongji.ctrlConnection.destroy');
|
|
|
|
});
|
|
|
|
zongji.emit('stopped');
|
|
|
|
this.debug('Zongji', 'Stopped.');
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
// RabbitMQ
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
await this.publisher.close();
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
// DB connection
|
|
|
|
|
|
|
|
this.db.off('error', this.onErrorListener);
|
|
|
|
// FIXME: mysql2/promise bug, db.end() ends process
|
|
|
|
this.db.on('error', () => {});
|
|
|
|
try {
|
|
|
|
await this.db.end();
|
|
|
|
} catch (err) {
|
|
|
|
if (!silent)
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Summary
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
this.debug('MyCDC', 'Ended.');
|
2022-10-21 14:37:35 +00:00
|
|
|
}
|
|
|
|
|
2022-10-23 19:46:07 +00:00
|
|
|
async tryRestart() {
|
|
|
|
try {
|
|
|
|
await this.init();
|
|
|
|
console.log('Process restarted.');
|
|
|
|
} catch(err) {
|
|
|
|
setTimeout(() => this.tryRestart(), 30);
|
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async onError(err) {
|
|
|
|
console.log(`Error: ${err.code}: ${err.message}`);
|
2022-10-23 19:46:07 +00:00
|
|
|
try {
|
|
|
|
await this.end(true);
|
|
|
|
} catch(e) {}
|
|
|
|
|
2022-10-21 14:37:35 +00:00
|
|
|
switch (err.code) {
|
|
|
|
case 'PROTOCOL_CONNECTION_LOST':
|
|
|
|
case 'ECONNRESET':
|
2022-10-23 19:46:07 +00:00
|
|
|
console.log('Trying to restart process.');
|
|
|
|
await this.tryRestart();
|
2022-10-21 14:37:35 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
process.exit();
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
}
|
2022-10-21 14:36:49 +00:00
|
|
|
|
2022-10-21 14:37:35 +00:00
|
|
|
onBinlog(evt) {
|
|
|
|
//evt.dump();
|
|
|
|
const eventName = evt.getEventName();
|
|
|
|
if (eventName === 'tablemap') return;
|
|
|
|
|
|
|
|
if (eventName === 'rotate') {
|
|
|
|
this.filename = evt.binlogName;
|
|
|
|
this.position = evt.position;
|
|
|
|
console.log(`[${eventName}] filename: ${this.filename}`, `position: ${this.position}`);
|
|
|
|
return;
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
|
|
|
const table = evt.tableMap[evt.tableId];
|
|
|
|
const tableMap = this.schemaMap.get(table.parentSchema);
|
|
|
|
if (!tableMap) return;
|
|
|
|
|
|
|
|
const tableInfo = tableMap.get(table.tableName);
|
|
|
|
if (!tableInfo) return;
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
const queueNames = tableInfo.events.get(eventName);
|
|
|
|
if (!queueNames) return;
|
2022-10-21 14:37:35 +00:00
|
|
|
|
|
|
|
const rows = evt.rows;
|
2022-10-24 16:11:25 +00:00
|
|
|
const queues = this.config.queues;
|
|
|
|
const tableQueues = tableInfo.queues;
|
|
|
|
|
|
|
|
const changes = new Map();
|
|
|
|
for (const queueName of queueNames) {
|
|
|
|
const change = {
|
|
|
|
mode: queues[queueName].mode
|
|
|
|
};
|
|
|
|
changes.set(queueName, change);
|
|
|
|
|
|
|
|
switch(change.mode) {
|
|
|
|
case 'fk':
|
|
|
|
change.fks = new Set();
|
|
|
|
break;
|
|
|
|
case 'changes':
|
2022-10-25 11:20:22 +00:00
|
|
|
change.rows = [];
|
2022-10-24 16:11:25 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-25 11:20:22 +00:00
|
|
|
function addChange(queueNames, row, old) {
|
2022-10-24 16:11:25 +00:00
|
|
|
for (const queueName of queueNames) {
|
|
|
|
const queueInfo = tableQueues.get(queueName);
|
|
|
|
const change = changes.get(queueName);
|
|
|
|
|
2022-10-25 11:20:22 +00:00
|
|
|
const key = row[queueInfo.key];
|
|
|
|
const oldKey = old ? old[queueInfo.key] : null;
|
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
switch(change.mode) {
|
|
|
|
case 'fk':
|
2022-10-25 11:20:22 +00:00
|
|
|
change.fks.add(key);
|
|
|
|
if (old && !equals(oldKey, key))
|
|
|
|
change.fks.add(oldKey);
|
2022-10-24 16:11:25 +00:00
|
|
|
break;
|
|
|
|
case 'changes':
|
|
|
|
const queueRow = {};
|
|
|
|
for (const column of queueInfo.columns)
|
|
|
|
if (row[column] !== undefined)
|
|
|
|
queueRow[column] = row[column];
|
2022-10-25 11:20:22 +00:00
|
|
|
change.rows.push(queueRow);
|
2022-10-24 16:11:25 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
const columnMap = tableInfo.columns;
|
|
|
|
const columns = columnMap.keys();
|
2022-10-23 19:46:07 +00:00
|
|
|
|
2022-10-21 14:37:35 +00:00
|
|
|
if (eventName === 'updaterows') {
|
2022-10-24 16:11:25 +00:00
|
|
|
const changedQueues = new Set();
|
|
|
|
for (const row of rows) {
|
|
|
|
changedQueues.clear();
|
|
|
|
const after = row.after;
|
|
|
|
|
|
|
|
for (const col of columns) {
|
|
|
|
if (after[col] === undefined
|
|
|
|
|| equals(after[col], row.before[col]))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
for (const queue of columnMap.get(col))
|
|
|
|
changedQueues.add(queue);
|
|
|
|
|
|
|
|
if (changedQueues.size === queueNames.length)
|
|
|
|
break;
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-24 16:11:25 +00:00
|
|
|
|
|
|
|
if (changedQueues.size)
|
2022-10-25 11:20:22 +00:00
|
|
|
addChange(changedQueues, after, row.before);
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
} else {
|
|
|
|
for (const row of rows)
|
2022-10-25 11:20:22 +00:00
|
|
|
addChange(queueNames, row);
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
for (const [queueName, change] of changes) {
|
|
|
|
const jsonData = {
|
|
|
|
eventName,
|
|
|
|
table: table.tableName,
|
|
|
|
schema: table.parentSchema,
|
|
|
|
mode: change.mode
|
|
|
|
};
|
2022-10-23 19:46:07 +00:00
|
|
|
|
2022-10-24 16:11:25 +00:00
|
|
|
let nChanges;
|
|
|
|
switch(change.mode) {
|
|
|
|
case 'fk':
|
|
|
|
jsonData.fks = Array.from(change.fks);
|
|
|
|
nChanges = change.fks.size;
|
|
|
|
break;
|
|
|
|
case 'changes':
|
|
|
|
jsonData.rows = change.rows;
|
|
|
|
nChanges = change.rows.length;
|
|
|
|
break;
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-24 16:11:25 +00:00
|
|
|
|
|
|
|
if (!nChanges) continue;
|
|
|
|
|
|
|
|
const data = JSON.stringify(jsonData);
|
|
|
|
this.channel.sendToQueue(queueName,
|
|
|
|
Buffer.from(data), {persistent: true});
|
|
|
|
|
2022-10-25 11:20:22 +00:00
|
|
|
console.debug('Queued:'.blue, `${queueName}:`.yellow, `${table.tableName}(${nChanges}) [${eventName}]`);
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
|
|
|
this.position = evt.nextPosition;
|
2022-10-23 19:46:07 +00:00
|
|
|
this.flushed = false;
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
|
|
|
async flushQueue() {
|
2022-10-23 19:46:07 +00:00
|
|
|
if (this.flushed) return;
|
|
|
|
this.debug('Flush', `filename: ${this.filename}, position: ${this.position}`);
|
2022-10-21 14:37:35 +00:00
|
|
|
|
|
|
|
const replaceQuery =
|
2022-10-25 11:20:22 +00:00
|
|
|
'REPLACE INTO `binlogQueue` SET `code` = ?, `logName` = ?, `position` = ?';
|
2022-10-23 19:46:07 +00:00
|
|
|
if (!this.config.testMode)
|
2022-10-25 11:20:22 +00:00
|
|
|
await this.db.query(replaceQuery, [this.config.code, this.filename, this.position]);
|
2022-10-23 19:46:07 +00:00
|
|
|
|
|
|
|
this.flushed = true;
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
|
|
|
|
async connectionPing() {
|
2022-10-23 19:46:07 +00:00
|
|
|
this.debug('Ping', 'Sending ping to database.');
|
|
|
|
|
|
|
|
// FIXME: Should Zongji.connection be pinged?
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
this.zongji.ctrlConnection.ping(err => {
|
|
|
|
if (err) return reject(err);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
})
|
2022-10-21 14:37:35 +00:00
|
|
|
await this.db.ping();
|
2022-10-21 14:36:49 +00:00
|
|
|
}
|
2022-10-23 19:46:07 +00:00
|
|
|
|
|
|
|
debug(namespace, message) {
|
|
|
|
if (this.config.debug)
|
|
|
|
console.debug(`${namespace}:`.blue, message.yellow);
|
|
|
|
}
|
2022-10-21 14:37:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function equals(a, b) {
|
|
|
|
if (a === b)
|
|
|
|
return true;
|
|
|
|
const type = typeof a;
|
|
|
|
if (a == null || b == null || type !== typeof b)
|
|
|
|
return false;
|
|
|
|
if (type === 'object' && a.constructor === b.constructor) {
|
|
|
|
if (a instanceof Date)
|
|
|
|
return a.getTime() === b.getTime();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatValue(value) {
|
|
|
|
if (value instanceof Date)
|
|
|
|
return value.toJSON();
|
|
|
|
return value;
|
|
|
|
}
|