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