@powersync/service-core 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/api/diagnostics.js +2 -0
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +28 -24
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/metrics/metrics-interfaces.d.ts +5 -1
- package/dist/metrics/metrics-interfaces.js.map +1 -1
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.d.ts +36 -0
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js +70 -0
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js.map +1 -0
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +6 -1
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
- package/dist/metrics/open-telemetry/util.js +7 -3
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/replication/AbstractReplicator.js +16 -6
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +9 -0
- package/dist/routes/endpoints/admin.d.ts +18 -0
- package/dist/routes/endpoints/admin.js +3 -1
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +3 -1
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +2 -1
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +28 -4
- package/dist/storage/BucketStorageFactory.js +8 -4
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedSyncConfigContent.d.ts +2 -0
- package/dist/storage/PersistedSyncConfigContent.js +23 -10
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/ReplicationEventPayload.d.ts +6 -1
- package/dist/storage/SourceEntity.d.ts +7 -2
- package/dist/storage/SourceTable.d.ts +37 -6
- package/dist/storage/SourceTable.js +42 -10
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SourceTableReconciler.d.ts +83 -0
- package/dist/storage/SourceTableReconciler.js +95 -0
- package/dist/storage/SourceTableReconciler.js.map +1 -0
- package/dist/storage/StorageVersionConfig.d.ts +4 -0
- package/dist/storage/StorageVersionConfig.js +10 -1
- package/dist/storage/StorageVersionConfig.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +45 -7
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +25 -3
- package/dist/storage/WriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +23 -7
- package/dist/storage/implementation/BucketDefinitionMapping.js +88 -14
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +9 -1
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -1
- package/dist/storage/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/storage/storage-metrics.js +120 -4
- package/dist/storage/storage-metrics.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +1 -1
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.js +37 -9
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.js +10 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +5 -0
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +1 -0
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -0
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/sync-rules-provider.d.ts +2 -0
- package/dist/util/config/sync-rules/sync-rules-provider.js +3 -0
- package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -0
- package/dist/util/protocol-types.d.ts +4 -4
- package/dist/util/protocol-types.js +3 -2
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/utils.d.ts +2 -1
- package/dist/util/utils.js +16 -1
- package/dist/util/utils.js.map +1 -1
- package/package.json +6 -5
- package/src/api/diagnostics.ts +2 -0
- package/src/entry/commands/compact-action.ts +32 -24
- package/src/metrics/metrics-interfaces.ts +6 -1
- package/src/metrics/open-telemetry/MetricAttributeFilteringExporter.ts +85 -0
- package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +7 -2
- package/src/metrics/open-telemetry/util.ts +7 -3
- package/src/replication/AbstractReplicator.ts +22 -6
- package/src/routes/endpoints/admin.ts +3 -1
- package/src/routes/endpoints/sync-rules.ts +3 -1
- package/src/storage/BucketStorageBatch.ts +2 -1
- package/src/storage/BucketStorageFactory.ts +44 -8
- package/src/storage/PersistedSyncConfigContent.ts +26 -11
- package/src/storage/ReplicationEventPayload.ts +6 -1
- package/src/storage/SourceEntity.ts +6 -2
- package/src/storage/SourceTable.ts +68 -11
- package/src/storage/SourceTableReconciler.ts +188 -0
- package/src/storage/StorageVersionConfig.ts +11 -1
- package/src/storage/SyncRulesBucketStorage.ts +53 -7
- package/src/storage/WriteCheckpointAPI.ts +32 -3
- package/src/storage/implementation/BucketDefinitionMapping.ts +120 -17
- package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +22 -5
- package/src/storage/storage-index.ts +1 -0
- package/src/storage/storage-metrics.ts +154 -8
- package/src/sync/BucketChecksumState.ts +3 -1
- package/src/sync/sync.ts +40 -11
- package/src/sync/util.ts +21 -8
- package/src/util/config/compound-config-collector.ts +6 -0
- package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +1 -0
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -0
- package/src/util/config/sync-rules/sync-rules-provider.ts +5 -0
- package/src/util/config/types.ts +1 -0
- package/src/util/protocol-types.ts +3 -2
- package/src/util/utils.ts +15 -1
- package/test/src/AbstractReplicator.test.ts +30 -0
- package/test/src/MetricAttributeFilteringExporter.test.ts +96 -0
- package/test/src/PersistedSyncConfigContent.test.ts +152 -0
- package/test/src/config.test.ts +40 -0
- package/test/src/diagnostics.test.ts +25 -0
- package/test/src/routes/admin.test.ts +8 -2
- package/test/src/routes/stream.test.ts +123 -2
- package/test/src/source-table-reconciler.test.ts +244 -0
- package/test/src/storage/SourceTable.test.ts +4 -1
- package/test/src/storage-metrics.test.ts +148 -0
- package/test/src/sync/util.test.ts +15 -0
- package/test/src/util/utils.test.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.26.0",
|
|
8
8
|
"license": "FSL-1.1-ALv2",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"exports": "./dist/index.js",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@js-sdsl/ordered-set": "^4.4.2",
|
|
13
13
|
"@opentelemetry/api": "^1.9.0",
|
|
14
|
+
"@opentelemetry/core": "^2.0.1",
|
|
14
15
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.203.0",
|
|
15
16
|
"@opentelemetry/exporter-prometheus": "^0.203.0",
|
|
16
17
|
"@opentelemetry/resources": "^2.0.1",
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
"uuid": "^14.0.0",
|
|
33
34
|
"winston": "^3.13.0",
|
|
34
35
|
"yaml": "^2.8.3",
|
|
35
|
-
"@powersync/lib-services-framework": "0.10.0",
|
|
36
36
|
"@powersync/service-jsonbig": "0.17.13",
|
|
37
|
-
"@powersync/service-rsocket-router": "0.2.
|
|
38
|
-
"@powersync/service-sync-rules": "0.
|
|
39
|
-
"@powersync/
|
|
37
|
+
"@powersync/service-rsocket-router": "0.2.26",
|
|
38
|
+
"@powersync/service-sync-rules": "0.41.0",
|
|
39
|
+
"@powersync/lib-services-framework": "0.10.1",
|
|
40
|
+
"@powersync/service-types": "0.18.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/async": "^3.2.24",
|
package/src/api/diagnostics.ts
CHANGED
|
@@ -65,6 +65,7 @@ export async function getSyncRulesStatus(
|
|
|
65
65
|
parsed = singleConfig;
|
|
66
66
|
} catch (e) {
|
|
67
67
|
return {
|
|
68
|
+
version_label: syncConfig.version_label,
|
|
68
69
|
content: include_content ? syncConfig.sync_rules_content : undefined,
|
|
69
70
|
connections: [],
|
|
70
71
|
errors: [{ level: 'fatal', message: e.message, ts: now }]
|
|
@@ -221,6 +222,7 @@ export async function getSyncRulesStatus(
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
return {
|
|
225
|
+
version_label: syncConfig.version_label,
|
|
224
226
|
content: include_content ? syncConfig.sync_rules_content : undefined,
|
|
225
227
|
connections: [
|
|
226
228
|
{
|
|
@@ -28,7 +28,11 @@ export function registerCompactAction(program: Command) {
|
|
|
28
28
|
.command(COMMAND_NAME)
|
|
29
29
|
.option(`-b, --buckets [buckets]`, 'Full bucket names, comma-separated (e.g., "global[],mybucket[\\"user1\\"]")')
|
|
30
30
|
.option('-p, --parameter-indexes', 'Compacting parameter indexes. Defaults to set unless --buckets is provided.')
|
|
31
|
-
.option('--no-parameter-indexes', 'Disabling compacting parameter indexes.')
|
|
31
|
+
.option('--no-parameter-indexes', 'Disabling compacting parameter indexes.')
|
|
32
|
+
.option(
|
|
33
|
+
'--incremental-only',
|
|
34
|
+
'[EXPERIMENTAL] Perform incremental compacting only. Parameter compaction runs on supported storage versions.'
|
|
35
|
+
);
|
|
32
36
|
|
|
33
37
|
wrapConfigCommand(compactCommand);
|
|
34
38
|
|
|
@@ -48,8 +52,9 @@ export function registerCompactAction(program: Command) {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
const incremental: boolean = options.incrementalOnly ?? false;
|
|
52
56
|
|
|
57
|
+
let compactParameters: boolean | null = options.parameterIndexes;
|
|
53
58
|
if (buckets == null) {
|
|
54
59
|
logger.info('Compacting storage for all buckets...');
|
|
55
60
|
} else if (buckets.length == 0) {
|
|
@@ -87,29 +92,32 @@ export function registerCompactAction(program: Command) {
|
|
|
87
92
|
Date.now() - config.api_parameters.checkpoint_request_retention_minutes * MINUTE_MS
|
|
88
93
|
);
|
|
89
94
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
const streams = await bucketStorage.getReplicatingReplicationStreams();
|
|
96
|
+
for (let stream of streams) {
|
|
97
|
+
const storage = bucketStorage.getInstance(stream);
|
|
98
|
+
const compactParameterData =
|
|
99
|
+
(compactParameters ?? buckets == null) && (!incremental || storage.supportsIncrementalParameterCompaction());
|
|
100
|
+
logger.info(`[${stream.replicationStreamName}] Performing compaction...`);
|
|
101
|
+
if (buckets != null) {
|
|
102
|
+
await storage.compact({
|
|
103
|
+
memoryLimitMB: COMPACT_MEMORY_LIMIT_MB,
|
|
104
|
+
compactBuckets: buckets,
|
|
105
|
+
compactParameterData,
|
|
106
|
+
incrementalOnly: incremental,
|
|
107
|
+
deleteCheckpointRequestsBefore,
|
|
108
|
+
signal: abortController.signal
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
await storage.compact({
|
|
112
|
+
memoryLimitMB: COMPACT_MEMORY_LIMIT_MB,
|
|
113
|
+
compactParameterData,
|
|
114
|
+
incrementalOnly: incremental,
|
|
115
|
+
deleteCheckpointRequestsBefore,
|
|
116
|
+
signal: abortController.signal
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
logger.info(`[${stream.replicationStreamName}] Successfully compacted storage.`);
|
|
111
120
|
}
|
|
112
|
-
logger.info('Successfully compacted storage.');
|
|
113
121
|
} catch (e) {
|
|
114
122
|
logger.error(`Failed to compact:`, e);
|
|
115
123
|
// Indirectly triggers lifeCycleEngine.stop
|
|
@@ -19,7 +19,12 @@ export interface ObservableGauge {
|
|
|
19
19
|
* Set a value provider that provides the value for the gauge at the time of observation.
|
|
20
20
|
* @param valueProvider
|
|
21
21
|
*/
|
|
22
|
-
setValueProvider(valueProvider: () => Promise<number | undefined>): void;
|
|
22
|
+
setValueProvider(valueProvider: () => Promise<number | ObservableGaugeObservation[] | undefined>): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ObservableGaugeObservation {
|
|
26
|
+
value: number;
|
|
27
|
+
attributes?: Record<string, string>;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export enum Precision {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Attributes } from '@opentelemetry/api';
|
|
2
|
+
import { ExportResult } from '@opentelemetry/core';
|
|
3
|
+
import {
|
|
4
|
+
AggregationSelector,
|
|
5
|
+
AggregationTemporalitySelector,
|
|
6
|
+
DataPoint,
|
|
7
|
+
DataPointType,
|
|
8
|
+
MetricData,
|
|
9
|
+
PushMetricExporter,
|
|
10
|
+
ResourceMetrics
|
|
11
|
+
} from '@opentelemetry/sdk-metrics';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Decorates a push exporter and removes selected attributes from every metric data point before
|
|
15
|
+
* delegating the export. The input metrics are not mutated, so another reader can export the
|
|
16
|
+
* original attributes.
|
|
17
|
+
*
|
|
18
|
+
* This exporter deliberately does not merge data points when filtering makes their attribute sets
|
|
19
|
+
* identical. Both points are forwarded with the same attributes. There is no generally correct
|
|
20
|
+
* merge operation at this layer: gauges, counters, and histograms have different aggregation
|
|
21
|
+
* semantics. Callers must therefore ensure that an excluded attribute does not distinguish data
|
|
22
|
+
* points for the same metric, resource, and instrumentation scope. Otherwise, the resulting
|
|
23
|
+
* duplicate series identity may be rejected or interpreted unpredictably downstream.
|
|
24
|
+
*
|
|
25
|
+
* Lifecycle methods and aggregation selection are delegated unchanged to the wrapped exporter.
|
|
26
|
+
*/
|
|
27
|
+
export class MetricAttributeFilteringExporter implements PushMetricExporter {
|
|
28
|
+
readonly selectAggregation?: AggregationSelector;
|
|
29
|
+
readonly selectAggregationTemporality?: AggregationTemporalitySelector;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
/** The exporter that receives the filtered copy of the metrics. */
|
|
33
|
+
private readonly delegate: PushMetricExporter,
|
|
34
|
+
/** Attribute names to remove from every exported data point. */
|
|
35
|
+
private readonly excludedAttributes: ReadonlySet<string>
|
|
36
|
+
) {
|
|
37
|
+
this.selectAggregation = delegate.selectAggregation?.bind(delegate);
|
|
38
|
+
this.selectAggregationTemporality = delegate.selectAggregationTemporality?.bind(delegate);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void {
|
|
42
|
+
this.delegate.export(this.filterAttributes(metrics), resultCallback);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
forceFlush(): Promise<void> {
|
|
46
|
+
return this.delegate.forceFlush();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
shutdown(): Promise<void> {
|
|
50
|
+
return this.delegate.shutdown();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private filterAttributes(metrics: ResourceMetrics): ResourceMetrics {
|
|
54
|
+
return {
|
|
55
|
+
...metrics,
|
|
56
|
+
scopeMetrics: metrics.scopeMetrics.map((scopeMetrics) => ({
|
|
57
|
+
...scopeMetrics,
|
|
58
|
+
metrics: scopeMetrics.metrics.map((metric) => this.filterMetricAttributes(metric))
|
|
59
|
+
}))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private filterMetricAttributes(metric: MetricData): MetricData {
|
|
64
|
+
switch (metric.dataPointType) {
|
|
65
|
+
case DataPointType.SUM:
|
|
66
|
+
case DataPointType.GAUGE:
|
|
67
|
+
return { ...metric, dataPoints: metric.dataPoints.map((dataPoint) => this.filterDataPoint(dataPoint)) };
|
|
68
|
+
case DataPointType.HISTOGRAM:
|
|
69
|
+
return { ...metric, dataPoints: metric.dataPoints.map((dataPoint) => this.filterDataPoint(dataPoint)) };
|
|
70
|
+
case DataPointType.EXPONENTIAL_HISTOGRAM:
|
|
71
|
+
return { ...metric, dataPoints: metric.dataPoints.map((dataPoint) => this.filterDataPoint(dataPoint)) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private filterDataPoint<T>(dataPoint: DataPoint<T>): DataPoint<T> {
|
|
76
|
+
return {
|
|
77
|
+
...dataPoint,
|
|
78
|
+
attributes: this.filterDataPointAttributes(dataPoint.attributes)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private filterDataPointAttributes(attributes: Attributes): Attributes {
|
|
83
|
+
return Object.fromEntries(Object.entries(attributes).filter(([name]) => !this.excludedAttributes.has(name)));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
MetricMetadata,
|
|
5
5
|
MetricsFactory,
|
|
6
6
|
ObservableGauge,
|
|
7
|
+
ObservableGaugeObservation,
|
|
7
8
|
Precision,
|
|
8
9
|
UpDownCounter
|
|
9
10
|
} from '../metrics-interfaces.js';
|
|
@@ -31,12 +32,16 @@ export class OpenTelemetryMetricsFactory implements MetricsFactory {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
return {
|
|
34
|
-
setValueProvider(valueProvider: () => Promise<number | undefined>) {
|
|
35
|
+
setValueProvider(valueProvider: () => Promise<number | ObservableGaugeObservation[] | undefined>) {
|
|
35
36
|
gauge.addCallback(async (result) => {
|
|
36
37
|
const value = await valueProvider();
|
|
37
38
|
|
|
38
|
-
if (value
|
|
39
|
+
if (typeof value === 'number') {
|
|
39
40
|
result.observe(value);
|
|
41
|
+
} else if (value != undefined) {
|
|
42
|
+
for (const observation of value) {
|
|
43
|
+
result.observe(observation.value, observation.attributes);
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
});
|
|
42
47
|
}
|
|
@@ -4,6 +4,7 @@ import { MeterProvider, MetricReader, PeriodicExportingMetricReader } from '@ope
|
|
|
4
4
|
import { logger } from '@powersync/lib-services-framework';
|
|
5
5
|
import { ServiceContext } from '../../system/ServiceContext.js';
|
|
6
6
|
import { MetricsFactory } from '../metrics-interfaces.js';
|
|
7
|
+
import { MetricAttributeFilteringExporter } from './MetricAttributeFilteringExporter.js';
|
|
7
8
|
import { OpenTelemetryMetricsFactory } from './OpenTelemetryMetricsFactory.js';
|
|
8
9
|
|
|
9
10
|
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
@@ -29,10 +30,13 @@ export function createOpenTelemetryMetricsFactory(context: ServiceContext): Metr
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (!configuration.telemetry.disable_telemetry_sharing) {
|
|
33
|
+
const otlpExporter = new OTLPMetricExporter({
|
|
34
|
+
url: configuration.telemetry.internal_service_endpoint
|
|
35
|
+
});
|
|
32
36
|
const periodicExporter = new PeriodicExportingMetricReader({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
// sync_config_id uniquely identifies these metric series after version_label is removed,
|
|
38
|
+
// so filtering cannot create duplicate data points.
|
|
39
|
+
exporter: new MetricAttributeFilteringExporter(otlpExporter, new Set(['version_label'])),
|
|
36
40
|
exportIntervalMillis: 1000 * 60 * 5 // 5 minutes
|
|
37
41
|
});
|
|
38
42
|
|
|
@@ -156,7 +156,8 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
156
156
|
|
|
157
157
|
let configuredLock: storage.ReplicationLock | undefined = undefined;
|
|
158
158
|
if (loadedSyncConfig != null) {
|
|
159
|
-
this.
|
|
159
|
+
const versionLabel = this.syncRuleProvider.versionLabel;
|
|
160
|
+
this.logger.info(`Loaded sync config${versionLabel == null ? '' : ` ${versionLabel}`}`);
|
|
160
161
|
try {
|
|
161
162
|
// Configure new sync config, if they have changed.
|
|
162
163
|
// In that case, also immediately take out a lock, so that another process doesn't start replication on it.
|
|
@@ -165,7 +166,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
165
166
|
// the same time.
|
|
166
167
|
|
|
167
168
|
const { lock } = await this.storage.configureSyncRules(
|
|
168
|
-
storage.updateSyncRulesFromYaml(loadedSyncConfig, {
|
|
169
|
+
storage.updateSyncRulesFromYaml(loadedSyncConfig, {
|
|
170
|
+
lock: true,
|
|
171
|
+
validate: this.syncRuleProvider.exitOnError,
|
|
172
|
+
version_label: versionLabel
|
|
173
|
+
})
|
|
169
174
|
);
|
|
170
175
|
if (lock) {
|
|
171
176
|
configuredLock = lock;
|
|
@@ -184,7 +189,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
184
189
|
while (!this.stopped) {
|
|
185
190
|
await container.probes.touch();
|
|
186
191
|
try {
|
|
187
|
-
const refreshResult = await this.refresh({
|
|
192
|
+
const refreshResult = await this.refresh({
|
|
193
|
+
configuredLock,
|
|
194
|
+
loadedSyncConfig,
|
|
195
|
+
loadedVersionLabel: this.syncRuleProvider.versionLabel
|
|
196
|
+
});
|
|
188
197
|
if (refreshResult.replicationJobStarted || Date.now() >= fastRefreshDeadline) {
|
|
189
198
|
useFastRefresh = false;
|
|
190
199
|
}
|
|
@@ -216,6 +225,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
216
225
|
private async refresh(options?: {
|
|
217
226
|
configuredLock?: storage.ReplicationLock;
|
|
218
227
|
loadedSyncConfig?: string;
|
|
228
|
+
loadedVersionLabel?: string;
|
|
219
229
|
}): Promise<{ replicationJobStarted: boolean }> {
|
|
220
230
|
if (this.stopped) {
|
|
221
231
|
return { replicationJobStarted: false };
|
|
@@ -233,7 +243,9 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
233
243
|
const jobId = replicationStream.replicationJobId;
|
|
234
244
|
const existingJob = existingJobs.get(jobId);
|
|
235
245
|
const syncConfigMismatchMessage = 'Ignoring replication stream for sync config not loaded by this process';
|
|
236
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
!this.shouldHandleReplicationStream(replicationStream, options?.loadedSyncConfig, options?.loadedVersionLabel)
|
|
248
|
+
) {
|
|
237
249
|
this.logReplicationStreamInfoOnce(replicationStream, 'sync-config-mismatch', () => {
|
|
238
250
|
replicationStream.logger.info(syncConfigMismatchMessage);
|
|
239
251
|
});
|
|
@@ -371,7 +383,8 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
371
383
|
*/
|
|
372
384
|
private shouldHandleReplicationStream(
|
|
373
385
|
replicationStream: storage.PersistedReplicationStream,
|
|
374
|
-
loadedSyncRules: string | undefined
|
|
386
|
+
loadedSyncRules: string | undefined,
|
|
387
|
+
loadedVersionLabel: string | undefined
|
|
375
388
|
) {
|
|
376
389
|
if (loadedSyncRules == null) {
|
|
377
390
|
return true;
|
|
@@ -380,7 +393,10 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
380
393
|
const processingConfig = replicationStream.syncConfigContent.find(
|
|
381
394
|
(syncConfig) => syncConfig.syncConfigState == storage.SyncRuleState.PROCESSING
|
|
382
395
|
);
|
|
383
|
-
return
|
|
396
|
+
return (
|
|
397
|
+
processingConfig == null ||
|
|
398
|
+
(processingConfig.sync_rules_content == loadedSyncRules && processingConfig.version_label == loadedVersionLabel)
|
|
399
|
+
);
|
|
384
400
|
}
|
|
385
401
|
|
|
386
402
|
protected createJobId(syncRuleId: number) {
|
|
@@ -165,7 +165,9 @@ export const reprocess = routeDefinition({
|
|
|
165
165
|
storage.updateSyncRulesFromYaml(active.content.sync_rules_content, {
|
|
166
166
|
// This sync config already passed validation. But if the config is not valid anymore due
|
|
167
167
|
// to a service change, we do want to report the error here.
|
|
168
|
-
validate: true
|
|
168
|
+
validate: true,
|
|
169
|
+
version_label: active.content.version_label,
|
|
170
|
+
forceNewReplicationStream: true
|
|
169
171
|
})
|
|
170
172
|
);
|
|
171
173
|
|
|
@@ -180,7 +180,9 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
180
180
|
updateSyncRulesFromYaml(sync_rules.sync_rules_content, {
|
|
181
181
|
// This sync config already passed validation. But if the rules are not valid anymore due
|
|
182
182
|
// to a service change, we do want to report the error here.
|
|
183
|
-
validate: true
|
|
183
|
+
validate: true,
|
|
184
|
+
version_label: sync_rules.version_label,
|
|
185
|
+
forceNewReplicationStream: true
|
|
184
186
|
})
|
|
185
187
|
);
|
|
186
188
|
return {
|
|
@@ -176,7 +176,8 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
176
176
|
* Queue a custom checkpoint request to be persisted after operations are
|
|
177
177
|
* flushed. Set `checkpoint_requested_at` when the custom checkpoint came from
|
|
178
178
|
* a client checkpoint request and should be cleaned up by request-retention
|
|
179
|
-
* compaction; omit it for persistent source-owned checkpoints.
|
|
179
|
+
* compaction; omit it for persistent source-owned checkpoints. Incremental
|
|
180
|
+
* event handlers should copy `event_id` from their {@link ReplicationEventPayload}.
|
|
180
181
|
*/
|
|
181
182
|
addCustomWriteCheckpoint(checkpoint: BatchedCustomWriteCheckpointOptions): void;
|
|
182
183
|
}
|
|
@@ -37,10 +37,17 @@ export abstract class BucketStorageFactory
|
|
|
37
37
|
const deploying = await this.getDeployingSyncConfig();
|
|
38
38
|
const active = await this.getActiveSyncConfig();
|
|
39
39
|
|
|
40
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
deploying?.content.sync_rules_content == options.config.yaml &&
|
|
42
|
+
deploying.content.version_label == options.version_label
|
|
43
|
+
) {
|
|
41
44
|
logger.info('Sync config unchanged');
|
|
42
45
|
return { updated: false };
|
|
43
|
-
} else if (
|
|
46
|
+
} else if (
|
|
47
|
+
deploying == null &&
|
|
48
|
+
active?.content.sync_rules_content == options.config.yaml &&
|
|
49
|
+
active.content.version_label == options.version_label
|
|
50
|
+
) {
|
|
44
51
|
logger.info('Sync config unchanged');
|
|
45
52
|
return { updated: false };
|
|
46
53
|
} else {
|
|
@@ -141,9 +148,32 @@ export interface StorageMetrics {
|
|
|
141
148
|
* Size of current_data.
|
|
142
149
|
*/
|
|
143
150
|
replication_size_bytes: number;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Size of active object-storage references, such as S3 bucket-data objects.
|
|
154
|
+
*/
|
|
155
|
+
object_storage_size_bytes?: number;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Per-sync-config storage sizes, when the storage backend can provide them.
|
|
159
|
+
*/
|
|
160
|
+
sync_config_metrics?: StorageSyncConfigMetrics[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface StorageSyncConfigMetrics {
|
|
164
|
+
sync_config_id: string;
|
|
165
|
+
sync_config_state: string;
|
|
166
|
+
/** Optional operator-supplied label identifying this sync config version. */
|
|
167
|
+
version_label?: string;
|
|
168
|
+
attributed_bucket_data_bytes: number;
|
|
169
|
+
attributed_parameter_indexes_bytes: number;
|
|
170
|
+
attributed_source_records_bytes: number;
|
|
171
|
+
attributed_object_storage_bytes: number;
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
export interface UpdateSyncRulesOptions {
|
|
175
|
+
/** Optional operator-supplied label identifying this deployed sync config version. */
|
|
176
|
+
version_label?: string;
|
|
147
177
|
config: {
|
|
148
178
|
yaml: string;
|
|
149
179
|
/**
|
|
@@ -161,6 +191,12 @@ export interface UpdateSyncRulesOptions {
|
|
|
161
191
|
lock?: boolean;
|
|
162
192
|
storageVersion?: number;
|
|
163
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Start a replacement replication stream instead of incrementally reprocessing a
|
|
196
|
+
* compatible active stream. Used when the current stream needs a full reprocess.
|
|
197
|
+
*/
|
|
198
|
+
forceNewReplicationStream?: boolean;
|
|
199
|
+
|
|
164
200
|
/**
|
|
165
201
|
* Only relevant if the result is used. This does not affect the persisted config.
|
|
166
202
|
*/
|
|
@@ -174,11 +210,10 @@ export interface SerializedSyncPlan {
|
|
|
174
210
|
plan: RawSerializedSyncPlan;
|
|
175
211
|
compatibility: SerializedCompatibilityContext;
|
|
176
212
|
/**
|
|
177
|
-
*
|
|
178
|
-
* yet.
|
|
213
|
+
* Raw event SQL persisted as a compatibility mirror for compiled {@link plan} events.
|
|
179
214
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
215
|
+
* Compiled events are an additive plan field. Older services ignore that field and restore these descriptors through
|
|
216
|
+
* the legacy evaluator. Keep dual-writing this field until a future plan version explicitly removes that support.
|
|
182
217
|
*/
|
|
183
218
|
eventDescriptors: Record<string, string[]>;
|
|
184
219
|
errors?: ReplicationError[];
|
|
@@ -206,13 +241,14 @@ export function updateSyncRulesFromConfig(
|
|
|
206
241
|
const { config, errors } = parsed;
|
|
207
242
|
if (config instanceof PrecompiledSyncConfig) {
|
|
208
243
|
const eventDescriptors: Record<string, string[]> = {};
|
|
209
|
-
for (const event of config.
|
|
210
|
-
eventDescriptors[event.name] = event.sourceQueries.map((
|
|
244
|
+
for (const event of config.plan.events) {
|
|
245
|
+
eventDescriptors[event.name] = event.sourceQueries.map((query) => query.sql);
|
|
211
246
|
}
|
|
212
247
|
|
|
213
248
|
plan = {
|
|
214
249
|
compatibility: config.compatibility.serialize(),
|
|
215
250
|
plan: serializeSyncPlan(config.plan),
|
|
251
|
+
// Dual-write raw SQL so older services can ignore additive compiled plan events without losing event behavior.
|
|
216
252
|
eventDescriptors,
|
|
217
253
|
errors: errors.map((e) => syncConfigYamlErrorToReplicationError(e))
|
|
218
254
|
};
|
|
@@ -2,6 +2,7 @@ import { logger as defaultLogger, ErrorCode, ServiceError } from '@powersync/lib
|
|
|
2
2
|
import {
|
|
3
3
|
CompatibilityContext,
|
|
4
4
|
CompatibilityOption,
|
|
5
|
+
compileEventDefinitions,
|
|
5
6
|
DEFAULT_HYDRATION_STATE,
|
|
6
7
|
deserializeSyncPlan,
|
|
7
8
|
ErrorLocation,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
HydrationState,
|
|
10
11
|
nodeSqlite,
|
|
11
12
|
PrecompiledSyncConfig,
|
|
12
|
-
SqlEventDescriptor,
|
|
13
13
|
SqlSyncRules,
|
|
14
14
|
SyncConfigWithErrors,
|
|
15
15
|
versionedHydrationState,
|
|
@@ -40,17 +40,28 @@ export function parsePersistedSyncConfigContent(options: ParsePersistedSyncConfi
|
|
|
40
40
|
|
|
41
41
|
const plan = deserializeSyncPlan(compiledPlan.plan);
|
|
42
42
|
const compatibility = CompatibilityContext.deserialize(compiledPlan.compatibility);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const errors: YamlError[] = [];
|
|
44
|
+
// Compiled events are additive to plan versions 1 and 2. New readers prefer them when present; when an older plan
|
|
45
|
+
// does not contain them, normalize the dual-written raw SQL at this loading boundary. This keeps legacy event
|
|
46
|
+
// evaluators out of PrecompiledSyncConfig while older binaries can continue reading the same persisted config.
|
|
47
|
+
if (compiledPlan.plan.events == null) {
|
|
48
|
+
const normalized = compileEventDefinitions(compiledPlan.eventDescriptors, {
|
|
49
|
+
...parseOptions,
|
|
50
|
+
// The legacy evaluator ignored event payload filters. Preserve that behavior for plans deployed before compiled
|
|
51
|
+
// events existed; a redeploy compiles and validates those filters before the replacement config is activated.
|
|
52
|
+
compileEventPayloadFilters: false
|
|
53
|
+
});
|
|
54
|
+
const fatalErrors = normalized.errors.filter((error) => error.type == 'fatal');
|
|
55
|
+
if (fatalErrors.length != 0) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to compile persisted replication events: ${fatalErrors.map((error) => error.message).join(', ')}`
|
|
58
|
+
);
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
plan.events = normalized.events;
|
|
61
|
+
errors.push(...normalized.errors.map((error) => new YamlError(error)));
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
const precompiled = new PrecompiledSyncConfig(plan, compatibility,
|
|
64
|
+
const precompiled = new PrecompiledSyncConfig(plan, compatibility, {
|
|
54
65
|
defaultSchema: parseOptions.defaultSchema,
|
|
55
66
|
sourceText: content
|
|
56
67
|
});
|
|
@@ -59,7 +70,6 @@ export function parsePersistedSyncConfigContent(options: ParsePersistedSyncConfi
|
|
|
59
70
|
// This means asUpdateOptions will not change the storage version, even if the default changes.
|
|
60
71
|
precompiled.storageVersion = storageVersion;
|
|
61
72
|
|
|
62
|
-
const errors: YamlError[] = [];
|
|
63
73
|
if (compiledPlan.errors) {
|
|
64
74
|
for (const error of compiledPlan.errors) {
|
|
65
75
|
const location: ErrorLocation | undefined = error.location && {
|
|
@@ -93,6 +103,7 @@ export abstract class PersistedSyncConfigContent implements PersistedSyncConfigC
|
|
|
93
103
|
readonly logger: Logger;
|
|
94
104
|
readonly syncConfigId: PersistedSyncConfigId | null;
|
|
95
105
|
readonly syncConfigState: SyncRuleState;
|
|
106
|
+
readonly version_label: string | undefined;
|
|
96
107
|
|
|
97
108
|
constructor(data: PersistedSyncConfigContentData) {
|
|
98
109
|
this.replicationStreamId = data.replicationStreamId;
|
|
@@ -102,7 +113,9 @@ export abstract class PersistedSyncConfigContent implements PersistedSyncConfigC
|
|
|
102
113
|
this.storageVersion = data.storageVersion;
|
|
103
114
|
this.syncConfigId = data.syncConfigId ?? null;
|
|
104
115
|
this.syncConfigState = data.syncConfigState;
|
|
105
|
-
this.
|
|
116
|
+
this.version_label = data.version_label;
|
|
117
|
+
const versionPrefix = this.version_label == null ? '' : `[${this.version_label}]`;
|
|
118
|
+
this.logger = defaultLogger.child({ prefix: `[${this.replicationStreamName}]${versionPrefix} ` });
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
/**
|
|
@@ -167,6 +180,7 @@ export abstract class PersistedSyncConfigContent implements PersistedSyncConfigC
|
|
|
167
180
|
const parsed = this.parseSingleConfig({ defaultSchema: 'not_applicable' });
|
|
168
181
|
return {
|
|
169
182
|
config: { yaml: this.sync_rules_content, plan: this.compiled_plan, parsed },
|
|
183
|
+
version_label: this.version_label,
|
|
170
184
|
...options
|
|
171
185
|
};
|
|
172
186
|
}
|
|
@@ -185,6 +199,7 @@ export interface PersistedSyncConfigContentData {
|
|
|
185
199
|
|
|
186
200
|
readonly syncConfigId?: PersistedSyncConfigId | null;
|
|
187
201
|
readonly syncConfigState: SyncRuleState;
|
|
202
|
+
readonly version_label?: string;
|
|
188
203
|
}
|
|
189
204
|
export type PersistedSyncConfigId = string;
|
|
190
205
|
export interface ParseSyncConfigOptions {
|
|
@@ -11,6 +11,11 @@ export type EventData = {
|
|
|
11
11
|
export type ReplicationEventPayload = {
|
|
12
12
|
batch: BucketStorageBatch;
|
|
13
13
|
data: EventData;
|
|
14
|
-
event: sync_rules.
|
|
14
|
+
event: sync_rules.HydratedEventDescriptor;
|
|
15
|
+
/**
|
|
16
|
+
* Storage-assigned id for this event definition. Present when storage uses persisted event mappings, allowing event
|
|
17
|
+
* handlers to route custom checkpoints without deriving an id from compiled event content.
|
|
18
|
+
*/
|
|
19
|
+
event_id?: sync_rules.EventDefinitionId;
|
|
15
20
|
table: SourceTable;
|
|
16
21
|
};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { SourceTableRef } from '@powersync/service-sync-rules';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Source-specific JSON metadata. Storage does not interpret it. Source-table APIs use null to
|
|
5
|
+
* represent the absence of metadata.
|
|
6
|
+
*/
|
|
7
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
8
|
+
|
|
3
9
|
export interface ColumnDescriptor {
|
|
4
10
|
name: string;
|
|
5
11
|
/**
|
|
@@ -19,8 +25,6 @@ export interface SourceEntityDescriptor extends SourceTableRef {
|
|
|
19
25
|
* If specified, this is specifically used to detect renames.
|
|
20
26
|
*/
|
|
21
27
|
objectId: number | string | undefined;
|
|
22
|
-
schema: string;
|
|
23
|
-
name: string;
|
|
24
28
|
/**
|
|
25
29
|
* The columns that are used to uniquely identify a record in the source entity.
|
|
26
30
|
*/
|