@powersync/service-module-mssql 0.0.1
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/LICENSE +67 -0
- package/README.md +3 -0
- package/ci/init-mssql.sql +50 -0
- package/dist/api/MSSQLRouteAPIAdapter.d.ts +21 -0
- package/dist/api/MSSQLRouteAPIAdapter.js +248 -0
- package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -0
- package/dist/common/LSN.d.ts +37 -0
- package/dist/common/LSN.js +64 -0
- package/dist/common/LSN.js.map +1 -0
- package/dist/common/MSSQLSourceTable.d.ts +27 -0
- package/dist/common/MSSQLSourceTable.js +35 -0
- package/dist/common/MSSQLSourceTable.js.map +1 -0
- package/dist/common/MSSQLSourceTableCache.d.ts +14 -0
- package/dist/common/MSSQLSourceTableCache.js +28 -0
- package/dist/common/MSSQLSourceTableCache.js.map +1 -0
- package/dist/common/mssqls-to-sqlite.d.ts +18 -0
- package/dist/common/mssqls-to-sqlite.js +143 -0
- package/dist/common/mssqls-to-sqlite.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/module/MSSQLModule.d.ts +15 -0
- package/dist/module/MSSQLModule.js +68 -0
- package/dist/module/MSSQLModule.js.map +1 -0
- package/dist/replication/CDCPoller.d.ts +67 -0
- package/dist/replication/CDCPoller.js +183 -0
- package/dist/replication/CDCPoller.js.map +1 -0
- package/dist/replication/CDCReplicationJob.d.ts +17 -0
- package/dist/replication/CDCReplicationJob.js +76 -0
- package/dist/replication/CDCReplicationJob.js.map +1 -0
- package/dist/replication/CDCReplicator.d.ts +18 -0
- package/dist/replication/CDCReplicator.js +55 -0
- package/dist/replication/CDCReplicator.js.map +1 -0
- package/dist/replication/CDCStream.d.ts +106 -0
- package/dist/replication/CDCStream.js +536 -0
- package/dist/replication/CDCStream.js.map +1 -0
- package/dist/replication/MSSQLConnectionManager.d.ts +23 -0
- package/dist/replication/MSSQLConnectionManager.js +97 -0
- package/dist/replication/MSSQLConnectionManager.js.map +1 -0
- package/dist/replication/MSSQLConnectionManagerFactory.d.ts +10 -0
- package/dist/replication/MSSQLConnectionManagerFactory.js +28 -0
- package/dist/replication/MSSQLConnectionManagerFactory.js.map +1 -0
- package/dist/replication/MSSQLErrorRateLimiter.d.ts +10 -0
- package/dist/replication/MSSQLErrorRateLimiter.js +34 -0
- package/dist/replication/MSSQLErrorRateLimiter.js.map +1 -0
- package/dist/replication/MSSQLSnapshotQuery.d.ts +71 -0
- package/dist/replication/MSSQLSnapshotQuery.js +190 -0
- package/dist/replication/MSSQLSnapshotQuery.js.map +1 -0
- package/dist/types/mssql-data-types.d.ts +66 -0
- package/dist/types/mssql-data-types.js +62 -0
- package/dist/types/mssql-data-types.js.map +1 -0
- package/dist/types/types.d.ts +177 -0
- package/dist/types/types.js +141 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/mssql.d.ts +80 -0
- package/dist/utils/mssql.js +329 -0
- package/dist/utils/mssql.js.map +1 -0
- package/dist/utils/schema.d.ts +21 -0
- package/dist/utils/schema.js +131 -0
- package/dist/utils/schema.js.map +1 -0
- package/package.json +51 -0
- package/src/api/MSSQLRouteAPIAdapter.ts +283 -0
- package/src/common/LSN.ts +77 -0
- package/src/common/MSSQLSourceTable.ts +54 -0
- package/src/common/MSSQLSourceTableCache.ts +36 -0
- package/src/common/mssqls-to-sqlite.ts +151 -0
- package/src/index.ts +1 -0
- package/src/module/MSSQLModule.ts +82 -0
- package/src/replication/CDCPoller.ts +241 -0
- package/src/replication/CDCReplicationJob.ts +87 -0
- package/src/replication/CDCReplicator.ts +70 -0
- package/src/replication/CDCStream.ts +688 -0
- package/src/replication/MSSQLConnectionManager.ts +113 -0
- package/src/replication/MSSQLConnectionManagerFactory.ts +33 -0
- package/src/replication/MSSQLErrorRateLimiter.ts +36 -0
- package/src/replication/MSSQLSnapshotQuery.ts +230 -0
- package/src/types/mssql-data-types.ts +79 -0
- package/src/types/types.ts +224 -0
- package/src/utils/mssql.ts +420 -0
- package/src/utils/schema.ts +172 -0
- package/test/src/CDCStream.test.ts +206 -0
- package/test/src/CDCStreamTestContext.ts +212 -0
- package/test/src/CDCStream_resumable_snapshot.test.ts +152 -0
- package/test/src/env.ts +11 -0
- package/test/src/mssql-to-sqlite.test.ts +474 -0
- package/test/src/setup.ts +12 -0
- package/test/src/util.ts +189 -0
- package/test/tsconfig.json +28 -0
- package/test/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.json +26 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Logger, logger as defaultLogger, ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
+
import timers from 'timers/promises';
|
|
3
|
+
import { MSSQLConnectionManager } from './MSSQLConnectionManager.js';
|
|
4
|
+
import { MSSQLSourceTable } from '../common/MSSQLSourceTable.js';
|
|
5
|
+
import { LSN } from '../common/LSN.js';
|
|
6
|
+
import sql from 'mssql';
|
|
7
|
+
import { getMinLSN, incrementLSN } from '../utils/mssql.js';
|
|
8
|
+
import { CDCPollingOptions } from '../types/types.js';
|
|
9
|
+
|
|
10
|
+
enum Operation {
|
|
11
|
+
DELETE = 1,
|
|
12
|
+
INSERT = 2,
|
|
13
|
+
UPDATE_BEFORE = 3,
|
|
14
|
+
UPDATE_AFTER = 4
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Schema changes that are detectable by inspecting query events.
|
|
18
|
+
* Create table statements are not included here, since new tables are automatically detected when row events
|
|
19
|
+
* are received for them.
|
|
20
|
+
*/
|
|
21
|
+
export enum SchemaChangeType {
|
|
22
|
+
RENAME_TABLE = 'Rename Table',
|
|
23
|
+
DROP_TABLE = 'Drop Table',
|
|
24
|
+
TRUNCATE_TABLE = 'Truncate Table',
|
|
25
|
+
ALTER_TABLE_COLUMN = 'Alter Table Column',
|
|
26
|
+
REPLICATION_IDENTITY = 'Alter Replication Identity'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SchemaChange {
|
|
30
|
+
type: SchemaChangeType;
|
|
31
|
+
/**
|
|
32
|
+
* The table that the schema change applies to.
|
|
33
|
+
*/
|
|
34
|
+
table: string;
|
|
35
|
+
schema: string;
|
|
36
|
+
/**
|
|
37
|
+
* Populated for table renames if the newTable was matched by the DatabaseFilter
|
|
38
|
+
*/
|
|
39
|
+
newTable?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CDCEventHandler {
|
|
43
|
+
onInsert: (row: any, table: MSSQLSourceTable, collumns: sql.IColumnMetadata) => Promise<void>;
|
|
44
|
+
onUpdate: (rowAfter: any, rowBefore: any, table: MSSQLSourceTable, collumns: sql.IColumnMetadata) => Promise<void>;
|
|
45
|
+
onDelete: (row: any, table: MSSQLSourceTable, collumns: sql.IColumnMetadata) => Promise<void>;
|
|
46
|
+
onCommit: (lsn: string, transactionCount: number) => Promise<void>;
|
|
47
|
+
onSchemaChange: (change: SchemaChange) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CDCPollerOptions {
|
|
51
|
+
connectionManager: MSSQLConnectionManager;
|
|
52
|
+
eventHandler: CDCEventHandler;
|
|
53
|
+
sourceTables: MSSQLSourceTable[];
|
|
54
|
+
startLSN: LSN;
|
|
55
|
+
pollingOptions: CDCPollingOptions;
|
|
56
|
+
logger?: Logger;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
*
|
|
61
|
+
*/
|
|
62
|
+
export class CDCPoller {
|
|
63
|
+
private connectionManager: MSSQLConnectionManager;
|
|
64
|
+
private eventHandler: CDCEventHandler;
|
|
65
|
+
private currentLSN: LSN;
|
|
66
|
+
private logger: Logger;
|
|
67
|
+
private listenerError: Error | null;
|
|
68
|
+
|
|
69
|
+
private isStopped: boolean = false;
|
|
70
|
+
private isStopping: boolean = false;
|
|
71
|
+
private isPolling: boolean = false;
|
|
72
|
+
|
|
73
|
+
constructor(public options: CDCPollerOptions) {
|
|
74
|
+
this.logger = options.logger ?? defaultLogger;
|
|
75
|
+
this.connectionManager = options.connectionManager;
|
|
76
|
+
this.eventHandler = options.eventHandler;
|
|
77
|
+
this.currentLSN = options.startLSN;
|
|
78
|
+
this.listenerError = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private get pollingBatchSize(): number {
|
|
82
|
+
return this.options.pollingOptions.batchSize;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private get pollingIntervalMs(): number {
|
|
86
|
+
return this.options.pollingOptions.intervalMs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private get sourceTables(): MSSQLSourceTable[] {
|
|
90
|
+
return this.options.sourceTables;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async stop(): Promise<void> {
|
|
94
|
+
if (!(this.isStopped || this.isStopping)) {
|
|
95
|
+
this.isStopping = true;
|
|
96
|
+
this.isStopped = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public async replicateUntilStopped(): Promise<void> {
|
|
101
|
+
this.logger.info(`CDC polling started with interval of ${this.pollingIntervalMs}ms...`);
|
|
102
|
+
this.logger.info(`Polling a maximum of [${this.pollingBatchSize}] transactions per polling cycle.`);
|
|
103
|
+
while (!this.isStopped) {
|
|
104
|
+
// Don't poll if already polling (concurrency guard)
|
|
105
|
+
if (this.isPolling) {
|
|
106
|
+
await timers.setTimeout(this.pollingIntervalMs);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const hasChanges = await this.poll();
|
|
112
|
+
if (!hasChanges) {
|
|
113
|
+
// No changes found, wait before next poll
|
|
114
|
+
await timers.setTimeout(this.pollingIntervalMs);
|
|
115
|
+
}
|
|
116
|
+
// If changes were found, poll immediately again (no wait)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!(this.isStopped || this.isStopping)) {
|
|
119
|
+
this.listenerError = error as Error;
|
|
120
|
+
this.logger.error('Error during CDC polling:', error);
|
|
121
|
+
this.stop();
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.listenerError) {
|
|
128
|
+
this.logger.error('CDC polling was stopped due to an error:', this.listenerError);
|
|
129
|
+
throw this.listenerError;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.logger.info(`CDC polling stopped...`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async poll(): Promise<boolean> {
|
|
136
|
+
// Set polling flag to prevent concurrent polling cycles
|
|
137
|
+
this.isPolling = true;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Calculate the LSN bounds for this batch
|
|
141
|
+
// CDC bounds are inclusive, so the new startLSN is the currentLSN incremented by 1
|
|
142
|
+
const startLSN = await incrementLSN(this.currentLSN, this.connectionManager);
|
|
143
|
+
|
|
144
|
+
const { recordset: results } = await this.connectionManager.query(
|
|
145
|
+
`SELECT TOP (${this.pollingBatchSize}) start_lsn
|
|
146
|
+
FROM cdc.lsn_time_mapping
|
|
147
|
+
WHERE start_lsn >= @startLSN
|
|
148
|
+
ORDER BY start_lsn ASC
|
|
149
|
+
`,
|
|
150
|
+
[{ name: 'startLSN', type: sql.VarBinary, value: startLSN.toBinary() }]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// No new LSNs found, no changes to process
|
|
154
|
+
if (results.length === 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The new endLSN is the largest LSN in the result
|
|
159
|
+
const endLSN = LSN.fromBinary(results[results.length - 1].start_lsn);
|
|
160
|
+
|
|
161
|
+
this.logger.info(`Polling bounds are ${startLSN} -> ${endLSN} spanning ${results.length} transaction(s).`);
|
|
162
|
+
|
|
163
|
+
let transactionCount = 0;
|
|
164
|
+
for (const table of this.sourceTables) {
|
|
165
|
+
const tableTransactionCount = await this.pollTable(table, { startLSN, endLSN });
|
|
166
|
+
// We poll for batch size transactions, but these include transactions not applicable to our Source Tables.
|
|
167
|
+
// Each Source Table may or may not have transactions that are applicable to it, so just keep track of the highest number of transactions processed for any Source Table.
|
|
168
|
+
if (tableTransactionCount > transactionCount) {
|
|
169
|
+
transactionCount = tableTransactionCount;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.logger.info(
|
|
174
|
+
`Processed ${results.length} transaction(s), including ${transactionCount} Source Table transaction(s).`
|
|
175
|
+
);
|
|
176
|
+
// Call eventHandler.onCommit() with toLSN after processing all tables
|
|
177
|
+
await this.eventHandler.onCommit(endLSN.toString(), transactionCount);
|
|
178
|
+
|
|
179
|
+
this.currentLSN = endLSN;
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
} finally {
|
|
183
|
+
// Always clear polling flag, even on error
|
|
184
|
+
this.isPolling = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async pollTable(table: MSSQLSourceTable, bounds: { startLSN: LSN; endLSN: LSN }): Promise<number> {
|
|
189
|
+
// Ensure that the startLSN is not before the minimum LSN for the table
|
|
190
|
+
const minLSN = await getMinLSN(this.connectionManager, table.captureInstance);
|
|
191
|
+
if (minLSN > bounds.endLSN) {
|
|
192
|
+
return 0;
|
|
193
|
+
} else if (minLSN >= bounds.startLSN) {
|
|
194
|
+
bounds.startLSN = minLSN;
|
|
195
|
+
}
|
|
196
|
+
const { recordset: results } = await this.connectionManager.query(
|
|
197
|
+
`
|
|
198
|
+
SELECT * FROM ${table.allChangesFunction}(@from_lsn, @to_lsn, 'all update old') ORDER BY __$start_lsn, __$seqval
|
|
199
|
+
`,
|
|
200
|
+
[
|
|
201
|
+
{ name: 'from_lsn', type: sql.VarBinary, value: bounds.startLSN.toBinary() },
|
|
202
|
+
{ name: 'to_lsn', type: sql.VarBinary, value: bounds.endLSN.toBinary() }
|
|
203
|
+
]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
let transactionCount = 0;
|
|
207
|
+
let updateBefore: any = null;
|
|
208
|
+
for (const row of results) {
|
|
209
|
+
const transactionLSN = LSN.fromBinary(row.__$start_lsn);
|
|
210
|
+
switch (row.__$operation) {
|
|
211
|
+
case Operation.DELETE:
|
|
212
|
+
await this.eventHandler.onDelete(row, table, results.columns);
|
|
213
|
+
transactionCount++;
|
|
214
|
+
this.logger.info(`Processed DELETE row LSN: ${transactionLSN}`);
|
|
215
|
+
break;
|
|
216
|
+
case Operation.INSERT:
|
|
217
|
+
await this.eventHandler.onInsert(row, table, results.columns);
|
|
218
|
+
transactionCount++;
|
|
219
|
+
this.logger.info(`Processed INSERT row LSN: ${transactionLSN}`);
|
|
220
|
+
break;
|
|
221
|
+
case Operation.UPDATE_BEFORE:
|
|
222
|
+
updateBefore = row;
|
|
223
|
+
this.logger.debug(`Processed UPDATE, before row LSN: ${transactionLSN}`);
|
|
224
|
+
break;
|
|
225
|
+
case Operation.UPDATE_AFTER:
|
|
226
|
+
if (updateBefore === null) {
|
|
227
|
+
throw new ReplicationAssertionError('Missing before image for update event.');
|
|
228
|
+
}
|
|
229
|
+
await this.eventHandler.onUpdate(row, updateBefore, table, results.columns);
|
|
230
|
+
updateBefore = null;
|
|
231
|
+
transactionCount++;
|
|
232
|
+
this.logger.info(`Processed UPDATE row LSN: ${transactionLSN}`);
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
this.logger.warn(`Unknown operation type [${row.__$operation}] encountered in CDC changes.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return transactionCount;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { replication } from '@powersync/service-core';
|
|
2
|
+
import { MSSQLConnectionManagerFactory } from './MSSQLConnectionManagerFactory.js';
|
|
3
|
+
import { container, logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
4
|
+
import { CDCDataExpiredError, CDCStream } from './CDCStream.js';
|
|
5
|
+
import { CDCPollingOptions } from '../types/types.js';
|
|
6
|
+
|
|
7
|
+
export interface CDCReplicationJobOptions extends replication.AbstractReplicationJobOptions {
|
|
8
|
+
connectionFactory: MSSQLConnectionManagerFactory;
|
|
9
|
+
pollingOptions: CDCPollingOptions;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CDCReplicationJob extends replication.AbstractReplicationJob {
|
|
13
|
+
private connectionFactory: MSSQLConnectionManagerFactory;
|
|
14
|
+
private lastStream: CDCStream | null = null;
|
|
15
|
+
private cdcReplicationJobOptions: CDCReplicationJobOptions;
|
|
16
|
+
|
|
17
|
+
constructor(options: CDCReplicationJobOptions) {
|
|
18
|
+
super(options);
|
|
19
|
+
this.logger = defaultLogger.child({ prefix: `[powersync_${this.options.storage.group_id}] ` });
|
|
20
|
+
this.connectionFactory = options.connectionFactory;
|
|
21
|
+
this.cdcReplicationJobOptions = options;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async keepAlive() {
|
|
25
|
+
// TODO Might need to leverage checkpoints table as a keepAlive
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async replicate() {
|
|
29
|
+
try {
|
|
30
|
+
await this.replicateOnce();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// Fatal exception
|
|
33
|
+
if (!this.isStopped) {
|
|
34
|
+
// Ignore aborted errors
|
|
35
|
+
this.logger.error(`Replication error`, e);
|
|
36
|
+
if (e.cause != null) {
|
|
37
|
+
this.logger.error(`cause`, e.cause);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
container.reporter.captureException(e, {
|
|
41
|
+
metadata: {}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// This sets the retry delay
|
|
45
|
+
this.rateLimiter.reportError(e);
|
|
46
|
+
}
|
|
47
|
+
if (e instanceof CDCDataExpiredError) {
|
|
48
|
+
// This stops replication and restarts with a new instance
|
|
49
|
+
await this.options.storage.factory.restartReplication(this.storage.group_id);
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
this.abortController.abort();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async replicateOnce() {
|
|
57
|
+
// New connections on every iteration (every error with retry),
|
|
58
|
+
// otherwise we risk repeating errors related to the connection,
|
|
59
|
+
// such as caused by cached PG schemas.
|
|
60
|
+
const connectionManager = this.connectionFactory.create({
|
|
61
|
+
idleTimeoutMillis: 30_000,
|
|
62
|
+
max: 2
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
await this.rateLimiter?.waitUntilAllowed({ signal: this.abortController.signal });
|
|
66
|
+
if (this.isStopped) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const stream = new CDCStream({
|
|
70
|
+
logger: this.logger,
|
|
71
|
+
abortSignal: this.abortController.signal,
|
|
72
|
+
storage: this.options.storage,
|
|
73
|
+
metrics: this.options.metrics,
|
|
74
|
+
connections: connectionManager,
|
|
75
|
+
pollingOptions: this.cdcReplicationJobOptions.pollingOptions
|
|
76
|
+
});
|
|
77
|
+
this.lastStream = stream;
|
|
78
|
+
await stream.replicate();
|
|
79
|
+
} finally {
|
|
80
|
+
await connectionManager.end();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getReplicationLagMillis(): Promise<number | undefined> {
|
|
85
|
+
return this.lastStream?.getReplicationLagMillis();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { replication, storage } from '@powersync/service-core';
|
|
2
|
+
import { MSSQLConnectionManagerFactory } from './MSSQLConnectionManagerFactory.js';
|
|
3
|
+
import { CDCReplicationJob } from './CDCReplicationJob.js';
|
|
4
|
+
import { MSSQLModule } from '../module/MSSQLModule.js';
|
|
5
|
+
import { CDCPollingOptions } from '../types/types.js';
|
|
6
|
+
|
|
7
|
+
export interface CDCReplicatorOptions extends replication.AbstractReplicatorOptions {
|
|
8
|
+
connectionFactory: MSSQLConnectionManagerFactory;
|
|
9
|
+
pollingOptions: CDCPollingOptions;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CDCReplicator extends replication.AbstractReplicator<CDCReplicationJob> {
|
|
13
|
+
private readonly connectionFactory: MSSQLConnectionManagerFactory;
|
|
14
|
+
private readonly cdcReplicatorOptions: CDCReplicatorOptions;
|
|
15
|
+
|
|
16
|
+
constructor(options: CDCReplicatorOptions) {
|
|
17
|
+
super(options);
|
|
18
|
+
this.connectionFactory = options.connectionFactory;
|
|
19
|
+
this.cdcReplicatorOptions = options;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
createJob(options: replication.CreateJobOptions): CDCReplicationJob {
|
|
23
|
+
return new CDCReplicationJob({
|
|
24
|
+
id: this.createJobId(options.storage.group_id),
|
|
25
|
+
storage: options.storage,
|
|
26
|
+
metrics: this.metrics,
|
|
27
|
+
lock: options.lock,
|
|
28
|
+
connectionFactory: this.connectionFactory,
|
|
29
|
+
rateLimiter: this.rateLimiter,
|
|
30
|
+
pollingOptions: this.cdcReplicatorOptions.pollingOptions
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async cleanUp(syncRulesStorage: storage.SyncRulesBucketStorage): Promise<void> {}
|
|
35
|
+
|
|
36
|
+
async stop(): Promise<void> {
|
|
37
|
+
await super.stop();
|
|
38
|
+
await this.connectionFactory.shutdown();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async testConnection() {
|
|
42
|
+
return await MSSQLModule.testConnection(this.connectionFactory.connectionConfig);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getReplicationLagMillis(): Promise<number | undefined> {
|
|
46
|
+
// TODO:Get replication lag
|
|
47
|
+
const lag = await super.getReplicationLagMillis();
|
|
48
|
+
if (lag != null) {
|
|
49
|
+
return lag;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Booting or in an error loop. Check last active replication status.
|
|
53
|
+
// This includes sync rules in an ERROR state.
|
|
54
|
+
const content = await this.storage.getActiveSyncRulesContent();
|
|
55
|
+
if (content == null) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
// Measure the lag from the last commit or keepalive timestamp.
|
|
59
|
+
// This is not 100% accurate since it is the commit time in the storage db rather than
|
|
60
|
+
// the source db, but it's the best we currently have for mssql.
|
|
61
|
+
const checkpointTs = content.last_checkpoint_ts?.getTime() ?? 0;
|
|
62
|
+
const keepaliveTs = content.last_keepalive_ts?.getTime() ?? 0;
|
|
63
|
+
const latestTs = Math.max(checkpointTs, keepaliveTs);
|
|
64
|
+
if (latestTs != 0) {
|
|
65
|
+
return Date.now() - latestTs;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|