@powersync/service-module-mysql 0.16.0 → 0.16.2
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 +40 -0
- package/dist/replication/BinLogReplicationJob.js +3 -1
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +6 -0
- package/dist/replication/BinLogStream.js +17 -5
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/MySQLConnectionManager.js +6 -0
- package/dist/replication/MySQLConnectionManager.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +24 -0
- package/dist/replication/zongji/BinLogListener.js +62 -1
- package/dist/replication/zongji/BinLogListener.js.map +1 -1
- package/dist/utils/mysql-utils.d.ts +6 -0
- package/dist/utils/mysql-utils.js +10 -0
- package/dist/utils/mysql-utils.js.map +1 -1
- package/package.json +10 -10
- package/src/replication/BinLogReplicationJob.ts +3 -1
- package/src/replication/BinLogStream.ts +19 -5
- package/src/replication/MySQLConnectionManager.ts +6 -0
- package/src/replication/zongji/BinLogListener.ts +70 -1
- package/src/utils/mysql-utils.ts +11 -0
- package/test/src/BinLogListener.test.ts +122 -1
- package/test/src/mysql-utils.test.ts +19 -1
- package/test/src/util.ts +3 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -79,6 +79,8 @@ export class BinLogStream {
|
|
|
79
79
|
|
|
80
80
|
private replicationLag = new ReplicationLagTracker();
|
|
81
81
|
|
|
82
|
+
private binLogListener: BinLogListener | null = null;
|
|
83
|
+
|
|
82
84
|
constructor(private options: BinLogStreamOptions) {
|
|
83
85
|
this.logger = options.logger ?? defaultLogger;
|
|
84
86
|
this.storage = options.storage;
|
|
@@ -278,7 +280,8 @@ export class BinLogStream {
|
|
|
278
280
|
logger: this.logger,
|
|
279
281
|
zeroLSN: common.ReplicatedGTID.ZERO(this.activeServerUuid!).comparable,
|
|
280
282
|
defaultSchema: this.defaultSchema,
|
|
281
|
-
storeCurrentData: false
|
|
283
|
+
storeCurrentData: false,
|
|
284
|
+
signal: this.abortSignal
|
|
282
285
|
},
|
|
283
286
|
async (batch) => {
|
|
284
287
|
for (let tablePattern of sourceTables) {
|
|
@@ -305,8 +308,8 @@ export class BinLogStream {
|
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
if (lastOp != null) {
|
|
308
|
-
//
|
|
309
|
-
await this.storage.
|
|
311
|
+
// Compact storage _after_ initial replication, but _before_ we switch to this replication stream.
|
|
312
|
+
await this.storage.compactInitialReplication({
|
|
310
313
|
// No checkpoint yet, but we do have the opId.
|
|
311
314
|
maxOpId: lastOp,
|
|
312
315
|
signal: this.abortSignal
|
|
@@ -405,7 +408,8 @@ export class BinLogStream {
|
|
|
405
408
|
logger: this.logger,
|
|
406
409
|
zeroLSN: common.ReplicatedGTID.ZERO(this.activeServerUuid!).comparable,
|
|
407
410
|
defaultSchema: this.defaultSchema,
|
|
408
|
-
storeCurrentData: false
|
|
411
|
+
storeCurrentData: false,
|
|
412
|
+
signal: this.abortSignal
|
|
409
413
|
},
|
|
410
414
|
async (batch) => {
|
|
411
415
|
for (let tablePattern of sourceTables) {
|
|
@@ -449,7 +453,8 @@ export class BinLogStream {
|
|
|
449
453
|
{
|
|
450
454
|
zeroLSN: common.ReplicatedGTID.ZERO(this.activeServerUuid!).comparable,
|
|
451
455
|
defaultSchema: this.defaultSchema,
|
|
452
|
-
storeCurrentData: false
|
|
456
|
+
storeCurrentData: false,
|
|
457
|
+
signal: this.abortSignal
|
|
453
458
|
},
|
|
454
459
|
async (batch) => {
|
|
455
460
|
const binlogEventHandler = this.createBinlogEventHandler(batch);
|
|
@@ -462,6 +467,7 @@ export class BinLogStream {
|
|
|
462
467
|
activeServerUuid: this.activeServerUuid!,
|
|
463
468
|
eventHandler: binlogEventHandler
|
|
464
469
|
});
|
|
470
|
+
this.binLogListener = binlogListener;
|
|
465
471
|
|
|
466
472
|
this.abortSignal.addEventListener(
|
|
467
473
|
'abort',
|
|
@@ -697,6 +703,14 @@ export class BinLogStream {
|
|
|
697
703
|
return this.replicationLag.getLagMillis();
|
|
698
704
|
}
|
|
699
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Probe the liveness of the BinLog Listener's control connection. Called from the replication
|
|
708
|
+
* job's keepAlive. Does nothing before streaming starts (during the initial snapshot).
|
|
709
|
+
*/
|
|
710
|
+
probeControlConnection(): void {
|
|
711
|
+
this.binLogListener?.probeControlConnection();
|
|
712
|
+
}
|
|
713
|
+
|
|
700
714
|
async tryRollback(promiseConnection: mysqlPromise.Connection) {
|
|
701
715
|
try {
|
|
702
716
|
await promiseConnection.query('ROLLBACK');
|
|
@@ -49,11 +49,17 @@ export class MySQLConnectionManager extends BaseObserver<MySQLConnectionManagerL
|
|
|
49
49
|
* Create a new replication listener
|
|
50
50
|
*/
|
|
51
51
|
createBinlogListener(): ZongJi {
|
|
52
|
+
// These options apply to both the binlog connection and the control connection Zongji creates.
|
|
52
53
|
const listener = new ZongJi({
|
|
53
54
|
host: this.options.hostname,
|
|
54
55
|
port: this.options.port,
|
|
55
56
|
user: this.options.username,
|
|
56
57
|
password: this.options.password,
|
|
58
|
+
// TCP keepalive is disabled by default in @vlasky/mysql. Without it, the idle control
|
|
59
|
+
// connection can be silently dropped by stateful firewalls, freezing replication on the
|
|
60
|
+
// next table metadata query until the TCP retransmission timeout (~950s).
|
|
61
|
+
enableKeepAlive: true,
|
|
62
|
+
keepAliveInitialDelay: mysql_utils.TCP_KEEPALIVE_INITIAL_DELAY,
|
|
57
63
|
// We want to avoid parsing date/time values to Date, because that drops sub-millisecond precision.
|
|
58
64
|
dateStrings: true,
|
|
59
65
|
timeZone: 'Z'
|
|
@@ -33,6 +33,19 @@ const { Parser } = pkg;
|
|
|
33
33
|
* Seconds of inactivity after which a keepalive event is sent by the MySQL server.
|
|
34
34
|
*/
|
|
35
35
|
export const KEEPALIVE_INACTIVITY_THRESHOLD = 30;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maximum time in milliseconds to wait for Zongji to stop before force-closing its control connection.
|
|
39
|
+
*/
|
|
40
|
+
export const ZONGJI_STOP_TIMEOUT = 5_000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maximum time in milliseconds a control connection liveness probe may execute before the
|
|
44
|
+
* connection is considered dead. Time the probe spends queued behind other control queries
|
|
45
|
+
* does not count towards this.
|
|
46
|
+
*/
|
|
47
|
+
export const CTRL_CONNECTION_PROBE_TIMEOUT = 5_000;
|
|
48
|
+
|
|
36
49
|
export type Row = Record<string, any>;
|
|
37
50
|
|
|
38
51
|
/**
|
|
@@ -87,6 +100,7 @@ export interface BinLogListenerOptions {
|
|
|
87
100
|
startGTID: common.ReplicatedGTID;
|
|
88
101
|
logger?: Logger;
|
|
89
102
|
keepAliveInactivitySeconds?: number;
|
|
103
|
+
ctrlConnectionProbeTimeoutMs?: number;
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
/**
|
|
@@ -104,6 +118,9 @@ export class BinLogListener {
|
|
|
104
118
|
private isStopped: boolean = false;
|
|
105
119
|
private isStopping: boolean = false;
|
|
106
120
|
|
|
121
|
+
// Set while a control connection probe is awaiting a response, so repeated probes do not pile up behind it.
|
|
122
|
+
private probePending: boolean = false;
|
|
123
|
+
|
|
107
124
|
// Flag to indicate if are currently in a transaction that involves multiple row mutation events.
|
|
108
125
|
private isTransactionOpen = false;
|
|
109
126
|
|
|
@@ -222,12 +239,29 @@ export class BinLogListener {
|
|
|
222
239
|
private async stopZongji(): Promise<void> {
|
|
223
240
|
if (!this.zongji.stopped) {
|
|
224
241
|
this.logger.info('Stopping BinLog Listener...');
|
|
225
|
-
|
|
242
|
+
const controlConnection = this.zongji.ctrlConnection;
|
|
243
|
+
let stopped = false;
|
|
244
|
+
const stopPromise = new Promise<void>((resolve) => {
|
|
226
245
|
this.zongji.once('stopped', () => {
|
|
246
|
+
stopped = true;
|
|
227
247
|
resolve();
|
|
228
248
|
});
|
|
229
249
|
this.zongji.stop();
|
|
230
250
|
});
|
|
251
|
+
// Zongji only emits 'stopped' once the KILL query on its control connection has completed.
|
|
252
|
+
// If that connection has been dead for a while, the query can block on TCP retransmissions
|
|
253
|
+
// for many minutes, so we destroy the socket after a timeout to unblock the stop.
|
|
254
|
+
const timeout = timers.setTimeout(ZONGJI_STOP_TIMEOUT, undefined, { ref: false }).then(() => {
|
|
255
|
+
if (!stopped) {
|
|
256
|
+
this.logger.warn('Timed out waiting for the BinLog Listener to stop. Closing the control connection.');
|
|
257
|
+
controlConnection._socket?.destroy();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
await Promise.race([stopPromise, timeout]);
|
|
261
|
+
// Zongji destroys the control connection when it stops, which drops pending query callbacks:
|
|
262
|
+
// a probe waiting on this connection would otherwise stay pending forever and disable
|
|
263
|
+
// probing after a restart.
|
|
264
|
+
this.probePending = false;
|
|
231
265
|
this.logger.info('BinLog Listener stopped.');
|
|
232
266
|
}
|
|
233
267
|
}
|
|
@@ -253,6 +287,41 @@ export class BinLogListener {
|
|
|
253
287
|
}
|
|
254
288
|
}
|
|
255
289
|
|
|
290
|
+
/**
|
|
291
|
+
* The binlog connection is kept alive by the MySQL server heartbeat, but the control connection
|
|
292
|
+
* carries no traffic between metadata queries. TCP keepalive stops it from being dropped when idle,
|
|
293
|
+
* but detects an already dead connection slowly, so the replication job's keepAlive additionally
|
|
294
|
+
* probes it with a lightweight query and stops the listener (restarting replication) if the probe
|
|
295
|
+
* does not respond in time.
|
|
296
|
+
*
|
|
297
|
+
* The driver starts the query timeout when the query begins executing, not when it is queued, so a
|
|
298
|
+
* probe waiting behind a legitimately slow metadata query does not produce a false failure. A probe
|
|
299
|
+
* queued on a dead socket is failed together with the queued query by TCP keepalive on the socket.
|
|
300
|
+
*/
|
|
301
|
+
public probeControlConnection(): void {
|
|
302
|
+
if (this.probePending || this.zongji.stopped || this.isStopped || this.isStopping) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.probePending = true;
|
|
306
|
+
const controlConnection = this.zongji.ctrlConnection;
|
|
307
|
+
const timeout = this.options.ctrlConnectionProbeTimeoutMs ?? CTRL_CONNECTION_PROBE_TIMEOUT;
|
|
308
|
+
controlConnection.query({ sql: 'SELECT 1', timeout }, (error) => {
|
|
309
|
+
this.probePending = false;
|
|
310
|
+
// Only act if the probe failed on a connection that is still supposed to be alive.
|
|
311
|
+
if (
|
|
312
|
+
error != null &&
|
|
313
|
+
this.zongji.ctrlConnection === controlConnection &&
|
|
314
|
+
!this.zongji.stopped &&
|
|
315
|
+
!(this.isStopped || this.isStopping)
|
|
316
|
+
) {
|
|
317
|
+
this.logger.warn('MySQL control connection is unresponsive. Stopping the BinLog Listener...');
|
|
318
|
+
this.listenerError = new Error('MySQL control connection is unresponsive.');
|
|
319
|
+
controlConnection._socket?.destroy();
|
|
320
|
+
this.stop();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
256
325
|
private createProcessingQueue(): async.QueueObject<BinLogEvent> {
|
|
257
326
|
const queue = async.queue(this.createQueueWorker(), 1);
|
|
258
327
|
|
package/src/utils/mysql-utils.ts
CHANGED
|
@@ -12,6 +12,13 @@ export type RetriedQueryOptions = {
|
|
|
12
12
|
retries?: number;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* TCP keepalive initial delay in milliseconds for connections to the MySQL server.
|
|
17
|
+
* Keepalive prevents long-lived idle connections from being silently dropped by stateful
|
|
18
|
+
* firewalls, which commonly time out idle flows after an hour.
|
|
19
|
+
*/
|
|
20
|
+
export const TCP_KEEPALIVE_INITIAL_DELAY = 40_000;
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Retry a simple query - up to 2 attempts total.
|
|
17
24
|
*/
|
|
@@ -54,6 +61,10 @@ export function createPool(config: types.NormalizedMySQLConnectionConfig, option
|
|
|
54
61
|
timezone: 'Z', // Ensure no auto timezone manipulation of the dates occur
|
|
55
62
|
jsonStrings: true, // Return JSON columns as strings
|
|
56
63
|
dateStrings: true, // We parse and format them ourselves
|
|
64
|
+
// mysql2 enables TCP keepalive by default, but without an initial delay the OS default of
|
|
65
|
+
// 7200 seconds applies, which is too late for common 3600 second firewall idle timeouts.
|
|
66
|
+
enableKeepAlive: true,
|
|
67
|
+
keepAliveInitialDelay: TCP_KEEPALIVE_INITIAL_DELAY,
|
|
57
68
|
// Apply URL connection parameters (explicit options override these via spread below)
|
|
58
69
|
...(params.connectTimeout != null ? { connectTimeout: params.connectTimeout } : {}),
|
|
59
70
|
...(params.connectionLimit != null ? { connectionLimit: params.connectionLimit } : {}),
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { MySQLConnectionManager } from '@module/replication/MySQLConnectionManager.js';
|
|
2
2
|
import { BinLogListener, SchemaChange, SchemaChangeType } from '@module/replication/zongji/BinLogListener.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getMySQLVersion,
|
|
5
|
+
qualifiedMySQLTable,
|
|
6
|
+
satisfiesVersion,
|
|
7
|
+
TCP_KEEPALIVE_INITIAL_DELAY
|
|
8
|
+
} from '@module/utils/mysql-utils.js';
|
|
9
|
+
import { MySQLConnection } from '@powersync/mysql-zongji';
|
|
4
10
|
import { TablePattern } from '@powersync/service-sync-rules';
|
|
5
11
|
import crypto from 'crypto';
|
|
6
12
|
import { v4 as uuid } from 'uuid';
|
|
@@ -13,6 +19,11 @@ import {
|
|
|
13
19
|
TestBinLogEventHandler
|
|
14
20
|
} from './util.js';
|
|
15
21
|
|
|
22
|
+
// The zongji type definitions do not expose the connection config.
|
|
23
|
+
type ConnectionWithConfig = MySQLConnection & {
|
|
24
|
+
config: { enableKeepAlive?: boolean; keepAliveInitialDelay?: number };
|
|
25
|
+
};
|
|
26
|
+
|
|
16
27
|
describe('BinlogListener tests', { timeout: 60_000 }, () => {
|
|
17
28
|
const MAX_QUEUE_CAPACITY_MB = 1;
|
|
18
29
|
const BINLOG_LISTENER_CONNECTION_OPTIONS = {
|
|
@@ -59,6 +70,116 @@ describe('BinlogListener tests', { timeout: 60_000 }, () => {
|
|
|
59
70
|
|
|
60
71
|
expect(stopSpy).toHaveBeenCalled();
|
|
61
72
|
expect(queueStopSpy).toHaveBeenCalled();
|
|
73
|
+
// Zongji destroys its control connection when stopping.
|
|
74
|
+
expect(binLogListener.zongji.ctrlConnection.state).toBe('disconnected');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('TCP keepalive is enabled on the binlog and control connections', async () => {
|
|
78
|
+
// Without keepalive, the control connection can idle for hours and be silently dropped by
|
|
79
|
+
// stateful firewalls. The next metadata query then blocks until the kernel gives up on TCP
|
|
80
|
+
// retransmissions, freezing the whole binlog pipeline for ~15 minutes.
|
|
81
|
+
const { connection } = binLogListener.zongji as unknown as { connection: ConnectionWithConfig };
|
|
82
|
+
const controlConnection = binLogListener.zongji.ctrlConnection as ConnectionWithConfig;
|
|
83
|
+
|
|
84
|
+
for (const conn of [connection, controlConnection]) {
|
|
85
|
+
expect(conn.config.enableKeepAlive).toBe(true);
|
|
86
|
+
expect(conn.config.keepAliveInitialDelay).toBe(TCP_KEEPALIVE_INITIAL_DELAY);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('Stop completes when the control connection is unresponsive', { timeout: 20_000 }, async () => {
|
|
91
|
+
await binLogListener.start();
|
|
92
|
+
|
|
93
|
+
// Simulate a control connection that was silently dropped by the network: the KILL query
|
|
94
|
+
// issued by zongji.stop() never gets a response.
|
|
95
|
+
vi.spyOn(binLogListener.zongji.ctrlConnection, 'query').mockImplementation(() => {});
|
|
96
|
+
|
|
97
|
+
await binLogListener.stop();
|
|
98
|
+
|
|
99
|
+
expect(binLogListener.zongji.stopped).toBeTruthy();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Probe on a healthy control connection passes', { timeout: 20_000 }, async () => {
|
|
103
|
+
await binLogListener.start();
|
|
104
|
+
|
|
105
|
+
// No mocking: the real driver must accept the options form of query and answer the probe.
|
|
106
|
+
const controlConnection = binLogListener.zongji.ctrlConnection;
|
|
107
|
+
const realQuery = controlConnection.query.bind(controlConnection);
|
|
108
|
+
const probeError = new Promise((resolve) => {
|
|
109
|
+
vi.spyOn(controlConnection, 'query').mockImplementation(((options: any, callback: any) => {
|
|
110
|
+
realQuery(options, (error: any, results: any, fields: any) => {
|
|
111
|
+
callback(error, results, fields);
|
|
112
|
+
resolve(error);
|
|
113
|
+
});
|
|
114
|
+
}) as any);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
binLogListener.probeControlConnection();
|
|
118
|
+
|
|
119
|
+
expect(await probeError).toBeNull();
|
|
120
|
+
expect(binLogListener.zongji.stopped).toBeFalsy();
|
|
121
|
+
|
|
122
|
+
await binLogListener.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('Probe detects an unresponsive control connection', { timeout: 20_000 }, async () => {
|
|
126
|
+
binLogListener = await createBinlogListener({
|
|
127
|
+
connectionManager,
|
|
128
|
+
sourceTables: [new TablePattern(connectionManager.databaseName, 'test_DATA')],
|
|
129
|
+
eventHandler,
|
|
130
|
+
ctrlConnectionProbeTimeoutMs: 500
|
|
131
|
+
});
|
|
132
|
+
await binLogListener.start();
|
|
133
|
+
|
|
134
|
+
// The probe query starts executing but never gets a response, like a connection that died
|
|
135
|
+
// without either side being notified: the driver's query timeout fires.
|
|
136
|
+
vi.spyOn(binLogListener.zongji.ctrlConnection, 'query').mockImplementation(((_options: any, callback: any) => {
|
|
137
|
+
const error: any = new Error('Query inactivity timeout');
|
|
138
|
+
error.code = 'PROTOCOL_SEQUENCE_TIMEOUT';
|
|
139
|
+
setTimeout(() => callback(error), 10);
|
|
140
|
+
}) as any);
|
|
141
|
+
|
|
142
|
+
const replication = binLogListener.replicateUntilStopped();
|
|
143
|
+
binLogListener.probeControlConnection();
|
|
144
|
+
|
|
145
|
+
await expect(replication).rejects.toThrow('control connection is unresponsive');
|
|
146
|
+
expect(binLogListener.zongji.stopped).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('Probe queued behind a busy control connection does not report it dead', { timeout: 20_000 }, async () => {
|
|
150
|
+
binLogListener = await createBinlogListener({
|
|
151
|
+
connectionManager,
|
|
152
|
+
sourceTables: [new TablePattern(connectionManager.databaseName, 'test_DATA')],
|
|
153
|
+
eventHandler,
|
|
154
|
+
ctrlConnectionProbeTimeoutMs: 500
|
|
155
|
+
});
|
|
156
|
+
await binLogListener.start();
|
|
157
|
+
|
|
158
|
+
// The probe never even starts executing, as if queued behind a long-running metadata query on
|
|
159
|
+
// a healthy connection. The probe must not report the connection dead, and further probes must
|
|
160
|
+
// not pile up behind the pending one.
|
|
161
|
+
const querySpy = vi.spyOn(binLogListener.zongji.ctrlConnection, 'query').mockImplementation((() => {}) as any);
|
|
162
|
+
|
|
163
|
+
binLogListener.probeControlConnection();
|
|
164
|
+
binLogListener.probeControlConnection();
|
|
165
|
+
|
|
166
|
+
expect(querySpy).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(binLogListener.zongji.stopped).toBeFalsy();
|
|
168
|
+
|
|
169
|
+
querySpy.mockRestore();
|
|
170
|
+
await binLogListener.stop();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('Control connection errors stop the listener', { timeout: 20_000 }, async () => {
|
|
174
|
+
await binLogListener.start();
|
|
175
|
+
|
|
176
|
+
const replication = binLogListener.replicateUntilStopped();
|
|
177
|
+
// The driver emits 'error' when the socket fails while no query is pending. Without the
|
|
178
|
+
// forwarding set up in createBinlogListener, this event has no listener and crashes the process.
|
|
179
|
+
(binLogListener.zongji.ctrlConnection as any).emit('error', new Error('Control connection failure'));
|
|
180
|
+
|
|
181
|
+
await expect(replication).rejects.toThrow('Control connection failure');
|
|
182
|
+
expect(binLogListener.zongji.stopped).toBeTruthy();
|
|
62
183
|
});
|
|
63
184
|
|
|
64
185
|
test('Zongji listener is stopped when processing queue reaches maximum memory size', async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as types from '@module/types/types.js';
|
|
2
|
+
import { createPool, isVersionAtLeast, TCP_KEEPALIVE_INITIAL_DELAY } from '@module/utils/mysql-utils.js';
|
|
2
3
|
import { describe, expect, test } from 'vitest';
|
|
3
4
|
|
|
4
5
|
describe('MySQL Utility Tests', () => {
|
|
@@ -14,4 +15,21 @@ describe('MySQL Utility Tests', () => {
|
|
|
14
15
|
expect(isVersionAtLeast(olderVersion, '8.0')).toBeFalsy();
|
|
15
16
|
expect(isVersionAtLeast(improperSemver, '5.7')).toBeTruthy();
|
|
16
17
|
});
|
|
18
|
+
|
|
19
|
+
test('Pool connections are configured with a TCP keepalive initial delay', async () => {
|
|
20
|
+
// mysql2 enables keepalive by default, but without an initial delay the OS default of
|
|
21
|
+
// 7200 seconds applies, which is too late for common 3600 second firewall idle timeouts.
|
|
22
|
+
const config = types.normalizeConnectionConfig({
|
|
23
|
+
type: 'mysql',
|
|
24
|
+
uri: 'mysql://root:password@localhost:3306/mydatabase'
|
|
25
|
+
});
|
|
26
|
+
// The pool is lazy, so no connection is made here.
|
|
27
|
+
const pool = createPool(config);
|
|
28
|
+
const { connectionConfig } = (pool as unknown as { config: { connectionConfig: Record<string, unknown> } }).config;
|
|
29
|
+
|
|
30
|
+
expect(connectionConfig.enableKeepAlive).toBe(true);
|
|
31
|
+
expect(connectionConfig.keepAliveInitialDelay).toBe(TCP_KEEPALIVE_INITIAL_DELAY);
|
|
32
|
+
|
|
33
|
+
await pool.promise().end();
|
|
34
|
+
});
|
|
17
35
|
});
|
package/test/src/util.ts
CHANGED
|
@@ -94,6 +94,7 @@ export interface CreateBinlogListenerParams {
|
|
|
94
94
|
eventHandler: BinLogEventHandler;
|
|
95
95
|
sourceTables: TablePattern[];
|
|
96
96
|
startGTID?: common.ReplicatedGTID;
|
|
97
|
+
ctrlConnectionProbeTimeoutMs?: number;
|
|
97
98
|
}
|
|
98
99
|
export async function createBinlogListener(params: CreateBinlogListenerParams): Promise<BinLogListener> {
|
|
99
100
|
let { connectionManager, eventHandler, sourceTables, startGTID } = params;
|
|
@@ -110,7 +111,8 @@ export async function createBinlogListener(params: CreateBinlogListenerParams):
|
|
|
110
111
|
startGTID: startGTID!,
|
|
111
112
|
sourceTables: sourceTables,
|
|
112
113
|
serverId: createRandomServerId(1),
|
|
113
|
-
activeServerUuid: activeServerUuid
|
|
114
|
+
activeServerUuid: activeServerUuid,
|
|
115
|
+
ctrlConnectionProbeTimeoutMs: params.ctrlConnectionProbeTimeoutMs
|
|
114
116
|
});
|
|
115
117
|
}
|
|
116
118
|
|