@powersync/service-core 1.23.1 → 1.23.3
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 +27 -0
- package/dist/api/RouteAPI.d.ts +25 -11
- package/dist/replication/AbstractReplicator.d.ts +8 -0
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/RouterEngine.d.ts +1 -0
- package/dist/routes/RouterEngine.js +6 -1
- package/dist/routes/RouterEngine.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +52 -44
- package/dist/routes/configure-fastify.js +15 -2
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +1 -3
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +2 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +60 -11
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/ChecksumCache.d.ts +11 -6
- package/dist/storage/ChecksumCache.js +5 -4
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +35 -5
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +1 -1
- package/dist/sync/BucketChecksumState.d.ts +5 -2
- package/dist/sync/BucketChecksumState.js +36 -8
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +7 -0
- package/dist/sync/RequestTracker.js +12 -0
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.js +118 -38
- package/dist/sync/sync.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +2 -0
- package/dist/system/ServiceContext.js +2 -0
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +7 -1
- package/dist/tracing/PerformanceTracer.js +8 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -1
- package/dist/util/checkpointing.d.ts +3 -8
- package/dist/util/checkpointing.js +1 -16
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +10 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/defaults.d.ts +1 -0
- package/dist/util/config/defaults.js +1 -0
- package/dist/util/config/defaults.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -0
- package/dist/util/util-index.d.ts +1 -0
- package/dist/util/util-index.js +1 -0
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/utils.d.ts +1 -1
- package/dist/util/utils.js +1 -1
- package/dist/util/write-checkpoint-batcher.d.ts +19 -0
- package/dist/util/write-checkpoint-batcher.js +90 -0
- package/dist/util/write-checkpoint-batcher.js.map +1 -0
- package/package.json +5 -5
- package/src/api/RouteAPI.ts +25 -11
- package/src/replication/AbstractReplicator.ts +8 -0
- package/src/routes/RouterEngine.ts +7 -1
- package/src/routes/configure-fastify.ts +25 -2
- package/src/routes/endpoints/checkpointing.ts +1 -4
- package/src/routes/endpoints/socket-route.ts +2 -1
- package/src/storage/BucketStorageBatch.ts +61 -11
- package/src/storage/ChecksumCache.ts +27 -14
- package/src/storage/SyncRulesBucketStorage.ts +42 -5
- package/src/storage/WriteCheckpointAPI.ts +1 -1
- package/src/sync/BucketChecksumState.ts +65 -8
- package/src/sync/RequestTracker.ts +20 -0
- package/src/sync/sync.ts +148 -44
- package/src/system/ServiceContext.ts +7 -0
- package/src/tracing/PerformanceTracer.ts +18 -1
- package/src/util/checkpointing.ts +3 -22
- package/src/util/config/compound-config-collector.ts +13 -1
- package/src/util/config/defaults.ts +1 -0
- package/src/util/config/types.ts +1 -0
- package/src/util/util-index.ts +1 -0
- package/src/util/utils.ts +1 -1
- package/src/util/write-checkpoint-batcher.ts +122 -0
- package/test/src/checksum_cache.test.ts +22 -0
- package/test/src/config.test.ts +42 -0
- package/test/src/sync/BucketChecksumState.test.ts +41 -1
- package/test/src/util/checkpointing.test.ts +170 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/util/utils.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export declare function isCompleteRow<V>(storeData: boolean, row: sync_rules.Toa
|
|
|
62
62
|
* All other operations are replaced with a single CLEAR operation,
|
|
63
63
|
* summing their checksums, and using a 0 as an op_id.
|
|
64
64
|
*
|
|
65
|
-
* This is the function $r(B)$, as described in /docs/bucket-properties.md.
|
|
65
|
+
* This is the function $r(B)$, as described in /docs/storage/bucket-properties.md.
|
|
66
66
|
*
|
|
67
67
|
* Used for tests.
|
|
68
68
|
*/
|
package/dist/util/utils.js
CHANGED
|
@@ -165,7 +165,7 @@ export function isCompleteRow(storeData, row) {
|
|
|
165
165
|
* All other operations are replaced with a single CLEAR operation,
|
|
166
166
|
* summing their checksums, and using a 0 as an op_id.
|
|
167
167
|
*
|
|
168
|
-
* This is the function $r(B)$, as described in /docs/bucket-properties.md.
|
|
168
|
+
* This is the function $r(B)$, as described in /docs/storage/bucket-properties.md.
|
|
169
169
|
*
|
|
170
170
|
* Used for tests.
|
|
171
171
|
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RouteAPI } from '../api/RouteAPI.js';
|
|
2
|
+
import { BucketStorageFactory } from '../storage/storage-index.js';
|
|
3
|
+
export interface CreateWriteCheckpointResult {
|
|
4
|
+
writeCheckpoint: string;
|
|
5
|
+
replicationHead: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class WriteCheckpointBatcher {
|
|
8
|
+
private readonly getAPI;
|
|
9
|
+
private readonly getStorage;
|
|
10
|
+
private pending;
|
|
11
|
+
private inFlight;
|
|
12
|
+
private scheduledPump;
|
|
13
|
+
constructor(getAPI: () => RouteAPI, getStorage: () => BucketStorageFactory);
|
|
14
|
+
enqueue(userId: string): Promise<CreateWriteCheckpointResult>;
|
|
15
|
+
private schedulePump;
|
|
16
|
+
private pump;
|
|
17
|
+
private executeBatch;
|
|
18
|
+
private createBatchWriteCheckpoints;
|
|
19
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ErrorCode, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
2
|
+
// Keep up to three source-head/storage batches executing concurrently under load.
|
|
3
|
+
// There is intentionally no explicit batch-size cap here: the HTTP request queue
|
|
4
|
+
// already bounds how many requests can be waiting in this process.
|
|
5
|
+
// Smaller values here have better efficiency, while larger numbers here may give slight improvements in latency.
|
|
6
|
+
// We want to limit the number of database connections in use for write checkpoints, so we keep the numbers low here.
|
|
7
|
+
const MAX_IN_FLIGHT_WRITE_CHECKPOINT_BATCHES = 3;
|
|
8
|
+
export class WriteCheckpointBatcher {
|
|
9
|
+
getAPI;
|
|
10
|
+
getStorage;
|
|
11
|
+
pending = [];
|
|
12
|
+
inFlight = 0;
|
|
13
|
+
scheduledPump;
|
|
14
|
+
constructor(getAPI, getStorage) {
|
|
15
|
+
this.getAPI = getAPI;
|
|
16
|
+
this.getStorage = getStorage;
|
|
17
|
+
}
|
|
18
|
+
enqueue(userId) {
|
|
19
|
+
const resolvers = Promise.withResolvers();
|
|
20
|
+
this.pending.push({ userId, resolvers });
|
|
21
|
+
this.schedulePump();
|
|
22
|
+
return resolvers.promise;
|
|
23
|
+
}
|
|
24
|
+
schedulePump() {
|
|
25
|
+
if (this.scheduledPump != null) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Delay dispatch by one timer turn so requests arriving together can share
|
|
29
|
+
// one source head and one storage write batch.
|
|
30
|
+
// While normally we wouldn't have http requests arriving at the exact same time, this is relevant when
|
|
31
|
+
// requests are queued by fastify: All current requests are typically batched together, and finish at the
|
|
32
|
+
// same time. Then we get many requests from the queue arriving at the same time. Without this delayed dispatch,
|
|
33
|
+
// the first request would form its own batch, and the rest will then wait for a new batch to become available.
|
|
34
|
+
// The delayed dispatch allow all those requests to be batched together.
|
|
35
|
+
this.scheduledPump = setTimeout(() => {
|
|
36
|
+
this.scheduledPump = undefined;
|
|
37
|
+
this.pump();
|
|
38
|
+
}, 0);
|
|
39
|
+
}
|
|
40
|
+
pump() {
|
|
41
|
+
while (this.inFlight < MAX_IN_FLIGHT_WRITE_CHECKPOINT_BATCHES && this.pending.length > 0) {
|
|
42
|
+
const batch = this.pending;
|
|
43
|
+
this.pending = [];
|
|
44
|
+
this.inFlight++;
|
|
45
|
+
void this.executeBatch(batch).finally(() => {
|
|
46
|
+
this.inFlight--;
|
|
47
|
+
this.schedulePump();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async executeBatch(batch) {
|
|
52
|
+
try {
|
|
53
|
+
const syncBucketStorage = (await this.getStorage().getActiveSyncConfig())?.storage;
|
|
54
|
+
if (!syncBucketStorage) {
|
|
55
|
+
throw new ServiceError(ErrorCode.PSYNC_S2302, `Cannot create Write Checkpoint since no sync config is active.`);
|
|
56
|
+
}
|
|
57
|
+
const { writeCheckpoints, currentCheckpoint } = await this.getAPI().createReplicationHead(async (currentCheckpoint) => {
|
|
58
|
+
const writeCheckpoints = await this.createBatchWriteCheckpoints(syncBucketStorage, batch, currentCheckpoint);
|
|
59
|
+
return { writeCheckpoints, currentCheckpoint };
|
|
60
|
+
});
|
|
61
|
+
const results = batch.map((request) => {
|
|
62
|
+
const writeCheckpoint = writeCheckpoints.get(request.userId);
|
|
63
|
+
if (writeCheckpoint == null) {
|
|
64
|
+
throw new ServiceAssertionError(`Write checkpoint storage did not return a checkpoint for user ${request.userId}`);
|
|
65
|
+
}
|
|
66
|
+
return { request, writeCheckpoint };
|
|
67
|
+
});
|
|
68
|
+
for (const { request, writeCheckpoint } of results) {
|
|
69
|
+
request.resolvers.resolve({
|
|
70
|
+
writeCheckpoint: String(writeCheckpoint),
|
|
71
|
+
replicationHead: currentCheckpoint
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Do not retry here. The route should pass failures through so clients can
|
|
77
|
+
// apply their normal write-checkpoint retry policy.
|
|
78
|
+
for (const request of batch) {
|
|
79
|
+
request.resolvers.reject(error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async createBatchWriteCheckpoints(syncBucketStorage, batch, currentCheckpoint) {
|
|
84
|
+
return syncBucketStorage.createManagedWriteCheckpoints(batch.map((request) => ({
|
|
85
|
+
user_id: request.userId,
|
|
86
|
+
heads: { '1': currentCheckpoint }
|
|
87
|
+
})));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=write-checkpoint-batcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-checkpoint-batcher.js","sourceRoot":"","sources":["../../src/util/write-checkpoint-batcher.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.23.
|
|
7
|
+
"version": "1.23.3",
|
|
8
8
|
"license": "FSL-1.1-ALv2",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"exports": "./dist/index.js",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"uuid": "^14.0.0",
|
|
33
33
|
"winston": "^3.13.0",
|
|
34
34
|
"yaml": "^2.8.3",
|
|
35
|
-
"@powersync/lib-services-framework": "0.9.
|
|
35
|
+
"@powersync/lib-services-framework": "0.9.8",
|
|
36
36
|
"@powersync/service-jsonbig": "0.17.13",
|
|
37
|
-
"@powersync/service-rsocket-router": "0.2.
|
|
38
|
-
"@powersync/service-sync-rules": "0.
|
|
39
|
-
"@powersync/service-types": "0.16.
|
|
37
|
+
"@powersync/service-rsocket-router": "0.2.24",
|
|
38
|
+
"@powersync/service-sync-rules": "0.39.0",
|
|
39
|
+
"@powersync/service-types": "0.16.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/async": "^3.2.24",
|
package/src/api/RouteAPI.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface SlotWalBudgetOptions {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Source-specific capabilities used by admin routes, sync config validation,
|
|
29
|
+
* checkpointing, diagnostics, and the sync API.
|
|
29
30
|
*/
|
|
30
31
|
export interface RouteAPI {
|
|
31
32
|
/**
|
|
@@ -40,16 +41,17 @@ export interface RouteAPI {
|
|
|
40
41
|
getConnectionStatus(): Promise<types.ConnectionStatusV2>;
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* Expand sync config table patterns into table metadata from the source connection.
|
|
45
|
+
*
|
|
46
|
+
* This is used by validation and diagnostics paths to explain what will be
|
|
47
|
+
* replicated for the current sync config.
|
|
44
48
|
*
|
|
45
49
|
* @param tablePatterns A set of table patterns which typically come from
|
|
46
50
|
* the tables listed in sync config definitions.
|
|
47
51
|
*
|
|
48
52
|
* @param sqlSyncRules
|
|
49
|
-
* @returns
|
|
50
|
-
*
|
|
51
|
-
* tables to ensure syncing should function according to the input
|
|
52
|
-
* pattern. Debug errors and warnings are reported per table.
|
|
53
|
+
* @returns Matching tables or collections, their columns or fields where available,
|
|
54
|
+
* and per-table warnings or errors for the requested table patterns.
|
|
53
55
|
*/
|
|
54
56
|
getDebugTablesInfo(tablePatterns: TablePattern[], sqlSyncRules: SyncConfig): Promise<PatternResult[]>;
|
|
55
57
|
|
|
@@ -69,14 +71,21 @@ export interface RouteAPI {
|
|
|
69
71
|
/**
|
|
70
72
|
* Get the current LSN or equivalent replication HEAD position identifier.
|
|
71
73
|
*
|
|
72
|
-
* The position is provided to the callback
|
|
73
|
-
*
|
|
74
|
+
* The position is provided to the callback so the caller can persist its
|
|
75
|
+
* write-checkpoint mapping before the source adapter forces any required
|
|
76
|
+
* source-side marker or keepalive. After the callback returns, the adapter
|
|
77
|
+
* must ensure that the replication stream will observe this position or a
|
|
78
|
+
* greater one, even when the source is otherwise idle.
|
|
74
79
|
*/
|
|
75
80
|
createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T>;
|
|
76
81
|
|
|
77
82
|
/**
|
|
78
|
-
* @returns The
|
|
79
|
-
*
|
|
83
|
+
* @returns The connected source schema in service-friendly table and column
|
|
84
|
+
* formats. Strict-schema sources should include actual tables,
|
|
85
|
+
* collections, columns, and types. Schemaless sources should return
|
|
86
|
+
* the best metadata available and document validation limitations in
|
|
87
|
+
* their adapter behavior. This is typically used to validate sync
|
|
88
|
+
* config and generate client schemas.
|
|
80
89
|
*/
|
|
81
90
|
getConnectionSchema(): Promise<types.DatabaseSchema[]>;
|
|
82
91
|
|
|
@@ -92,7 +101,12 @@ export interface RouteAPI {
|
|
|
92
101
|
shutdown(): Promise<void>;
|
|
93
102
|
|
|
94
103
|
/**
|
|
95
|
-
* Get
|
|
104
|
+
* Get source-specific sync config parsing defaults, such as the default
|
|
105
|
+
* schema or database to use when sync config references an unqualified table
|
|
106
|
+
* name.
|
|
107
|
+
*
|
|
108
|
+
* This is used wherever sync config is parsed for this source, including
|
|
109
|
+
* validation, diagnostics, and sync routing.
|
|
96
110
|
*/
|
|
97
111
|
getParseSyncRulesOptions(): ParseSyncConfigOptions;
|
|
98
112
|
}
|
|
@@ -72,6 +72,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
72
72
|
this.logger = logger.child({ name: `Replicator:${options.id}` });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Create the source-specific replication job for a locked storage stream.
|
|
77
|
+
*
|
|
78
|
+
* The job owns the per-stream snapshot and streaming work for this attempt.
|
|
79
|
+
*/
|
|
75
80
|
abstract createJob(options: CreateJobOptions): T;
|
|
76
81
|
|
|
77
82
|
/**
|
|
@@ -374,6 +379,9 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
374
379
|
syncRuleStorage.logger.info(`Successfully terminated replication stream`);
|
|
375
380
|
}
|
|
376
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Check source connectivity using the replicator's decoded connection config.
|
|
384
|
+
*/
|
|
377
385
|
abstract testConnection(): Promise<ConnectionTestResult>;
|
|
378
386
|
|
|
379
387
|
/**
|
|
@@ -11,6 +11,7 @@ export type RouterSetupResponse = {
|
|
|
11
11
|
|
|
12
12
|
export type RouterEngineRoutes = {
|
|
13
13
|
api_routes: RouteDefinition[];
|
|
14
|
+
checkpointing_routes: RouteDefinition[];
|
|
14
15
|
stream_routes: RouteDefinition[];
|
|
15
16
|
socket_routes: SocketRouteGenerator[];
|
|
16
17
|
};
|
|
@@ -42,6 +43,7 @@ export class RouterEngine {
|
|
|
42
43
|
|
|
43
44
|
this.routes = {
|
|
44
45
|
api_routes: [],
|
|
46
|
+
checkpointing_routes: [],
|
|
45
47
|
stream_routes: [],
|
|
46
48
|
socket_routes: []
|
|
47
49
|
};
|
|
@@ -49,13 +51,17 @@ export class RouterEngine {
|
|
|
49
51
|
|
|
50
52
|
public registerRoutes(routes: Partial<RouterEngineRoutes>) {
|
|
51
53
|
this.routes.api_routes.push(...(routes.api_routes ?? []));
|
|
54
|
+
this.routes.checkpointing_routes.push(...(routes.checkpointing_routes ?? []));
|
|
52
55
|
this.routes.stream_routes.push(...(routes.stream_routes ?? []));
|
|
53
56
|
this.routes.socket_routes.push(...(routes.socket_routes ?? []));
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
public get hasRoutes() {
|
|
57
60
|
return (
|
|
58
|
-
this.routes.api_routes.length > 0 ||
|
|
61
|
+
this.routes.api_routes.length > 0 ||
|
|
62
|
+
this.routes.checkpointing_routes.length > 0 ||
|
|
63
|
+
this.routes.stream_routes.length > 0 ||
|
|
64
|
+
this.routes.socket_routes.length > 0
|
|
59
65
|
);
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -31,6 +31,7 @@ export type RouteRegistrationOptions = {
|
|
|
31
31
|
*/
|
|
32
32
|
export type RouteDefinitions = {
|
|
33
33
|
api?: Partial<RouteRegistrationOptions>;
|
|
34
|
+
checkpointing?: Partial<RouteRegistrationOptions>;
|
|
34
35
|
sync_stream?: Partial<RouteRegistrationOptions>;
|
|
35
36
|
};
|
|
36
37
|
|
|
@@ -41,12 +42,20 @@ export type FastifyServerConfig = {
|
|
|
41
42
|
|
|
42
43
|
export const DEFAULT_ROUTE_OPTIONS = {
|
|
43
44
|
api: {
|
|
44
|
-
routes: [...ADMIN_ROUTES, ...
|
|
45
|
+
routes: [...ADMIN_ROUTES, ...SYNC_RULES_ROUTES, ...PROBES_ROUTES],
|
|
45
46
|
queue_options: {
|
|
46
47
|
concurrency: 10,
|
|
47
48
|
max_queue_depth: 20
|
|
48
49
|
}
|
|
49
50
|
},
|
|
51
|
+
checkpointing: {
|
|
52
|
+
routes: [...CHECKPOINT_ROUTES],
|
|
53
|
+
queue_options: {
|
|
54
|
+
// Note that the values here has an effect on WriteCheckpointBatcher
|
|
55
|
+
concurrency: 100,
|
|
56
|
+
max_queue_depth: 100
|
|
57
|
+
}
|
|
58
|
+
},
|
|
50
59
|
sync_stream: {
|
|
51
60
|
routes: [...SYNC_STREAM_ROUTES],
|
|
52
61
|
queue_options: {
|
|
@@ -72,6 +81,7 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
|
|
|
72
81
|
|
|
73
82
|
// Set on the outer server so both child scopes inherit.
|
|
74
83
|
registerFastifyErrorHandler(server);
|
|
84
|
+
registerFastifyNotFoundHandler(server);
|
|
75
85
|
|
|
76
86
|
/**
|
|
77
87
|
* Fastify creates an encapsulated context for each `.register` call.
|
|
@@ -81,7 +91,6 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
|
|
|
81
91
|
*/
|
|
82
92
|
server.register(async function (childContext) {
|
|
83
93
|
registerFastifyRoutes(childContext, generateContext, routes.api?.routes ?? DEFAULT_ROUTE_OPTIONS.api.routes);
|
|
84
|
-
registerFastifyNotFoundHandler(childContext);
|
|
85
94
|
|
|
86
95
|
// Limit the active concurrent requests
|
|
87
96
|
childContext.addHook(
|
|
@@ -90,6 +99,20 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
|
|
|
90
99
|
);
|
|
91
100
|
});
|
|
92
101
|
|
|
102
|
+
server.register(async function (childContext) {
|
|
103
|
+
registerFastifyRoutes(
|
|
104
|
+
childContext,
|
|
105
|
+
generateContext,
|
|
106
|
+
routes.checkpointing?.routes ?? DEFAULT_ROUTE_OPTIONS.checkpointing.routes
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Limit the active concurrent requests
|
|
110
|
+
childContext.addHook(
|
|
111
|
+
'onRequest',
|
|
112
|
+
createRequestQueueHook(routes.checkpointing?.queue_options ?? DEFAULT_ROUTE_OPTIONS.checkpointing.queue_options)
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
93
116
|
// Create a separate context for concurrency queueing
|
|
94
117
|
server.register(async function (childContext) {
|
|
95
118
|
registerFastifyRoutes(
|
|
@@ -54,13 +54,10 @@ export const writeCheckpoint2 = routeDefinition({
|
|
|
54
54
|
handler: async (payload) => {
|
|
55
55
|
const { token_payload, service_context } = payload.context;
|
|
56
56
|
|
|
57
|
-
const apiHandler = service_context.routerEngine.getAPI();
|
|
58
|
-
|
|
59
57
|
const { replicationHead, writeCheckpoint } = await util.createWriteCheckpoint({
|
|
60
58
|
userId: token_payload!.userIdString,
|
|
61
59
|
clientId: payload.params.client_id,
|
|
62
|
-
|
|
63
|
-
storage: service_context.storageEngine.activeBucketStorage
|
|
60
|
+
batcher: service_context.writeCheckpointBatcher
|
|
64
61
|
});
|
|
65
62
|
|
|
66
63
|
logger.info(
|
|
@@ -163,8 +163,9 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
163
163
|
} catch (ex) {
|
|
164
164
|
// Convert to our standard form before responding.
|
|
165
165
|
// This ensures the error can be serialized.
|
|
166
|
+
// However, use the original error for the logs, so that we have the stack trace.
|
|
166
167
|
const error = new errors.InternalServerError(ex);
|
|
167
|
-
logger.error('Sync stream error',
|
|
168
|
+
logger.error('Sync stream error', ex);
|
|
168
169
|
closeReason ??= 'stream error';
|
|
169
170
|
responder.onError(error);
|
|
170
171
|
} finally {
|
|
@@ -12,6 +12,15 @@ export const DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS: ResolvedBucketBatchCommitOptio
|
|
|
12
12
|
oldestUncommittedChange: null
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Storage writer for a source-specific unit of replication work.
|
|
17
|
+
*
|
|
18
|
+
* A batch evaluates source row changes against the hydrated sync config,
|
|
19
|
+
* persists bucket operations and parameter lookup rows, tracks source table and
|
|
20
|
+
* snapshot state, records the committed source position for visible
|
|
21
|
+
* checkpoints, and stores restart cursors for sources with PowerSync-managed
|
|
22
|
+
* resume state.
|
|
23
|
+
*/
|
|
15
24
|
export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageListener>, AsyncDisposable {
|
|
16
25
|
/**
|
|
17
26
|
* Alias for [Symbol.asyncDispose]
|
|
@@ -29,21 +38,28 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
29
38
|
readonly skipExistingRows: boolean;
|
|
30
39
|
|
|
31
40
|
/**
|
|
32
|
-
*
|
|
41
|
+
* Queue one normalized source row operation and potentially flush.
|
|
33
42
|
*
|
|
34
|
-
*
|
|
43
|
+
* When flushed, the operation is evaluated against data and parameter
|
|
44
|
+
* queries. That may produce bucket operations, parameter lookup rows, and
|
|
45
|
+
* current-data updates. `save()` may trigger that flush automatically when
|
|
46
|
+
* the batch reaches its size limits, but a flush does not create a checkpoint
|
|
47
|
+
* by itself.
|
|
35
48
|
*/
|
|
36
49
|
save(record: SaveOptions): Promise<FlushedResult | null>;
|
|
37
50
|
|
|
38
51
|
/**
|
|
39
|
-
* Replicate a truncate
|
|
52
|
+
* Replicate a truncate operation by removing all currently replicated rows
|
|
53
|
+
* for the specified source tables.
|
|
40
54
|
*/
|
|
41
55
|
truncate(sourceTables: SourceTable[]): Promise<FlushedResult | null>;
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
|
-
* Drop one or more
|
|
58
|
+
* Drop one or more source table mappings.
|
|
45
59
|
*
|
|
46
|
-
* This
|
|
60
|
+
* This performs the same logical row removal as `truncate()`, then removes
|
|
61
|
+
* the stored `SourceTable` record. It is used for table drops, renames,
|
|
62
|
+
* replica identity changes, and superseded mappings.
|
|
47
63
|
*/
|
|
48
64
|
drop(sourceTables: SourceTable[]): Promise<FlushedResult | null>;
|
|
49
65
|
|
|
@@ -58,21 +74,26 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
58
74
|
flush(options?: BatchBucketFlushOptions): Promise<FlushedResult | null>;
|
|
59
75
|
|
|
60
76
|
/**
|
|
61
|
-
* Flush
|
|
77
|
+
* Flush saved ops and advance the committed source checkpoint position.
|
|
62
78
|
*
|
|
63
|
-
* Only call this
|
|
79
|
+
* Only call this at a source transaction, page, snapshot, or other boundary
|
|
80
|
+
* where it is valid for clients to observe all flushed changes together. A
|
|
81
|
+
* single commit may include multiple complete source transactions, but must
|
|
82
|
+
* not split one source transaction across checkpoint boundaries.
|
|
64
83
|
*/
|
|
65
84
|
commit(lsn: string, options?: BucketBatchCommitOptions): Promise<CheckpointResult>;
|
|
66
85
|
|
|
67
86
|
/**
|
|
68
|
-
* Advance the checkpoint
|
|
87
|
+
* Advance the committed source checkpoint position without associated row
|
|
88
|
+
* operations.
|
|
69
89
|
*
|
|
70
|
-
*
|
|
90
|
+
* Use this for source heartbeats, marker-only events, and idle-source write
|
|
91
|
+
* checkpoint progress. This must only be called outside a source transaction.
|
|
71
92
|
*/
|
|
72
93
|
keepalive(lsn: string): Promise<CheckpointResult>;
|
|
73
94
|
|
|
74
95
|
/**
|
|
75
|
-
* Set the
|
|
96
|
+
* Set the source position that replication should resume from after restart.
|
|
76
97
|
*
|
|
77
98
|
* This can be used for:
|
|
78
99
|
* 1. Setting the LSN for a snapshot, before starting replication.
|
|
@@ -80,6 +101,10 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
80
101
|
*
|
|
81
102
|
* Not required if the source database keeps track of this, for example with
|
|
82
103
|
* PostgreSQL logical replication slots.
|
|
104
|
+
*
|
|
105
|
+
* This is for restart safety only. If the same source position should become
|
|
106
|
+
* visible to clients, also call `commit()` or `keepalive()` at a valid source
|
|
107
|
+
* boundary.
|
|
83
108
|
*/
|
|
84
109
|
setResumeLsn(lsn: string): Promise<void>;
|
|
85
110
|
|
|
@@ -90,7 +115,21 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
90
115
|
*/
|
|
91
116
|
resumeFromLsn: string | null;
|
|
92
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Mark table snapshots complete and record the source position that
|
|
120
|
+
* checkpoints must not precede for those tables.
|
|
121
|
+
*/
|
|
93
122
|
markTableSnapshotDone(tables: SourceTable[], no_checkpoint_before_lsn?: string): Promise<SourceTable[]>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Mark a source table as requiring snapshot work again.
|
|
126
|
+
*
|
|
127
|
+
* Use this when a table that was previously considered snapshot-complete
|
|
128
|
+
* must be copied again before it can safely contribute to checkpoints. Common
|
|
129
|
+
* cases include detected schema or replica identity changes, interrupted
|
|
130
|
+
* re-snapshot work, or other source metadata changes that make the existing
|
|
131
|
+
* replicated rows no longer trustworthy.
|
|
132
|
+
*/
|
|
94
133
|
markTableSnapshotRequired(table: SourceTable): Promise<void>;
|
|
95
134
|
|
|
96
135
|
/**
|
|
@@ -113,6 +152,10 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
113
152
|
*/
|
|
114
153
|
markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
|
|
115
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Persist resumable table snapshot progress such as row counts or source
|
|
157
|
+
* cursors.
|
|
158
|
+
*/
|
|
116
159
|
updateTableProgress(table: SourceTable, progress: Partial<TableSnapshotStatus>): Promise<SourceTable>;
|
|
117
160
|
|
|
118
161
|
/**
|
|
@@ -120,10 +163,17 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
120
163
|
*/
|
|
121
164
|
getSourceTableStatus(table: SourceTable): Promise<SourceTable | null>;
|
|
122
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Resolve discovered source metadata against the active sync config and
|
|
168
|
+
* persisted source table state.
|
|
169
|
+
*
|
|
170
|
+
* @returns Current mappings that should receive replicated rows, plus
|
|
171
|
+
* outdated mappings that the source connector should drop.
|
|
172
|
+
*/
|
|
123
173
|
resolveTables(options: ResolveTablesOptions): Promise<ResolveTablesResult>;
|
|
124
174
|
|
|
125
175
|
/**
|
|
126
|
-
*
|
|
176
|
+
* Queue a custom write checkpoint to be persisted after operations are flushed.
|
|
127
177
|
*/
|
|
128
178
|
addCustomWriteCheckpoint(checkpoint: BatchedCustomWriteCheckpointOptions): void;
|
|
129
179
|
}
|
|
@@ -5,7 +5,7 @@ import { BucketChecksum } from '../util/protocol-types.js';
|
|
|
5
5
|
import { addBucketChecksums, ChecksumMap, InternalOpId, PartialChecksum } from '../util/utils.js';
|
|
6
6
|
import { BucketChecksumRequest } from './SyncRulesBucketStorage.js';
|
|
7
7
|
|
|
8
|
-
interface ChecksumFetchContext {
|
|
8
|
+
interface ChecksumFetchContext<TContext> {
|
|
9
9
|
fetch(bucket: string): Promise<BucketChecksum>;
|
|
10
10
|
checkpoint: InternalOpId;
|
|
11
11
|
}
|
|
@@ -20,26 +20,35 @@ export interface FetchPartialBucketChecksum {
|
|
|
20
20
|
export type PartialOrFullChecksum = PartialChecksum | BucketChecksum;
|
|
21
21
|
export type PartialChecksumMap = Map<string, PartialOrFullChecksum>;
|
|
22
22
|
|
|
23
|
-
export type FetchChecksums =
|
|
23
|
+
export type FetchChecksums<TContext = undefined> = (
|
|
24
|
+
batch: FetchPartialBucketChecksum[],
|
|
25
|
+
context: TContext
|
|
26
|
+
) => Promise<PartialChecksumMap>;
|
|
24
27
|
|
|
25
|
-
export interface ChecksumCacheOptions {
|
|
28
|
+
export interface ChecksumCacheOptions<TContext = undefined> {
|
|
26
29
|
/**
|
|
27
30
|
* Upstream checksum implementation.
|
|
28
31
|
*
|
|
29
32
|
* This fetches a batch of either entire bucket checksums, or a partial range.
|
|
30
33
|
*/
|
|
31
|
-
fetchChecksums: FetchChecksums
|
|
34
|
+
fetchChecksums: FetchChecksums<TContext>;
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Maximum number of cached checksums.
|
|
35
38
|
*/
|
|
36
39
|
maxSize?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How long to keep cached checksums before refreshing them.
|
|
43
|
+
*/
|
|
44
|
+
ttlMs?: number;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
// Approximately 5MB of memory, if we assume 50 bytes per entry
|
|
40
48
|
const DEFAULT_MAX_SIZE = 100_000;
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
// Default to 1h
|
|
51
|
+
export const DEFAULT_CHECKSUM_CACHE_TTL_MS = 1 * 3_600_000;
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
54
|
* Implement a LRU cache for checksum requests. Each (bucket, checkpoint) request is cached separately,
|
|
@@ -50,22 +59,22 @@ const TTL_MS = 3_600_000;
|
|
|
50
59
|
*
|
|
51
60
|
* We use the LRUCache fetchMethod to deduplicate in-progress requests.
|
|
52
61
|
*/
|
|
53
|
-
export class ChecksumCache {
|
|
62
|
+
export class ChecksumCache<TContext = undefined> {
|
|
54
63
|
/**
|
|
55
64
|
* The primary checksum cache, with key of `${checkpoint}/${bucket}`.
|
|
56
65
|
*/
|
|
57
|
-
private cache: LRUCache<string, BucketChecksum, ChecksumFetchContext
|
|
66
|
+
private cache: LRUCache<string, BucketChecksum, ChecksumFetchContext<TContext>>;
|
|
58
67
|
/**
|
|
59
68
|
* For each bucket, an ordered set of cached checkpoints.
|
|
60
69
|
*/
|
|
61
70
|
private bucketCheckpoints = new Map<string, OrderedSet<bigint>>();
|
|
62
71
|
|
|
63
|
-
private fetchChecksums: FetchChecksums
|
|
72
|
+
private fetchChecksums: FetchChecksums<TContext>;
|
|
64
73
|
|
|
65
|
-
constructor(options: ChecksumCacheOptions) {
|
|
74
|
+
constructor(options: ChecksumCacheOptions<TContext>) {
|
|
66
75
|
this.fetchChecksums = options.fetchChecksums;
|
|
67
76
|
|
|
68
|
-
this.cache = new LRUCache<string, BucketChecksum, ChecksumFetchContext
|
|
77
|
+
this.cache = new LRUCache<string, BucketChecksum, ChecksumFetchContext<TContext>>({
|
|
69
78
|
max: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
70
79
|
fetchMethod: async (cacheKey, _staleValue, options) => {
|
|
71
80
|
// Called when this checksum hasn't been cached yet.
|
|
@@ -105,7 +114,7 @@ export class ChecksumCache {
|
|
|
105
114
|
// We use a TTL so that counts can eventually be refreshed
|
|
106
115
|
// after a compact. This only has effect if the bucket has
|
|
107
116
|
// not been checked in the meantime.
|
|
108
|
-
ttl:
|
|
117
|
+
ttl: options.ttlMs ?? DEFAULT_CHECKSUM_CACHE_TTL_MS,
|
|
109
118
|
ttlResolution: 1_000,
|
|
110
119
|
allowStale: false
|
|
111
120
|
});
|
|
@@ -129,7 +138,11 @@ export class ChecksumCache {
|
|
|
129
138
|
*
|
|
130
139
|
* @returns a Map with exactly one entry for each bucket requested
|
|
131
140
|
*/
|
|
132
|
-
async getChecksumMap(
|
|
141
|
+
async getChecksumMap(
|
|
142
|
+
checkpoint: InternalOpId,
|
|
143
|
+
buckets: BucketChecksumRequest[],
|
|
144
|
+
fetchContext?: TContext
|
|
145
|
+
): Promise<ChecksumMap> {
|
|
133
146
|
// Buckets that don't have a cached checksum for this checkpoint yet
|
|
134
147
|
let toFetch = new Set<string>();
|
|
135
148
|
|
|
@@ -147,7 +160,7 @@ export class ChecksumCache {
|
|
|
147
160
|
// Accumulated results - both from cached checksums, and fetched checksums
|
|
148
161
|
let finalResults = new Map<string, BucketChecksum>();
|
|
149
162
|
|
|
150
|
-
const context: ChecksumFetchContext = {
|
|
163
|
+
const context: ChecksumFetchContext<TContext> = {
|
|
151
164
|
async fetch(bucket) {
|
|
152
165
|
await fetchPromise;
|
|
153
166
|
if (!toFetch.has(bucket)) {
|
|
@@ -260,7 +273,7 @@ export class ChecksumCache {
|
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
// Fetch partial checksums from upstream
|
|
263
|
-
const results = await this.fetchChecksums(bucketRequests);
|
|
276
|
+
const results = await this.fetchChecksums(bucketRequests, fetchContext as TContext);
|
|
264
277
|
|
|
265
278
|
for (let bucket of toFetch) {
|
|
266
279
|
const result = results.get(bucket);
|