@powersync/service-module-mongodb-storage 0.20.0 → 0.22.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 +62 -0
- package/dist/storage/MongoBucketStorage.d.ts +36 -3
- package/dist/storage/MongoBucketStorage.js +372 -207
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +17 -5
- package/dist/storage/implementation/MongoBucketBatch.js +44 -17
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +7 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
- package/dist/storage/implementation/common/PersistedBatch.js +49 -6
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +9 -1
- package/dist/storage/implementation/db.js +27 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +31 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +33 -4
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
- package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +10 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +100 -26
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +82 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +698 -159
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +76 -42
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +84 -4
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +6 -6
- package/dist/storage/implementation/v3/source-table-utils.js +30 -37
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +28 -1
- package/dist/types/types.js +28 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +1 -0
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +507 -269
- package/src/storage/implementation/MongoBucketBatch.ts +67 -24
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +15 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +66 -10
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +35 -2
- package/src/storage/implementation/models.ts +34 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +46 -6
- package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +138 -34
- package/src/storage/implementation/v3/MongoCompactorV3.ts +958 -225
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +93 -48
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +91 -3
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
- package/src/storage/implementation/v3/source-table-utils.ts +39 -41
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +1 -0
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +267 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +273 -22
- package/test/src/storage_compacting.test.ts +968 -246
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +290 -2
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +336 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -5,7 +5,13 @@ import {
|
|
|
5
5
|
storage
|
|
6
6
|
} from '@powersync/service-core';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DO_NOT_LOG,
|
|
10
|
+
ErrorCode,
|
|
11
|
+
logger,
|
|
12
|
+
ReplicationAssertionError,
|
|
13
|
+
ServiceError
|
|
14
|
+
} from '@powersync/lib-services-framework';
|
|
9
15
|
import { v4 as uuid } from 'uuid';
|
|
10
16
|
|
|
11
17
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
@@ -16,8 +22,9 @@ import {
|
|
|
16
22
|
formatIncrementalSyncConfigUpdateLog,
|
|
17
23
|
isCompatible
|
|
18
24
|
} from '@powersync/service-core';
|
|
25
|
+
import { Semaphore } from 'async-mutex';
|
|
19
26
|
import { ObjectId } from 'bson';
|
|
20
|
-
import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../types/types.js';
|
|
27
|
+
import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE, normalizeChunkCompactionConcurrency } from '../types/types.js';
|
|
21
28
|
import { generateReplicationStreamName } from '../utils/util.js';
|
|
22
29
|
import type { MongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
|
|
23
30
|
import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
|
|
@@ -25,9 +32,10 @@ import { PowerSyncMongo } from './implementation/db.js';
|
|
|
25
32
|
import { getMongoStorageConfig, StorageConfig, SyncRuleDocumentBase } from './implementation/models.js';
|
|
26
33
|
import { MongoChecksumOptions } from './implementation/MongoChecksums.js';
|
|
27
34
|
import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
|
|
28
|
-
import {
|
|
35
|
+
import { stopReplicationStreamPipeline } from './implementation/SyncRuleStateUpdate.js';
|
|
29
36
|
import { SyncRuleDocumentV1 } from './implementation/v1/models.js';
|
|
30
37
|
import { ObjectStorage } from './implementation/v3/object-storage/ObjectStorage.js';
|
|
38
|
+
import { ObjectStorageUsage } from './implementation/v3/object-storage/ObjectStorageUsage.js';
|
|
31
39
|
import { VersionedPowerSyncMongoV3 } from './implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
32
40
|
import { ReplicationStreamDocumentV3, SyncConfigDefinition, SyncRuleConfigStateV3 } from './storage-index.js';
|
|
33
41
|
|
|
@@ -35,6 +43,8 @@ export interface MongoBucketStorageOptions {
|
|
|
35
43
|
checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
|
|
36
44
|
objectStorage?: ObjectStorage;
|
|
37
45
|
inlineThresholdBytes?: number;
|
|
46
|
+
/** Shared across chunk-compaction jobs. Default: 4 with object storage, otherwise 2. */
|
|
47
|
+
chunkCompactionConcurrency?: number;
|
|
38
48
|
/**
|
|
39
49
|
* Prefix for replication stream name and Postgres logical replication slot name.
|
|
40
50
|
*/
|
|
@@ -42,6 +52,7 @@ export interface MongoBucketStorageOptions {
|
|
|
42
52
|
readPreference?: mongo.ReadPreference;
|
|
43
53
|
checksumCacheTtlMs?: number;
|
|
44
54
|
clearBatchThrottleRate?: number;
|
|
55
|
+
defaultStorageVersion?: number;
|
|
45
56
|
/**
|
|
46
57
|
* Reuse a compatible active replication stream by appending a new sync config.
|
|
47
58
|
*
|
|
@@ -56,21 +67,27 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
56
67
|
[DO_NOT_LOG] = true;
|
|
57
68
|
|
|
58
69
|
private readonly client: mongo.MongoClient;
|
|
59
|
-
private readonly session: mongo.ClientSession;
|
|
60
70
|
public readonly replicationStreamNamePrefix: string;
|
|
61
71
|
|
|
62
72
|
private activeStorageCache: MongoSyncBucketStorage | undefined;
|
|
63
73
|
|
|
64
74
|
public readonly db: PowerSyncMongo;
|
|
75
|
+
public readonly chunkCompactionConcurrency: number;
|
|
76
|
+
public readonly chunkCompactionSlots: Semaphore;
|
|
65
77
|
|
|
66
78
|
constructor(
|
|
67
79
|
db: PowerSyncMongo,
|
|
68
80
|
private options: MongoBucketStorageOptions
|
|
69
81
|
) {
|
|
70
82
|
super();
|
|
83
|
+
this.chunkCompactionConcurrency = normalizeChunkCompactionConcurrency(
|
|
84
|
+
options.chunkCompactionConcurrency,
|
|
85
|
+
options.objectStorage != null
|
|
86
|
+
);
|
|
87
|
+
// All replication streams created by this factory share the configured limit.
|
|
88
|
+
this.chunkCompactionSlots = new Semaphore(this.chunkCompactionConcurrency);
|
|
71
89
|
this.client = db.client;
|
|
72
90
|
this.db = db;
|
|
73
|
-
this.session = this.client.startSession();
|
|
74
91
|
this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
|
|
75
92
|
}
|
|
76
93
|
|
|
@@ -138,187 +155,255 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
async restartReplication(replicationStreamId: number) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
await this.withTransaction(async (session) => {
|
|
159
|
+
const next = await this.getDeployingSyncConfigInternal(session);
|
|
160
|
+
const active = await this.getActiveSyncConfigInternal(session);
|
|
161
|
+
|
|
162
|
+
if (next != null && next.content.replicationStreamId == replicationStreamId) {
|
|
163
|
+
// We need to redo the "next" replication stream.
|
|
164
|
+
// This creates a new stream, and stops the existing PROCESSING one.
|
|
165
|
+
await this.updateSyncRulesInTransaction(
|
|
166
|
+
next.content.asUpdateOptions({ forceNewReplicationStream: true }),
|
|
167
|
+
session
|
|
168
|
+
);
|
|
169
|
+
const sharedActiveStream = active?.content.replicationStreamId == replicationStreamId;
|
|
170
|
+
if (sharedActiveStream) {
|
|
171
|
+
// The same stream was also used for the ACTIVE config. Transition that to ERRORED
|
|
172
|
+
await this.errorActiveStreamForReplacement(active, session);
|
|
173
|
+
}
|
|
174
|
+
} else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
|
|
175
|
+
// Slot removed for "active" replication stream, while there is no "next" one.
|
|
176
|
+
await this.updateSyncRulesInTransaction(
|
|
177
|
+
active.content.asUpdateOptions({ forceNewReplicationStream: true }),
|
|
178
|
+
session
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// In this case we keep the old one as active for clients, so that that existing clients
|
|
182
|
+
// can still get the latest data while we replicate the new ones.
|
|
183
|
+
// It will however not replicate anymore.
|
|
184
|
+
await this.errorActiveStreamForReplacement(active, session);
|
|
185
|
+
} else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
|
|
186
|
+
// Already have next replication stream, but need to stop replicating the active one.
|
|
187
|
+
await this.errorActiveStreamForReplacement(active, session);
|
|
188
|
+
} else {
|
|
189
|
+
// replicationStreamId does not match the ACTIVE or PROCESSING streams - no-op.
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
await this.db.notifyCheckpoint();
|
|
193
|
+
}
|
|
163
194
|
|
|
164
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Keep an active config readable after its replication stream is invalidated.
|
|
197
|
+
*
|
|
198
|
+
* Incremental V3 streams can retain stopped historical configs and embed a deploying config.
|
|
199
|
+
* Read-side storage requires exactly one ACTIVE or ERRORED config, so only the active config
|
|
200
|
+
* transitions to ERRORED; every other embedded config remains or becomes STOP.
|
|
201
|
+
*/
|
|
202
|
+
private async errorActiveStreamForReplacement(active: storage.ResolvedSyncConfig, session: mongo.ClientSession) {
|
|
203
|
+
const activeSyncConfigId = active.content.syncConfigId;
|
|
204
|
+
if (activeSyncConfigId == null) {
|
|
205
|
+
// V1 documents
|
|
206
|
+
const result = await this.db.sync_rules.updateOne(
|
|
165
207
|
{
|
|
166
208
|
_id: active.content.replicationStreamId,
|
|
167
|
-
state: storage.SyncRuleState.ACTIVE
|
|
209
|
+
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
168
210
|
},
|
|
169
|
-
syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED)
|
|
170
|
-
);
|
|
171
|
-
await this.db.notifyCheckpoint();
|
|
172
|
-
} else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
|
|
173
|
-
// Already have next replication stream, but need to stop replicating the active one.
|
|
174
|
-
|
|
175
|
-
await this.db.sync_rules.updateOne(
|
|
176
211
|
{
|
|
177
|
-
|
|
178
|
-
|
|
212
|
+
$set: {
|
|
213
|
+
state: storage.SyncRuleState.ERRORED
|
|
214
|
+
}
|
|
179
215
|
},
|
|
180
|
-
|
|
216
|
+
{ session }
|
|
217
|
+
);
|
|
218
|
+
if (result.matchedCount != 1) {
|
|
219
|
+
// This should generally not happen, since the entire process runs in a transaction.
|
|
220
|
+
// The filters just double-check that the state is what we expect, and fails hard otherwise.
|
|
221
|
+
throw new ReplicationAssertionError(
|
|
222
|
+
`Active replication stream ${active.content.replicationStreamId} changed during restart`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const activeConfigObjectId = new ObjectId(activeSyncConfigId);
|
|
229
|
+
const result = await this.db.sync_rules.updateOne(
|
|
230
|
+
{
|
|
231
|
+
_id: active.content.replicationStreamId,
|
|
232
|
+
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
|
|
233
|
+
sync_configs: {
|
|
234
|
+
// Confirm that this is still the ACTIVE/ERRORED one.
|
|
235
|
+
$elemMatch: {
|
|
236
|
+
_id: activeConfigObjectId,
|
|
237
|
+
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
$set: {
|
|
243
|
+
state: storage.SyncRuleState.ERRORED,
|
|
244
|
+
// There must be exactly one config with an ERRORED state: This will be used to serve
|
|
245
|
+
// clients until there is a new ACTIVE one.
|
|
246
|
+
'sync_configs.$[activeConfig].state': storage.SyncRuleState.ERRORED,
|
|
247
|
+
// PROCESSING configs must stop. Historical STOP configs remain unchanged.
|
|
248
|
+
'sync_configs.$[processingConfig].state': storage.SyncRuleState.STOP
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
session,
|
|
253
|
+
arrayFilters: [
|
|
254
|
+
{
|
|
255
|
+
'activeConfig._id': activeConfigObjectId,
|
|
256
|
+
'activeConfig.state': { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
'processingConfig._id': { $ne: activeConfigObjectId },
|
|
260
|
+
'processingConfig.state': storage.SyncRuleState.PROCESSING
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
if (result.matchedCount != 1) {
|
|
266
|
+
// This should generally not happen, since the entire process runs in a transaction.
|
|
267
|
+
// The filters just double-check that the state is what we expect, and fails hard otherwise.
|
|
268
|
+
throw new ReplicationAssertionError(
|
|
269
|
+
`Active replication stream ${active.content.replicationStreamId} changed during restart`
|
|
181
270
|
);
|
|
182
|
-
await this.db.notifyCheckpoint();
|
|
183
271
|
}
|
|
184
272
|
}
|
|
185
273
|
|
|
186
|
-
private async
|
|
274
|
+
private async updateSyncRulesInTransaction(options: storage.UpdateSyncRulesOptions, session: mongo.ClientSession) {
|
|
275
|
+
const storageVersion =
|
|
276
|
+
options.storageVersion ??
|
|
277
|
+
options.config.parsed.config.storageVersion ??
|
|
278
|
+
this.options.defaultStorageVersion ??
|
|
279
|
+
storage.CURRENT_STORAGE_VERSION;
|
|
280
|
+
const storageConfig = getMongoStorageConfig(storageVersion);
|
|
281
|
+
if (storageConfig.incrementalReprocessing) {
|
|
282
|
+
return this.updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session);
|
|
283
|
+
}
|
|
284
|
+
return this.updateSyncConfigV1InTransaction(options, storageVersion, session);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Update to a V3 sync config.
|
|
289
|
+
*
|
|
290
|
+
* Does support cases where existing replication streams are on V1.
|
|
291
|
+
*/
|
|
292
|
+
private async updateSyncConfigV3InTransaction(
|
|
187
293
|
options: storage.UpdateSyncRulesOptions,
|
|
188
294
|
storageVersion: number,
|
|
189
|
-
storageConfig: StorageConfig
|
|
295
|
+
storageConfig: StorageConfig,
|
|
296
|
+
session: mongo.ClientSession
|
|
190
297
|
): Promise<MongoPersistedReplicationStream> {
|
|
191
|
-
let rules: MongoPersistedReplicationStream | undefined = undefined;
|
|
192
298
|
const versioned = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
|
|
193
299
|
|
|
194
|
-
const
|
|
300
|
+
const active = await this.db.sync_rules.findOne<ReplicationStreamDocumentV3>(
|
|
301
|
+
{
|
|
302
|
+
state: storage.SyncRuleState.ACTIVE,
|
|
303
|
+
storage_version: storageVersion
|
|
304
|
+
},
|
|
305
|
+
{ session, sort: { _id: -1 }, limit: 1 }
|
|
306
|
+
);
|
|
307
|
+
if (active != null) {
|
|
308
|
+
const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
!options.forceNewReplicationStream &&
|
|
312
|
+
this.options.supportsMultipleSyncConfigs &&
|
|
313
|
+
isCompatible(
|
|
314
|
+
existingConfigDocs.map((d) => d.serialized_plan ?? null),
|
|
315
|
+
options.config,
|
|
316
|
+
logger
|
|
317
|
+
)
|
|
318
|
+
) {
|
|
319
|
+
logger.info(`Using incremental reprocessing`);
|
|
320
|
+
await this.stopExistingProcessingWork(session);
|
|
321
|
+
return await this.appendSyncConfigToStream({
|
|
322
|
+
versioned,
|
|
323
|
+
existing: active,
|
|
324
|
+
existingConfigDocs,
|
|
325
|
+
options,
|
|
326
|
+
storageVersion,
|
|
327
|
+
session
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
195
331
|
|
|
196
|
-
await
|
|
197
|
-
const active = await this.db.sync_rules.findOne<ReplicationStreamDocumentV3>(
|
|
198
|
-
{
|
|
199
|
-
state: storage.SyncRuleState.ACTIVE,
|
|
200
|
-
storage_version: storageVersion
|
|
201
|
-
},
|
|
202
|
-
{ session, sort: { _id: -1 }, limit: 1 }
|
|
203
|
-
);
|
|
204
|
-
if (active != null) {
|
|
205
|
-
const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
|
|
332
|
+
await this.stopExistingProcessingWork(session);
|
|
206
333
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
) {
|
|
215
|
-
logger.info(`Using incremental reprocessing`);
|
|
216
|
-
await this.db.sync_rules.updateMany(
|
|
217
|
-
{
|
|
218
|
-
state: storage.SyncRuleState.PROCESSING
|
|
219
|
-
},
|
|
220
|
-
syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
|
|
221
|
-
{ session }
|
|
222
|
-
);
|
|
223
|
-
await this.stopEmbeddedDeployingConfigs(active, session);
|
|
224
|
-
rules = await this.appendSyncConfigToStream({
|
|
225
|
-
versioned,
|
|
226
|
-
existing: active,
|
|
227
|
-
existingConfigDocs,
|
|
228
|
-
options,
|
|
229
|
-
storageVersion,
|
|
230
|
-
session
|
|
231
|
-
});
|
|
232
|
-
return;
|
|
334
|
+
const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
|
|
335
|
+
{
|
|
336
|
+
_id: 'sync_rules'
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
$inc: {
|
|
340
|
+
op_id: 1n
|
|
233
341
|
}
|
|
234
|
-
|
|
235
|
-
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
upsert: true,
|
|
345
|
+
returnDocument: 'after',
|
|
346
|
+
session
|
|
236
347
|
}
|
|
348
|
+
);
|
|
237
349
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
350
|
+
const id = Number(id_doc!.op_id);
|
|
351
|
+
const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
|
|
352
|
+
|
|
353
|
+
const mapping =
|
|
354
|
+
options.config.plan == null
|
|
355
|
+
? // For legacy sync rules and streams, use the parsed config directly to create a mapping
|
|
356
|
+
SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
|
|
357
|
+
: // For new sync streams, always use the serialized version
|
|
358
|
+
SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans(
|
|
359
|
+
[],
|
|
360
|
+
options.config.plan.plan,
|
|
361
|
+
[]
|
|
362
|
+
);
|
|
246
363
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
364
|
+
const syncConfigDoc: SyncConfigDefinition = {
|
|
365
|
+
_id: new ObjectId(),
|
|
366
|
+
replication_stream_id: id,
|
|
367
|
+
created_at: new Date(),
|
|
368
|
+
storage_version: storageVersion,
|
|
369
|
+
content: options.config.yaml,
|
|
370
|
+
version_label: options.version_label,
|
|
371
|
+
serialized_plan: options.config.plan,
|
|
372
|
+
rule_mapping: mapping.serialize()
|
|
373
|
+
};
|
|
374
|
+
await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
|
|
375
|
+
|
|
376
|
+
const doc: ReplicationStreamDocumentV3 = {
|
|
377
|
+
_id: id,
|
|
378
|
+
storage_version: storageVersion,
|
|
379
|
+
sync_configs: [
|
|
256
380
|
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
381
|
+
_id: syncConfigDoc._id,
|
|
382
|
+
state: storage.SyncRuleState.PROCESSING,
|
|
383
|
+
last_checkpoint: null,
|
|
384
|
+
last_checkpoint_lsn: null,
|
|
385
|
+
no_checkpoint_before: null,
|
|
386
|
+
snapshot_done: false
|
|
260
387
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
: // For new sync streams, always use the serialized version
|
|
271
|
-
SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans(
|
|
272
|
-
[],
|
|
273
|
-
options.config.plan.plan,
|
|
274
|
-
[]
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const syncConfigDoc: SyncConfigDefinition = {
|
|
278
|
-
_id: new ObjectId(),
|
|
279
|
-
replication_stream_id: id,
|
|
280
|
-
created_at: new Date(),
|
|
281
|
-
storage_version: storageVersion,
|
|
282
|
-
content: options.config.yaml,
|
|
283
|
-
serialized_plan: options.config.plan,
|
|
284
|
-
rule_mapping: mapping.serialize()
|
|
285
|
-
};
|
|
286
|
-
await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
|
|
287
|
-
|
|
288
|
-
const doc: ReplicationStreamDocumentV3 = {
|
|
289
|
-
_id: id,
|
|
290
|
-
storage_version: storageVersion,
|
|
291
|
-
sync_configs: [
|
|
292
|
-
{
|
|
293
|
-
_id: syncConfigDoc._id,
|
|
294
|
-
state: storage.SyncRuleState.PROCESSING,
|
|
295
|
-
last_checkpoint: null,
|
|
296
|
-
last_checkpoint_lsn: null,
|
|
297
|
-
no_checkpoint_before: null,
|
|
298
|
-
snapshot_done: false
|
|
299
|
-
}
|
|
300
|
-
],
|
|
301
|
-
snapshot_lsn: undefined,
|
|
302
|
-
state: storage.SyncRuleState.PROCESSING,
|
|
303
|
-
slot_name: replicationStreamName,
|
|
304
|
-
last_checkpoint_ts: null,
|
|
305
|
-
last_fatal_error: null,
|
|
306
|
-
last_fatal_error_ts: null,
|
|
307
|
-
last_keepalive_ts: null
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
await this.db.sync_rules.insertOne(doc, { session });
|
|
311
|
-
rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
|
|
312
|
-
if (options.lock) {
|
|
313
|
-
// The lock is persisted on rules.current_lock
|
|
314
|
-
await rules.lock(session);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Notify only after the transaction has committed, so listeners cannot observe pre-commit state.
|
|
319
|
-
await this.db.notifyCheckpoint();
|
|
388
|
+
],
|
|
389
|
+
snapshot_lsn: undefined,
|
|
390
|
+
state: storage.SyncRuleState.PROCESSING,
|
|
391
|
+
slot_name: replicationStreamName,
|
|
392
|
+
last_checkpoint_ts: null,
|
|
393
|
+
last_fatal_error: null,
|
|
394
|
+
last_fatal_error_ts: null,
|
|
395
|
+
last_keepalive_ts: null
|
|
396
|
+
};
|
|
320
397
|
|
|
321
|
-
|
|
398
|
+
await this.db.sync_rules.insertOne(doc, { session });
|
|
399
|
+
const rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
|
|
400
|
+
if (options.lock) {
|
|
401
|
+
// We only lock when creating a new stream - otherwise we'll likely get lock contention
|
|
402
|
+
// from an existing job on the stream, which would fail the entire transaction.
|
|
403
|
+
// The lock is persisted on rules.current_lock
|
|
404
|
+
await rules.lock(session);
|
|
405
|
+
}
|
|
406
|
+
return rules;
|
|
322
407
|
}
|
|
323
408
|
|
|
324
409
|
private async loadSyncConfigDefinitions(
|
|
@@ -363,31 +448,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
363
448
|
.toArray();
|
|
364
449
|
}
|
|
365
450
|
|
|
366
|
-
private async stopEmbeddedDeployingConfigs(existing: ReplicationStreamDocumentV3, session: mongo.ClientSession) {
|
|
367
|
-
const deployingConfigs = existing.sync_configs
|
|
368
|
-
.filter((config) => config.state == storage.SyncRuleState.PROCESSING)
|
|
369
|
-
.map((config) => config._id);
|
|
370
|
-
if (deployingConfigs.length == 0) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
await this.db.sync_rules.updateOne(
|
|
375
|
-
{
|
|
376
|
-
_id: existing._id,
|
|
377
|
-
'sync_configs._id': { $in: deployingConfigs }
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
$set: {
|
|
381
|
-
'sync_configs.$[config].state': storage.SyncRuleState.STOP
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
session,
|
|
386
|
-
arrayFilters: [{ 'config._id': { $in: deployingConfigs } }]
|
|
387
|
-
}
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
451
|
private logIncrementalDefinitionChanges(changes: ReturnType<typeof describeIncrementalSyncConfigUpdate>) {
|
|
392
452
|
logger.info(`Incremental reprocessing sync config update:\n${formatIncrementalSyncConfigUpdateLog(changes)}`);
|
|
393
453
|
}
|
|
@@ -432,6 +492,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
432
492
|
created_at: new Date(),
|
|
433
493
|
storage_version: storageVersion,
|
|
434
494
|
content: updateOptions.config.yaml,
|
|
495
|
+
version_label: updateOptions.version_label,
|
|
435
496
|
serialized_plan: updateOptions.config.plan,
|
|
436
497
|
rule_mapping: mapping.serialize()
|
|
437
498
|
};
|
|
@@ -477,92 +538,143 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
477
538
|
}
|
|
478
539
|
|
|
479
540
|
async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<MongoPersistedReplicationStream> {
|
|
480
|
-
|
|
481
|
-
|
|
541
|
+
let rules: MongoPersistedReplicationStream | undefined;
|
|
542
|
+
await this.withTransaction(async (session) => {
|
|
543
|
+
rules = await this.updateSyncRulesInTransaction(options, session);
|
|
544
|
+
});
|
|
545
|
+
await this.db.notifyCheckpoint();
|
|
546
|
+
return rules!;
|
|
547
|
+
}
|
|
482
548
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
549
|
+
private async withTransaction<T>(callback: (session: mongo.ClientSession) => Promise<T>): Promise<T> {
|
|
550
|
+
return this.client.withSession((session) => session.withTransaction(callback));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Update to a V1 sync config.
|
|
555
|
+
*
|
|
556
|
+
* Does support cases where existing replication streams are on V3.
|
|
557
|
+
*/
|
|
558
|
+
private async updateSyncConfigV1InTransaction(
|
|
559
|
+
options: storage.UpdateSyncRulesOptions,
|
|
560
|
+
storageVersion: number,
|
|
561
|
+
session: mongo.ClientSession
|
|
562
|
+
): Promise<MongoPersistedReplicationStream> {
|
|
563
|
+
await this.stopExistingProcessingWork(session);
|
|
564
|
+
|
|
565
|
+
const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
|
|
566
|
+
{
|
|
567
|
+
_id: 'sync_rules'
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
$inc: {
|
|
571
|
+
op_id: 1n
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
upsert: true,
|
|
576
|
+
returnDocument: 'after',
|
|
577
|
+
session
|
|
578
|
+
}
|
|
579
|
+
);
|
|
487
580
|
|
|
488
|
-
|
|
581
|
+
const id = Number(id_doc!.op_id);
|
|
582
|
+
const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
|
|
489
583
|
|
|
490
|
-
|
|
584
|
+
// All V1 replication streams share both the `main` op id sequence and the `bucket_parameters`
|
|
585
|
+
// collection, so every parameter entry this stream writes gets an op id above the current
|
|
586
|
+
// head. Seeding the parameter compaction cursor with that head keeps the stream's first
|
|
587
|
+
// compaction from scanning other streams' history, which would otherwise be repeated for
|
|
588
|
+
// every new deployment. A concurrent replication flush can only advance the head after this
|
|
589
|
+
// read, which makes the seed conservative, never too high.
|
|
590
|
+
const opSequence = await this.db.op_id_sequence.findOne({ _id: 'main' }, { session });
|
|
491
591
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
592
|
+
const doc: SyncRuleDocumentV1 = {
|
|
593
|
+
_id: id,
|
|
594
|
+
storage_version: storageVersion,
|
|
595
|
+
content: options.config.yaml,
|
|
596
|
+
version_label: options.version_label,
|
|
597
|
+
serialized_plan: options.config.plan,
|
|
598
|
+
last_checkpoint: null,
|
|
599
|
+
last_checkpoint_lsn: null,
|
|
600
|
+
no_checkpoint_before: null,
|
|
601
|
+
keepalive_op: null,
|
|
602
|
+
snapshot_done: false,
|
|
603
|
+
snapshot_lsn: undefined,
|
|
604
|
+
state: storage.SyncRuleState.PROCESSING,
|
|
605
|
+
slot_name: slot_name,
|
|
606
|
+
last_checkpoint_ts: null,
|
|
607
|
+
last_fatal_error: null,
|
|
608
|
+
last_fatal_error_ts: null,
|
|
609
|
+
last_keepalive_ts: null,
|
|
610
|
+
parameter_compaction: { compacted_before: opSequence?.op_id ?? 0n }
|
|
611
|
+
};
|
|
501
612
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
upsert: true,
|
|
513
|
-
returnDocument: 'after',
|
|
514
|
-
session
|
|
515
|
-
}
|
|
516
|
-
);
|
|
613
|
+
await this.db.sync_rules.insertOne(doc, { session });
|
|
614
|
+
const rules = new MongoPersistedReplicationStream(this.db, doc);
|
|
615
|
+
if (options.lock) {
|
|
616
|
+
// The lock is persisted on rules.current_lock
|
|
617
|
+
await rules.lock(session);
|
|
618
|
+
}
|
|
619
|
+
return rules;
|
|
620
|
+
}
|
|
517
621
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
last_keepalive_ts: null
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
await this.db.sync_rules.insertOne(doc, { session });
|
|
541
|
-
await this.db.notifyCheckpoint();
|
|
542
|
-
rules = new MongoPersistedReplicationStream(this.db, doc);
|
|
543
|
-
if (options.lock) {
|
|
544
|
-
// The lock is persisted on rules.current_lock
|
|
545
|
-
await rules.lock(session);
|
|
622
|
+
/**
|
|
623
|
+
* Stop top-level and embedded processing work before creating a replacement stream.
|
|
624
|
+
*
|
|
625
|
+
* Use in the same transaction that the replacement stream is created.
|
|
626
|
+
*/
|
|
627
|
+
private async stopExistingProcessingWork(session: mongo.ClientSession) {
|
|
628
|
+
await this.db.sync_rules.updateMany(
|
|
629
|
+
{
|
|
630
|
+
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
|
|
631
|
+
'sync_configs.state': storage.SyncRuleState.PROCESSING
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
$set: {
|
|
635
|
+
'sync_configs.$[config].state': storage.SyncRuleState.STOP
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
session,
|
|
640
|
+
arrayFilters: [{ 'config.state': storage.SyncRuleState.PROCESSING }]
|
|
546
641
|
}
|
|
547
|
-
|
|
642
|
+
);
|
|
548
643
|
|
|
549
|
-
|
|
644
|
+
await this.db.sync_rules.updateMany(
|
|
645
|
+
{
|
|
646
|
+
state: storage.SyncRuleState.PROCESSING
|
|
647
|
+
},
|
|
648
|
+
stopReplicationStreamPipeline(),
|
|
649
|
+
{ session }
|
|
650
|
+
);
|
|
550
651
|
}
|
|
551
652
|
|
|
552
653
|
async getActiveSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
|
|
654
|
+
return this.getActiveSyncConfigInternal();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private async getActiveSyncConfigInternal(session?: mongo.ClientSession): Promise<storage.ResolvedSyncConfig | null> {
|
|
553
658
|
const doc = await this.db.sync_rules.findOne(
|
|
554
659
|
{
|
|
555
660
|
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
556
661
|
},
|
|
557
|
-
{ sort: { _id: -1 }, limit: 1 }
|
|
662
|
+
{ session, sort: { _id: -1 }, limit: 1 }
|
|
558
663
|
);
|
|
559
664
|
|
|
560
|
-
return this.resolvedSyncConfigFromDoc(
|
|
561
|
-
|
|
562
|
-
|
|
665
|
+
return this.resolvedSyncConfigFromDoc(
|
|
666
|
+
doc,
|
|
667
|
+
[storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED],
|
|
668
|
+
{ cacheActiveStorage: session == null },
|
|
669
|
+
session
|
|
670
|
+
);
|
|
563
671
|
}
|
|
564
672
|
|
|
565
|
-
private async replicationStreamFromDoc(
|
|
673
|
+
private async replicationStreamFromDoc(
|
|
674
|
+
doc: SyncRuleDocumentBase | null,
|
|
675
|
+
stateFilter: storage.SyncRuleState[],
|
|
676
|
+
session?: mongo.ClientSession
|
|
677
|
+
) {
|
|
566
678
|
if (doc == null) {
|
|
567
679
|
return null;
|
|
568
680
|
}
|
|
@@ -580,9 +692,12 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
580
692
|
// It is not a major bottleneck though, since it only runs once every couple of seconds at most.
|
|
581
693
|
const db = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
|
|
582
694
|
const syncConfigDocs = await db.syncConfigDefinitions
|
|
583
|
-
.find(
|
|
584
|
-
|
|
585
|
-
|
|
695
|
+
.find(
|
|
696
|
+
{
|
|
697
|
+
_id: { $in: matching.map((config) => config._id) }
|
|
698
|
+
},
|
|
699
|
+
{ session }
|
|
700
|
+
)
|
|
586
701
|
.toArray();
|
|
587
702
|
|
|
588
703
|
if (syncConfigDocs.length == 0) {
|
|
@@ -595,14 +710,20 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
595
710
|
}
|
|
596
711
|
|
|
597
712
|
async getDeployingSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
|
|
713
|
+
return this.getDeployingSyncConfigInternal();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private async getDeployingSyncConfigInternal(
|
|
717
|
+
session?: mongo.ClientSession
|
|
718
|
+
): Promise<storage.ResolvedSyncConfig | null> {
|
|
598
719
|
const doc = await this.db.sync_rules.findOne(
|
|
599
720
|
{
|
|
600
721
|
$or: [{ state: storage.SyncRuleState.PROCESSING }, { 'sync_configs.state': storage.SyncRuleState.PROCESSING }]
|
|
601
722
|
},
|
|
602
|
-
{ sort: { _id: -1 }, limit: 1 }
|
|
723
|
+
{ session, sort: { _id: -1 }, limit: 1 }
|
|
603
724
|
);
|
|
604
725
|
|
|
605
|
-
return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING]);
|
|
726
|
+
return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING], {}, session);
|
|
606
727
|
}
|
|
607
728
|
|
|
608
729
|
async getReplicatingReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
|
|
@@ -640,9 +761,10 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
640
761
|
private async resolvedSyncConfigFromDoc(
|
|
641
762
|
doc: SyncRuleDocumentBase | null,
|
|
642
763
|
stateFilter: storage.SyncRuleState[],
|
|
643
|
-
options: { cacheActiveStorage?: boolean } = {}
|
|
764
|
+
options: { cacheActiveStorage?: boolean } = {},
|
|
765
|
+
session?: mongo.ClientSession
|
|
644
766
|
): Promise<storage.ResolvedSyncConfig | null> {
|
|
645
|
-
const stream = await this.replicationStreamFromDoc(doc, stateFilter);
|
|
767
|
+
const stream = await this.replicationStreamFromDoc(doc, stateFilter, session);
|
|
646
768
|
if (stream == null) {
|
|
647
769
|
return null;
|
|
648
770
|
}
|
|
@@ -675,7 +797,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
675
797
|
|
|
676
798
|
async getStorageMetrics(): Promise<storage.StorageMetrics> {
|
|
677
799
|
const ignoreNotExisting = (e: unknown) => {
|
|
678
|
-
if (lib_mongo.
|
|
800
|
+
if (lib_mongo.isMongoNamespaceNotFoundError(e)) {
|
|
679
801
|
// Collection doesn't exist - return 0
|
|
680
802
|
return [{ storageStats: { size: 0 } }];
|
|
681
803
|
} else {
|
|
@@ -683,9 +805,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
683
805
|
}
|
|
684
806
|
};
|
|
685
807
|
|
|
686
|
-
// For now, we get storage metrics over all v1 and v3 collections.
|
|
687
|
-
// In the future, we may split these metrics to report separately for active replication streams versus processing streams.
|
|
688
|
-
|
|
689
808
|
const aggregateStaticCollection = async <T extends mongo.Document>(collection: mongo.Collection<T>) => {
|
|
690
809
|
// We check whether the collection exists before getting the statistics. This avoids repeated
|
|
691
810
|
// errors in the MongoDB logs if the collection hasn't been created yet.
|
|
@@ -709,8 +828,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
709
828
|
};
|
|
710
829
|
|
|
711
830
|
const operations_aggregate = await aggregateStaticCollection(this.db.bucket_data);
|
|
831
|
+
const v3OperationCollections = await this.db.listBucketDataCollectionsV3();
|
|
712
832
|
const v3_operation_aggregates = await Promise.all(
|
|
713
|
-
|
|
833
|
+
v3OperationCollections.map((collection) =>
|
|
714
834
|
collection
|
|
715
835
|
.aggregate([
|
|
716
836
|
{
|
|
@@ -726,8 +846,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
726
846
|
|
|
727
847
|
const parameters_aggregate = await aggregateStaticCollection(this.db.bucket_parameters);
|
|
728
848
|
|
|
849
|
+
const v3ParameterCollections = await this.db.listAllParameterIndexCollectionsV3();
|
|
729
850
|
const v3_parameter_aggregates = await Promise.all(
|
|
730
|
-
|
|
851
|
+
v3ParameterCollections.map((collection) =>
|
|
731
852
|
collection
|
|
732
853
|
.aggregate([
|
|
733
854
|
{
|
|
@@ -743,8 +864,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
743
864
|
|
|
744
865
|
const v1_source_record_aggregate = await aggregateStaticCollection(this.db.current_data);
|
|
745
866
|
|
|
867
|
+
const v3SourceRecordCollections = await this.db.listAllSourceRecordCollectionsV3();
|
|
746
868
|
const source_record_aggregates = await Promise.all(
|
|
747
|
-
|
|
869
|
+
v3SourceRecordCollections.map((collection) =>
|
|
748
870
|
collection
|
|
749
871
|
.aggregate([
|
|
750
872
|
{
|
|
@@ -757,6 +879,120 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
757
879
|
.catch(ignoreNotExisting)
|
|
758
880
|
)
|
|
759
881
|
);
|
|
882
|
+
|
|
883
|
+
const v3StorageConfig = getMongoStorageConfig(storage.STORAGE_VERSION_3) as StorageConfig & {
|
|
884
|
+
incrementalReprocessing: true;
|
|
885
|
+
};
|
|
886
|
+
const v3Db = this.db.versioned(v3StorageConfig) as VersionedPowerSyncMongoV3;
|
|
887
|
+
const objectStorageDefinitionUsage = await ObjectStorageUsage.readAllDefinitionUsage(v3Db);
|
|
888
|
+
|
|
889
|
+
const v3StreamDocs = (await this.db.sync_rules
|
|
890
|
+
.find({ storage_version: { $gte: storage.STORAGE_VERSION_3 } }, { projection: { _id: 1, sync_configs: 1 } })
|
|
891
|
+
.toArray()) as unknown as Pick<ReplicationStreamDocumentV3, '_id' | 'sync_configs'>[];
|
|
892
|
+
|
|
893
|
+
const collectionSizes = new Map<string, number>();
|
|
894
|
+
const addCollectionSize = (
|
|
895
|
+
collection: { collectionName: string },
|
|
896
|
+
aggregate: { storageStats?: { size?: number | bigint } }[],
|
|
897
|
+
prefix: string,
|
|
898
|
+
sizes: Map<string, number>
|
|
899
|
+
) => {
|
|
900
|
+
const match = collection.collectionName.match(new RegExp(`^${prefix}(\\d+)_(.+)$`));
|
|
901
|
+
if (match == null) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
sizes.set(`${prefix}${match[1]}:${match[2]}`, Number(aggregate[0]?.storageStats?.size ?? 0));
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
v3OperationCollections.forEach((collection, index) =>
|
|
908
|
+
addCollectionSize(collection, v3_operation_aggregates[index], 'bucket_data_', collectionSizes)
|
|
909
|
+
);
|
|
910
|
+
v3ParameterCollections.forEach((collection, index) =>
|
|
911
|
+
addCollectionSize(collection, v3_parameter_aggregates[index], 'parameter_index_', collectionSizes)
|
|
912
|
+
);
|
|
913
|
+
v3SourceRecordCollections.forEach((collection, index) =>
|
|
914
|
+
addCollectionSize(collection, source_record_aggregates[index], 'source_records_', collectionSizes)
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
const syncConfigIds = v3StreamDocs.flatMap((stream) => (stream.sync_configs ?? []).map((config) => config._id));
|
|
918
|
+
const syncConfigDefinitions =
|
|
919
|
+
syncConfigIds.length == 0
|
|
920
|
+
? []
|
|
921
|
+
: await v3Db.syncConfigDefinitions
|
|
922
|
+
.find({ _id: { $in: syncConfigIds } }, { projection: { _id: 1, rule_mapping: 1, version_label: 1 } })
|
|
923
|
+
.toArray();
|
|
924
|
+
const syncConfigDefinitionsById = new Map(
|
|
925
|
+
syncConfigDefinitions.map((definition) => [definition._id.toHexString(), definition])
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const sourceTablesByStream = new Map<
|
|
929
|
+
number,
|
|
930
|
+
{ _id: ObjectId; bucket_data_source_ids: string[]; parameter_lookup_source_ids: string[] }[]
|
|
931
|
+
>();
|
|
932
|
+
await Promise.all(
|
|
933
|
+
v3StreamDocs.map(async (stream) => {
|
|
934
|
+
const sourceTables = await v3Db
|
|
935
|
+
.sourceTables(stream._id)
|
|
936
|
+
.find({}, { projection: { _id: 1, bucket_data_source_ids: 1, parameter_lookup_source_ids: 1 } })
|
|
937
|
+
.toArray();
|
|
938
|
+
sourceTablesByStream.set(stream._id, sourceTables);
|
|
939
|
+
})
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
const objectStorageSizeByDefinition = new Map<string, number>();
|
|
943
|
+
for (const usage of objectStorageDefinitionUsage) {
|
|
944
|
+
objectStorageSizeByDefinition.set(
|
|
945
|
+
`${usage.replication_stream_id}:${usage.definition_id}`,
|
|
946
|
+
Number(usage.active_bytes)
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const sumCollectionSizes = (prefix: string, streamId: number, ids: ReadonlySet<string>) =>
|
|
951
|
+
[...ids].reduce((total, id) => total + (collectionSizes.get(`${prefix}${streamId}:${id}`) ?? 0), 0);
|
|
952
|
+
|
|
953
|
+
const syncConfigMetrics: storage.StorageSyncConfigMetrics[] = [];
|
|
954
|
+
for (const stream of v3StreamDocs) {
|
|
955
|
+
const sourceTables = sourceTablesByStream.get(stream._id) ?? [];
|
|
956
|
+
for (const syncConfig of stream.sync_configs ?? []) {
|
|
957
|
+
const definition = syncConfigDefinitionsById.get(syncConfig._id.toHexString());
|
|
958
|
+
if (definition == null) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const mapping = SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(definition.rule_mapping);
|
|
963
|
+
const bucketDefinitionIds = mapping.allBucketDefinitionIds();
|
|
964
|
+
const parameterIndexIds = mapping.allParameterIndexIds();
|
|
965
|
+
const bucketDefinitionIdSet = new Set(bucketDefinitionIds);
|
|
966
|
+
const parameterIndexIdSet = new Set(parameterIndexIds);
|
|
967
|
+
const replicationSize = sourceTables.reduce((total, sourceTable) => {
|
|
968
|
+
if (
|
|
969
|
+
!(sourceTable.bucket_data_source_ids ?? []).some((id) => bucketDefinitionIdSet.has(id)) &&
|
|
970
|
+
!(sourceTable.parameter_lookup_source_ids ?? []).some((id) => parameterIndexIdSet.has(id))
|
|
971
|
+
) {
|
|
972
|
+
return total;
|
|
973
|
+
}
|
|
974
|
+
return total + (collectionSizes.get(`source_records_${stream._id}:${sourceTable._id.toHexString()}`) ?? 0);
|
|
975
|
+
}, 0);
|
|
976
|
+
|
|
977
|
+
syncConfigMetrics.push({
|
|
978
|
+
sync_config_id: syncConfig._id.toHexString(),
|
|
979
|
+
sync_config_state: String(syncConfig.state),
|
|
980
|
+
version_label: definition.version_label,
|
|
981
|
+
attributed_bucket_data_bytes: sumCollectionSizes('bucket_data_', stream._id, bucketDefinitionIdSet),
|
|
982
|
+
attributed_parameter_indexes_bytes: sumCollectionSizes('parameter_index_', stream._id, parameterIndexIdSet),
|
|
983
|
+
attributed_source_records_bytes: replicationSize,
|
|
984
|
+
attributed_object_storage_bytes: [...bucketDefinitionIdSet].reduce(
|
|
985
|
+
(total, definitionId) => total + (objectStorageSizeByDefinition.get(`${stream._id}:${definitionId}`) ?? 0),
|
|
986
|
+
0
|
|
987
|
+
)
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const totalObjectStorageSize = objectStorageDefinitionUsage.reduce(
|
|
993
|
+
(total, usage) => total + usage.active_bytes,
|
|
994
|
+
0n
|
|
995
|
+
);
|
|
760
996
|
return {
|
|
761
997
|
operations_size_bytes:
|
|
762
998
|
Number(operations_aggregate[0].storageStats.size) +
|
|
@@ -766,7 +1002,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
766
1002
|
v3_parameter_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
|
|
767
1003
|
replication_size_bytes:
|
|
768
1004
|
Number(v1_source_record_aggregate[0]?.storageStats?.size ?? 0) +
|
|
769
|
-
source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0)
|
|
1005
|
+
source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0),
|
|
1006
|
+
object_storage_size_bytes: Number(totalObjectStorageSize),
|
|
1007
|
+
sync_config_metrics: syncConfigMetrics
|
|
770
1008
|
};
|
|
771
1009
|
}
|
|
772
1010
|
|