@powersync/service-module-mysql 0.6.4 → 0.7.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 +33 -0
- package/dist/api/MySQLRouteAPIAdapter.d.ts +1 -1
- package/dist/api/MySQLRouteAPIAdapter.js +1 -1
- package/dist/api/MySQLRouteAPIAdapter.js.map +1 -1
- package/dist/replication/BinLogReplicationJob.d.ts +2 -0
- package/dist/replication/BinLogReplicationJob.js +10 -3
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogReplicator.d.ts +1 -0
- package/dist/replication/BinLogReplicator.js +22 -0
- package/dist/replication/BinLogReplicator.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +17 -1
- package/dist/replication/BinLogStream.js +126 -174
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/MySQLConnectionManager.d.ts +1 -1
- package/dist/replication/MySQLConnectionManager.js +2 -1
- package/dist/replication/MySQLConnectionManager.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +54 -0
- package/dist/replication/zongji/BinLogListener.js +192 -0
- package/dist/replication/zongji/BinLogListener.js.map +1 -0
- package/dist/replication/zongji/zongji-utils.d.ts +5 -4
- package/dist/replication/zongji/zongji-utils.js +3 -0
- package/dist/replication/zongji/zongji-utils.js.map +1 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/types.js +5 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/mysql-utils.js +1 -0
- package/dist/utils/mysql-utils.js.map +1 -1
- package/package.json +10 -10
- package/src/api/MySQLRouteAPIAdapter.ts +1 -1
- package/src/replication/BinLogReplicationJob.ts +11 -3
- package/src/replication/BinLogReplicator.ts +25 -0
- package/src/replication/BinLogStream.ts +151 -201
- package/src/replication/MySQLConnectionManager.ts +2 -1
- package/src/replication/zongji/BinLogListener.ts +243 -0
- package/src/replication/zongji/zongji-utils.ts +10 -5
- package/src/types/types.ts +8 -1
- package/src/utils/mysql-utils.ts +1 -0
- package/test/src/BinLogListener.test.ts +161 -0
- package/test/src/BinLogStream.test.ts +4 -9
- package/test/src/mysql-to-sqlite.test.ts +1 -1
- package/test/src/util.ts +12 -0
- package/test/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/replication/zongji/zongji.d.ts +0 -129
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Logger,
|
|
3
|
+
logger as defaultLogger,
|
|
4
|
+
ReplicationAbortedError,
|
|
5
|
+
ReplicationAssertionError
|
|
6
|
+
} from '@powersync/lib-services-framework';
|
|
2
7
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
3
|
-
import async from 'async';
|
|
4
8
|
|
|
5
9
|
import {
|
|
6
10
|
ColumnDescriptor,
|
|
@@ -9,22 +13,22 @@ import {
|
|
|
9
13
|
MetricsEngine,
|
|
10
14
|
storage
|
|
11
15
|
} from '@powersync/service-core';
|
|
12
|
-
import mysql
|
|
13
|
-
|
|
14
|
-
import { BinLogEvent, StartOptions, TableMapEntry } from '@powersync/mysql-zongji';
|
|
16
|
+
import mysql from 'mysql2';
|
|
15
17
|
import mysqlPromise from 'mysql2/promise';
|
|
18
|
+
|
|
19
|
+
import { TableMapEntry } from '@powersync/mysql-zongji';
|
|
16
20
|
import * as common from '../common/common-index.js';
|
|
17
|
-
import { isBinlogStillAvailable, ReplicatedGTID, toColumnDescriptors } from '../common/common-index.js';
|
|
18
21
|
import { createRandomServerId, escapeMysqlTableName } from '../utils/mysql-utils.js';
|
|
19
22
|
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
20
|
-
import * as zongji_utils from './zongji/zongji-utils.js';
|
|
21
23
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
24
|
+
import { BinLogEventHandler, BinLogListener, Row } from './zongji/BinLogListener.js';
|
|
22
25
|
|
|
23
26
|
export interface BinLogStreamOptions {
|
|
24
27
|
connections: MySQLConnectionManager;
|
|
25
28
|
storage: storage.SyncRulesBucketStorage;
|
|
26
29
|
metrics: MetricsEngine;
|
|
27
30
|
abortSignal: AbortSignal;
|
|
31
|
+
logger?: Logger;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
interface MysqlRelId {
|
|
@@ -34,16 +38,14 @@ interface MysqlRelId {
|
|
|
34
38
|
|
|
35
39
|
interface WriteChangePayload {
|
|
36
40
|
type: storage.SaveOperationTag;
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
row: Row;
|
|
42
|
+
previous_row?: Row;
|
|
39
43
|
database: string;
|
|
40
44
|
table: string;
|
|
41
45
|
sourceTable: storage.SourceTable;
|
|
42
46
|
columns: Map<string, ColumnDescriptor>;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
export type Data = Record<string, any>;
|
|
46
|
-
|
|
47
49
|
export class BinlogConfigurationError extends Error {
|
|
48
50
|
constructor(message: string) {
|
|
49
51
|
super(message);
|
|
@@ -70,7 +72,21 @@ export class BinLogStream {
|
|
|
70
72
|
|
|
71
73
|
private tableCache = new Map<string | number, storage.SourceTable>();
|
|
72
74
|
|
|
75
|
+
private logger: Logger;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Time of the oldest uncommitted change, according to the source db.
|
|
79
|
+
* This is used to determine the replication lag.
|
|
80
|
+
*/
|
|
81
|
+
private oldestUncommittedChange: Date | null = null;
|
|
82
|
+
/**
|
|
83
|
+
* Keep track of whether we have done a commit or keepalive yet.
|
|
84
|
+
* We can only compute replication lag if isStartingReplication == false, or oldestUncommittedChange is present.
|
|
85
|
+
*/
|
|
86
|
+
private isStartingReplication = true;
|
|
87
|
+
|
|
73
88
|
constructor(private options: BinLogStreamOptions) {
|
|
89
|
+
this.logger = options.logger ?? defaultLogger;
|
|
74
90
|
this.storage = options.storage;
|
|
75
91
|
this.connections = options.connections;
|
|
76
92
|
this.syncRules = options.storage.getParsedSyncRules({ defaultSchema: this.defaultSchema });
|
|
@@ -148,7 +164,7 @@ export class BinLogStream {
|
|
|
148
164
|
await this.snapshotTable(connection.connection, batch, result.table);
|
|
149
165
|
await promiseConnection.query('COMMIT');
|
|
150
166
|
} catch (e) {
|
|
151
|
-
await tryRollback(promiseConnection);
|
|
167
|
+
await this.tryRollback(promiseConnection);
|
|
152
168
|
throw e;
|
|
153
169
|
}
|
|
154
170
|
} finally {
|
|
@@ -206,7 +222,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
206
222
|
[tablePattern.schema, tablePattern.name]
|
|
207
223
|
);
|
|
208
224
|
if (result[0].length == 0) {
|
|
209
|
-
logger.info(`Skipping ${tablePattern.schema}.${name} - no table exists/is not a base table`);
|
|
225
|
+
this.logger.info(`Skipping ${tablePattern.schema}.${name} - no table exists/is not a base table`);
|
|
210
226
|
continue;
|
|
211
227
|
}
|
|
212
228
|
|
|
@@ -241,16 +257,16 @@ AND table_type = 'BASE TABLE';`,
|
|
|
241
257
|
const status = await this.storage.getStatus();
|
|
242
258
|
const lastKnowGTID = status.checkpoint_lsn ? common.ReplicatedGTID.fromSerialized(status.checkpoint_lsn) : null;
|
|
243
259
|
if (status.snapshot_done && status.checkpoint_lsn) {
|
|
244
|
-
logger.info(`Initial replication already done.`);
|
|
260
|
+
this.logger.info(`Initial replication already done.`);
|
|
245
261
|
|
|
246
262
|
if (lastKnowGTID) {
|
|
247
263
|
// Check if the binlog is still available. If it isn't we need to snapshot again.
|
|
248
264
|
const connection = await this.connections.getConnection();
|
|
249
265
|
try {
|
|
250
|
-
const isAvailable = await isBinlogStillAvailable(connection, lastKnowGTID.position.filename);
|
|
266
|
+
const isAvailable = await common.isBinlogStillAvailable(connection, lastKnowGTID.position.filename);
|
|
251
267
|
if (!isAvailable) {
|
|
252
|
-
logger.info(
|
|
253
|
-
`
|
|
268
|
+
this.logger.info(
|
|
269
|
+
`BinLog file ${lastKnowGTID.position.filename} is no longer available, starting initial replication again.`
|
|
254
270
|
);
|
|
255
271
|
}
|
|
256
272
|
return isAvailable;
|
|
@@ -272,14 +288,14 @@ AND table_type = 'BASE TABLE';`,
|
|
|
272
288
|
* and starts again from scratch.
|
|
273
289
|
*/
|
|
274
290
|
async startInitialReplication() {
|
|
275
|
-
await this.storage.clear();
|
|
291
|
+
await this.storage.clear({ signal: this.abortSignal });
|
|
276
292
|
// Replication will be performed in a single transaction on this connection
|
|
277
293
|
const connection = await this.connections.getStreamingConnection();
|
|
278
294
|
const promiseConnection = (connection as mysql.Connection).promise();
|
|
279
295
|
const headGTID = await common.readExecutedGtid(promiseConnection);
|
|
280
|
-
logger.info(`Using snapshot checkpoint GTID: '${headGTID}'`);
|
|
296
|
+
this.logger.info(`Using snapshot checkpoint GTID: '${headGTID}'`);
|
|
281
297
|
try {
|
|
282
|
-
logger.info(`Starting initial replication`);
|
|
298
|
+
this.logger.info(`Starting initial replication`);
|
|
283
299
|
await promiseConnection.query<mysqlPromise.RowDataPacket[]>(
|
|
284
300
|
'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY'
|
|
285
301
|
);
|
|
@@ -288,7 +304,12 @@ AND table_type = 'BASE TABLE';`,
|
|
|
288
304
|
|
|
289
305
|
const sourceTables = this.syncRules.getSourceTables();
|
|
290
306
|
await this.storage.startBatch(
|
|
291
|
-
{
|
|
307
|
+
{
|
|
308
|
+
logger: this.logger,
|
|
309
|
+
zeroLSN: common.ReplicatedGTID.ZERO.comparable,
|
|
310
|
+
defaultSchema: this.defaultSchema,
|
|
311
|
+
storeCurrentData: true
|
|
312
|
+
},
|
|
292
313
|
async (batch) => {
|
|
293
314
|
for (let tablePattern of sourceTables) {
|
|
294
315
|
const tables = await this.getQualifiedTableNames(batch, tablePattern);
|
|
@@ -301,10 +322,10 @@ AND table_type = 'BASE TABLE';`,
|
|
|
301
322
|
await batch.commit(headGTID.comparable);
|
|
302
323
|
}
|
|
303
324
|
);
|
|
304
|
-
logger.info(`Initial replication done`);
|
|
325
|
+
this.logger.info(`Initial replication done`);
|
|
305
326
|
await promiseConnection.query('COMMIT');
|
|
306
327
|
} catch (e) {
|
|
307
|
-
await tryRollback(promiseConnection);
|
|
328
|
+
await this.tryRollback(promiseConnection);
|
|
308
329
|
throw e;
|
|
309
330
|
} finally {
|
|
310
331
|
connection.release();
|
|
@@ -316,7 +337,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
316
337
|
batch: storage.BucketStorageBatch,
|
|
317
338
|
table: storage.SourceTable
|
|
318
339
|
) {
|
|
319
|
-
logger.info(`Replicating ${table.qualifiedName}`);
|
|
340
|
+
this.logger.info(`Replicating ${table.qualifiedName}`);
|
|
320
341
|
// TODO count rows and log progress at certain batch sizes
|
|
321
342
|
|
|
322
343
|
// MAX_EXECUTION_TIME(0) hint disables execution timeout for this query
|
|
@@ -324,9 +345,9 @@ AND table_type = 'BASE TABLE';`,
|
|
|
324
345
|
const stream = query.stream();
|
|
325
346
|
|
|
326
347
|
let columns: Map<string, ColumnDescriptor> | undefined = undefined;
|
|
327
|
-
stream.on('fields', (fields: FieldPacket[]) => {
|
|
348
|
+
stream.on('fields', (fields: mysql.FieldPacket[]) => {
|
|
328
349
|
// Map the columns and their types
|
|
329
|
-
columns = toColumnDescriptors(fields);
|
|
350
|
+
columns = common.toColumnDescriptors(fields);
|
|
330
351
|
});
|
|
331
352
|
|
|
332
353
|
for await (let row of stream) {
|
|
@@ -359,7 +380,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
359
380
|
// all connections automatically closed, including this one.
|
|
360
381
|
await this.initReplication();
|
|
361
382
|
await this.streamChanges();
|
|
362
|
-
logger.info('
|
|
383
|
+
this.logger.info('BinLogStream has been shut down');
|
|
363
384
|
} catch (e) {
|
|
364
385
|
await this.storage.reportError(e);
|
|
365
386
|
throw e;
|
|
@@ -372,7 +393,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
372
393
|
connection.release();
|
|
373
394
|
|
|
374
395
|
if (errors.length > 0) {
|
|
375
|
-
throw new BinlogConfigurationError(`
|
|
396
|
+
throw new BinlogConfigurationError(`BinLog Configuration Errors: ${errors.join(', ')}`);
|
|
376
397
|
}
|
|
377
398
|
|
|
378
399
|
const initialReplicationCompleted = await this.checkInitialReplicated();
|
|
@@ -383,7 +404,12 @@ AND table_type = 'BASE TABLE';`,
|
|
|
383
404
|
// This is needed for includeSchema to work correctly.
|
|
384
405
|
const sourceTables = this.syncRules.getSourceTables();
|
|
385
406
|
await this.storage.startBatch(
|
|
386
|
-
{
|
|
407
|
+
{
|
|
408
|
+
logger: this.logger,
|
|
409
|
+
zeroLSN: common.ReplicatedGTID.ZERO.comparable,
|
|
410
|
+
defaultSchema: this.defaultSchema,
|
|
411
|
+
storeCurrentData: true
|
|
412
|
+
},
|
|
387
413
|
async (batch) => {
|
|
388
414
|
for (let tablePattern of sourceTables) {
|
|
389
415
|
await this.getQualifiedTableNames(batch, tablePattern);
|
|
@@ -407,14 +433,12 @@ AND table_type = 'BASE TABLE';`,
|
|
|
407
433
|
// Auto-activate as soon as initial replication is done
|
|
408
434
|
await this.storage.autoActivate();
|
|
409
435
|
const serverId = createRandomServerId(this.storage.group_id);
|
|
410
|
-
logger.info(`Starting replication. Created replica client with serverId:${serverId}`);
|
|
411
436
|
|
|
412
437
|
const connection = await this.connections.getConnection();
|
|
413
438
|
const { checkpoint_lsn } = await this.storage.getStatus();
|
|
414
439
|
if (checkpoint_lsn) {
|
|
415
|
-
logger.info(`Existing checkpoint found: ${checkpoint_lsn}`);
|
|
440
|
+
this.logger.info(`Existing checkpoint found: ${checkpoint_lsn}`);
|
|
416
441
|
}
|
|
417
|
-
|
|
418
442
|
const fromGTID = checkpoint_lsn
|
|
419
443
|
? common.ReplicatedGTID.fromSerialized(checkpoint_lsn)
|
|
420
444
|
: await common.readExecutedGtid(connection);
|
|
@@ -423,179 +447,92 @@ AND table_type = 'BASE TABLE';`,
|
|
|
423
447
|
|
|
424
448
|
if (!this.stopped) {
|
|
425
449
|
await this.storage.startBatch(
|
|
426
|
-
{ zeroLSN: ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: true },
|
|
450
|
+
{ zeroLSN: common.ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: true },
|
|
427
451
|
async (batch) => {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
let currentGTID: common.ReplicatedGTID | null = null;
|
|
431
|
-
|
|
432
|
-
const queue = async.queue(async (evt: BinLogEvent) => {
|
|
433
|
-
// State machine
|
|
434
|
-
switch (true) {
|
|
435
|
-
case zongji_utils.eventIsGTIDLog(evt):
|
|
436
|
-
currentGTID = common.ReplicatedGTID.fromBinLogEvent({
|
|
437
|
-
raw_gtid: {
|
|
438
|
-
server_id: evt.serverId,
|
|
439
|
-
transaction_range: evt.transactionRange
|
|
440
|
-
},
|
|
441
|
-
position: {
|
|
442
|
-
filename: binLogPositionState.filename,
|
|
443
|
-
offset: evt.nextPosition
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
break;
|
|
447
|
-
case zongji_utils.eventIsRotation(evt):
|
|
448
|
-
// Update the position
|
|
449
|
-
binLogPositionState.filename = evt.binlogName;
|
|
450
|
-
binLogPositionState.offset = evt.position;
|
|
451
|
-
break;
|
|
452
|
-
case zongji_utils.eventIsWriteMutation(evt):
|
|
453
|
-
const writeTableInfo = evt.tableMap[evt.tableId];
|
|
454
|
-
await this.writeChanges(batch, {
|
|
455
|
-
type: storage.SaveOperationTag.INSERT,
|
|
456
|
-
data: evt.rows,
|
|
457
|
-
tableEntry: writeTableInfo
|
|
458
|
-
});
|
|
459
|
-
break;
|
|
460
|
-
case zongji_utils.eventIsUpdateMutation(evt):
|
|
461
|
-
const updateTableInfo = evt.tableMap[evt.tableId];
|
|
462
|
-
await this.writeChanges(batch, {
|
|
463
|
-
type: storage.SaveOperationTag.UPDATE,
|
|
464
|
-
data: evt.rows.map((row) => row.after),
|
|
465
|
-
previous_data: evt.rows.map((row) => row.before),
|
|
466
|
-
tableEntry: updateTableInfo
|
|
467
|
-
});
|
|
468
|
-
break;
|
|
469
|
-
case zongji_utils.eventIsDeleteMutation(evt):
|
|
470
|
-
const deleteTableInfo = evt.tableMap[evt.tableId];
|
|
471
|
-
await this.writeChanges(batch, {
|
|
472
|
-
type: storage.SaveOperationTag.DELETE,
|
|
473
|
-
data: evt.rows,
|
|
474
|
-
tableEntry: deleteTableInfo
|
|
475
|
-
});
|
|
476
|
-
break;
|
|
477
|
-
case zongji_utils.eventIsXid(evt):
|
|
478
|
-
this.metrics.getCounter(ReplicationMetric.TRANSACTIONS_REPLICATED).add(1);
|
|
479
|
-
// Need to commit with a replicated GTID with updated next position
|
|
480
|
-
await batch.commit(
|
|
481
|
-
new common.ReplicatedGTID({
|
|
482
|
-
raw_gtid: currentGTID!.raw,
|
|
483
|
-
position: {
|
|
484
|
-
filename: binLogPositionState.filename,
|
|
485
|
-
offset: evt.nextPosition
|
|
486
|
-
}
|
|
487
|
-
}).comparable
|
|
488
|
-
);
|
|
489
|
-
currentGTID = null;
|
|
490
|
-
// chunks_replicated_total.add(1);
|
|
491
|
-
break;
|
|
492
|
-
}
|
|
493
|
-
}, 1);
|
|
494
|
-
|
|
495
|
-
zongji.on('binlog', (evt: BinLogEvent) => {
|
|
496
|
-
if (!this.stopped) {
|
|
497
|
-
logger.info(`Received Binlog event:${evt.getEventName()}`);
|
|
498
|
-
queue.push(evt);
|
|
499
|
-
} else {
|
|
500
|
-
logger.info(`Replication is busy stopping, ignoring event ${evt.getEventName()}`);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// Set a heartbeat interval for the Zongji replication connection
|
|
505
|
-
// Zongji does not explicitly handle the heartbeat events - they are categorized as event:unknown
|
|
506
|
-
// The heartbeat events are enough to keep the connection alive for setTimeout to work on the socket.
|
|
507
|
-
await new Promise((resolve, reject) => {
|
|
508
|
-
zongji.connection.query(
|
|
509
|
-
// In nanoseconds, 10^9 = 1s
|
|
510
|
-
'set @master_heartbeat_period=28*1000000000',
|
|
511
|
-
function (error: any, results: any, fields: any) {
|
|
512
|
-
if (error) {
|
|
513
|
-
reject(error);
|
|
514
|
-
} else {
|
|
515
|
-
resolve(results);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
});
|
|
520
|
-
logger.info('Successfully set up replication connection heartbeat...');
|
|
521
|
-
|
|
522
|
-
// The _socket member is only set after a query is run on the connection, so we set the timeout after setting the heartbeat.
|
|
523
|
-
// The timeout here must be greater than the master_heartbeat_period.
|
|
524
|
-
const socket = zongji.connection._socket!;
|
|
525
|
-
socket.setTimeout(60_000, () => {
|
|
526
|
-
socket.destroy(new Error('Replication connection timeout.'));
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
if (this.stopped) {
|
|
530
|
-
// Powersync is shutting down, don't start replicating
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
logger.info(`Reading binlog from: ${binLogPositionState.filename}:${binLogPositionState.offset}`);
|
|
452
|
+
const binlogEventHandler = this.createBinlogEventHandler(batch);
|
|
535
453
|
// Only listen for changes to tables in the sync rules
|
|
536
454
|
const includedTables = [...this.tableCache.values()].map((table) => table.table);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
serverId: serverId
|
|
545
|
-
} satisfies StartOptions);
|
|
546
|
-
|
|
547
|
-
// Forever young
|
|
548
|
-
await new Promise<void>((resolve, reject) => {
|
|
549
|
-
zongji.on('error', (error) => {
|
|
550
|
-
logger.error('Binlog listener error:', error);
|
|
551
|
-
zongji.stop();
|
|
552
|
-
queue.kill();
|
|
553
|
-
reject(error);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
zongji.on('stopped', () => {
|
|
557
|
-
logger.info('Binlog listener stopped. Replication ended.');
|
|
558
|
-
resolve();
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
queue.error((error) => {
|
|
562
|
-
logger.error('Binlog listener queue error:', error);
|
|
563
|
-
zongji.stop();
|
|
564
|
-
queue.kill();
|
|
565
|
-
reject(error);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const stop = () => {
|
|
569
|
-
logger.info('Abort signal received, stopping replication...');
|
|
570
|
-
zongji.stop();
|
|
571
|
-
queue.kill();
|
|
572
|
-
resolve();
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
this.abortSignal.addEventListener('abort', stop, { once: true });
|
|
576
|
-
|
|
577
|
-
if (this.stopped) {
|
|
578
|
-
// Generally this should have been picked up early, but we add this here as a failsafe.
|
|
579
|
-
stop();
|
|
580
|
-
}
|
|
455
|
+
const binlogListener = new BinLogListener({
|
|
456
|
+
logger: this.logger,
|
|
457
|
+
includedTables: includedTables,
|
|
458
|
+
startPosition: binLogPositionState,
|
|
459
|
+
connectionManager: this.connections,
|
|
460
|
+
serverId: serverId,
|
|
461
|
+
eventHandler: binlogEventHandler
|
|
581
462
|
});
|
|
463
|
+
|
|
464
|
+
this.abortSignal.addEventListener(
|
|
465
|
+
'abort',
|
|
466
|
+
() => {
|
|
467
|
+
this.logger.info('Abort signal received, stopping replication...');
|
|
468
|
+
binlogListener.stop();
|
|
469
|
+
},
|
|
470
|
+
{ once: true }
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Only returns when the replication is stopped or interrupted by an error
|
|
474
|
+
await binlogListener.start();
|
|
582
475
|
}
|
|
583
476
|
);
|
|
584
477
|
}
|
|
585
478
|
}
|
|
586
479
|
|
|
480
|
+
private createBinlogEventHandler(batch: storage.BucketStorageBatch): BinLogEventHandler {
|
|
481
|
+
return {
|
|
482
|
+
onWrite: async (rows: Row[], tableMap: TableMapEntry) => {
|
|
483
|
+
await this.writeChanges(batch, {
|
|
484
|
+
type: storage.SaveOperationTag.INSERT,
|
|
485
|
+
rows: rows,
|
|
486
|
+
tableEntry: tableMap
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
onUpdate: async (rowsAfter: Row[], rowsBefore: Row[], tableMap: TableMapEntry) => {
|
|
491
|
+
await this.writeChanges(batch, {
|
|
492
|
+
type: storage.SaveOperationTag.UPDATE,
|
|
493
|
+
rows: rowsAfter,
|
|
494
|
+
rows_before: rowsBefore,
|
|
495
|
+
tableEntry: tableMap
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
onDelete: async (rows: Row[], tableMap: TableMapEntry) => {
|
|
499
|
+
await this.writeChanges(batch, {
|
|
500
|
+
type: storage.SaveOperationTag.DELETE,
|
|
501
|
+
rows: rows,
|
|
502
|
+
tableEntry: tableMap
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
onCommit: async (lsn: string) => {
|
|
506
|
+
this.metrics.getCounter(ReplicationMetric.TRANSACTIONS_REPLICATED).add(1);
|
|
507
|
+
const didCommit = await batch.commit(lsn, { oldestUncommittedChange: this.oldestUncommittedChange });
|
|
508
|
+
if (didCommit) {
|
|
509
|
+
this.oldestUncommittedChange = null;
|
|
510
|
+
this.isStartingReplication = false;
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
onTransactionStart: async (options) => {
|
|
514
|
+
if (this.oldestUncommittedChange == null) {
|
|
515
|
+
this.oldestUncommittedChange = options.timestamp;
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
onRotate: async () => {
|
|
519
|
+
this.isStartingReplication = false;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
587
524
|
private async writeChanges(
|
|
588
525
|
batch: storage.BucketStorageBatch,
|
|
589
526
|
msg: {
|
|
590
527
|
type: storage.SaveOperationTag;
|
|
591
|
-
|
|
592
|
-
|
|
528
|
+
rows: Row[];
|
|
529
|
+
rows_before?: Row[];
|
|
593
530
|
tableEntry: TableMapEntry;
|
|
594
531
|
}
|
|
595
532
|
): Promise<storage.FlushedResult | null> {
|
|
596
|
-
const columns = toColumnDescriptors(msg.tableEntry);
|
|
533
|
+
const columns = common.toColumnDescriptors(msg.tableEntry);
|
|
597
534
|
|
|
598
|
-
for (const [index, row] of msg.
|
|
535
|
+
for (const [index, row] of msg.rows.entries()) {
|
|
599
536
|
await this.writeChange(batch, {
|
|
600
537
|
type: msg.type,
|
|
601
538
|
database: msg.tableEntry.parentSchema,
|
|
@@ -607,8 +544,8 @@ AND table_type = 'BASE TABLE';`,
|
|
|
607
544
|
),
|
|
608
545
|
table: msg.tableEntry.tableName,
|
|
609
546
|
columns: columns,
|
|
610
|
-
|
|
611
|
-
|
|
547
|
+
row: row,
|
|
548
|
+
previous_row: msg.rows_before?.[index]
|
|
612
549
|
});
|
|
613
550
|
}
|
|
614
551
|
return null;
|
|
@@ -621,7 +558,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
621
558
|
switch (payload.type) {
|
|
622
559
|
case storage.SaveOperationTag.INSERT:
|
|
623
560
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
624
|
-
const record = common.toSQLiteRow(payload.
|
|
561
|
+
const record = common.toSQLiteRow(payload.row, payload.columns);
|
|
625
562
|
return await batch.save({
|
|
626
563
|
tag: storage.SaveOperationTag.INSERT,
|
|
627
564
|
sourceTable: payload.sourceTable,
|
|
@@ -634,10 +571,10 @@ AND table_type = 'BASE TABLE';`,
|
|
|
634
571
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
635
572
|
// "before" may be null if the replica id columns are unchanged
|
|
636
573
|
// It's fine to treat that the same as an insert.
|
|
637
|
-
const beforeUpdated = payload.
|
|
638
|
-
? common.toSQLiteRow(payload.
|
|
574
|
+
const beforeUpdated = payload.previous_row
|
|
575
|
+
? common.toSQLiteRow(payload.previous_row, payload.columns)
|
|
639
576
|
: undefined;
|
|
640
|
-
const after = common.toSQLiteRow(payload.
|
|
577
|
+
const after = common.toSQLiteRow(payload.row, payload.columns);
|
|
641
578
|
|
|
642
579
|
return await batch.save({
|
|
643
580
|
tag: storage.SaveOperationTag.UPDATE,
|
|
@@ -652,7 +589,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
652
589
|
|
|
653
590
|
case storage.SaveOperationTag.DELETE:
|
|
654
591
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
655
|
-
const beforeDeleted = common.toSQLiteRow(payload.
|
|
592
|
+
const beforeDeleted = common.toSQLiteRow(payload.row, payload.columns);
|
|
656
593
|
|
|
657
594
|
return await batch.save({
|
|
658
595
|
tag: storage.SaveOperationTag.DELETE,
|
|
@@ -666,12 +603,25 @@ AND table_type = 'BASE TABLE';`,
|
|
|
666
603
|
return null;
|
|
667
604
|
}
|
|
668
605
|
}
|
|
669
|
-
}
|
|
670
606
|
|
|
671
|
-
async
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
607
|
+
async getReplicationLagMillis(): Promise<number | undefined> {
|
|
608
|
+
if (this.oldestUncommittedChange == null) {
|
|
609
|
+
if (this.isStartingReplication) {
|
|
610
|
+
// We don't have anything to compute replication lag with yet.
|
|
611
|
+
return undefined;
|
|
612
|
+
} else {
|
|
613
|
+
// We don't have any uncommitted changes, so replication is up-to-date.
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return Date.now() - this.oldestUncommittedChange.getTime();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async tryRollback(promiseConnection: mysqlPromise.Connection) {
|
|
621
|
+
try {
|
|
622
|
+
await promiseConnection.query('ROLLBACK');
|
|
623
|
+
} catch (e) {
|
|
624
|
+
this.logger.error('Failed to rollback transaction', e);
|
|
625
|
+
}
|
|
676
626
|
}
|
|
677
627
|
}
|
|
@@ -2,8 +2,8 @@ import { NormalizedMySQLConnectionConfig } from '../types/types.js';
|
|
|
2
2
|
import mysqlPromise from 'mysql2/promise';
|
|
3
3
|
import mysql, { FieldPacket, RowDataPacket } from 'mysql2';
|
|
4
4
|
import * as mysql_utils from '../utils/mysql-utils.js';
|
|
5
|
-
import ZongJi from '@powersync/mysql-zongji';
|
|
6
5
|
import { logger } from '@powersync/lib-services-framework';
|
|
6
|
+
import { ZongJi } from '@powersync/mysql-zongji';
|
|
7
7
|
|
|
8
8
|
export class MySQLConnectionManager {
|
|
9
9
|
/**
|
|
@@ -46,6 +46,7 @@ export class MySQLConnectionManager {
|
|
|
46
46
|
createBinlogListener(): ZongJi {
|
|
47
47
|
const listener = new ZongJi({
|
|
48
48
|
host: this.options.hostname,
|
|
49
|
+
port: this.options.port,
|
|
49
50
|
user: this.options.username,
|
|
50
51
|
password: this.options.password
|
|
51
52
|
});
|