mysql-binlog-emitter
Version:
A nodejs mysql/mariadb slave replication event emitter.
409 lines (326 loc) • 10.2 kB
JavaScript
const Event = require('./protocol/constants/Event');
const BinlogEvent = require('./protocol/constants/BinlogEventType');
const ReqisterSlave = require('./protocol/sequences/ReqisterSlave');
const Binlog = require('./protocol/sequences/Binlog');
const mysql = require('mysql');
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {
/**
* @param Object conf
* @param Object conf.mysql - mysql pool connection options;
* @param Object [conf.binlog=null]
* @param Object [conf.binlog.slaveId=1] - When using multiple clients against the same mysql server this ID must be counted up
* @param Object [conf.binlog.lastPos=0] - Start emitting events after this position
* @param Object [conf.binlog.lastTime=0] - Start emitting events after this time
* @param Object [conf.binlog.recoverTimeout=240]
*/
constructor(conf) {
super();
// Create mysql pool
this.pool = mysql.createPool(Object.assign({}, (conf.mysql ? conf.mysql : {})));
// Check connection
this.pool.getConnection(function(err, conn) {
if (err) throw err;
conn.ping(function(err) {
if (err) throw err;
conn.release();
});
});
// Conf
conf.binlog = Object.assign({
slaveId: 1,
lastPos: 0,
lastTime: 0,
values: false,
recoverTimeout: 240,
hasChecksum: null,
version: 0,
},
(conf.binlog ? conf.binlog : {}));
this._conf = {
binlog: // ComBinlogDumpPacket
{
slaveId: conf.binlog.slaveId,
},
packet: // BinlogPacket
{
last: {
pos: conf.binlog.lastPos,
time: conf.binlog.lastTime,
},
values: conf.binlog.values,
hasChecksum: conf.binlog.hasChecksum,
version: conf.binlog.version
},
slave: // ComRegisterSlavePacket
{
slaveId: conf.binlog.slaveId,
},
recoverTimeout: conf.binlog.recoverTimeout
};
// Whether to emit native binlog | rows events
this._binlogNativeEvents = false;
this._binlogRowsEvents = false;
this._wildcardEvents = false;
this.on('newListener', function(event, listener) {
if (
typeof event == 'number' &&
event < Event.ANY
)
this._binlogNativeEvents = true;
if (
event == BinlogEvent.ROWS_EVENT ||
event == BinlogEvent.WRITE_ROWS_EVENT ||
event == BinlogEvent.UPDATE_ROWS_EVENT ||
event == BinlogEvent.DELETE_ROWS_EVENT
) this._binlogRowsEvents = true;
if (event == Event.ANY) {
this._binlogNativeEvents = true;
this._binlogRowsEvents = true;
this._wildcardEvents = true;
}
}.bind(this));
this._conn = null; // active connection
this._bl = null; // binlog sequence
}
/* Shortcuts */
get pos() {
return this._conf.packet.last.pos;
}
get time() {
return this._conf.packet.last.time;
}
/**
* @param Function [cb]
*/
start(cb) {
this._loadHasChecksum(cb ? cb : (err) => { if (err) throw err; });
}
_loadHasChecksum(cb) {
if (this._conf.packet.hasChecksum === null) {
this.pool.query(
'SELECT `VARIABLE_VALUE` FROM `information_schema`.`GLOBAL_VARIABLES` WHERE `VARIABLE_NAME` LIKE "MASTER_VERIFY_CHECKSUM"',
function(err, res) {
if (err) return cb(err);
this._conf.packet.hasChecksum = (res.length > 0 && res[0].VARIABLE_VALUE == 'ON');
this._connect(cb);
}.bind(this));
} else {
this._connect(cb);
}
}
_connect(cb) {
// Dont connect twice
if (this._conn) {
cb(new Error('Already connected'));
return;
}
// Get connection
this.pool.getConnection(function(err, conn) {
if (err) return cb(err);
this._conn = conn;
this._conn.on('error', this.emit.bind(this, Event.ERROR_SQL));
this._conn.on('unhandledError', this.emit.bind(this, Event.ERROR_SQL));
this._conn._implyConnect();
// Register Slave
var rg = new ReqisterSlave(this._conf.slave);
rg.on('error', this.emit.bind(this, Event.ERROR_COM));
rg.on('unhandledError', this.emit.bind(this, Event.ERROR_COM));
rg.on('timeout', this.emit.bind(this, Event.ERROR_COM));
rg.on('end', function() {
// Binlog
this._bl = new Binlog({
binlog: this._conf.binlog,
packet: this._conf.packet
},
null,
this._binlog_cb.bind(this)
);
this._bl.on('error', this.emit.bind(this, Event.ERROR_COM));
this._bl.on('unhandledError', this.emit.bind(this, Event.ERROR_COM));
this._bl.on('timeout', this.emit.bind(this, Event.TIMEOUT));
this._bl.on('end', this.emit.bind(this, Event.END));
this._bl.on('handshake', function() {
this.emit(Event.CONNECTED);
cb();
}.bind(this));
this._conn._protocol._enqueue(this._bl);
}.bind(this));
this._conn._protocol._enqueue(rg);
}.bind(this));
}
_binlog_cb(err, packet) {
if (err) {
this.emit(Event.ERROR_COM, err);
} else if (
packet.error ||
packet.data_error ||
packet.data_row_errors
) {
let err1 = packet.error;
let err2 = packet.data_error;
let err3 = packet.data_row_errors;
delete packet.error;
delete packet.data_error;
delete packet.data_row_errors;
if (err1) {
this.emit(Event.ERROR_PARSE, err1, packet);
}
if (err2) {
this.emit(Event.ERROR_PARSE_DATA, err2, packet);
}
if (err3) {
for (let i = 0, l = err3.length; i < l; i++) {
this.emit(Event.ERROR_PARSE_DATA, err3[i], packet);
}
}
} else {
packet.skipped ?
this.emit(Event.SKIP, packet) :
this.emit(Event.BINLOG, packet);
}
}
/** Emit aditional events */
emit(type, ...data) {
// Super
super.emit.apply(this, arguments);
// Wildcard Events
if (this._wildcardEvents) {
super.emit.call(this, Event.ANY, type, ...data);
}
// Native events
if (
type == Event.BINLOG &&
(
this._binlogNativeEvents ||
this._binlogRowsEvents
) &&
data[0] && // has packet object
data[0].eventType // has event type
) {
var pck = data[0];
// native rows event
if (
this._binlogRowsEvents &&
(
pck.eventType == BinlogEvent.TABLE_MAP_EVENT ||
pck.eventType == BinlogEvent.WRITE_ROWS_EVENTv0 ||
pck.eventType == BinlogEvent.WRITE_ROWS_EVENTv1 ||
pck.eventType == BinlogEvent.WRITE_ROWS_EVENTv2 ||
pck.eventType == BinlogEvent.UPDATE_ROWS_EVENTv0 ||
pck.eventType == BinlogEvent.UPDATE_ROWS_EVENTv1 ||
pck.eventType == BinlogEvent.UPDATE_ROWS_EVENTv2 ||
pck.eventType == BinlogEvent.DELETE_ROWS_EVENTv0 ||
pck.eventType == BinlogEvent.DELETE_ROWS_EVENTv1 ||
pck.eventType == BinlogEvent.DELETE_ROWS_EVENTv2
)
) {
switch (pck.eventType) {
case BinlogEvent.WRITE_ROWS_EVENTv0:
case BinlogEvent.WRITE_ROWS_EVENTv1:
case BinlogEvent.WRITE_ROWS_EVENTv2:
pck.eventType = BinlogEvent.WRITE_ROWS_EVENT;
this.emit(BinlogEvent.WRITE_ROWS_EVENT, pck);
break;
case BinlogEvent.UPDATE_ROWS_EVENTv0:
case BinlogEvent.UPDATE_ROWS_EVENTv1:
case BinlogEvent.UPDATE_ROWS_EVENTv2:
pck.eventType = BinlogEvent.UPDATE_ROWS_EVENT;
this.emit(BinlogEvent.UPDATE_ROWS_EVENT, pck);
break;
case BinlogEvent.DELETE_ROWS_EVENTv0:
case BinlogEvent.DELETE_ROWS_EVENTv1:
case BinlogEvent.DELETE_ROWS_EVENTv2:
pck.eventType = BinlogEvent.DELETE_ROWS_EVENT;
this.emit(BinlogEvent.DELETE_ROWS_EVENT, pck);
break;
default:
}
this.emit(BinlogEvent.ROWS_EVENT, pck);
}
// native binlog event
if (this._binlogNativeEvents) {
this.emit(pck.eventType, pck);
}
return;
}
// Event.ERROR
switch (type) {
case Event.ERROR_SQL:
case Event.ERROR_COM:
case Event.ERROR_PARSE:
case Event.ERROR_PARSE_DATA:
case Event.ERROR_RECOVER:
super.emit.call(this, Event.ERROR, ...data);
break;
default:
}
// recover
switch (type) {
case Event.ERROR_SQL:
case Event.ERROR_COM:
case Event.ERROR_RECOVER:
case Event.TIMEOUT:
case Event.END:
this._recover();
break;
default:
}
}
_disconnect(cb) {
try {
this._bl.removeAllListeners(); // stop recovering
this._conn.removeAllListeners();
this._bl.end();
this._conn._protocol.quit(); // throws a squence error
this._conn.destroy(); // maybe the connection is broke
} catch (erro) {
// nothing to be done here
} finally {
this._bl = null;
this._conn = null;
this.emit(Event.DISCONNECTED);
cb();
}
}
_reconnect(cb) {
this.emit(Event.RECONNECTING);
this._disconnect(function(err) {
if (err) return cb(err);
this._connect(cb);
}.bind(this));
}
_recover() {
this.emit(Event.RECOVERING);
this._disconnect(function() {
setTimeout(function() {
this._connect(function(err) {
if (err) {
this.emit(Event.ERROR_RECOVER, err);
if (!this._conn || !this._bl) this._recover();
}
}.bind(this));
}.bind(this),
this._conf.recoverTimeout
);
}.bind(this));
}
/**
* @param Function [cb]
*/
stop(cb) {
this._disconnect(cb ? cb : (err) => { throw err; });
}
/**
* @param Function [cb]
*/
restart(cb) {
this.stop(function(err) {
if (err) { if (cb) cb(err); return; }
this.start(cb);
}.bind(this));
}
}
MyEmitter.Events = Event;
MyEmitter.BinlogEvents = BinlogEvent;
module.exports = MyEmitter;