@vlasky/zongji 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +71 -12
- package/index.d.ts +284 -7
- package/index.js +350 -28
- package/lib/binlog_event.js +879 -24
- package/lib/charset_map.js +86 -0
- package/lib/code_map.js +44 -1
- package/lib/common.js +376 -20
- package/lib/gtid_set.js +237 -0
- package/lib/mariadb_gtid.js +107 -0
- package/lib/packet/binlog.js +7 -1
- package/lib/rows_event.js +39 -7
- package/lib/sequence/binlog.js +178 -26
- package/package.json +6 -3
package/lib/sequence/binlog.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import initBinlogPacketClass from '../packet/binlog.js';
|
|
3
3
|
import { Parser } from '../reader.js';
|
|
4
|
+
import { GtidSet } from '../gtid_set.js';
|
|
4
5
|
|
|
5
6
|
const BINLOG_DUMP_COMMAND = 0x12;
|
|
7
|
+
// Auto-position dump: the server locates the starting binlog itself and
|
|
8
|
+
// skips transactions already in the supplied GTID set (requires
|
|
9
|
+
// gtid_mode=ON server-side)
|
|
10
|
+
const BINLOG_DUMP_GTID_COMMAND = 0x1e;
|
|
6
11
|
|
|
7
12
|
class Command extends EventEmitter {
|
|
8
13
|
constructor() {
|
|
@@ -75,6 +80,16 @@ class SimplePacket {
|
|
|
75
80
|
this.writeInt8(0);
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
writeUInt64(value) {
|
|
84
|
+
this.buffer.writeBigUInt64LE(BigInt(value), this.offset);
|
|
85
|
+
this.offset += 8;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeBuffer(value) {
|
|
89
|
+
value.copy(this.buffer, this.offset);
|
|
90
|
+
this.offset += value.length;
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
writeHeader(sequenceId) {
|
|
79
94
|
const offset = this.offset;
|
|
80
95
|
this.offset = 0;
|
|
@@ -95,20 +110,56 @@ export default function initBinlogClass(zongji) {
|
|
|
95
110
|
|
|
96
111
|
start(packet, connection) {
|
|
97
112
|
const options = zongji.get([
|
|
98
|
-
'serverId', 'position', 'filename', 'nonBlock',
|
|
113
|
+
'serverId', 'position', 'filename', 'nonBlock', 'gtidSet',
|
|
114
|
+
'requestAnnotateRows',
|
|
99
115
|
]);
|
|
100
|
-
const binlogPos = options.position || 4;
|
|
101
116
|
const serverId = options.serverId || 1;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
// Only BINLOG_DUMP_NON_BLOCK (0x01) exists server-side; the
|
|
118
|
+
// BINLOG_THROUGH_* flags from the retired protocol docs were never
|
|
119
|
+
// read by any MySQL server, and 0x02 now means "send v2 heartbeats"
|
|
120
|
+
let flags = options.nonBlock ? 1 : 0;
|
|
121
|
+
// MariaDB reuses 0x02 as BINLOG_SEND_ANNOTATE_ROWS_EVENT: without
|
|
122
|
+
// it a capability>=2 client never receives ANNOTATE_ROWS events.
|
|
123
|
+
// Never set it for MySQL, where the same bit requests v2 heartbeats
|
|
124
|
+
if (zongji.isMariaDb && options.requestAnnotateRows) {
|
|
125
|
+
flags |= 2;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let outPacket;
|
|
129
|
+
// MariaDB has no COM_BINLOG_DUMP_GTID (it answers 0x1e with
|
|
130
|
+
// ER_UNKNOWN_COM_ERROR): its GTID resume state was already sent via
|
|
131
|
+
// SET @slave_connect_state, and the plain dump below then has its
|
|
132
|
+
// filename/position ignored by the server
|
|
133
|
+
if (options.gtidSet != null && !zongji.isMariaDb) {
|
|
134
|
+
// COM_BINLOG_DUMP_GTID: empty filename and position 4 tell the
|
|
135
|
+
// server to locate the first binlog file not fully contained in
|
|
136
|
+
// the set; transactions in the set are skipped server-side.
|
|
137
|
+
// Layout (all integers little-endian): flags(2), serverId(4),
|
|
138
|
+
// filename length(4) + filename, position(8), set length(4) +
|
|
139
|
+
// encoded set. An empty set encodes as eight zero bytes and
|
|
140
|
+
// requests the server's complete history.
|
|
141
|
+
const gtidData = GtidSet.parse(options.gtidSet).encode();
|
|
142
|
+
outPacket = new SimplePacket(4 + 1 + 2 + 4 + 4 + 8 + 4 +
|
|
143
|
+
gtidData.length);
|
|
144
|
+
outPacket.writeInt8(BINLOG_DUMP_GTID_COMMAND);
|
|
145
|
+
outPacket.writeInt16(flags);
|
|
146
|
+
outPacket.writeInt32(serverId);
|
|
147
|
+
outPacket.writeInt32(0); // empty filename
|
|
148
|
+
outPacket.writeUInt64(4);
|
|
149
|
+
outPacket.writeInt32(gtidData.length);
|
|
150
|
+
outPacket.writeBuffer(gtidData);
|
|
151
|
+
} else {
|
|
152
|
+
const binlogPos = options.position || 4;
|
|
153
|
+
const filename = options.filename || '';
|
|
154
|
+
|
|
155
|
+
outPacket = new SimplePacket(
|
|
156
|
+
16 + Buffer.byteLength(filename, 'utf8'));
|
|
157
|
+
outPacket.writeInt8(BINLOG_DUMP_COMMAND);
|
|
158
|
+
outPacket.writeInt32(binlogPos);
|
|
159
|
+
outPacket.writeInt16(flags);
|
|
160
|
+
outPacket.writeInt32(serverId);
|
|
161
|
+
outPacket.writeNullTerminatedString(filename, 'utf8');
|
|
162
|
+
}
|
|
112
163
|
connection.writePacket(outPacket);
|
|
113
164
|
return Binlog.prototype.binlogData;
|
|
114
165
|
}
|
|
@@ -141,39 +192,140 @@ export default function initBinlogClass(zongji) {
|
|
|
141
192
|
let event;
|
|
142
193
|
let error;
|
|
143
194
|
|
|
195
|
+
// Guards resume-pair bookkeeping below against a stale Command:
|
|
196
|
+
// after stop()+start() a packet buffered on the old connection
|
|
197
|
+
// must not mutate the new stream's options
|
|
198
|
+
const isCurrent = !zongji.stopped && zongji.connection === connection;
|
|
199
|
+
|
|
144
200
|
// Track the current transaction GTID before event filtering, so
|
|
145
201
|
// subsequent events can carry event.gtid even when 'gtid' events
|
|
146
|
-
// themselves are excluded by includeEvents
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
202
|
+
// themselves are excluded by includeEvents. When executed-set
|
|
203
|
+
// tracking is active, commit markers and Previous_gtids also need
|
|
204
|
+
// parsing before filtering. Rotate events are parsed pre-filter
|
|
205
|
+
// too: the (filename, position) resume pair must stay coherent
|
|
206
|
+
// even when 'rotate' is excluded by includeEvents, otherwise
|
|
207
|
+
// later filtered events would advance the position into the new
|
|
208
|
+
// file while the filename still named the old one.
|
|
209
|
+
const isGtidEvent = eventName === 'gtid' ||
|
|
210
|
+
eventName === 'anonymousgtid' || eventName === 'mariadbgtid';
|
|
211
|
+
const isRotate = eventName === 'rotate';
|
|
212
|
+
// Commit markers are always parsed pre-filter: they detach the
|
|
213
|
+
// in-flight GTID from subsequent events and end the resume
|
|
214
|
+
// position freeze below, whatever includeEvents says
|
|
215
|
+
const isCommitCandidate = eventName === 'xid' ||
|
|
216
|
+
eventName === 'query' || eventName === 'xaprepare';
|
|
217
|
+
const isTrackedEvent = isGtidEvent || isRotate ||
|
|
218
|
+
isCommitCandidate ||
|
|
219
|
+
((zongji._executedGtids !== null || zongji._seedGtidsFromStream) &&
|
|
220
|
+
(eventName === 'previousgtids' ||
|
|
221
|
+
eventName === 'mariadbgtidlist'));
|
|
222
|
+
|
|
223
|
+
if (isCurrent && eventName === 'tablemap') {
|
|
224
|
+
// A multi-table statement writes all its TableMaps before
|
|
225
|
+
// any row event, so no position between the first TableMap
|
|
226
|
+
// and the commit marker is safe to resume from: with the
|
|
227
|
+
// later TableMaps unseen, their rows would be silently
|
|
228
|
+
// dropped. Freeze the resume position here; the commit
|
|
229
|
+
// marker (or, defensively, the next transaction's GTID
|
|
230
|
+
// event or a rotate, once parsed) unfreezes it, so a
|
|
231
|
+
// persisted position always replays whole transactions
|
|
232
|
+
// (at-least-once).
|
|
233
|
+
zongji._positionFrozen = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Set when this event definitively closes the current transaction;
|
|
237
|
+
// its GTID is detached only after the event itself is delivered
|
|
238
|
+
let closesTransaction = false;
|
|
239
|
+
if (isTrackedEvent) {
|
|
150
240
|
try {
|
|
151
241
|
event = binlogPacket.getEvent();
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
// All GTID/position bookkeeping is gated on isCurrent: after
|
|
243
|
+
// stop()+start() a stale packet must not fold a previous
|
|
244
|
+
// stream's GTID (possibly of the other server flavour) into
|
|
245
|
+
// the new tracker state
|
|
246
|
+
if (isCurrent) {
|
|
247
|
+
if (isGtidEvent || isRotate) {
|
|
248
|
+
// A successfully parsed group start or file boundary
|
|
249
|
+
// means any open transaction ended; a parse failure
|
|
250
|
+
// leaves the position freeze in place (stale positions
|
|
251
|
+
// are recoverable, premature advances drop rows)
|
|
252
|
+
zongji._positionFrozen = false;
|
|
253
|
+
}
|
|
254
|
+
if (isGtidEvent) {
|
|
255
|
+
// Anonymous transactions have no usable GTID; do not let
|
|
256
|
+
// a previous transaction's GTID leak onto their events
|
|
257
|
+
zongji._currentGtid =
|
|
258
|
+
eventName === 'anonymousgtid' ? undefined : event.gtid;
|
|
259
|
+
}
|
|
260
|
+
if (isRotate) {
|
|
261
|
+
// The payload position is the first event of the NEW
|
|
262
|
+
// file: the only value coherent with binlogName (the
|
|
263
|
+
// header nextPosition refers to the OLD file, 0 for the
|
|
264
|
+
// artificial rotate at dump start)
|
|
265
|
+
zongji.options.filename = event.binlogName;
|
|
266
|
+
zongji.options.position = event.position;
|
|
267
|
+
}
|
|
268
|
+
closesTransaction =
|
|
269
|
+
zongji._trackGtidProgress(eventName, event);
|
|
270
|
+
if (closesTransaction) {
|
|
271
|
+
// Unfrozen before the position updates below, so the
|
|
272
|
+
// commit event's own end position is adopted
|
|
273
|
+
zongji._positionFrozen = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
156
276
|
} catch (err) {
|
|
157
|
-
|
|
277
|
+
// A GTID that cannot be parsed must not label other events;
|
|
278
|
+
// an unparseable commit marker may have ended the current
|
|
279
|
+
// transaction, so its GTID must not linger either. The
|
|
280
|
+
// position freeze deliberately stays: the failed event might
|
|
281
|
+
// equally be mid-transaction, and a frozen position is safe
|
|
282
|
+
// (extra redelivery) where a premature advance drops rows
|
|
283
|
+
if (isCurrent && (isGtidEvent || isCommitCandidate)) {
|
|
284
|
+
zongji._currentGtid = undefined;
|
|
285
|
+
}
|
|
158
286
|
error = err;
|
|
159
287
|
}
|
|
160
288
|
}
|
|
161
289
|
|
|
162
|
-
// A
|
|
163
|
-
//
|
|
164
|
-
//
|
|
290
|
+
// A parse failure in a tracked event corrupts GTID/position
|
|
291
|
+
// bookkeeping, so it must surface even when the event itself is
|
|
292
|
+
// filtered out
|
|
165
293
|
if (zongji._skipEvent(eventName) && !error) {
|
|
294
|
+
// Filtered events must still advance the resume position, under
|
|
295
|
+
// the same rules as delivered ones (never inside a frozen
|
|
296
|
+
// transaction span, never from a rotate header, never adopting
|
|
297
|
+
// MariaDB's zero end_log_pos). On MariaDB only commits and
|
|
298
|
+
// standalone events carry a real end position, so filtering out
|
|
299
|
+
// 'xid'/'query' would otherwise freeze options.position at the
|
|
300
|
+
// start point.
|
|
301
|
+
if (isCurrent && eventName !== 'tablemap' && !isRotate &&
|
|
302
|
+
!zongji._positionFrozen && binlogPacket.nextPosition > 0) {
|
|
303
|
+
zongji.options.position = binlogPacket.nextPosition;
|
|
304
|
+
}
|
|
305
|
+
if (closesTransaction) {
|
|
306
|
+
zongji._currentGtid = undefined;
|
|
307
|
+
}
|
|
166
308
|
return Binlog.prototype.binlogData;
|
|
167
309
|
}
|
|
168
310
|
|
|
169
|
-
if (!
|
|
311
|
+
if (!isTrackedEvent && !error) {
|
|
170
312
|
try {
|
|
171
313
|
event = binlogPacket.getEvent();
|
|
172
314
|
} catch (err) {
|
|
173
315
|
error = err;
|
|
174
316
|
}
|
|
175
317
|
}
|
|
176
|
-
|
|
318
|
+
// The commit event itself still carries the transaction's GTID;
|
|
319
|
+
// whatever follows before the next GTID event belongs to no
|
|
320
|
+
// transaction. The finally preserves that invariant even if a
|
|
321
|
+
// 'binlog' handler throws while the commit event is delivered
|
|
322
|
+
try {
|
|
323
|
+
this._callback.call(this, error, event);
|
|
324
|
+
} finally {
|
|
325
|
+
if (closesTransaction) {
|
|
326
|
+
zongji._currentGtid = undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
177
329
|
}
|
|
178
330
|
|
|
179
331
|
return Binlog.prototype.binlogData;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vlasky/zongji",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MySQL binlog-based change data capture (CDC) for Node.js",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "MySQL and MariaDB binlog-based change data capture (CDC) for Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"mysql",
|
|
27
|
-
"
|
|
27
|
+
"mariadb",
|
|
28
|
+
"binlog",
|
|
29
|
+
"cdc",
|
|
30
|
+
"replication"
|
|
28
31
|
],
|
|
29
32
|
"author": "Vlad Lasky <github@vladlasky.com> (https://github.com/vlasky)",
|
|
30
33
|
"contributors": [
|