@powersync/service-core 0.0.0-dev-20250310190630 → 0.0.0-dev-20250312090341
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 +2 -7
- package/dist/api/api-index.d.ts +0 -1
- package/dist/api/api-index.js +0 -1
- package/dist/api/api-index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/metrics/Metrics.d.ts +30 -0
- package/dist/metrics/Metrics.js +202 -0
- package/dist/metrics/Metrics.js.map +1 -0
- package/dist/replication/AbstractReplicationJob.d.ts +0 -2
- package/dist/replication/AbstractReplicationJob.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +0 -3
- package/dist/replication/AbstractReplicator.js +0 -3
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +0 -7
- package/dist/replication/ReplicationModule.js +0 -1
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/replication/replication-index.d.ts +0 -1
- package/dist/replication/replication-index.js +0 -1
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +5 -5
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +6 -6
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +2 -1
- package/dist/storage/SyncRulesBucketStorage.js +1 -1
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/bson.d.ts +1 -0
- package/dist/storage/bson.js +6 -10
- package/dist/storage/bson.js.map +1 -1
- package/dist/storage/storage-index.d.ts +0 -1
- package/dist/storage/storage-index.js +0 -1
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +3 -0
- package/dist/sync/BucketChecksumState.js +51 -21
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +0 -3
- package/dist/sync/RequestTracker.js +3 -8
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/util.d.ts +1 -0
- package/dist/sync/util.js +11 -0
- package/dist/sync/util.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +3 -3
- package/dist/system/ServiceContext.js +3 -7
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +1 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/package.json +7 -7
- package/src/api/api-index.ts +0 -1
- package/src/index.ts +2 -2
- package/src/metrics/Metrics.ts +255 -0
- package/src/replication/AbstractReplicationJob.ts +0 -2
- package/src/replication/AbstractReplicator.ts +0 -7
- package/src/replication/ReplicationModule.ts +0 -10
- package/src/replication/replication-index.ts +0 -1
- package/src/routes/endpoints/socket-route.ts +5 -6
- package/src/routes/endpoints/sync-stream.ts +6 -7
- package/src/storage/SyncRulesBucketStorage.ts +3 -2
- package/src/storage/bson.ts +7 -9
- package/src/storage/storage-index.ts +0 -1
- package/src/sync/BucketChecksumState.ts +54 -22
- package/src/sync/RequestTracker.ts +3 -9
- package/src/sync/util.ts +12 -0
- package/src/system/ServiceContext.ts +4 -9
- package/src/util/config/compound-config-collector.ts +1 -2
- package/test/src/sync/BucketChecksumState.test.ts +3 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/api/api-metrics.d.ts +0 -11
- package/dist/api/api-metrics.js +0 -30
- package/dist/api/api-metrics.js.map +0 -1
- package/dist/metrics/MetricsEngine.d.ts +0 -21
- package/dist/metrics/MetricsEngine.js +0 -79
- package/dist/metrics/MetricsEngine.js.map +0 -1
- package/dist/metrics/metrics-index.d.ts +0 -4
- package/dist/metrics/metrics-index.js +0 -5
- package/dist/metrics/metrics-index.js.map +0 -1
- package/dist/metrics/metrics-interfaces.d.ts +0 -36
- package/dist/metrics/metrics-interfaces.js +0 -6
- package/dist/metrics/metrics-interfaces.js.map +0 -1
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.d.ts +0 -10
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +0 -51
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +0 -1
- package/dist/metrics/open-telemetry/util.d.ts +0 -6
- package/dist/metrics/open-telemetry/util.js +0 -56
- package/dist/metrics/open-telemetry/util.js.map +0 -1
- package/dist/replication/replication-metrics.d.ts +0 -11
- package/dist/replication/replication-metrics.js +0 -39
- package/dist/replication/replication-metrics.js.map +0 -1
- package/dist/storage/storage-metrics.d.ts +0 -4
- package/dist/storage/storage-metrics.js +0 -56
- package/dist/storage/storage-metrics.js.map +0 -1
- package/src/api/api-metrics.ts +0 -35
- package/src/metrics/MetricsEngine.ts +0 -98
- package/src/metrics/metrics-index.ts +0 -4
- package/src/metrics/metrics-interfaces.ts +0 -41
- package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +0 -66
- package/src/metrics/open-telemetry/util.ts +0 -74
- package/src/replication/replication-metrics.ts +0 -45
- package/src/storage/storage-metrics.ts +0 -67
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Attributes, Counter, ObservableGauge, UpDownCounter, ValueType } from '@opentelemetry/api';
|
|
2
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
3
|
+
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
|
|
4
|
+
import { Resource } from '@opentelemetry/resources';
|
|
5
|
+
import { MeterProvider, MetricReader, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
6
|
+
import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
7
|
+
import * as storage from '../storage/storage-index.js';
|
|
8
|
+
import * as util from '../util/util-index.js';
|
|
9
|
+
|
|
10
|
+
export interface MetricsOptions {
|
|
11
|
+
disable_telemetry_sharing: boolean;
|
|
12
|
+
powersync_instance_id: string;
|
|
13
|
+
internal_metrics_endpoint: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Metrics {
|
|
17
|
+
private static instance: Metrics;
|
|
18
|
+
|
|
19
|
+
private prometheusExporter: PrometheusExporter;
|
|
20
|
+
private meterProvider: MeterProvider;
|
|
21
|
+
|
|
22
|
+
// Metrics
|
|
23
|
+
// 1. Data processing / month
|
|
24
|
+
|
|
25
|
+
// 1a. Postgres -> PowerSync
|
|
26
|
+
// Record on replication pod
|
|
27
|
+
public data_replicated_bytes: Counter<Attributes>;
|
|
28
|
+
// 1b. PowerSync -> clients
|
|
29
|
+
// Record on API pod
|
|
30
|
+
public data_synced_bytes: Counter<Attributes>;
|
|
31
|
+
// Unused for pricing
|
|
32
|
+
// Record on replication pod
|
|
33
|
+
public rows_replicated_total: Counter<Attributes>;
|
|
34
|
+
// Unused for pricing
|
|
35
|
+
// Record on replication pod
|
|
36
|
+
public transactions_replicated_total: Counter<Attributes>;
|
|
37
|
+
// Unused for pricing
|
|
38
|
+
// Record on replication pod
|
|
39
|
+
public chunks_replicated_total: Counter<Attributes>;
|
|
40
|
+
|
|
41
|
+
// 2. Sync operations / month
|
|
42
|
+
|
|
43
|
+
// Record on API pod
|
|
44
|
+
public operations_synced_total: Counter<Attributes>;
|
|
45
|
+
|
|
46
|
+
// 3. Data hosted on PowerSync sync service
|
|
47
|
+
|
|
48
|
+
// Record on replication pod
|
|
49
|
+
// 3a. Replication storage -> raw data as received from Postgres.
|
|
50
|
+
public replication_storage_size_bytes: ObservableGauge<Attributes>;
|
|
51
|
+
// 3b. Operations storage -> transformed history, as will be synced to clients
|
|
52
|
+
public operation_storage_size_bytes: ObservableGauge<Attributes>;
|
|
53
|
+
// 3c. Parameter storage -> used for parameter queries
|
|
54
|
+
public parameter_storage_size_bytes: ObservableGauge<Attributes>;
|
|
55
|
+
|
|
56
|
+
// 4. Peak concurrent connections
|
|
57
|
+
|
|
58
|
+
// Record on API pod
|
|
59
|
+
public concurrent_connections: UpDownCounter<Attributes>;
|
|
60
|
+
|
|
61
|
+
private constructor(meterProvider: MeterProvider, prometheusExporter: PrometheusExporter) {
|
|
62
|
+
this.meterProvider = meterProvider;
|
|
63
|
+
this.prometheusExporter = prometheusExporter;
|
|
64
|
+
const meter = meterProvider.getMeter('powersync');
|
|
65
|
+
|
|
66
|
+
this.data_replicated_bytes = meter.createCounter('powersync_data_replicated_bytes_total', {
|
|
67
|
+
description: 'Uncompressed size of replicated data',
|
|
68
|
+
unit: 'bytes',
|
|
69
|
+
valueType: ValueType.INT
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.data_synced_bytes = meter.createCounter('powersync_data_synced_bytes_total', {
|
|
73
|
+
description: 'Uncompressed size of synced data',
|
|
74
|
+
unit: 'bytes',
|
|
75
|
+
valueType: ValueType.INT
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.rows_replicated_total = meter.createCounter('powersync_rows_replicated_total', {
|
|
79
|
+
description: 'Total number of replicated rows',
|
|
80
|
+
valueType: ValueType.INT
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.transactions_replicated_total = meter.createCounter('powersync_transactions_replicated_total', {
|
|
84
|
+
description: 'Total number of replicated transactions',
|
|
85
|
+
valueType: ValueType.INT
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.chunks_replicated_total = meter.createCounter('powersync_chunks_replicated_total', {
|
|
89
|
+
description: 'Total number of replication chunks',
|
|
90
|
+
valueType: ValueType.INT
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.operations_synced_total = meter.createCounter('powersync_operations_synced_total', {
|
|
94
|
+
description: 'Number of operations synced',
|
|
95
|
+
valueType: ValueType.INT
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.replication_storage_size_bytes = meter.createObservableGauge('powersync_replication_storage_size_bytes', {
|
|
99
|
+
description: 'Size of current data stored in PowerSync',
|
|
100
|
+
unit: 'bytes',
|
|
101
|
+
valueType: ValueType.INT
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.operation_storage_size_bytes = meter.createObservableGauge('powersync_operation_storage_size_bytes', {
|
|
105
|
+
description: 'Size of operations stored in PowerSync',
|
|
106
|
+
unit: 'bytes',
|
|
107
|
+
valueType: ValueType.INT
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.parameter_storage_size_bytes = meter.createObservableGauge('powersync_parameter_storage_size_bytes', {
|
|
111
|
+
description: 'Size of parameter data stored in PowerSync',
|
|
112
|
+
unit: 'bytes',
|
|
113
|
+
valueType: ValueType.INT
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.concurrent_connections = meter.createUpDownCounter('powersync_concurrent_connections', {
|
|
117
|
+
description: 'Number of concurrent sync connections',
|
|
118
|
+
valueType: ValueType.INT
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generally only useful for tests. Note: gauges are ignored here.
|
|
123
|
+
resetCounters() {
|
|
124
|
+
this.data_replicated_bytes.add(0);
|
|
125
|
+
this.data_synced_bytes.add(0);
|
|
126
|
+
this.rows_replicated_total.add(0);
|
|
127
|
+
this.transactions_replicated_total.add(0);
|
|
128
|
+
this.chunks_replicated_total.add(0);
|
|
129
|
+
this.operations_synced_total.add(0);
|
|
130
|
+
this.concurrent_connections.add(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public static getInstance(): Metrics {
|
|
134
|
+
if (!Metrics.instance) {
|
|
135
|
+
throw new ServiceAssertionError('Metrics have not been initialized');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return Metrics.instance;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public static async initialise(options: MetricsOptions): Promise<void> {
|
|
142
|
+
if (Metrics.instance) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
logger.info('Configuring telemetry.');
|
|
146
|
+
|
|
147
|
+
logger.info(
|
|
148
|
+
`
|
|
149
|
+
Attention:
|
|
150
|
+
PowerSync collects completely anonymous telemetry regarding usage.
|
|
151
|
+
This information is used to shape our roadmap to better serve our customers.
|
|
152
|
+
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
|
|
153
|
+
https://docs.powersync.com/self-hosting/telemetry
|
|
154
|
+
Anonymous telemetry is currently: ${options.disable_telemetry_sharing ? 'disabled' : 'enabled'}
|
|
155
|
+
`.trim()
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const configuredExporters: MetricReader[] = [];
|
|
159
|
+
|
|
160
|
+
const port: number = util.env.METRICS_PORT ?? 0;
|
|
161
|
+
const prometheusExporter = new PrometheusExporter({ port: port, preventServerStart: true });
|
|
162
|
+
configuredExporters.push(prometheusExporter);
|
|
163
|
+
|
|
164
|
+
if (!options.disable_telemetry_sharing) {
|
|
165
|
+
logger.info('Sharing anonymous telemetry');
|
|
166
|
+
const periodicExporter = new PeriodicExportingMetricReader({
|
|
167
|
+
exporter: new OTLPMetricExporter({
|
|
168
|
+
url: options.internal_metrics_endpoint
|
|
169
|
+
}),
|
|
170
|
+
exportIntervalMillis: 1000 * 60 * 5 // 5 minutes
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
configuredExporters.push(periodicExporter);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const meterProvider = new MeterProvider({
|
|
177
|
+
resource: new Resource({
|
|
178
|
+
['service']: 'PowerSync',
|
|
179
|
+
['instance_id']: options.powersync_instance_id
|
|
180
|
+
}),
|
|
181
|
+
readers: configuredExporters
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (port > 0) {
|
|
185
|
+
await prometheusExporter.startServer();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Metrics.instance = new Metrics(meterProvider, prometheusExporter);
|
|
189
|
+
|
|
190
|
+
logger.info('Telemetry configuration complete.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public async shutdown(): Promise<void> {
|
|
194
|
+
await this.meterProvider.shutdown();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public configureApiMetrics() {
|
|
198
|
+
// Initialize the metric, so that it reports a value before connections
|
|
199
|
+
// have been opened.
|
|
200
|
+
this.concurrent_connections.add(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public configureReplicationMetrics(bucketStorage: storage.BucketStorageFactory) {
|
|
204
|
+
// Rate limit collection of these stats, since it may be an expensive query
|
|
205
|
+
const MINIMUM_INTERVAL = 60_000;
|
|
206
|
+
|
|
207
|
+
let cachedRequest: Promise<storage.StorageMetrics | null> | undefined = undefined;
|
|
208
|
+
let cacheTimestamp = 0;
|
|
209
|
+
|
|
210
|
+
function getMetrics() {
|
|
211
|
+
if (cachedRequest == null || Date.now() - cacheTimestamp > MINIMUM_INTERVAL) {
|
|
212
|
+
cachedRequest = bucketStorage.getStorageMetrics().catch((e) => {
|
|
213
|
+
logger.error(`Failed to get storage metrics`, e);
|
|
214
|
+
return null;
|
|
215
|
+
});
|
|
216
|
+
cacheTimestamp = Date.now();
|
|
217
|
+
}
|
|
218
|
+
return cachedRequest;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.operation_storage_size_bytes.addCallback(async (result) => {
|
|
222
|
+
const metrics = await getMetrics();
|
|
223
|
+
if (metrics) {
|
|
224
|
+
result.observe(metrics.operations_size_bytes);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.parameter_storage_size_bytes.addCallback(async (result) => {
|
|
229
|
+
const metrics = await getMetrics();
|
|
230
|
+
if (metrics) {
|
|
231
|
+
result.observe(metrics.parameters_size_bytes);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
this.replication_storage_size_bytes.addCallback(async (result) => {
|
|
236
|
+
const metrics = await getMetrics();
|
|
237
|
+
if (metrics) {
|
|
238
|
+
result.observe(metrics.replication_size_bytes);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async getMetricValueForTests(name: string): Promise<number | undefined> {
|
|
244
|
+
const metrics = await this.prometheusExporter.collect();
|
|
245
|
+
const scoped = metrics.resourceMetrics.scopeMetrics[0].metrics;
|
|
246
|
+
const metric = scoped.find((metric) => metric.descriptor.name == name);
|
|
247
|
+
if (metric == null) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Cannot find metric ${name}. Options: ${scoped.map((metric) => metric.descriptor.name).join(',')}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const point = metric.dataPoints[metric.dataPoints.length - 1];
|
|
253
|
+
return point?.value as number;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -2,12 +2,10 @@ import { container, logger } from '@powersync/lib-services-framework';
|
|
|
2
2
|
import winston from 'winston';
|
|
3
3
|
import * as storage from '../storage/storage-index.js';
|
|
4
4
|
import { ErrorRateLimiter } from './ErrorRateLimiter.js';
|
|
5
|
-
import { MetricsEngine } from '../metrics/MetricsEngine.js';
|
|
6
5
|
|
|
7
6
|
export interface AbstractReplicationJobOptions {
|
|
8
7
|
id: string;
|
|
9
8
|
storage: storage.SyncRulesBucketStorage;
|
|
10
|
-
metrics: MetricsEngine;
|
|
11
9
|
lock: storage.ReplicationLock;
|
|
12
10
|
rateLimiter: ErrorRateLimiter;
|
|
13
11
|
}
|
|
@@ -7,7 +7,6 @@ import { SyncRulesProvider } from '../util/config/sync-rules/sync-rules-provider
|
|
|
7
7
|
import { AbstractReplicationJob } from './AbstractReplicationJob.js';
|
|
8
8
|
import { ErrorRateLimiter } from './ErrorRateLimiter.js';
|
|
9
9
|
import { ConnectionTestResult } from './ReplicationModule.js';
|
|
10
|
-
import { MetricsEngine } from '../metrics/MetricsEngine.js';
|
|
11
10
|
|
|
12
11
|
// 5 minutes
|
|
13
12
|
const PING_INTERVAL = 1_000_000_000n * 300n;
|
|
@@ -20,7 +19,6 @@ export interface CreateJobOptions {
|
|
|
20
19
|
export interface AbstractReplicatorOptions {
|
|
21
20
|
id: string;
|
|
22
21
|
storageEngine: StorageEngine;
|
|
23
|
-
metricsEngine: MetricsEngine;
|
|
24
22
|
syncRuleProvider: SyncRulesProvider;
|
|
25
23
|
/**
|
|
26
24
|
* This limits the effect of retries when there is a persistent issue.
|
|
@@ -35,7 +33,6 @@ export interface AbstractReplicatorOptions {
|
|
|
35
33
|
*/
|
|
36
34
|
export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
|
|
37
35
|
protected logger: winston.Logger;
|
|
38
|
-
|
|
39
36
|
/**
|
|
40
37
|
* Map of replication jobs by sync rule id. Usually there is only one running job, but there could be two when
|
|
41
38
|
* transitioning to a new set of sync rules.
|
|
@@ -75,10 +72,6 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
75
72
|
return this.options.rateLimiter;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
protected get metrics() {
|
|
79
|
-
return this.options.metricsEngine;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
75
|
public async start(): Promise<void> {
|
|
83
76
|
this.runLoop().catch((e) => {
|
|
84
77
|
this.logger.error('Data source fatal replication error', e);
|
|
@@ -64,14 +64,6 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
|
|
|
64
64
|
*/
|
|
65
65
|
protected abstract createReplicator(context: system.ServiceContext): AbstractReplicator;
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Any additional initialization specific to the module should be added here. Will be called if necessary after the
|
|
69
|
-
* main initialization has been completed
|
|
70
|
-
* @param context
|
|
71
|
-
* @protected
|
|
72
|
-
*/
|
|
73
|
-
protected abstract onInitialized(context: system.ServiceContext): Promise<void>;
|
|
74
|
-
|
|
75
67
|
public abstract testConnection(config: TConfig): Promise<ConnectionTestResult>;
|
|
76
68
|
|
|
77
69
|
/**
|
|
@@ -101,8 +93,6 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
|
|
|
101
93
|
|
|
102
94
|
context.replicationEngine?.register(this.createReplicator(context));
|
|
103
95
|
context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
|
|
104
|
-
|
|
105
|
-
await this.onInitialized(context);
|
|
106
96
|
}
|
|
107
97
|
|
|
108
98
|
protected decodeConfig(config: TConfig): void {
|
|
@@ -2,19 +2,18 @@ import { ErrorCode, errors, logger, schema } from '@powersync/lib-services-frame
|
|
|
2
2
|
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
3
3
|
import { serialize } from 'bson';
|
|
4
4
|
|
|
5
|
+
import { Metrics } from '../../metrics/Metrics.js';
|
|
5
6
|
import * as sync from '../../sync/sync-index.js';
|
|
6
7
|
import * as util from '../../util/util-index.js';
|
|
7
8
|
import { SocketRouteGenerator } from '../router-socket.js';
|
|
8
9
|
import { SyncRoutes } from './sync-stream.js';
|
|
9
10
|
|
|
10
|
-
import { APIMetric } from '@powersync/service-types';
|
|
11
|
-
|
|
12
11
|
export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
13
12
|
router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
|
|
14
13
|
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
15
14
|
handler: async ({ context, params, responder, observer, initialN, signal: upstreamSignal }) => {
|
|
16
15
|
const { service_context } = context;
|
|
17
|
-
const { routerEngine,
|
|
16
|
+
const { routerEngine, syncContext } = service_context;
|
|
18
17
|
|
|
19
18
|
// Create our own controller that we can abort directly
|
|
20
19
|
const controller = new AbortController();
|
|
@@ -70,8 +69,8 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
70
69
|
controller.abort();
|
|
71
70
|
});
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
const tracker = new sync.RequestTracker(
|
|
72
|
+
Metrics.getInstance().concurrent_connections.add(1);
|
|
73
|
+
const tracker = new sync.RequestTracker();
|
|
75
74
|
try {
|
|
76
75
|
for await (const data of sync.streamResponse({
|
|
77
76
|
syncContext: syncContext,
|
|
@@ -148,7 +147,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
148
147
|
operations_synced: tracker.operationsSynced,
|
|
149
148
|
data_synced_bytes: tracker.dataSyncedBytes
|
|
150
149
|
});
|
|
151
|
-
|
|
150
|
+
Metrics.getInstance().concurrent_connections.add(-1);
|
|
152
151
|
}
|
|
153
152
|
}
|
|
154
153
|
});
|
|
@@ -5,11 +5,10 @@ import { Readable } from 'stream';
|
|
|
5
5
|
import * as sync from '../../sync/sync-index.js';
|
|
6
6
|
import * as util from '../../util/util-index.js';
|
|
7
7
|
|
|
8
|
+
import { Metrics } from '../../metrics/Metrics.js';
|
|
8
9
|
import { authUser } from '../auth.js';
|
|
9
10
|
import { routeDefinition } from '../router.js';
|
|
10
11
|
|
|
11
|
-
import { APIMetric } from '@powersync/service-types';
|
|
12
|
-
|
|
13
12
|
export enum SyncRoutes {
|
|
14
13
|
STREAM = '/sync/stream'
|
|
15
14
|
}
|
|
@@ -21,7 +20,7 @@ export const syncStreamed = routeDefinition({
|
|
|
21
20
|
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
22
21
|
handler: async (payload) => {
|
|
23
22
|
const { service_context } = payload.context;
|
|
24
|
-
const { routerEngine, storageEngine,
|
|
23
|
+
const { routerEngine, storageEngine, syncContext } = service_context;
|
|
25
24
|
const headers = payload.request.headers;
|
|
26
25
|
const userAgent = headers['x-user-agent'] ?? headers['user-agent'];
|
|
27
26
|
const clientId = payload.params.client_id;
|
|
@@ -50,9 +49,9 @@ export const syncStreamed = routeDefinition({
|
|
|
50
49
|
const syncRules = bucketStorage.getParsedSyncRules(routerEngine!.getAPI().getParseSyncRulesOptions());
|
|
51
50
|
|
|
52
51
|
const controller = new AbortController();
|
|
53
|
-
const tracker = new sync.RequestTracker(
|
|
52
|
+
const tracker = new sync.RequestTracker();
|
|
54
53
|
try {
|
|
55
|
-
|
|
54
|
+
Metrics.getInstance().concurrent_connections.add(1);
|
|
56
55
|
const stream = Readable.from(
|
|
57
56
|
sync.transformToBytesTracked(
|
|
58
57
|
sync.ndjson(
|
|
@@ -97,7 +96,7 @@ export const syncStreamed = routeDefinition({
|
|
|
97
96
|
data: stream,
|
|
98
97
|
afterSend: async () => {
|
|
99
98
|
controller.abort();
|
|
100
|
-
|
|
99
|
+
Metrics.getInstance().concurrent_connections.add(-1);
|
|
101
100
|
logger.info(`Sync stream complete`, {
|
|
102
101
|
user_id: syncParams.user_id,
|
|
103
102
|
client_id: clientId,
|
|
@@ -109,7 +108,7 @@ export const syncStreamed = routeDefinition({
|
|
|
109
108
|
});
|
|
110
109
|
} catch (ex) {
|
|
111
110
|
controller.abort();
|
|
112
|
-
|
|
111
|
+
Metrics.getInstance().concurrent_connections.add(-1);
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
});
|
|
@@ -253,13 +253,14 @@ export interface GetCheckpointChangesOptions {
|
|
|
253
253
|
export interface CheckpointChanges {
|
|
254
254
|
updatedDataBuckets: string[];
|
|
255
255
|
invalidateDataBuckets: boolean;
|
|
256
|
-
|
|
256
|
+
/** Serialized using JSONBig */
|
|
257
|
+
updatedParameterLookups: Set<string>;
|
|
257
258
|
invalidateParameterBuckets: boolean;
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
export const CHECKPOINT_INVALIDATE_ALL: CheckpointChanges = {
|
|
261
262
|
updatedDataBuckets: [],
|
|
262
263
|
invalidateDataBuckets: true,
|
|
263
|
-
|
|
264
|
+
updatedParameterLookups: new Set<string>(),
|
|
264
265
|
invalidateParameterBuckets: true
|
|
265
266
|
};
|
package/src/storage/bson.ts
CHANGED
|
@@ -25,22 +25,20 @@ export const BSON_DESERIALIZE_DATA_OPTIONS: bson.DeserializeOptions = {
|
|
|
25
25
|
* @param lookup
|
|
26
26
|
*/
|
|
27
27
|
export const serializeLookupBuffer = (lookup: SqliteJsonValue[]): NodeBuffer => {
|
|
28
|
-
|
|
29
|
-
if (typeof value == 'number' && Number.isInteger(value)) {
|
|
30
|
-
return BigInt(value);
|
|
31
|
-
} else {
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
return bson.serialize({ l: normalized }) as NodeBuffer;
|
|
28
|
+
return bson.serialize({ l: lookup }) as NodeBuffer;
|
|
36
29
|
};
|
|
37
30
|
|
|
38
31
|
export const serializeLookup = (lookup: SqliteJsonValue[]) => {
|
|
39
32
|
return new bson.Binary(serializeLookupBuffer(lookup));
|
|
40
33
|
};
|
|
41
34
|
|
|
42
|
-
export const
|
|
35
|
+
export const deserializeParameterLookup = (lookup: bson.Binary) => {
|
|
43
36
|
const parsed = bson.deserialize(lookup.buffer, BSON_DESERIALIZE_INTERNAL_OPTIONS).l as SqliteJsonValue[];
|
|
37
|
+
return parsed;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getLookupBucketDefinitionName = (lookup: bson.Binary) => {
|
|
41
|
+
const parsed = deserializeParameterLookup(lookup);
|
|
44
42
|
return parsed[0] as string;
|
|
45
43
|
};
|
|
46
44
|
|
|
@@ -6,7 +6,6 @@ export * from './SourceEntity.js';
|
|
|
6
6
|
export * from './SourceTable.js';
|
|
7
7
|
export * from './StorageEngine.js';
|
|
8
8
|
export * from './StorageProvider.js';
|
|
9
|
-
export * from './storage-metrics.js';
|
|
10
9
|
export * from './WriteCheckpointAPI.js';
|
|
11
10
|
export * from './BucketStorageFactory.js';
|
|
12
11
|
export * from './BucketStorageBatch.js';
|
|
@@ -7,6 +7,8 @@ import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersy
|
|
|
7
7
|
import { BucketParameterQuerier } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
|
|
8
8
|
import { BucketSyncState } from './sync.js';
|
|
9
9
|
import { SyncContext } from './SyncContext.js';
|
|
10
|
+
import { JSONBig } from '@powersync/service-jsonbig';
|
|
11
|
+
import { hasIntersection } from './util.js';
|
|
10
12
|
|
|
11
13
|
export interface BucketChecksumStateOptions {
|
|
12
14
|
syncContext: SyncContext;
|
|
@@ -268,6 +270,10 @@ export class BucketParameterState {
|
|
|
268
270
|
public readonly syncParams: RequestParameters;
|
|
269
271
|
private readonly querier: BucketParameterQuerier;
|
|
270
272
|
private readonly staticBuckets: Map<string, BucketDescription>;
|
|
273
|
+
private cachedDynamicBuckets: BucketDescription[] | null = null;
|
|
274
|
+
private cachedDynamicBucketSet: Set<string> | null = null;
|
|
275
|
+
|
|
276
|
+
private readonly lookups: Set<string>;
|
|
271
277
|
|
|
272
278
|
constructor(
|
|
273
279
|
context: SyncContext,
|
|
@@ -282,6 +288,7 @@ export class BucketParameterState {
|
|
|
282
288
|
|
|
283
289
|
this.querier = syncRules.getBucketParameterQuerier(this.syncParams);
|
|
284
290
|
this.staticBuckets = new Map<string, BucketDescription>(this.querier.staticBuckets.map((b) => [b.bucket, b]));
|
|
291
|
+
this.lookups = new Set<string>(this.querier.parameterQueryLookups.map((l) => JSONBig.stringify(l)));
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
async getCheckpointUpdate(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate | null> {
|
|
@@ -361,36 +368,61 @@ export class BucketParameterState {
|
|
|
361
368
|
const staticBuckets = querier.staticBuckets;
|
|
362
369
|
const update = checkpoint.update;
|
|
363
370
|
|
|
364
|
-
let
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
371
|
+
let hasParameterChange = false;
|
|
372
|
+
let invalidateDataBuckets = false;
|
|
373
|
+
// If hasParameterChange == true, then invalidateDataBuckets = true
|
|
374
|
+
// If invalidateDataBuckets == true, we ignore updatedBuckets
|
|
375
|
+
let updatedBuckets = new Set<string>();
|
|
376
|
+
|
|
377
|
+
if (update.invalidateDataBuckets) {
|
|
378
|
+
invalidateDataBuckets = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (update.invalidateParameterBuckets) {
|
|
382
|
+
hasParameterChange = true;
|
|
369
383
|
} else {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
384
|
+
if (hasIntersection(this.lookups, update.updatedParameterLookups)) {
|
|
385
|
+
// This is a very coarse re-check of all queries
|
|
386
|
+
hasParameterChange = true;
|
|
375
387
|
}
|
|
376
388
|
}
|
|
377
389
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
390
|
+
let dynamicBuckets: BucketDescription[];
|
|
391
|
+
if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
|
|
392
|
+
dynamicBuckets = await querier.queryDynamicBucketDescriptions({
|
|
393
|
+
getParameterSets(lookups) {
|
|
394
|
+
return storage.getParameterSets(checkpoint.base.checkpoint, lookups);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
this.cachedDynamicBuckets = dynamicBuckets;
|
|
398
|
+
this.cachedDynamicBucketSet = new Set<string>(dynamicBuckets.map((b) => b.bucket));
|
|
399
|
+
invalidateDataBuckets = true;
|
|
400
|
+
} else {
|
|
401
|
+
dynamicBuckets = this.cachedDynamicBuckets;
|
|
381
402
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
403
|
+
if (!invalidateDataBuckets) {
|
|
404
|
+
// TODO: Do set intersection instead
|
|
405
|
+
for (let bucket of update.updatedDataBuckets ?? []) {
|
|
406
|
+
if (this.staticBuckets.has(bucket) || this.cachedDynamicBucketSet.has(bucket)) {
|
|
407
|
+
updatedBuckets.add(bucket);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
385
410
|
}
|
|
386
|
-
}
|
|
411
|
+
}
|
|
387
412
|
const allBuckets = [...staticBuckets, ...dynamicBuckets];
|
|
388
413
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
414
|
+
if (invalidateDataBuckets) {
|
|
415
|
+
return {
|
|
416
|
+
buckets: allBuckets,
|
|
417
|
+
// We cannot track individual bucket updates for dynamic lookups yet
|
|
418
|
+
updatedBuckets: null
|
|
419
|
+
};
|
|
420
|
+
} else {
|
|
421
|
+
return {
|
|
422
|
+
buckets: allBuckets,
|
|
423
|
+
updatedBuckets: updatedBuckets
|
|
424
|
+
};
|
|
425
|
+
}
|
|
394
426
|
}
|
|
395
427
|
}
|
|
396
428
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { APIMetric } from '@powersync/service-types';
|
|
1
|
+
import { Metrics } from '../metrics/Metrics.js';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Record sync stats per request stream.
|
|
@@ -9,19 +7,15 @@ export class RequestTracker {
|
|
|
9
7
|
operationsSynced = 0;
|
|
10
8
|
dataSyncedBytes = 0;
|
|
11
9
|
|
|
12
|
-
constructor(private metrics: MetricsEngine) {
|
|
13
|
-
this.metrics = metrics;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
10
|
addOperationsSynced(operations: number) {
|
|
17
11
|
this.operationsSynced += operations;
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
Metrics.getInstance().operations_synced_total.add(operations);
|
|
20
14
|
}
|
|
21
15
|
|
|
22
16
|
addDataSynced(bytes: number) {
|
|
23
17
|
this.dataSyncedBytes += bytes;
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
Metrics.getInstance().data_synced_bytes.add(bytes);
|
|
26
20
|
}
|
|
27
21
|
}
|
package/src/sync/util.ts
CHANGED
|
@@ -153,3 +153,15 @@ export function settledPromise<T>(promise: Promise<T>): Promise<PromiseSettledRe
|
|
|
153
153
|
}
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
export function hasIntersection<T>(a: Set<T>, b: Set<T>) {
|
|
158
|
+
if (a.size > b.size) {
|
|
159
|
+
[a, b] = [b, a];
|
|
160
|
+
}
|
|
161
|
+
// Now, a is always smaller than b, so iterate over a
|
|
162
|
+
for (let value of a) {
|
|
163
|
+
if (b.has(value)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|