@powersync/service-core 1.22.0 → 1.23.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/CHANGELOG.md +23 -0
- package/dist/api/RouteAPI.d.ts +2 -2
- package/dist/api/diagnostics.d.ts +12 -1
- package/dist/api/diagnostics.js +34 -22
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +1 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +6 -2
- package/dist/modules/AbstractModule.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +14 -3
- package/dist/replication/AbstractReplicator.js +122 -62
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +3 -3
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +12 -12
- package/dist/routes/endpoints/admin.d.ts +24 -24
- package/dist/routes/endpoints/admin.js +46 -35
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +1 -1
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +1 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +22 -13
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +1 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/runner/teardown.js +8 -8
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +0 -6
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +22 -27
- package/dist/storage/BucketStorageFactory.js +4 -18
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
- package/dist/storage/ParsedSyncConfigSet.js +2 -0
- package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/PersistedReplicationStream.d.ts +32 -0
- package/dist/storage/PersistedReplicationStream.js +26 -0
- package/dist/storage/PersistedReplicationStream.js.map +1 -0
- package/dist/storage/PersistedSyncConfigContent.d.ts +63 -0
- package/dist/storage/PersistedSyncConfigContent.js +125 -0
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
- package/dist/storage/PersistedSyncConfigStatus.js +2 -0
- package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
- package/dist/storage/SourceTable.d.ts +9 -3
- package/dist/storage/SourceTable.js +13 -3
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +59 -16
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +117 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +277 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +62 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
- package/dist/storage/storage-index.d.ts +8 -1
- package/dist/storage/storage-index.js +8 -1
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/sync.js +5 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -1
- package/dist/util/lsn.d.ts +4 -0
- package/dist/util/lsn.js +10 -0
- package/dist/util/lsn.js.map +1 -1
- package/package.json +6 -7
- package/src/api/RouteAPI.ts +2 -2
- package/src/api/diagnostics.ts +43 -26
- package/src/entry/commands/compact-action.ts +1 -1
- package/src/modules/AbstractModule.ts +6 -2
- package/src/replication/AbstractReplicator.ts +145 -65
- package/src/replication/ReplicationModule.ts +2 -3
- package/src/routes/endpoints/admin.ts +72 -50
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +23 -13
- package/src/routes/endpoints/sync-stream.ts +1 -1
- package/src/runner/teardown.ts +8 -8
- package/src/storage/BucketStorageBatch.ts +0 -7
- package/src/storage/BucketStorageFactory.ts +30 -39
- package/src/storage/ParsedSyncConfigSet.ts +25 -0
- package/src/storage/PersistedReplicationStream.ts +55 -0
- package/src/storage/PersistedSyncConfigContent.ts +192 -0
- package/src/storage/PersistedSyncConfigStatus.ts +14 -0
- package/src/storage/SourceTable.ts +20 -3
- package/src/storage/SyncRulesBucketStorage.ts +64 -16
- package/src/storage/implementation/BucketDefinitionMapping.ts +436 -0
- package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
- package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +109 -0
- package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
- package/src/storage/storage-index.ts +9 -1
- package/src/sync/sync.ts +5 -0
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/types.ts +1 -1
- package/src/util/lsn.ts +9 -0
- package/test/src/diagnostics.test.ts +62 -34
- package/test/src/module-loader.test.ts +33 -1
- package/test/src/routes/admin.test.ts +182 -18
- package/test/src/routes/mocks.ts +6 -2
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
- package/dist/storage/PersistedSyncRulesContent.js +0 -100
- package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
- package/src/storage/PersistedSyncRulesContent.ts +0 -174
package/dist/util/lsn.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lsn.js","sourceRoot":"","sources":["../../src/util/lsn.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,CAA4B,EAAE,CAA4B;IAC/E,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,IAAI,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC"}
|
|
1
|
+
{"version":3,"file":"lsn.js","sourceRoot":"","sources":["../../src/util/lsn.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,CAA4B,EAAE,CAA4B;IAC/E,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,IAAI,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,CAA4B,EAAE,CAA4B;IAC/E,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,IAAI,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/service-core",
|
|
3
3
|
"repository": "https://github.com/powersync-ja/powersync-service",
|
|
4
|
-
"types": "dist/index.d.ts",
|
|
5
4
|
"publishConfig": {
|
|
6
5
|
"access": "public"
|
|
7
6
|
},
|
|
8
|
-
"version": "1.
|
|
9
|
-
"main": "dist/index.js",
|
|
7
|
+
"version": "1.23.1",
|
|
10
8
|
"license": "FSL-1.1-ALv2",
|
|
11
9
|
"type": "module",
|
|
10
|
+
"exports": "./dist/index.js",
|
|
12
11
|
"dependencies": {
|
|
13
12
|
"@js-sdsl/ordered-set": "^4.4.2",
|
|
14
13
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -33,11 +32,11 @@
|
|
|
33
32
|
"uuid": "^14.0.0",
|
|
34
33
|
"winston": "^3.13.0",
|
|
35
34
|
"yaml": "^2.8.3",
|
|
36
|
-
"@powersync/lib-services-framework": "0.9.
|
|
35
|
+
"@powersync/lib-services-framework": "0.9.6",
|
|
37
36
|
"@powersync/service-jsonbig": "0.17.13",
|
|
38
|
-
"@powersync/service-rsocket-router": "0.2.
|
|
39
|
-
"@powersync/service-sync-rules": "0.
|
|
40
|
-
"@powersync/service-types": "0.
|
|
37
|
+
"@powersync/service-rsocket-router": "0.2.22",
|
|
38
|
+
"@powersync/service-sync-rules": "0.38.0",
|
|
39
|
+
"@powersync/service-types": "0.16.0"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@types/async": "^3.2.24",
|
package/src/api/RouteAPI.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SyncConfig, TablePattern } from '@powersync/service-sync-rules';
|
|
2
2
|
import * as types from '@powersync/service-types';
|
|
3
|
-
import {
|
|
3
|
+
import { ParseSyncConfigOptions, SyncRulesBucketStorage } from '../storage/storage-index.js';
|
|
4
4
|
|
|
5
5
|
export interface PatternResult {
|
|
6
6
|
schema: string;
|
|
@@ -94,7 +94,7 @@ export interface RouteAPI {
|
|
|
94
94
|
/**
|
|
95
95
|
* Get the default schema (or database) when only a table name is specified in sync config.
|
|
96
96
|
*/
|
|
97
|
-
getParseSyncRulesOptions():
|
|
97
|
+
getParseSyncRulesOptions(): ParseSyncConfigOptions;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export type ReplicationHeadCallback<T> = (head: string) => Promise<T>;
|
package/src/api/diagnostics.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
1
|
+
import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
2
|
import { DEFAULT_TAG, SourceTableRef, SyncConfigWithErrors } from '@powersync/service-sync-rules';
|
|
3
3
|
import { ReplicationError, SyncRulesStatus, TableInfo } from '@powersync/service-types';
|
|
4
4
|
|
|
@@ -19,6 +19,11 @@ export interface DiagnosticsOptions {
|
|
|
19
19
|
*/
|
|
20
20
|
live_status: boolean;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Whether this is the active sync config or not.
|
|
24
|
+
*/
|
|
25
|
+
active: boolean;
|
|
26
|
+
|
|
22
27
|
/**
|
|
23
28
|
* Check against the source postgres connection.
|
|
24
29
|
*/
|
|
@@ -28,12 +33,18 @@ export interface DiagnosticsOptions {
|
|
|
28
33
|
export const DEFAULT_DATASOURCE_ID = 'default';
|
|
29
34
|
|
|
30
35
|
export async function getSyncRulesStatus(
|
|
31
|
-
bucketStorage: storage.BucketStorageFactory,
|
|
32
36
|
apiHandler: RouteAPI,
|
|
33
|
-
|
|
34
|
-
options: DiagnosticsOptions
|
|
37
|
+
syncConfig: storage.PersistedSyncConfigContent | null,
|
|
38
|
+
options: DiagnosticsOptions,
|
|
39
|
+
/**
|
|
40
|
+
* Storage instance for the replication stream of this config.
|
|
41
|
+
*
|
|
42
|
+
* Required to populate live status (snapshot/checkpoint info). The content object
|
|
43
|
+
* itself is no longer a replication stream, so the caller must resolve this.
|
|
44
|
+
*/
|
|
45
|
+
systemStorage?: storage.SyncRulesBucketStorage
|
|
35
46
|
): Promise<SyncRulesStatus | undefined> {
|
|
36
|
-
if (
|
|
47
|
+
if (syncConfig == null) {
|
|
37
48
|
return undefined;
|
|
38
49
|
}
|
|
39
50
|
|
|
@@ -43,13 +54,18 @@ export async function getSyncRulesStatus(
|
|
|
43
54
|
const now = new Date().toISOString();
|
|
44
55
|
|
|
45
56
|
let parsed: SyncConfigWithErrors;
|
|
46
|
-
let persisted: storage.
|
|
57
|
+
let persisted: storage.ParsedSyncConfigSet;
|
|
47
58
|
try {
|
|
48
|
-
persisted =
|
|
49
|
-
parsed
|
|
59
|
+
persisted = syncConfig.parsed(apiHandler.getParseSyncRulesOptions());
|
|
60
|
+
// A content object represents a single sync config, so its parsed result has exactly one entry.
|
|
61
|
+
const [singleConfig] = persisted.syncConfigs;
|
|
62
|
+
if (singleConfig == null) {
|
|
63
|
+
throw new ServiceAssertionError('Expected one sync config');
|
|
64
|
+
}
|
|
65
|
+
parsed = singleConfig;
|
|
50
66
|
} catch (e) {
|
|
51
67
|
return {
|
|
52
|
-
content: include_content ?
|
|
68
|
+
content: include_content ? syncConfig.sync_rules_content : undefined,
|
|
53
69
|
connections: [],
|
|
54
70
|
errors: [{ level: 'fatal', message: e.message, ts: now }]
|
|
55
71
|
};
|
|
@@ -60,8 +76,7 @@ export async function getSyncRulesStatus(
|
|
|
60
76
|
// This method can run under some situations if no connection is configured yet.
|
|
61
77
|
// It will return a default tag in such a case. This default tag is not module specific.
|
|
62
78
|
const tag = sourceConfig.tag ?? DEFAULT_TAG;
|
|
63
|
-
const
|
|
64
|
-
const status = await systemStorage?.getStatus();
|
|
79
|
+
const status = live_status ? await systemStorage?.getStatus() : undefined;
|
|
65
80
|
let replication_lag_bytes: number | undefined = undefined;
|
|
66
81
|
let slot_wal_budget: SlotWalBudgetInfo | undefined = undefined;
|
|
67
82
|
|
|
@@ -90,10 +105,10 @@ export async function getSyncRulesStatus(
|
|
|
90
105
|
logger.warn(`Unable to get replication lag`, e);
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
if (apiHandler.getSlotWalBudget &&
|
|
108
|
+
if (apiHandler.getSlotWalBudget && syncConfig.replicationStreamName) {
|
|
94
109
|
try {
|
|
95
110
|
slot_wal_budget = await apiHandler.getSlotWalBudget({
|
|
96
|
-
slotName:
|
|
111
|
+
slotName: syncConfig.replicationStreamName
|
|
97
112
|
});
|
|
98
113
|
} catch (e) {
|
|
99
114
|
logger.warn(`Unable to get WAL budget`, e);
|
|
@@ -136,11 +151,13 @@ export async function getSyncRulesStatus(
|
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
const errors = tables_flat.flatMap((info) => info.errors);
|
|
139
|
-
|
|
154
|
+
const statusSource = await syncConfig.getSyncConfigStatus();
|
|
155
|
+
|
|
156
|
+
if (statusSource?.last_fatal_error) {
|
|
140
157
|
errors.push({
|
|
141
158
|
level: 'fatal',
|
|
142
|
-
message:
|
|
143
|
-
ts:
|
|
159
|
+
message: statusSource.last_fatal_error,
|
|
160
|
+
ts: statusSource.last_fatal_error_ts?.toISOString()
|
|
144
161
|
});
|
|
145
162
|
}
|
|
146
163
|
errors.push(...syncRuleErrors.map((error) => syncConfigYamlErrorToReplicationError(error, now)));
|
|
@@ -168,10 +185,10 @@ export async function getSyncRulesStatus(
|
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
187
|
|
|
171
|
-
if (live_status &&
|
|
188
|
+
if (live_status && options.active) {
|
|
172
189
|
// Check replication lag for active replication stream.
|
|
173
190
|
// Right now we exclude mysql, since it we don't have consistent keepalives for it.
|
|
174
|
-
if (
|
|
191
|
+
if (statusSource?.last_checkpoint_ts == null && statusSource?.last_keepalive_ts == null) {
|
|
175
192
|
errors.push({
|
|
176
193
|
level: 'warning',
|
|
177
194
|
message: 'No checkpoint found, cannot calculate replication lag',
|
|
@@ -179,8 +196,8 @@ export async function getSyncRulesStatus(
|
|
|
179
196
|
});
|
|
180
197
|
} else {
|
|
181
198
|
const lastTime = Math.max(
|
|
182
|
-
|
|
183
|
-
|
|
199
|
+
statusSource.last_checkpoint_ts?.getTime() ?? 0,
|
|
200
|
+
statusSource.last_keepalive_ts?.getTime() ?? 0
|
|
184
201
|
);
|
|
185
202
|
const lagSeconds = Math.round((Date.now() - lastTime) / 1000);
|
|
186
203
|
// On idle instances, keepalive messages are only persisted every 60 seconds.
|
|
@@ -204,17 +221,17 @@ export async function getSyncRulesStatus(
|
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
return {
|
|
207
|
-
content: include_content ?
|
|
224
|
+
content: include_content ? syncConfig.sync_rules_content : undefined,
|
|
208
225
|
connections: [
|
|
209
226
|
{
|
|
210
227
|
id: sourceConfig.id ?? DEFAULT_DATASOURCE_ID,
|
|
211
228
|
tag: tag,
|
|
212
|
-
slot_name:
|
|
213
|
-
initial_replication_done: status?.
|
|
229
|
+
slot_name: syncConfig.replicationStreamName,
|
|
230
|
+
initial_replication_done: status?.snapshotDone ?? false,
|
|
214
231
|
// TODO: Rename?
|
|
215
|
-
last_lsn: status?.
|
|
216
|
-
last_checkpoint_ts:
|
|
217
|
-
last_keepalive_ts:
|
|
232
|
+
last_lsn: status?.resumeLsn ?? undefined,
|
|
233
|
+
last_checkpoint_ts: statusSource?.last_checkpoint_ts?.toISOString(),
|
|
234
|
+
last_keepalive_ts: statusSource?.last_keepalive_ts?.toISOString(),
|
|
218
235
|
replication_lag_bytes: replication_lag_bytes,
|
|
219
236
|
wal_status: slot_wal_budget?.wal_status,
|
|
220
237
|
safe_wal_size: slot_wal_budget?.safe_wal_size,
|
|
@@ -83,7 +83,7 @@ export function registerCompactAction(program: Command) {
|
|
|
83
83
|
await serviceContext.lifeCycleEngine.start();
|
|
84
84
|
const bucketStorage = serviceContext.storageEngine.activeBucketStorage;
|
|
85
85
|
|
|
86
|
-
const active = await bucketStorage.
|
|
86
|
+
const active = (await bucketStorage.getActiveSyncConfig())?.storage;
|
|
87
87
|
if (active == null) {
|
|
88
88
|
logger.info('No active instance to compact');
|
|
89
89
|
return;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
2
|
import winston from 'winston';
|
|
3
|
-
import {
|
|
3
|
+
import { PersistedReplicationStream } from '../storage/storage-index.js';
|
|
4
4
|
import { ServiceContextContainer } from '../system/ServiceContext.js';
|
|
5
5
|
|
|
6
6
|
export interface TearDownOptions {
|
|
7
7
|
/**
|
|
8
8
|
* If required, tear down any configuration/state for the specific replication stream
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
replicationStreams?: PersistedReplicationStream[];
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Use replicationStreams.
|
|
13
|
+
*/
|
|
14
|
+
syncRules?: PersistedReplicationStream[];
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export interface AbstractModuleOptions {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { container, ErrorCode, logger } from '@powersync/lib-services-framework';
|
|
2
2
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
3
3
|
import { hrtime } from 'node:process';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
5
|
import winston from 'winston';
|
|
5
6
|
import { MetricsEngine } from '../metrics/MetricsEngine.js';
|
|
6
7
|
import * as storage from '../storage/storage-index.js';
|
|
@@ -13,6 +14,12 @@ import { ConnectionTestResult } from './ReplicationModule.js';
|
|
|
13
14
|
// 1 minute
|
|
14
15
|
const PING_INTERVAL = 1_000_000_000n * 60n;
|
|
15
16
|
|
|
17
|
+
// In the initial startup period, we use a short refresh interval. This helps to take over replication quickly in the case
|
|
18
|
+
// of rolling deploys. After the initial period, we switch to a longer refresh interval to reduce load.
|
|
19
|
+
const FAST_REFRESH_INTERVAL_MS = 500;
|
|
20
|
+
const FAST_REFRESH_TIMEOUT_MS = 120_000;
|
|
21
|
+
const REFRESH_INTERVAL_MS = 5_000;
|
|
22
|
+
|
|
16
23
|
export interface CreateJobOptions {
|
|
17
24
|
lock: storage.ReplicationLock;
|
|
18
25
|
storage: storage.SyncRulesBucketStorage;
|
|
@@ -36,12 +43,12 @@ export interface AbstractReplicatorOptions {
|
|
|
36
43
|
*/
|
|
37
44
|
export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
|
|
38
45
|
protected logger: winston.Logger;
|
|
39
|
-
private
|
|
46
|
+
private lastReplicationStreamInfoLogs = new Map<string, string>();
|
|
40
47
|
/**
|
|
41
|
-
* Map of replication jobs by replication stream id. Usually there is only one running job, but there could be two
|
|
42
|
-
* transitioning to a new replication stream.
|
|
48
|
+
* Map of replication jobs by replication stream job id. Usually there is only one running job, but there could be two
|
|
49
|
+
* when transitioning to a new replication stream.
|
|
43
50
|
*/
|
|
44
|
-
private replicationJobs = new Map<
|
|
51
|
+
private replicationJobs = new Map<string, T>();
|
|
45
52
|
|
|
46
53
|
/**
|
|
47
54
|
* Map of replciation stream ids to promises that are clearing the replication stream.
|
|
@@ -131,17 +138,20 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
private async runLoop() {
|
|
134
|
-
const
|
|
141
|
+
const loadedSyncConfig = await this.syncRuleProvider.get();
|
|
135
142
|
|
|
136
143
|
let configuredLock: storage.ReplicationLock | undefined = undefined;
|
|
137
|
-
if (
|
|
144
|
+
if (loadedSyncConfig != null) {
|
|
138
145
|
this.logger.info('Loaded sync config');
|
|
139
146
|
try {
|
|
140
147
|
// Configure new sync config, if they have changed.
|
|
141
148
|
// In that case, also immediately take out a lock, so that another process doesn't start replication on it.
|
|
149
|
+
// This upfront lock is mostly superseded by the replicationStreamLoadedSyncConfigMatch() check. However, that doesn't cover old
|
|
150
|
+
// versions before that check was added, so we keep the lock for now - for where te service version and sync config is updated at
|
|
151
|
+
// the same time.
|
|
142
152
|
|
|
143
153
|
const { lock } = await this.storage.configureSyncRules(
|
|
144
|
-
storage.updateSyncRulesFromYaml(
|
|
154
|
+
storage.updateSyncRulesFromYaml(loadedSyncConfig, { lock: true, validate: this.syncRuleProvider.exitOnError })
|
|
145
155
|
);
|
|
146
156
|
if (lock) {
|
|
147
157
|
configuredLock = lock;
|
|
@@ -155,10 +165,15 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
155
165
|
} else {
|
|
156
166
|
this.logger.info('No sync streams or rules configured - configure via API');
|
|
157
167
|
}
|
|
168
|
+
let useFastRefresh = true;
|
|
169
|
+
const fastRefreshDeadline = Date.now() + FAST_REFRESH_TIMEOUT_MS;
|
|
158
170
|
while (!this.stopped) {
|
|
159
171
|
await container.probes.touch();
|
|
160
172
|
try {
|
|
161
|
-
await this.refresh({
|
|
173
|
+
const refreshResult = await this.refresh({ configuredLock, loadedSyncConfig });
|
|
174
|
+
if (refreshResult.replicationJobStarted || Date.now() >= fastRefreshDeadline) {
|
|
175
|
+
useFastRefresh = false;
|
|
176
|
+
}
|
|
162
177
|
// The lock is only valid on the first refresh.
|
|
163
178
|
configuredLock = undefined;
|
|
164
179
|
|
|
@@ -177,77 +192,59 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
177
192
|
} catch (e) {
|
|
178
193
|
this.logger.error('Failed to refresh replication jobs', e);
|
|
179
194
|
}
|
|
180
|
-
|
|
195
|
+
if (Date.now() >= fastRefreshDeadline) {
|
|
196
|
+
useFastRefresh = false;
|
|
197
|
+
}
|
|
198
|
+
await sleep(useFastRefresh ? FAST_REFRESH_INTERVAL_MS : REFRESH_INTERVAL_MS);
|
|
181
199
|
}
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
private async refresh(options?: {
|
|
202
|
+
private async refresh(options?: {
|
|
203
|
+
configuredLock?: storage.ReplicationLock;
|
|
204
|
+
loadedSyncConfig?: string;
|
|
205
|
+
}): Promise<{ replicationJobStarted: boolean }> {
|
|
185
206
|
if (this.stopped) {
|
|
186
|
-
return;
|
|
207
|
+
return { replicationJobStarted: false };
|
|
187
208
|
}
|
|
188
209
|
|
|
189
|
-
let configuredLock = options?.
|
|
210
|
+
let configuredLock = options?.configuredLock;
|
|
211
|
+
let replicationJobStarted = false;
|
|
190
212
|
|
|
191
|
-
const existingJobs = new Map<
|
|
192
|
-
const
|
|
193
|
-
const newJobs = new Map<
|
|
213
|
+
const existingJobs = new Map<string, T>(this.replicationJobs.entries());
|
|
214
|
+
const replicatingStreams = await this.storage.getReplicatingReplicationStreams();
|
|
215
|
+
const newJobs = new Map<string, T>();
|
|
216
|
+
const streamsToStart: storage.PersistedReplicationStream[] = [];
|
|
194
217
|
let activeJob: T | undefined = undefined;
|
|
195
|
-
for (let
|
|
196
|
-
const
|
|
197
|
-
|
|
218
|
+
for (let replicationStream of replicatingStreams) {
|
|
219
|
+
const jobId = replicationStream.replicationJobId;
|
|
220
|
+
const existingJob = existingJobs.get(jobId);
|
|
221
|
+
const syncConfigMismatchMessage = 'Ignoring replication stream for sync config not loaded by this process';
|
|
222
|
+
if (!this.shouldHandleReplicationStream(replicationStream, options?.loadedSyncConfig)) {
|
|
223
|
+
this.logReplicationStreamInfoOnce(replicationStream, 'sync-config-mismatch', () => {
|
|
224
|
+
replicationStream.logger.info(syncConfigMismatchMessage);
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
this.clearReplicationStreamInfoLog(replicationStream, 'sync-config-mismatch');
|
|
229
|
+
if (replicationStream.state == storage.SyncRuleState.ACTIVE && activeJob == null) {
|
|
198
230
|
activeJob = existingJob;
|
|
199
231
|
}
|
|
200
232
|
if (existingJob && !existingJob.isStopped) {
|
|
201
233
|
// No change
|
|
202
|
-
existingJobs.delete(
|
|
203
|
-
newJobs.set(
|
|
234
|
+
existingJobs.delete(jobId);
|
|
235
|
+
newJobs.set(jobId, existingJob);
|
|
204
236
|
} else if (existingJob && existingJob.isStopped) {
|
|
205
237
|
// Stopped (e.g. fatal error).
|
|
206
238
|
// Remove from the list. Next refresh call will restart the job.
|
|
207
|
-
existingJobs.delete(
|
|
239
|
+
existingJobs.delete(jobId);
|
|
208
240
|
} else {
|
|
209
241
|
// New sync config was found (or resume after restart)
|
|
210
|
-
|
|
211
|
-
let lock: storage.ReplicationLock;
|
|
212
|
-
if (configuredLock?.sync_rules_id == syncRules.id) {
|
|
213
|
-
lock = configuredLock;
|
|
214
|
-
} else {
|
|
215
|
-
lock = await syncRules.lock();
|
|
216
|
-
}
|
|
217
|
-
const storage = this.storage.getInstance(syncRules);
|
|
218
|
-
const newJob = this.createJob({
|
|
219
|
-
lock: lock,
|
|
220
|
-
storage: storage
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
newJobs.set(syncRules.id, newJob);
|
|
224
|
-
newJob.start();
|
|
225
|
-
if (syncRules.active) {
|
|
226
|
-
activeJob = newJob;
|
|
227
|
-
}
|
|
228
|
-
this.lockAlerted = false;
|
|
229
|
-
} catch (e) {
|
|
230
|
-
if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
|
|
231
|
-
if (!this.lockAlerted) {
|
|
232
|
-
syncRules.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
|
|
233
|
-
this.lockAlerted = true;
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
// Could be a sync config parse error,
|
|
237
|
-
// for example from stricter validation that was added.
|
|
238
|
-
// This will be retried every couple of seconds.
|
|
239
|
-
// When new (valid) sync config is deployed and processed, this one be disabled.
|
|
240
|
-
syncRules.logger.error('Failed to start replication for new sync config', e);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
242
|
+
streamsToStart.push(replicationStream);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Stop any orphaned jobs that no longer have a replication stream.
|
|
250
|
-
// Termination happens below
|
|
246
|
+
// Stop any orphaned jobs that no longer have a replication stream before starting replacements.
|
|
247
|
+
// Termination happens below.
|
|
251
248
|
for (let job of existingJobs.values()) {
|
|
252
249
|
// Old - stop and clean up
|
|
253
250
|
try {
|
|
@@ -258,10 +255,50 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
258
255
|
}
|
|
259
256
|
}
|
|
260
257
|
|
|
258
|
+
for (let replicationStream of streamsToStart) {
|
|
259
|
+
const jobId = replicationStream.replicationJobId;
|
|
260
|
+
try {
|
|
261
|
+
let lock: storage.ReplicationLock;
|
|
262
|
+
if (configuredLock?.sync_rules_id == replicationStream.replicationStreamId) {
|
|
263
|
+
lock = configuredLock;
|
|
264
|
+
} else {
|
|
265
|
+
lock = await replicationStream.lock();
|
|
266
|
+
}
|
|
267
|
+
const syncRuleStorage = this.storage.getInstance(replicationStream);
|
|
268
|
+
const newJob = this.createJob({
|
|
269
|
+
lock: lock,
|
|
270
|
+
storage: syncRuleStorage
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
newJobs.set(jobId, newJob);
|
|
274
|
+
newJob.start();
|
|
275
|
+
replicationJobStarted = true;
|
|
276
|
+
if (replicationStream.state == storage.SyncRuleState.ACTIVE) {
|
|
277
|
+
activeJob = newJob;
|
|
278
|
+
}
|
|
279
|
+
this.lastReplicationStreamInfoLogs.delete(replicationStream.replicationStreamName);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
|
|
282
|
+
this.logReplicationStreamInfoOnce(replicationStream, 'replication-stream-locked', () => {
|
|
283
|
+
replicationStream.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
// Could be a sync config parse error,
|
|
287
|
+
// for example from stricter validation that was added.
|
|
288
|
+
// This will be retried every couple of seconds.
|
|
289
|
+
// When new (valid) sync config is deployed and processed, this one be disabled.
|
|
290
|
+
replicationStream.logger.error('Failed to start replication for new sync config', e);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.replicationJobs = newJobs;
|
|
296
|
+
this.activeReplicationJob = activeJob;
|
|
297
|
+
|
|
261
298
|
// Replication stream stopped previously, including by a different process.
|
|
262
|
-
const stopped = await this.storage.
|
|
263
|
-
for (let
|
|
264
|
-
if (this.clearingJobs.has(
|
|
299
|
+
const stopped = await this.storage.getStoppedReplicationStreams();
|
|
300
|
+
for (let replicationStream of stopped) {
|
|
301
|
+
if (this.clearingJobs.has(replicationStream.replicationStreamId)) {
|
|
265
302
|
// Already in progress
|
|
266
303
|
continue;
|
|
267
304
|
}
|
|
@@ -269,16 +306,59 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
269
306
|
// We clear storage asynchronously.
|
|
270
307
|
// It is important to be able to continue running the refresh loop, otherwise we cannot
|
|
271
308
|
// retry locked replication stream, for example.
|
|
272
|
-
const syncRuleStorage = this.storage.getInstance(
|
|
309
|
+
const syncRuleStorage = this.storage.getInstance(replicationStream, { skipLifecycleHooks: true });
|
|
273
310
|
const promise = this.terminateSyncRules(syncRuleStorage)
|
|
274
311
|
.catch((e) => {
|
|
275
312
|
syncRuleStorage.logger.warn(`Failed clean up replication config`, e);
|
|
276
313
|
})
|
|
277
314
|
.finally(() => {
|
|
278
|
-
this.clearingJobs.delete(
|
|
315
|
+
this.clearingJobs.delete(replicationStream.replicationStreamId);
|
|
279
316
|
});
|
|
280
|
-
this.clearingJobs.set(
|
|
317
|
+
this.clearingJobs.set(replicationStream.replicationStreamId, promise);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { replicationJobStarted };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private logReplicationStreamInfoOnce(
|
|
324
|
+
replicationStream: storage.PersistedReplicationStream,
|
|
325
|
+
category: string,
|
|
326
|
+
log: () => void
|
|
327
|
+
) {
|
|
328
|
+
if (this.lastReplicationStreamInfoLogs.get(replicationStream.replicationStreamName) == category) {
|
|
329
|
+
return;
|
|
281
330
|
}
|
|
331
|
+
|
|
332
|
+
log();
|
|
333
|
+
this.lastReplicationStreamInfoLogs.set(replicationStream.replicationStreamName, category);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private clearReplicationStreamInfoLog(replicationStream: storage.PersistedReplicationStream, category: string) {
|
|
337
|
+
if (this.lastReplicationStreamInfoLogs.get(replicationStream.replicationStreamName) == category) {
|
|
338
|
+
this.lastReplicationStreamInfoLogs.delete(replicationStream.replicationStreamName);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* When we load sync config from the filesystem/config source, we ignore any config loaded by other processes.
|
|
344
|
+
* The idea is that if a different process loads updated config, that process should process it, not this one.
|
|
345
|
+
*
|
|
346
|
+
* This specifically helps for cases with rolling deploys.
|
|
347
|
+
*
|
|
348
|
+
* This does not apply when sync config is loaded from the database/API, instead of in the config.
|
|
349
|
+
*/
|
|
350
|
+
private shouldHandleReplicationStream(
|
|
351
|
+
replicationStream: storage.PersistedReplicationStream,
|
|
352
|
+
loadedSyncRules: string | undefined
|
|
353
|
+
) {
|
|
354
|
+
if (loadedSyncRules == null) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const processingConfig = replicationStream.syncConfigContent.find(
|
|
359
|
+
(syncConfig) => syncConfig.syncConfigState == storage.SyncRuleState.PROCESSING
|
|
360
|
+
);
|
|
361
|
+
return processingConfig == null || processingConfig.sync_rules_content == loadedSyncRules;
|
|
282
362
|
}
|
|
283
363
|
|
|
284
364
|
protected createJobId(syncRuleId: number) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DataSourceConfig } from '@powersync/service-types/dist/config/PowerSyncConfig.js';
|
|
2
1
|
import * as t from 'ts-codec';
|
|
3
2
|
|
|
4
3
|
import { schema } from '@powersync/lib-services-framework';
|
|
@@ -18,7 +17,7 @@ export interface ConnectionTestResult {
|
|
|
18
17
|
/**
|
|
19
18
|
* Provides a common interface for testing the connection to a DataSource.
|
|
20
19
|
*/
|
|
21
|
-
export interface ConnectionTester<TConfig extends DataSourceConfig> {
|
|
20
|
+
export interface ConnectionTester<TConfig extends types.configFile.DataSourceConfig> {
|
|
22
21
|
/**
|
|
23
22
|
* Confirm if a connection can be established to the datasource for the provided datasource configuration
|
|
24
23
|
* @param config
|
|
@@ -35,7 +34,7 @@ export interface ReplicationModuleOptions extends modules.AbstractModuleOptions
|
|
|
35
34
|
* A replication module describes all the functionality that PowerSync requires to
|
|
36
35
|
* replicate data from a DataSource. Whenever a new data source is added to powersync this class should be extended.
|
|
37
36
|
*/
|
|
38
|
-
export abstract class ReplicationModule<TConfig extends DataSourceConfig>
|
|
37
|
+
export abstract class ReplicationModule<TConfig extends types.configFile.DataSourceConfig>
|
|
39
38
|
extends modules.AbstractModule
|
|
40
39
|
implements ConnectionTester<TConfig>
|
|
41
40
|
{
|