@powersync/service-core 1.20.5 → 1.21.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 +35 -0
- package/dist/api/RouteAPI.d.ts +3 -3
- package/dist/api/diagnostics.d.ts +1 -1
- package/dist/api/diagnostics.js +18 -2
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/teardown-action.js +1 -1
- package/dist/entry/commands/teardown-action.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/modules/AbstractModule.d.ts +1 -1
- package/dist/replication/AbstractReplicationJob.js +1 -1
- package/dist/replication/AbstractReplicationJob.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +6 -6
- package/dist/replication/AbstractReplicator.js +21 -21
- package/dist/replication/AbstractReplicator.js.map +1 -1
- 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 +7 -7
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +2 -2
- package/dist/routes/endpoints/sync-stream.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/BucketStorageFactory.d.ts +23 -18
- package/dist/storage/BucketStorageFactory.js +12 -11
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +3 -1
- package/dist/storage/PersistedSyncRulesContent.js +10 -3
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/SourceTable.d.ts +3 -3
- package/dist/storage/SourceTable.js +3 -3
- 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/sync/BucketChecksumState.d.ts +2 -5
- package/dist/sync/BucketChecksumState.js +116 -57
- package/dist/sync/BucketChecksumState.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/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/env.js +1 -1
- package/dist/util/protocol-types.d.ts +1 -1
- package/dist/util/protocol-types.js +1 -1
- package/package.json +11 -11
- package/src/api/RouteAPI.ts +3 -3
- package/src/api/diagnostics.ts +26 -3
- package/src/entry/commands/teardown-action.ts +1 -1
- package/src/index.ts +2 -0
- package/src/modules/AbstractModule.ts +1 -1
- package/src/replication/AbstractReplicationJob.ts +1 -1
- package/src/replication/AbstractReplicator.ts +23 -23
- 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 +7 -7
- package/src/routes/endpoints/sync-stream.ts +2 -2
- package/src/runner/teardown.ts +4 -4
- package/src/storage/BucketStorage.ts +9 -9
- package/src/storage/BucketStorageFactory.ts +29 -22
- package/src/storage/PersistedSyncRulesContent.ts +12 -4
- package/src/storage/SourceTable.ts +3 -3
- 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/sync/BucketChecksumState.ts +137 -73
- package/src/sync/sync.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/compound-config-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
- package/src/util/env.ts +1 -1
- package/src/util/protocol-types.ts +1 -1
- package/test/src/auth.test.ts +109 -1
- package/test/src/diagnostics.test.ts +151 -0
- package/test/src/sync/BucketChecksumState.test.ts +221 -65
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,9 +2,10 @@ import { Logger, ObserverClient } from '@powersync/lib-services-framework';
|
|
|
2
2
|
import {
|
|
3
3
|
BucketDataSource,
|
|
4
4
|
HydratedSyncRules,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
ParameterLookupRows,
|
|
6
|
+
ScopedParameterLookup
|
|
7
7
|
} from '@powersync/service-sync-rules';
|
|
8
|
+
import { PerformanceTracer } from '../tracing/PerformanceTracer.js';
|
|
8
9
|
import * as util from '../util/util-index.js';
|
|
9
10
|
import { BucketStorageBatch, FlushedResult, SaveUpdate } from './BucketStorageBatch.js';
|
|
10
11
|
import { BucketStorageFactory } from './BucketStorageFactory.js';
|
|
@@ -14,7 +15,7 @@ import { SourceTable } from './SourceTable.js';
|
|
|
14
15
|
import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Storage for a specific
|
|
18
|
+
* Storage for a specific replication stream.
|
|
18
19
|
*/
|
|
19
20
|
export interface SyncRulesBucketStorage
|
|
20
21
|
extends ObserverClient<SyncRulesBucketStorageListener>,
|
|
@@ -23,6 +24,7 @@ export interface SyncRulesBucketStorage
|
|
|
23
24
|
readonly slot_name: string;
|
|
24
25
|
|
|
25
26
|
readonly factory: BucketStorageFactory;
|
|
27
|
+
readonly logger: Logger;
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Resolve a table, keeping track of it internally.
|
|
@@ -47,11 +49,11 @@ export interface SyncRulesBucketStorage
|
|
|
47
49
|
getParsedSyncRules(options: ParseSyncRulesOptions): HydratedSyncRules;
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
* Terminate the
|
|
52
|
+
* Terminate the replication stream.
|
|
51
53
|
*
|
|
52
54
|
* This clears the storage, and sets state to TERMINATED.
|
|
53
55
|
*
|
|
54
|
-
* Must only be called on stopped
|
|
56
|
+
* Must only be called on stopped replication streams.
|
|
55
57
|
*/
|
|
56
58
|
terminate(options?: TerminateOptions): Promise<void>;
|
|
57
59
|
|
|
@@ -98,7 +100,7 @@ export interface SyncRulesBucketStorage
|
|
|
98
100
|
/**
|
|
99
101
|
* Yields the latest user write checkpoint whenever the sync checkpoint updates.
|
|
100
102
|
*
|
|
101
|
-
* The stream stops or errors if this is not the active sync
|
|
103
|
+
* The stream stops or errors if this is not the active sync config (anymore).
|
|
102
104
|
*/
|
|
103
105
|
watchCheckpointChanges(options: WatchWriteCheckpointOptions): AsyncIterable<StorageCheckpointUpdate>;
|
|
104
106
|
|
|
@@ -196,6 +198,8 @@ export interface CreateWriterOptions extends ParseSyncRulesOptions {
|
|
|
196
198
|
*/
|
|
197
199
|
markRecordUnavailable?: BucketStorageMarkRecordUnavailable;
|
|
198
200
|
|
|
201
|
+
tracer?: PerformanceTracer<'storage' | 'evaluate'>;
|
|
202
|
+
|
|
199
203
|
logger?: Logger;
|
|
200
204
|
}
|
|
201
205
|
|
|
@@ -265,6 +269,8 @@ export interface CompactOptions {
|
|
|
265
269
|
compactParameterCacheLimit?: number;
|
|
266
270
|
|
|
267
271
|
signal?: AbortSignal;
|
|
272
|
+
|
|
273
|
+
logger?: Logger;
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
export interface PopulateChecksumCacheOptions {
|
|
@@ -320,8 +326,41 @@ export interface ReplicationCheckpoint {
|
|
|
320
326
|
* Used to resolve "dynamic" parameter queries.
|
|
321
327
|
*
|
|
322
328
|
* This gets parameter sets specific to this checkpoint.
|
|
329
|
+
*
|
|
330
|
+
* @throws {@link ParameterSetLimitExceededError}
|
|
331
|
+
* Thrown if resolved lookups in bucket storage exceed the `limit` parameter.
|
|
332
|
+
*/
|
|
333
|
+
getParameterSets(lookups: ScopedParameterLookup[], limit: number): Promise<ParameterLookupRows[]>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* An exception thrown by {@link ReplicationCheckpoint} implementations if there are too many parameter results.
|
|
338
|
+
*
|
|
339
|
+
* This is not a suitable exception to show to users, `BucketParameterState` adds additional context.
|
|
340
|
+
*/
|
|
341
|
+
export class ParameterSetLimitExceededError extends Error {
|
|
342
|
+
constructor(
|
|
343
|
+
readonly limit: number,
|
|
344
|
+
readonly breakdown?: ParameterQueryInvocationLog[]
|
|
345
|
+
) {
|
|
346
|
+
super(`Too many parameter results (limit was ${limit})`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface ParameterQueryInvocationLog {
|
|
351
|
+
/**
|
|
352
|
+
* The definition for which a parameter query was invoked.
|
|
353
|
+
*
|
|
354
|
+
* The exact format of definition is unspecified, it's shown to users to help them debug this failure.
|
|
355
|
+
*/
|
|
356
|
+
definition: string;
|
|
357
|
+
/**
|
|
358
|
+
* If {@link didExceedLimit} is false, the amount of rows returned by the invocation.
|
|
359
|
+
*
|
|
360
|
+
* Otherwise, the maximum amount of rows this invocation was allowed to return.
|
|
323
361
|
*/
|
|
324
|
-
|
|
362
|
+
resultsOrLimit: number;
|
|
363
|
+
didExceedLimit: boolean;
|
|
325
364
|
}
|
|
326
365
|
|
|
327
366
|
export interface WatchWriteCheckpointOptions {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export enum WriteCheckpointMode {
|
|
2
2
|
/**
|
|
3
3
|
* Raw mappings of `user_id` to `write_checkpoint`s should
|
|
4
|
-
* be supplied for each
|
|
4
|
+
* be supplied for each replication stream.
|
|
5
5
|
*/
|
|
6
6
|
CUSTOM = 'custom',
|
|
7
7
|
/**
|
|
@@ -21,7 +21,7 @@ export interface BaseWriteCheckpointIdentifier {
|
|
|
21
21
|
|
|
22
22
|
export interface CustomWriteCheckpointFilters extends BaseWriteCheckpointIdentifier {
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Replication stream which was active when this checkpoint was created.
|
|
25
25
|
*/
|
|
26
26
|
sync_rules_id: number;
|
|
27
27
|
}
|
|
@@ -35,7 +35,7 @@ export interface BatchedCustomWriteCheckpointOptions extends BaseWriteCheckpoint
|
|
|
35
35
|
|
|
36
36
|
export interface CustomWriteCheckpointOptions extends BatchedCustomWriteCheckpointOptions {
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Replication stream which was active when this checkpoint was created.
|
|
39
39
|
*/
|
|
40
40
|
sync_rules_id: number;
|
|
41
41
|
}
|
|
@@ -63,8 +63,8 @@ export interface BaseWriteCheckpointAPI {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Write Checkpoint API to be used in conjunction with a {@link SyncRulesBucketStorage}.
|
|
66
|
-
* This storage corresponds with a
|
|
67
|
-
*
|
|
66
|
+
* This storage corresponds with a replication stream. These APIs don't require specifying a
|
|
67
|
+
* replication stream id.
|
|
68
68
|
*/
|
|
69
69
|
export interface SyncStorageWriteCheckpointAPI extends BaseWriteCheckpointAPI {
|
|
70
70
|
lastWriteCheckpoint(filters: SyncStorageLastWriteCheckpointFilters): Promise<bigint | null>;
|
|
@@ -72,7 +72,7 @@ export interface SyncStorageWriteCheckpointAPI extends BaseWriteCheckpointAPI {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Write Checkpoint API which is interfaced directly with the storage layer. This requires
|
|
75
|
-
*
|
|
75
|
+
* replication stream identifiers for custom write checkpoints.
|
|
76
76
|
*/
|
|
77
77
|
export interface WriteCheckpointAPI extends BaseWriteCheckpointAPI {
|
|
78
78
|
lastWriteCheckpoint(filters: LastWriteCheckpointFilters): Promise<bigint | null>;
|
package/src/storage/bson.ts
CHANGED
|
@@ -40,11 +40,6 @@ export const deserializeParameterLookup = (lookup: bson.Binary) => {
|
|
|
40
40
|
return parsed;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
export const getLookupBucketDefinitionName = (lookup: bson.Binary) => {
|
|
44
|
-
const parsed = deserializeParameterLookup(lookup);
|
|
45
|
-
return parsed[0] as string;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
43
|
/**
|
|
49
44
|
* True if this is a bson.UUID.
|
|
50
45
|
*
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ServiceError
|
|
22
22
|
} from '@powersync/lib-services-framework';
|
|
23
23
|
import { JwtPayload } from '../auth/JwtPayload.js';
|
|
24
|
+
import { ParameterSetLimitExceededError } from '../storage/storage-index.js';
|
|
24
25
|
import { SyncContext } from './SyncContext.js';
|
|
25
26
|
import { getIntersection, hasIntersection } from './util.js';
|
|
26
27
|
|
|
@@ -118,16 +119,38 @@ export class BucketChecksumState {
|
|
|
118
119
|
const storage = this.bucketStorage;
|
|
119
120
|
|
|
120
121
|
const update = await this.parameterState.getCheckpointUpdate(next);
|
|
121
|
-
const { buckets: allBuckets, updatedBuckets,
|
|
122
|
+
const { buckets: allBuckets, updatedBuckets, usedParameterResults } = update;
|
|
122
123
|
|
|
123
124
|
/** Set of all buckets in this checkpoint. */
|
|
124
125
|
const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
|
|
125
126
|
|
|
126
127
|
if (bucketDescriptionMap.size > this.context.maxBuckets) {
|
|
127
|
-
|
|
128
|
+
const error = new ServiceError(
|
|
128
129
|
ErrorCode.PSYNC_S2305,
|
|
129
130
|
`Too many buckets: ${bucketDescriptionMap.size} (limit of ${this.context.maxBuckets})`
|
|
130
131
|
);
|
|
132
|
+
|
|
133
|
+
let errorMessage = error.message;
|
|
134
|
+
const logData: any = {
|
|
135
|
+
checkpoint: next.base.checkpoint,
|
|
136
|
+
user_id: this.parameterState.syncParams.userId,
|
|
137
|
+
buckets: allBuckets.length
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Count buckets per definition.
|
|
141
|
+
const bucketsByDefinition = new Map<string, number>();
|
|
142
|
+
for (const bucket of bucketDescriptionMap.values()) {
|
|
143
|
+
const definition = bucket.definition;
|
|
144
|
+
const count = bucketsByDefinition.get(definition) ?? 0;
|
|
145
|
+
bucketsByDefinition.set(definition, count + 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const breakdown = formatBucketDefinitionBreakdown(bucketsByDefinition);
|
|
149
|
+
errorMessage += breakdown.message;
|
|
150
|
+
logData.buckets_by_definition = breakdown.countsByDefinition;
|
|
151
|
+
|
|
152
|
+
this.logger.error(errorMessage, logData);
|
|
153
|
+
throw error;
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
let checksumMap: util.ChecksumMap;
|
|
@@ -211,13 +234,10 @@ export class BucketChecksumState {
|
|
|
211
234
|
});
|
|
212
235
|
|
|
213
236
|
deferredLog = () => {
|
|
214
|
-
const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
|
|
215
237
|
let message = `Updated checkpoint: ${base.checkpoint} | `;
|
|
216
238
|
message += `write: ${writeCheckpoint} | `;
|
|
217
239
|
message += `buckets: ${allBuckets.length} | `;
|
|
218
|
-
|
|
219
|
-
message += `param_results: ${totalParamResults} | `;
|
|
220
|
-
}
|
|
240
|
+
message += `param_results: ${usedParameterResults} | `;
|
|
221
241
|
message += `updated: ${limitedBuckets(diff.updatedBuckets, 20)} | `;
|
|
222
242
|
message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
|
|
223
243
|
logCheckpoint(
|
|
@@ -230,7 +250,7 @@ export class BucketChecksumState {
|
|
|
230
250
|
updated: diff.updatedBuckets.length,
|
|
231
251
|
removed: diff.removedBuckets.length
|
|
232
252
|
},
|
|
233
|
-
|
|
253
|
+
usedParameterResults
|
|
234
254
|
);
|
|
235
255
|
};
|
|
236
256
|
|
|
@@ -244,12 +264,9 @@ export class BucketChecksumState {
|
|
|
244
264
|
} satisfies util.StreamingSyncCheckpointDiff;
|
|
245
265
|
} else {
|
|
246
266
|
deferredLog = () => {
|
|
247
|
-
const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
|
|
248
267
|
let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
|
|
249
268
|
message += `buckets: ${allBuckets.length}`;
|
|
250
|
-
|
|
251
|
-
message += ` | param_results: ${totalParamResults}`;
|
|
252
|
-
}
|
|
269
|
+
message += ` | param_results: ${usedParameterResults}`;
|
|
253
270
|
message += ` ${limitedBuckets(allBuckets, 20)}`;
|
|
254
271
|
logCheckpoint(
|
|
255
272
|
this.logger,
|
|
@@ -259,7 +276,7 @@ export class BucketChecksumState {
|
|
|
259
276
|
user_id: userIdForLogs,
|
|
260
277
|
buckets: allBuckets.length
|
|
261
278
|
},
|
|
262
|
-
|
|
279
|
+
usedParameterResults
|
|
263
280
|
);
|
|
264
281
|
};
|
|
265
282
|
bucketsToFetch = allBuckets;
|
|
@@ -392,11 +409,8 @@ export interface CheckpointUpdate {
|
|
|
392
409
|
*/
|
|
393
410
|
updatedBuckets: Set<string> | typeof INVALIDATE_ALL_BUCKETS;
|
|
394
411
|
|
|
395
|
-
/**
|
|
396
|
-
|
|
397
|
-
* Map from definition name to count.
|
|
398
|
-
*/
|
|
399
|
-
parameterQueryResultsByDefinition?: Map<string, number>;
|
|
412
|
+
/** The amount of rows fetched from parameters indexes. */
|
|
413
|
+
usedParameterResults: number;
|
|
400
414
|
}
|
|
401
415
|
|
|
402
416
|
export class BucketParameterState {
|
|
@@ -502,36 +516,41 @@ export class BucketParameterState {
|
|
|
502
516
|
const querier = this.querier;
|
|
503
517
|
let update: CheckpointUpdate;
|
|
504
518
|
if (querier.hasDynamicBuckets) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
519
|
+
try {
|
|
520
|
+
update = await this.getCheckpointUpdateDynamic(checkpoint);
|
|
521
|
+
} catch (e: unknown) {
|
|
522
|
+
if (e instanceof ParameterSetLimitExceededError) {
|
|
523
|
+
// Too many parameter results, create a breakdown of which streams are responsible for the most queries and
|
|
524
|
+
// then abort.
|
|
525
|
+
const error = new ServiceError(
|
|
526
|
+
ErrorCode.PSYNC_S2305,
|
|
527
|
+
`Too many parameter query results (limit of ${this.context.maxParameterQueryResults})`
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
let errorMessage = error.message;
|
|
531
|
+
const logData: any = {
|
|
532
|
+
checkpoint: checkpoint.base.checkpoint,
|
|
533
|
+
user_id: this.syncParams.userId
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (e.breakdown) {
|
|
537
|
+
const breakdown = formatParameterQueryBreakdown(e.breakdown);
|
|
538
|
+
if (breakdown) {
|
|
539
|
+
errorMessage += breakdown.message;
|
|
540
|
+
logData.parameterResults = breakdown.largestResults;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
517
543
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
user_id: this.syncParams.userId,
|
|
522
|
-
parameter_query_results: update.buckets.length
|
|
523
|
-
};
|
|
544
|
+
this.logger.error(errorMessage, logData);
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
524
547
|
|
|
525
|
-
|
|
526
|
-
const breakdown = formatParameterQueryBreakdown(update.parameterQueryResultsByDefinition);
|
|
527
|
-
errorMessage += breakdown.message;
|
|
528
|
-
logData.parameter_query_results_by_definition = breakdown.countsByDefinition;
|
|
548
|
+
throw e;
|
|
529
549
|
}
|
|
530
|
-
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
throw error;
|
|
550
|
+
} else {
|
|
551
|
+
update = await this.getCheckpointUpdateStatic(checkpoint);
|
|
534
552
|
}
|
|
553
|
+
|
|
535
554
|
return update;
|
|
536
555
|
}
|
|
537
556
|
|
|
@@ -545,14 +564,16 @@ export class BucketParameterState {
|
|
|
545
564
|
if (update.invalidateDataBuckets) {
|
|
546
565
|
return {
|
|
547
566
|
buckets: staticBuckets,
|
|
548
|
-
updatedBuckets: INVALIDATE_ALL_BUCKETS
|
|
567
|
+
updatedBuckets: INVALIDATE_ALL_BUCKETS,
|
|
568
|
+
usedParameterResults: 0
|
|
549
569
|
};
|
|
550
570
|
}
|
|
551
571
|
|
|
552
572
|
const updatedBuckets = new Set<string>(getIntersection(this.staticBuckets, update.updatedDataBuckets));
|
|
553
573
|
return {
|
|
554
574
|
buckets: staticBuckets,
|
|
555
|
-
updatedBuckets
|
|
575
|
+
updatedBuckets,
|
|
576
|
+
usedParameterResults: 0
|
|
556
577
|
};
|
|
557
578
|
}
|
|
558
579
|
|
|
@@ -584,27 +605,55 @@ export class BucketParameterState {
|
|
|
584
605
|
}
|
|
585
606
|
|
|
586
607
|
let dynamicBuckets: ResolvedBucket[];
|
|
587
|
-
let
|
|
608
|
+
let usedParameterResults = 0;
|
|
588
609
|
if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
|
|
589
610
|
const recordedLookups = new Set<string>();
|
|
611
|
+
const parameterLimit = this.context.maxParameterQueryResults;
|
|
612
|
+
let remainingBudget = parameterLimit;
|
|
613
|
+
|
|
614
|
+
// Log of lookups to provide a breakdown if we exceed the dynamic lookup limit.
|
|
615
|
+
// FIXME: This is horrible, Sync Streams will invoke the callback concurrently and we can't properly deal with
|
|
616
|
+
// that. We should replace queriers for Sync Streams with an explicit graph structure based on sync plans instead
|
|
617
|
+
// of adding these checks to the imperative querier interface.
|
|
618
|
+
const lookupLog: storage.ParameterQueryInvocationLog[] = [];
|
|
590
619
|
|
|
591
620
|
dynamicBuckets = await querier.queryDynamicBucketDescriptions({
|
|
592
|
-
getParameterSets(lookups) {
|
|
621
|
+
getParameterSets: async (lookups, definition) => {
|
|
622
|
+
if (lookups.length > parameterLimit) {
|
|
623
|
+
// Sync Streams can chain parameter lookups, so a large output from an earlier call may become the input
|
|
624
|
+
// here. We reuse the output limit as a generous upper bound; legitimate queries are much smaller.
|
|
625
|
+
const msg = `Attempted to fetch ${lookups.length} lookups at once, a maximum of ${parameterLimit} lookups are allowed.`;
|
|
626
|
+
this.logger.error(msg, {
|
|
627
|
+
user_id: this.syncParams.userId,
|
|
628
|
+
checkpoint: checkpoint.base.checkpoint,
|
|
629
|
+
cause: definition
|
|
630
|
+
});
|
|
631
|
+
throw new ServiceError(ErrorCode.PSYNC_S2305, msg);
|
|
632
|
+
}
|
|
633
|
+
|
|
593
634
|
for (const lookup of lookups) {
|
|
594
635
|
recordedLookups.add(lookup.serializedRepresentation);
|
|
595
636
|
}
|
|
596
637
|
|
|
597
|
-
|
|
638
|
+
try {
|
|
639
|
+
const results = await checkpoint.base.getParameterSets(lookups, remainingBudget);
|
|
640
|
+
const numRows = results.reduce((a, b) => a + b.rows.length, 0);
|
|
641
|
+
|
|
642
|
+
lookupLog.push({ definition, resultsOrLimit: numRows, didExceedLimit: false });
|
|
643
|
+
remainingBudget -= numRows;
|
|
644
|
+
usedParameterResults += numRows;
|
|
645
|
+
return results;
|
|
646
|
+
} catch (e: unknown) {
|
|
647
|
+
if (e instanceof ParameterSetLimitExceededError) {
|
|
648
|
+
lookupLog.push({ definition, resultsOrLimit: remainingBudget, didExceedLimit: true });
|
|
649
|
+
throw new ParameterSetLimitExceededError(parameterLimit, lookupLog);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
throw e;
|
|
653
|
+
}
|
|
598
654
|
}
|
|
599
655
|
});
|
|
600
656
|
|
|
601
|
-
// Count parameter query results per definition (before deduplication)
|
|
602
|
-
parameterQueryResultsByDefinition = new Map<string, number>();
|
|
603
|
-
for (const bucket of dynamicBuckets) {
|
|
604
|
-
const count = parameterQueryResultsByDefinition.get(bucket.definition) ?? 0;
|
|
605
|
-
parameterQueryResultsByDefinition.set(bucket.definition, count + 1);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
657
|
this.cachedDynamicBuckets = dynamicBuckets;
|
|
609
658
|
this.cachedDynamicBucketSet = new Set<string>(dynamicBuckets.map((b) => b.bucket));
|
|
610
659
|
this.lookupsFromPreviousCheckpoint = recordedLookups;
|
|
@@ -628,13 +677,13 @@ export class BucketParameterState {
|
|
|
628
677
|
buckets: allBuckets,
|
|
629
678
|
// We cannot track individual bucket updates for dynamic lookups yet
|
|
630
679
|
updatedBuckets: INVALIDATE_ALL_BUCKETS,
|
|
631
|
-
|
|
680
|
+
usedParameterResults
|
|
632
681
|
};
|
|
633
682
|
} else {
|
|
634
683
|
return {
|
|
635
684
|
buckets: allBuckets,
|
|
636
685
|
updatedBuckets: updatedBuckets,
|
|
637
|
-
|
|
686
|
+
usedParameterResults
|
|
638
687
|
};
|
|
639
688
|
}
|
|
640
689
|
}
|
|
@@ -668,18 +717,6 @@ export interface CheckpointLine {
|
|
|
668
717
|
// Use a more specific type to simplify testing
|
|
669
718
|
export type BucketChecksumStateStorage = Pick<storage.SyncRulesBucketStorage, 'getChecksums'>;
|
|
670
719
|
|
|
671
|
-
/**
|
|
672
|
-
* Compute the total number of parameter query results across all definitions.
|
|
673
|
-
*/
|
|
674
|
-
function computeTotalParamResults(
|
|
675
|
-
parameterQueryResultsByDefinition: Map<string, number> | undefined
|
|
676
|
-
): number | undefined {
|
|
677
|
-
if (!parameterQueryResultsByDefinition) {
|
|
678
|
-
return undefined;
|
|
679
|
-
}
|
|
680
|
-
return Array.from(parameterQueryResultsByDefinition.values()).reduce((sum, count) => sum + count, 0);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
720
|
/**
|
|
684
721
|
* Log a checkpoint message, enriching it with parameter query result counts if available.
|
|
685
722
|
*
|
|
@@ -701,20 +738,20 @@ function logCheckpoint(
|
|
|
701
738
|
}
|
|
702
739
|
|
|
703
740
|
/**
|
|
704
|
-
* Format a breakdown of
|
|
741
|
+
* Format a breakdown of dynamic bucket by sync stream definition.
|
|
705
742
|
*
|
|
706
743
|
* Sorts definitions by count (descending), includes the top 10, and returns both the
|
|
707
744
|
* formatted message string and the counts record suitable for structured log data.
|
|
708
745
|
*/
|
|
709
|
-
function
|
|
746
|
+
function formatBucketDefinitionBreakdown(bucketsByDefinition: Map<string, number>): {
|
|
710
747
|
message: string;
|
|
711
748
|
countsByDefinition: Record<string, number>;
|
|
712
749
|
} {
|
|
713
750
|
// Sort definitions by count (descending) and take top 10
|
|
714
|
-
const allSorted = Array.from(
|
|
751
|
+
const allSorted = Array.from(bucketsByDefinition.entries()).sort((a, b) => b[1] - a[1]);
|
|
715
752
|
const sortedDefinitions = allSorted.slice(0, 10);
|
|
716
753
|
|
|
717
|
-
let message = '\
|
|
754
|
+
let message = '\Buckets by definition:';
|
|
718
755
|
const countsByDefinition: Record<string, number> = {};
|
|
719
756
|
for (const [definition, count] of sortedDefinitions) {
|
|
720
757
|
message += `\n ${definition}: ${count}`;
|
|
@@ -730,6 +767,33 @@ function formatParameterQueryBreakdown(parameterQueryResultsByDefinition: Map<st
|
|
|
730
767
|
return { message, countsByDefinition };
|
|
731
768
|
}
|
|
732
769
|
|
|
770
|
+
function formatParameterQueryBreakdown(log: storage.ParameterQueryInvocationLog[]) {
|
|
771
|
+
if (log.length == 0) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// When an exception about too many parameter results is thrown, the last entry is the one that finally exceeded the
|
|
776
|
+
// limit.
|
|
777
|
+
const results = Array.from(log);
|
|
778
|
+
const failure = results.pop()!;
|
|
779
|
+
|
|
780
|
+
let message = '\nInvoked parameter queries by definition:';
|
|
781
|
+
results.sort((a, b) => b.resultsOrLimit - a.resultsOrLimit);
|
|
782
|
+
const largestResults = results.splice(0, 9);
|
|
783
|
+
|
|
784
|
+
for (const entry of largestResults) {
|
|
785
|
+
message += `\n ${entry.definition}: ${entry.resultsOrLimit} results.`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (largestResults.length < log.length) {
|
|
789
|
+
message += `\n ... and ${log.length - largestResults.length} more invocations`;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
message += `\n ${failure.definition} exceeded the remaining limit of ${failure.resultsOrLimit} available results.`;
|
|
793
|
+
largestResults.push(failure);
|
|
794
|
+
return { message, largestResults };
|
|
795
|
+
}
|
|
796
|
+
|
|
733
797
|
function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number) {
|
|
734
798
|
buckets = buckets.map((b) => {
|
|
735
799
|
if (typeof b != 'string') {
|
package/src/sync/sync.ts
CHANGED
|
@@ -267,7 +267,7 @@ interface BucketDataRequest {
|
|
|
267
267
|
* This signal also fires when abort_connection fires.
|
|
268
268
|
*/
|
|
269
269
|
abort_batch: AbortSignal;
|
|
270
|
-
/** User id for debug purposes, not for sync
|
|
270
|
+
/** User id for debug purposes, not for sync config. */
|
|
271
271
|
userIdForLogs?: SqliteJsonValue;
|
|
272
272
|
forPriority: BucketPriority | null;
|
|
273
273
|
onRowsSent: (stats: OperationsSentStats) => void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { traceWriter } from './TraceWriter.js';
|
|
2
|
+
|
|
3
|
+
export interface Span extends Disposable {
|
|
4
|
+
name: string;
|
|
5
|
+
startAt: number;
|
|
6
|
+
endAt: number;
|
|
7
|
+
selfDuration: number;
|
|
8
|
+
nestedSince: number | undefined;
|
|
9
|
+
subtrackFromSelf: number;
|
|
10
|
+
nestedDurations: Record<string, number>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* End the span - same as [Symbol.dispose]().
|
|
14
|
+
*
|
|
15
|
+
* Safe to call multiple times. Any nested spans will automatically end as well.
|
|
16
|
+
*
|
|
17
|
+
* Returns an aggregate record of category -> "selfDuration".
|
|
18
|
+
*/
|
|
19
|
+
end(): Record<string, number>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function now() {
|
|
23
|
+
return Number(process.hrtime.bigint() / 1000n);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let nextThreadId = 1;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lightweight tracing helper, with two main goals:
|
|
30
|
+
* 1. Generate aggregate timing info with low overhead.
|
|
31
|
+
* 2. Optional support for generating trace files during development.
|
|
32
|
+
*
|
|
33
|
+
* This is only intended for a single "thread" - concurrent operations on the same instance have undefined behavior.
|
|
34
|
+
* To trace concurrent operations, use separate instances of PerformanceTracer.
|
|
35
|
+
*
|
|
36
|
+
* Spans cannot be overlapping: If a parent span is ended, all nested spans are automatically ended.
|
|
37
|
+
*/
|
|
38
|
+
export class PerformanceTracer<K extends string> {
|
|
39
|
+
stack: Span[] = [];
|
|
40
|
+
threadId: number;
|
|
41
|
+
|
|
42
|
+
constructor(traceName: string) {
|
|
43
|
+
this.threadId = nextThreadId;
|
|
44
|
+
nextThreadId += 1;
|
|
45
|
+
traceWriter?.write({
|
|
46
|
+
ph: 'M',
|
|
47
|
+
cat: '__metadata',
|
|
48
|
+
name: 'thread_name',
|
|
49
|
+
pid: process.pid,
|
|
50
|
+
tid: this.threadId,
|
|
51
|
+
args: { name: `PowerSync ${traceName}` }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recommended usage:
|
|
57
|
+
*
|
|
58
|
+
* using _ = tracer.span('cat', 'details');
|
|
59
|
+
*
|
|
60
|
+
* The above automatically ends the span when it goes out of scope. Alternatively, call
|
|
61
|
+
* .end() on the span to end it earlier.
|
|
62
|
+
*
|
|
63
|
+
* @param category one of the defined categories
|
|
64
|
+
* @param subcat optional subcategory. Not used for calculating "self" durations in the aggregate API.
|
|
65
|
+
*/
|
|
66
|
+
span(category: K, subcat?: string): Span {
|
|
67
|
+
const stack = this.stack;
|
|
68
|
+
const index = this.stack.length;
|
|
69
|
+
const parent = this.stack[this.stack.length - 1];
|
|
70
|
+
const threadId = this.threadId;
|
|
71
|
+
const startAt = now();
|
|
72
|
+
if (parent != null) {
|
|
73
|
+
parent.nestedSince ??= startAt;
|
|
74
|
+
}
|
|
75
|
+
let name: string = category;
|
|
76
|
+
if (subcat) {
|
|
77
|
+
name += ':' + subcat;
|
|
78
|
+
}
|
|
79
|
+
const s: Span = {
|
|
80
|
+
name,
|
|
81
|
+
startAt: now(),
|
|
82
|
+
selfDuration: 0,
|
|
83
|
+
endAt: 0,
|
|
84
|
+
nestedSince: undefined,
|
|
85
|
+
subtrackFromSelf: 0,
|
|
86
|
+
nestedDurations: {},
|
|
87
|
+
end() {
|
|
88
|
+
if (this.endAt != 0) {
|
|
89
|
+
return this.nestedDurations;
|
|
90
|
+
}
|
|
91
|
+
while (stack.length - 1 > index) {
|
|
92
|
+
stack[stack.length - 1].end();
|
|
93
|
+
}
|
|
94
|
+
const endAt = now();
|
|
95
|
+
this.endAt = endAt;
|
|
96
|
+
const endTime = this.nestedSince ?? endAt;
|
|
97
|
+
this.selfDuration = endTime - startAt - this.subtrackFromSelf;
|
|
98
|
+
traceWriter?.write({
|
|
99
|
+
name,
|
|
100
|
+
cat: 'powersync',
|
|
101
|
+
ph: 'X',
|
|
102
|
+
ts: this.startAt,
|
|
103
|
+
dur: endAt - startAt,
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
tid: threadId
|
|
106
|
+
});
|
|
107
|
+
stack.pop();
|
|
108
|
+
if (parent != null) {
|
|
109
|
+
parent.subtrackFromSelf += endAt - parent.nestedSince!;
|
|
110
|
+
for (let key in this.nestedDurations) {
|
|
111
|
+
parent.nestedDurations[key] = (parent.nestedDurations[key] ?? 0) + this.nestedDurations[key];
|
|
112
|
+
}
|
|
113
|
+
parent.nestedDurations[category] = (parent.nestedDurations[category] ?? 0) + this.selfDuration;
|
|
114
|
+
parent.nestedSince = undefined;
|
|
115
|
+
}
|
|
116
|
+
return this.nestedDurations;
|
|
117
|
+
},
|
|
118
|
+
[Symbol.dispose]() {
|
|
119
|
+
this.end();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
this.stack.push(s);
|
|
123
|
+
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
}
|