@powersync/service-core 0.0.0-dev-20260313100403 → 0.0.0-dev-20260515144844
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 +86 -7
- package/dist/api/RouteAPI.d.ts +17 -3
- package/dist/api/api-index.d.ts +1 -1
- package/dist/api/api-index.js +1 -1
- package/dist/api/api-index.js.map +1 -1
- package/dist/api/api-metrics.js.map +1 -1
- package/dist/api/diagnostics.d.ts +1 -1
- package/dist/api/diagnostics.js +32 -14
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/CachedKeyCollector.js +1 -1
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/CompoundKeyCollector.js.map +1 -1
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/StaticKeyCollector.d.ts +1 -1
- package/dist/auth/StaticKeyCollector.js.map +1 -1
- package/dist/auth/StaticSupabaseKeyCollector.d.ts +1 -1
- package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
- package/dist/entry/commands/compact-action.js +26 -5
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/teardown-action.js +2 -2
- package/dist/entry/commands/teardown-action.js.map +1 -1
- package/dist/entry/entry-index.d.ts +1 -1
- package/dist/entry/entry-index.js +1 -1
- package/dist/entry/entry-index.js.map +1 -1
- package/dist/events/EventsEngine.js +1 -1
- package/dist/events/EventsEngine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/metrics/MetricsEngine.d.ts +1 -1
- package/dist/metrics/RollingBucketMax.d.ts +28 -0
- package/dist/metrics/RollingBucketMax.js +80 -0
- package/dist/metrics/RollingBucketMax.js.map +1 -0
- package/dist/metrics/metrics-index.d.ts +3 -2
- package/dist/metrics/metrics-index.js +3 -2
- package/dist/metrics/metrics-index.js.map +1 -1
- package/dist/metrics/open-telemetry/util.js +1 -1
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/metrics/register-metrics.js +2 -2
- package/dist/metrics/register-metrics.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +2 -2
- package/dist/modules/AbstractModule.js.map +1 -1
- package/dist/modules/modules-index.d.ts +1 -1
- package/dist/modules/modules-index.js +1 -1
- package/dist/modules/modules-index.js.map +1 -1
- package/dist/replication/AbstractReplicationJob.d.ts +2 -2
- package/dist/replication/AbstractReplicationJob.js +1 -1
- package/dist/replication/AbstractReplicationJob.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +7 -7
- package/dist/replication/AbstractReplicator.js +31 -28
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationLagTracker.d.ts +50 -0
- package/dist/replication/ReplicationLagTracker.js +78 -0
- package/dist/replication/ReplicationLagTracker.js.map +1 -0
- package/dist/replication/replication-index.d.ts +3 -2
- package/dist/replication/replication-index.js +3 -2
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/replication/replication-metrics.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +59 -32
- package/dist/routes/endpoints/admin.d.ts +108 -54
- package/dist/routes/endpoints/admin.js +7 -3
- 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 +10 -10
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +10 -10
- package/dist/routes/endpoints/sync-stream.js +2 -2
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/hooks.js +1 -1
- package/dist/routes/hooks.js.map +1 -1
- package/dist/routes/route-register.js.map +1 -1
- package/dist/runner/teardown.js +4 -4
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +9 -9
- package/dist/storage/BucketStorage.js +9 -9
- package/dist/storage/BucketStorageBatch.d.ts +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +27 -20
- package/dist/storage/BucketStorageFactory.js +19 -16
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +3 -1
- package/dist/storage/PersistedSyncRulesContent.js +24 -5
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/ReplicationEventPayload.d.ts +1 -1
- package/dist/storage/ReportStorage.d.ts +3 -3
- package/dist/storage/SourceTable.d.ts +4 -4
- package/dist/storage/SourceTable.js +3 -3
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +1 -1
- package/dist/storage/StorageVersionConfig.js +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +38 -6
- package/dist/storage/SyncRulesBucketStorage.js +14 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
- package/dist/storage/WriteCheckpointAPI.js +1 -1
- package/dist/storage/bson.d.ts +0 -1
- package/dist/storage/bson.js +0 -4
- package/dist/storage/bson.js.map +1 -1
- package/dist/storage/storage-index.d.ts +8 -8
- package/dist/storage/storage-index.js +8 -8
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/storage/storage-metrics.js.map +1 -1
- package/dist/streams/streams-index.d.ts +2 -2
- package/dist/streams/streams-index.js +2 -2
- package/dist/streams/streams-index.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +2 -5
- package/dist/sync/BucketChecksumState.js +119 -75
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.js +1 -1
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync-index.d.ts +2 -2
- package/dist/sync/sync-index.js +2 -2
- package/dist/sync/sync-index.js.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +1 -1
- package/dist/system/ServiceContext.js +1 -1
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +44 -0
- package/dist/tracing/PerformanceTracer.js +102 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -0
- package/dist/tracing/TraceWriter.d.ts +22 -0
- package/dist/tracing/TraceWriter.js +63 -0
- package/dist/tracing/TraceWriter.js.map +1 -0
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/config/collectors/impl/base64-config-collector.d.ts +1 -1
- package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/filesystem-config-collector.d.ts +1 -1
- package/dist/util/config/collectors/impl/filesystem-config-collector.js +1 -1
- package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
- package/dist/util/config/compound-config-collector.d.ts +1 -1
- package/dist/util/config/compound-config-collector.js +2 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
- package/dist/util/config.js +1 -1
- package/dist/util/config.js.map +1 -1
- package/dist/util/env.js +1 -1
- package/dist/util/errors.d.ts +3 -0
- package/dist/util/errors.js +15 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/protocol-types.d.ts +3 -3
- package/dist/util/protocol-types.js +1 -1
- package/dist/util/util-index.d.ts +1 -1
- package/dist/util/util-index.js +1 -1
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/utils.d.ts +1 -1
- package/package.json +11 -11
- package/src/api/RouteAPI.ts +20 -3
- package/src/api/api-index.ts +1 -1
- package/src/api/api-metrics.ts +1 -1
- package/src/api/diagnostics.ts +42 -20
- package/src/auth/CachedKeyCollector.ts +2 -3
- package/src/auth/CompoundKeyCollector.ts +2 -3
- package/src/auth/KeyStore.ts +1 -1
- package/src/auth/RemoteJWKSCollector.ts +0 -1
- package/src/auth/StaticKeyCollector.ts +1 -1
- package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
- package/src/entry/commands/compact-action.ts +29 -5
- package/src/entry/commands/teardown-action.ts +2 -2
- package/src/entry/entry-index.ts +1 -1
- package/src/events/EventsEngine.ts +1 -1
- package/src/index.ts +2 -0
- package/src/metrics/MetricsEngine.ts +1 -1
- package/src/metrics/RollingBucketMax.ts +109 -0
- package/src/metrics/metrics-index.ts +3 -2
- package/src/metrics/open-telemetry/util.ts +1 -1
- package/src/metrics/register-metrics.ts +3 -3
- package/src/modules/AbstractModule.ts +2 -2
- package/src/modules/modules-index.ts +1 -1
- package/src/replication/AbstractReplicationJob.ts +3 -3
- package/src/replication/AbstractReplicator.ts +32 -30
- package/src/replication/ReplicationLagTracker.ts +86 -0
- package/src/replication/replication-index.ts +3 -2
- package/src/replication/replication-metrics.ts +1 -1
- package/src/routes/endpoints/admin.ts +7 -3
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +10 -12
- package/src/routes/endpoints/sync-stream.ts +2 -2
- package/src/routes/hooks.ts +2 -2
- package/src/routes/route-register.ts +2 -10
- package/src/runner/teardown.ts +4 -4
- package/src/storage/BucketStorage.ts +9 -9
- package/src/storage/BucketStorageBatch.ts +1 -1
- package/src/storage/BucketStorageFactory.ts +45 -34
- package/src/storage/ChecksumCache.ts +1 -1
- package/src/storage/PersistedSyncRulesContent.ts +30 -6
- package/src/storage/ReplicationEventPayload.ts +1 -1
- package/src/storage/ReportStorage.ts +3 -3
- package/src/storage/SourceTable.ts +4 -4
- package/src/storage/StorageVersionConfig.ts +1 -1
- package/src/storage/SyncRulesBucketStorage.ts +46 -7
- package/src/storage/WriteCheckpointAPI.ts +6 -6
- package/src/storage/bson.ts +0 -5
- package/src/storage/storage-index.ts +8 -8
- package/src/storage/storage-metrics.ts +2 -2
- package/src/streams/streams-index.ts +2 -2
- package/src/sync/BucketChecksumState.ts +141 -93
- package/src/sync/RequestTracker.ts +1 -1
- package/src/sync/sync-index.ts +2 -2
- package/src/sync/sync.ts +2 -8
- package/src/sync/util.ts +1 -1
- package/src/system/ServiceContext.ts +1 -1
- package/src/tracing/PerformanceTracer.ts +126 -0
- package/src/tracing/TraceWriter.ts +67 -0
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/collectors/impl/base64-config-collector.ts +1 -1
- package/src/util/config/collectors/impl/filesystem-config-collector.ts +2 -2
- package/src/util/config/compound-config-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
- package/src/util/config/sync-rules/sync-rules-provider.ts +1 -1
- package/src/util/config.ts +1 -1
- package/src/util/env.ts +1 -1
- package/src/util/errors.ts +21 -0
- package/src/util/protocol-types.ts +1 -1
- package/src/util/util-index.ts +1 -1
- package/src/util/utils.ts +1 -1
- package/test/src/ReplicationLagTracker.test.ts +53 -0
- package/test/src/RollingBucketMax.test.ts +106 -0
- package/test/src/auth.test.ts +115 -7
- package/test/src/diagnostics.test.ts +151 -0
- package/test/src/module-loader.test.ts +1 -1
- package/test/src/routes/mocks.ts +1 -1
- package/test/src/routes/stream.test.ts +1 -2
- package/test/src/sync/BucketChecksumState.test.ts +223 -67
- package/test/src/util/protocol_types.test.ts +1 -1
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +1 -1
|
@@ -32,19 +32,19 @@ export interface AbstractReplicatorOptions {
|
|
|
32
32
|
/**
|
|
33
33
|
* A replicator manages the mechanics for replicating data from a data source to a storage bucket.
|
|
34
34
|
* This includes copying across the original data set and then keeping it in sync with the data source using Replication Jobs.
|
|
35
|
-
* It also handles any changes to the sync
|
|
35
|
+
* It also handles any changes to the sync config.
|
|
36
36
|
*/
|
|
37
37
|
export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
|
|
38
38
|
protected logger: winston.Logger;
|
|
39
39
|
private lockAlerted: boolean = false;
|
|
40
40
|
/**
|
|
41
|
-
* Map of replication jobs by
|
|
42
|
-
* transitioning to a new
|
|
41
|
+
* Map of replication jobs by replication stream id. Usually there is only one running job, but there could be two when
|
|
42
|
+
* transitioning to a new replication stream.
|
|
43
43
|
*/
|
|
44
44
|
private replicationJobs = new Map<number, T>();
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Map of
|
|
47
|
+
* Map of replciation stream ids to promises that are clearing the replication stream.
|
|
48
48
|
*
|
|
49
49
|
* We primarily do this to keep track of what we're currently clearing, but don't currently
|
|
50
50
|
* use the Promise value.
|
|
@@ -68,8 +68,8 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
68
68
|
abstract createJob(options: CreateJobOptions): T;
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Clean up any configuration or state for the specified replication stream on the datasource.
|
|
72
|
+
* Should be a no-op if the replication stream has already been cleared
|
|
73
73
|
*/
|
|
74
74
|
abstract cleanUp(syncRuleStorage: storage.SyncRulesBucketStorage): Promise<void>;
|
|
75
75
|
|
|
@@ -100,22 +100,24 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
100
100
|
public async start(): Promise<void> {
|
|
101
101
|
this.abortController = new AbortController();
|
|
102
102
|
this.runLoop().catch((e) => {
|
|
103
|
-
this.logger.error('
|
|
103
|
+
this.logger.error('Fatal replication error', e);
|
|
104
104
|
container.reporter.captureException(e);
|
|
105
105
|
setTimeout(() => {
|
|
106
106
|
process.exit(1);
|
|
107
107
|
}, 1000);
|
|
108
108
|
});
|
|
109
109
|
this.metrics.getObservableGauge(ReplicationMetric.REPLICATION_LAG_SECONDS).setValueProvider(async () => {
|
|
110
|
-
|
|
110
|
+
try {
|
|
111
|
+
const lag = this.getReplicationLagMillis();
|
|
112
|
+
if (lag == null) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
// ms to seconds
|
|
116
|
+
return Math.round(lag / 1000);
|
|
117
|
+
} catch (e) {
|
|
111
118
|
this.logger.error('Failed to get replication lag', e);
|
|
112
119
|
return undefined;
|
|
113
|
-
});
|
|
114
|
-
if (lag == null) {
|
|
115
|
-
return undefined;
|
|
116
120
|
}
|
|
117
|
-
// ms to seconds
|
|
118
|
-
return Math.round(lag / 1000);
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -133,9 +135,9 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
133
135
|
|
|
134
136
|
let configuredLock: storage.ReplicationLock | undefined = undefined;
|
|
135
137
|
if (syncRules != null) {
|
|
136
|
-
this.logger.info('Loaded sync
|
|
138
|
+
this.logger.info('Loaded sync config');
|
|
137
139
|
try {
|
|
138
|
-
// Configure new sync
|
|
140
|
+
// Configure new sync config, if they have changed.
|
|
139
141
|
// In that case, also immediately take out a lock, so that another process doesn't start replication on it.
|
|
140
142
|
|
|
141
143
|
const { lock } = await this.storage.configureSyncRules(
|
|
@@ -147,11 +149,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
147
149
|
} catch (e) {
|
|
148
150
|
// Log and re-raise to exit.
|
|
149
151
|
// Should only reach this due to validation errors if exit_on_error is true.
|
|
150
|
-
this.logger.error(`Failed to update sync
|
|
152
|
+
this.logger.error(`Failed to update sync config`, e);
|
|
151
153
|
throw e;
|
|
152
154
|
}
|
|
153
155
|
} else {
|
|
154
|
-
this.logger.info('No sync rules configured - configure via API');
|
|
156
|
+
this.logger.info('No sync streams or rules configured - configure via API');
|
|
155
157
|
}
|
|
156
158
|
while (!this.stopped) {
|
|
157
159
|
await container.probes.touch();
|
|
@@ -204,7 +206,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
204
206
|
// Remove from the list. Next refresh call will restart the job.
|
|
205
207
|
existingJobs.delete(syncRules.id);
|
|
206
208
|
} else {
|
|
207
|
-
// New sync
|
|
209
|
+
// New sync config was found (or resume after restart)
|
|
208
210
|
try {
|
|
209
211
|
let lock: storage.ReplicationLock;
|
|
210
212
|
if (configuredLock?.sync_rules_id == syncRules.id) {
|
|
@@ -227,15 +229,15 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
227
229
|
} catch (e) {
|
|
228
230
|
if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
|
|
229
231
|
if (!this.lockAlerted) {
|
|
230
|
-
|
|
232
|
+
syncRules.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
|
|
231
233
|
this.lockAlerted = true;
|
|
232
234
|
}
|
|
233
235
|
} else {
|
|
234
|
-
// Could be a sync
|
|
236
|
+
// Could be a sync config parse error,
|
|
235
237
|
// for example from stricter validation that was added.
|
|
236
238
|
// This will be retried every couple of seconds.
|
|
237
|
-
// When new (valid) sync
|
|
238
|
-
|
|
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);
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
}
|
|
@@ -244,7 +246,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
244
246
|
this.replicationJobs = newJobs;
|
|
245
247
|
this.activeReplicationJob = activeJob;
|
|
246
248
|
|
|
247
|
-
// Stop any orphaned jobs that no longer have
|
|
249
|
+
// Stop any orphaned jobs that no longer have a replication stream.
|
|
248
250
|
// Termination happens below
|
|
249
251
|
for (let job of existingJobs.values()) {
|
|
250
252
|
// Old - stop and clean up
|
|
@@ -252,11 +254,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
252
254
|
await job.stop();
|
|
253
255
|
} catch (e) {
|
|
254
256
|
// This will be retried
|
|
255
|
-
|
|
257
|
+
job.storage.logger.warn('Failed to stop old replication job', e);
|
|
256
258
|
}
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
//
|
|
261
|
+
// Replication stream stopped previously, including by a different process.
|
|
260
262
|
const stopped = await this.storage.getStoppedSyncRules();
|
|
261
263
|
for (let syncRules of stopped) {
|
|
262
264
|
if (this.clearingJobs.has(syncRules.id)) {
|
|
@@ -266,11 +268,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
266
268
|
|
|
267
269
|
// We clear storage asynchronously.
|
|
268
270
|
// It is important to be able to continue running the refresh loop, otherwise we cannot
|
|
269
|
-
// retry locked
|
|
271
|
+
// retry locked replication stream, for example.
|
|
270
272
|
const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
|
|
271
273
|
const promise = this.terminateSyncRules(syncRuleStorage)
|
|
272
274
|
.catch((e) => {
|
|
273
|
-
|
|
275
|
+
syncRuleStorage.logger.warn(`Failed clean up replication config`, e);
|
|
274
276
|
})
|
|
275
277
|
.finally(() => {
|
|
276
278
|
this.clearingJobs.delete(syncRules.id);
|
|
@@ -284,12 +286,12 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
protected async terminateSyncRules(syncRuleStorage: storage.SyncRulesBucketStorage) {
|
|
287
|
-
|
|
289
|
+
syncRuleStorage.logger.info(`Terminating replication stream...`);
|
|
288
290
|
// This deletes postgres replication slots - should complete quickly.
|
|
289
291
|
// It is safe to do before or after clearing the data in the storage.
|
|
290
292
|
await this.cleanUp(syncRuleStorage);
|
|
291
293
|
await syncRuleStorage.terminate({ signal: this.abortController?.signal, clearStorage: true });
|
|
292
|
-
|
|
294
|
+
syncRuleStorage.logger.info(`Successfully terminated replication stream`);
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
abstract testConnection(): Promise<ConnectionTestResult>;
|
|
@@ -312,7 +314,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
312
314
|
*
|
|
313
315
|
* "processing" replication streams are not taken into account for this metric.
|
|
314
316
|
*/
|
|
315
|
-
|
|
317
|
+
getReplicationLagMillis(): number | undefined {
|
|
316
318
|
return this.activeReplicationJob?.getReplicationLagMillis();
|
|
317
319
|
}
|
|
318
320
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { RollingBucketMax } from '../metrics/RollingBucketMax.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks replication lag across the current in-flight transaction and a rolling
|
|
5
|
+
* max of recently observed lag values.
|
|
6
|
+
*/
|
|
7
|
+
export class ReplicationLagTracker {
|
|
8
|
+
private readonly rollingReplicationLag = new RollingBucketMax();
|
|
9
|
+
private _oldestUncommittedChange: Date | null = null;
|
|
10
|
+
private _isStartingReplication = true;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The oldest source timestamp still part of the current in-flight work.
|
|
14
|
+
*/
|
|
15
|
+
get oldestUncommittedChange(): Date | null {
|
|
16
|
+
return this._oldestUncommittedChange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True until replication has seen its first completed commit or equivalent keepalive.
|
|
21
|
+
*/
|
|
22
|
+
get isStartingReplication(): boolean {
|
|
23
|
+
return this._isStartingReplication;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registers the first source timestamp for the current in-flight work,
|
|
28
|
+
* for example the start of a transaction
|
|
29
|
+
*/
|
|
30
|
+
trackUncommittedChange(timestamp: Date | null | undefined): void {
|
|
31
|
+
if (this._oldestUncommittedChange == null && timestamp != null) {
|
|
32
|
+
this._oldestUncommittedChange = timestamp;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clears the current in-flight timestamp without changing startup state.
|
|
38
|
+
*/
|
|
39
|
+
clearUncommittedChange(): void {
|
|
40
|
+
this._oldestUncommittedChange = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Marks replication as started even if no committed transaction lag was recorded.
|
|
45
|
+
*/
|
|
46
|
+
markStarted(): void {
|
|
47
|
+
this._isStartingReplication = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mark the current pending changes as "committed".
|
|
52
|
+
*
|
|
53
|
+
* Records the current in-flight lag into the rolling window and clears it.
|
|
54
|
+
* The current lag is calculated as the differnence between current time and the oldest change,
|
|
55
|
+
* as marked by trackUncommittedChange.
|
|
56
|
+
*/
|
|
57
|
+
markCommitted(timestampMs = Date.now()): void {
|
|
58
|
+
if (this._oldestUncommittedChange != null) {
|
|
59
|
+
this.rollingReplicationLag.report(timestampMs - this._oldestUncommittedChange.getTime(), timestampMs);
|
|
60
|
+
}
|
|
61
|
+
this.clearUncommittedChange();
|
|
62
|
+
this.markStarted();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns the lag for the current in-flight work.
|
|
67
|
+
*
|
|
68
|
+
* 0 if idle (no pending changes to replicate).
|
|
69
|
+
*
|
|
70
|
+
* undefined when replication is still starting up.
|
|
71
|
+
*/
|
|
72
|
+
getCurrentLagMillis(timestampMs = Date.now()): number | undefined {
|
|
73
|
+
if (this._oldestUncommittedChange == null) {
|
|
74
|
+
return this._isStartingReplication ? undefined : 0;
|
|
75
|
+
}
|
|
76
|
+
return timestampMs - this._oldestUncommittedChange.getTime();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the rolling lag metric value, including the current in-flight lag when present.
|
|
81
|
+
*/
|
|
82
|
+
getLagMillis(timestampMs = Date.now()): number | undefined {
|
|
83
|
+
this.rollingReplicationLag.report(this.getCurrentLagMillis(timestampMs), timestampMs);
|
|
84
|
+
return this.rollingReplicationLag.getRollingMax(timestampMs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './AbstractReplicationJob.js';
|
|
2
2
|
export * from './AbstractReplicator.js';
|
|
3
3
|
export * from './ErrorRateLimiter.js';
|
|
4
|
+
export * from './RelationCache.js';
|
|
5
|
+
export * from './replication-metrics.js';
|
|
4
6
|
export * from './ReplicationEngine.js';
|
|
7
|
+
export * from './ReplicationLagTracker.js';
|
|
5
8
|
export * from './ReplicationModule.js';
|
|
6
|
-
export * from './replication-metrics.js';
|
|
7
|
-
export * from './RelationCache.js';
|
|
@@ -119,7 +119,7 @@ export const reprocess = routeDefinition({
|
|
|
119
119
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
120
120
|
const next = await activeBucketStorage.getNextSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
121
121
|
if (next != null) {
|
|
122
|
-
throw new Error(`Busy processing sync
|
|
122
|
+
throw new Error(`Busy processing sync config - cannot reprocess`);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const active = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
@@ -127,13 +127,17 @@ export const reprocess = routeDefinition({
|
|
|
127
127
|
throw new errors.ServiceError({
|
|
128
128
|
status: 422,
|
|
129
129
|
code: ErrorCode.PSYNC_S4104,
|
|
130
|
-
description: 'No active sync
|
|
130
|
+
description: 'No active sync config'
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// There are some differences between this and using asUpdateOptions():
|
|
135
|
+
// 1. This always re-parses the source YAML. If there are changes to the sync stream compiler, that can affect the sync plan.
|
|
136
|
+
// 2. If the source does not set the storage version, this will update it do the current version.
|
|
137
|
+
// We can consider tweaking this behavior in the future.
|
|
134
138
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
135
139
|
storage.updateSyncRulesFromYaml(active.sync_rules.config.content, {
|
|
136
|
-
//
|
|
140
|
+
// This sync config already passed validation. But if the config is not valid anymore due
|
|
137
141
|
// to a service change, we do want to report the error here.
|
|
138
142
|
validate: true
|
|
139
143
|
})
|
|
@@ -33,7 +33,7 @@ export const writeCheckpoint = routeDefinition({
|
|
|
33
33
|
const bucketStorage = await service_context.storageEngine.activeBucketStorage.getActiveStorage();
|
|
34
34
|
const cp = await bucketStorage?.getCheckpoint();
|
|
35
35
|
if (cp == null) {
|
|
36
|
-
throw new Error('No sync
|
|
36
|
+
throw new Error('No sync config available');
|
|
37
37
|
}
|
|
38
38
|
if (cp.lsn && cp.lsn >= head) {
|
|
39
39
|
logger.info(`Got write checkpoint: ${head} : ${cp.checkpoint}`);
|
|
@@ -77,7 +77,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
77
77
|
new errors.ServiceError({
|
|
78
78
|
status: 500,
|
|
79
79
|
code: ErrorCode.PSYNC_S2302,
|
|
80
|
-
description: 'No sync
|
|
80
|
+
description: 'No sync config available'
|
|
81
81
|
})
|
|
82
82
|
);
|
|
83
83
|
responder.onComplete();
|
|
@@ -4,9 +4,9 @@ import type { FastifyPluginAsync } from 'fastify';
|
|
|
4
4
|
import * as t from 'ts-codec';
|
|
5
5
|
|
|
6
6
|
import { RouteAPI } from '../../api/RouteAPI.js';
|
|
7
|
+
import { updateSyncRulesFromConfig, updateSyncRulesFromYaml } from '../../storage/BucketStorageFactory.js';
|
|
7
8
|
import { authApi } from '../auth.js';
|
|
8
9
|
import { routeDefinition } from '../router.js';
|
|
9
|
-
import { updateSyncRulesFromConfig, updateSyncRulesFromYaml } from '../../storage/BucketStorageFactory.js';
|
|
10
10
|
|
|
11
11
|
const DeploySyncRulesRequest = t.object({
|
|
12
12
|
content: t.string
|
|
@@ -43,12 +43,12 @@ export const deploySyncRules = routeDefinition({
|
|
|
43
43
|
const { storageEngine } = service_context;
|
|
44
44
|
|
|
45
45
|
if (service_context.configuration.sync_rules.present) {
|
|
46
|
-
// If sync
|
|
46
|
+
// If sync config is configured via the service config, disable deploy via the API.
|
|
47
47
|
throw new errors.ServiceError({
|
|
48
48
|
status: 422,
|
|
49
49
|
code: ErrorCode.PSYNC_S4105,
|
|
50
|
-
description: 'Sync
|
|
51
|
-
details: '
|
|
50
|
+
description: 'Sync config API disabled',
|
|
51
|
+
details: 'Update sync config in the service configuration'
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
const content = payload.params.content;
|
|
@@ -56,7 +56,7 @@ export const deploySyncRules = routeDefinition({
|
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
58
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
59
|
-
syncConfig = SqlSyncRules.fromYaml(
|
|
59
|
+
syncConfig = SqlSyncRules.fromYaml(content, {
|
|
60
60
|
...apiHandler.getParseSyncRulesOptions(),
|
|
61
61
|
// We don't do any schema-level validation at this point
|
|
62
62
|
schema: undefined
|
|
@@ -65,14 +65,12 @@ export const deploySyncRules = routeDefinition({
|
|
|
65
65
|
throw new errors.ServiceError({
|
|
66
66
|
status: 422,
|
|
67
67
|
code: ErrorCode.PSYNC_R0001,
|
|
68
|
-
description: 'Sync
|
|
68
|
+
description: 'Sync config parsing failed',
|
|
69
69
|
details: e.message
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const sync_rules = await storageEngine.activeBucketStorage.updateSyncRules(
|
|
74
|
-
updateSyncRulesFromConfig(syncConfig.config)
|
|
75
|
-
);
|
|
73
|
+
const sync_rules = await storageEngine.activeBucketStorage.updateSyncRules(updateSyncRulesFromConfig(syncConfig));
|
|
76
74
|
|
|
77
75
|
return {
|
|
78
76
|
slot_name: sync_rules.slot_name
|
|
@@ -117,7 +115,7 @@ export const currentSyncRules = routeDefinition({
|
|
|
117
115
|
throw new errors.ServiceError({
|
|
118
116
|
status: 422,
|
|
119
117
|
code: ErrorCode.PSYNC_S4104,
|
|
120
|
-
description: 'No active sync
|
|
118
|
+
description: 'No active sync config'
|
|
121
119
|
});
|
|
122
120
|
}
|
|
123
121
|
|
|
@@ -164,13 +162,13 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
164
162
|
throw new errors.ServiceError({
|
|
165
163
|
status: 422,
|
|
166
164
|
code: ErrorCode.PSYNC_S4104,
|
|
167
|
-
description: 'No active sync
|
|
165
|
+
description: 'No active sync config'
|
|
168
166
|
});
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
172
170
|
updateSyncRulesFromYaml(sync_rules.sync_rules.config.content, {
|
|
173
|
-
//
|
|
171
|
+
// This sync config already passed validation. But if the rules are not valid anymore due
|
|
174
172
|
// to a service change, we do want to report the error here.
|
|
175
173
|
validate: true
|
|
176
174
|
})
|
|
@@ -68,7 +68,7 @@ export const syncStreamed = routeDefinition({
|
|
|
68
68
|
throw new errors.ServiceError({
|
|
69
69
|
status: 500,
|
|
70
70
|
code: ErrorCode.PSYNC_S2302,
|
|
71
|
-
description: 'No sync
|
|
71
|
+
description: 'No sync config available'
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -121,7 +121,7 @@ export const syncStreamed = routeDefinition({
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
stream.on('end', () => {
|
|
124
|
-
// Auth failure or switch to new sync
|
|
124
|
+
// Auth failure or switch to new sync config
|
|
125
125
|
closeReason ??= 'service closing stream';
|
|
126
126
|
});
|
|
127
127
|
|
package/src/routes/hooks.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import type fastify from 'fastify';
|
|
2
2
|
import * as uuid from 'uuid';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
ErrorCode,
|
|
6
|
-
errors,
|
|
7
|
-
HTTPMethod,
|
|
8
|
-
logger,
|
|
9
|
-
RouteNotFound,
|
|
10
|
-
router,
|
|
11
|
-
ServiceError
|
|
12
|
-
} from '@powersync/lib-services-framework';
|
|
13
|
-
import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
|
|
4
|
+
import { errors, HTTPMethod, logger, RouteNotFound, router, ServiceError } from '@powersync/lib-services-framework';
|
|
14
5
|
import { FastifyReply } from 'fastify';
|
|
6
|
+
import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
|
|
15
7
|
|
|
16
8
|
export type FastifyEndpoint<I, O, C> = RequestEndpoint<I, O, C> & {
|
|
17
9
|
parse?: boolean;
|
package/src/runner/teardown.ts
CHANGED
|
@@ -35,13 +35,13 @@ export async function teardown(runnerConfig: utils.RunnerConfig) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function terminateSyncRules(storageFactory: storage.BucketStorageFactory, moduleManager: modules.ModuleManager) {
|
|
38
|
-
logger.info(`Terminating
|
|
38
|
+
logger.info(`Terminating replication stream...`);
|
|
39
39
|
const start = Date.now();
|
|
40
40
|
const locks: storage.ReplicationLock[] = [];
|
|
41
41
|
while (Date.now() - start < 120_000) {
|
|
42
42
|
let retry = false;
|
|
43
43
|
const replicatingSyncRules = await storageFactory.getReplicatingSyncRules();
|
|
44
|
-
// Lock all the replicating
|
|
44
|
+
// Lock all the replicating replication streams
|
|
45
45
|
for (const replicatingSyncRule of replicatingSyncRules) {
|
|
46
46
|
const lock = await replicatingSyncRule.lock();
|
|
47
47
|
locks.push(lock);
|
|
@@ -50,10 +50,10 @@ async function terminateSyncRules(storageFactory: storage.BucketStorageFactory,
|
|
|
50
50
|
const stoppedSyncRules = await storageFactory.getStoppedSyncRules();
|
|
51
51
|
const combinedSyncRules = [...replicatingSyncRules, ...stoppedSyncRules];
|
|
52
52
|
try {
|
|
53
|
-
// Clean up any module specific configuration for the
|
|
53
|
+
// Clean up any module specific configuration for the replication stream
|
|
54
54
|
await moduleManager.tearDown({ syncRules: combinedSyncRules });
|
|
55
55
|
|
|
56
|
-
// Mark the
|
|
56
|
+
// Mark the replication stream as terminated
|
|
57
57
|
for (let syncRules of combinedSyncRules) {
|
|
58
58
|
const syncRulesStorage = storageFactory.getInstance(syncRules);
|
|
59
59
|
// The storage will be dropped at the end of the teardown, so we don't need to clear it here
|
|
@@ -2,36 +2,36 @@ import { ToastableSqliteRow } from '@powersync/service-sync-rules';
|
|
|
2
2
|
|
|
3
3
|
export enum SyncRuleState {
|
|
4
4
|
/**
|
|
5
|
-
* New
|
|
5
|
+
* New replication stream - needs to be processed (initial replication).
|
|
6
6
|
*
|
|
7
|
-
* While multiple
|
|
7
|
+
* While multiple replication streams _can_ be in PROCESSING,
|
|
8
8
|
* it's generally pointless, so we only keep one in that state.
|
|
9
9
|
*/
|
|
10
10
|
PROCESSING = 'PROCESSING',
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Intial processing is done, and can be used for sync.
|
|
14
14
|
*
|
|
15
|
-
* Only one
|
|
15
|
+
* Only one replication stream should be in ACTIVE or ERRORED state.
|
|
16
16
|
*/
|
|
17
17
|
ACTIVE = 'ACTIVE',
|
|
18
18
|
/**
|
|
19
|
-
* This state is used when the
|
|
19
|
+
* This state is used when the replication stream has been replaced,
|
|
20
20
|
* and replication is or should be stopped.
|
|
21
21
|
*/
|
|
22
22
|
STOP = 'STOP',
|
|
23
23
|
/**
|
|
24
|
-
* After
|
|
24
|
+
* After replication stream has been stopped, the data needs to be
|
|
25
25
|
* deleted. Once deleted, the state is TERMINATED.
|
|
26
26
|
*/
|
|
27
27
|
TERMINATED = 'TERMINATED',
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* is still the "active"
|
|
30
|
+
* Replication stream has run into a permanent replication error. It
|
|
31
|
+
* is still the "active" replication stram for syncing to users,
|
|
32
32
|
* but should not replicate anymore.
|
|
33
33
|
*
|
|
34
|
-
* It will transition to STOP when a new
|
|
34
|
+
* It will transition to STOP when a new replication stream is activated.
|
|
35
35
|
*/
|
|
36
36
|
ERRORED = 'ERRORED'
|
|
37
37
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ObserverClient } from '@powersync/lib-services-framework';
|
|
2
2
|
import { EvaluatedParameters, EvaluatedRow, SqliteRow, ToastableSqliteRow } from '@powersync/service-sync-rules';
|
|
3
3
|
import { BSON } from 'bson';
|
|
4
|
+
import { InternalOpId } from '../util/utils.js';
|
|
4
5
|
import { ReplicationEventPayload } from './ReplicationEventPayload.js';
|
|
5
6
|
import { SourceTable, TableSnapshotStatus } from './SourceTable.js';
|
|
6
7
|
import { BatchedCustomWriteCheckpointOptions } from './storage-index.js';
|
|
7
|
-
import { InternalOpId } from '../util/utils.js';
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS: ResolvedBucketBatchCommitOptions = {
|
|
10
10
|
createEmptyCheckpoints: true,
|