@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716
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 +129 -7
- package/dist/storage/MongoBucketStorage.d.ts +42 -5
- package/dist/storage/MongoBucketStorage.js +404 -215
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
- package/dist/storage/implementation/MongoBucketBatch.js +169 -98
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
- package/dist/storage/implementation/MongoIdSequence.js +14 -1
- package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
- 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 +35 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +8 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
- 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 +57 -14
- 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 +28 -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 +11 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
- 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 +66 -17
- 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 +14 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -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 +92 -32
- 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 +77 -45
- 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 +108 -0
- package/dist/storage/implementation/v3/compact-utils.js +223 -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 +90 -6
- 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 +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 +28 -1
- package/dist/types/types.js +28 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -2
- package/dist/utils/test-utils.js +7 -4
- 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 +545 -277
- package/src/storage/implementation/MongoBucketBatch.ts +258 -124
- package/src/storage/implementation/MongoCompactor.ts +37 -346
- package/src/storage/implementation/MongoIdSequence.ts +21 -1
- package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
- package/src/storage/implementation/MongoStorageProvider.ts +16 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
- 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 +76 -20
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +36 -2
- package/src/storage/implementation/models.ts +39 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
- 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 +88 -28
- 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 +339 -247
- package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -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 +112 -42
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
- 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 +329 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +97 -4
- 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 +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +9 -5
- 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 +794 -205
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
- package/test/src/compact-utils.test.ts +251 -0
- package/test/src/concurrent_writers.test.ts +599 -0
- package/test/src/connection_config.test.ts +159 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/helpers/concurrentWriter.mjs +29 -0
- package/test/src/helpers/s3TestFactory.ts +12 -12
- package/test/src/object_storage_usage.test.ts +268 -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 +280 -24
- package/test/src/storage_compacting.test.ts +1097 -253
- package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
- package/test/src/storage_s3_checksums.test.ts +6 -7
- package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
- package/test/src/storage_s3_reading.test.ts +294 -6
- package/test/src/storage_s3_writing.test.ts +4 -3
- package/test/src/storage_sync.test.ts +412 -80
- package/test/src/storeCurrentData.test.ts +5 -5
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { storage } from '@powersync/service-core';
|
|
2
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { InternalOpId, storage } from '@powersync/service-core';
|
|
4
|
+
import { EventDefinitionId } from '@powersync/service-sync-rules';
|
|
4
5
|
import * as bson from 'bson';
|
|
5
6
|
import { mongoTableId } from '../../../utils/util.js';
|
|
6
7
|
import { canCheckpointState } from '../CheckpointState.js';
|
|
7
8
|
import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
|
|
8
9
|
import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
|
|
9
|
-
import {
|
|
10
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
11
|
+
import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
|
|
10
12
|
import { PersistedBatch } from '../common/PersistedBatch.js';
|
|
11
13
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
14
|
+
import { SyncRuleDocumentBase } from '../models.js';
|
|
12
15
|
import { PersistedBatchV3 } from './PersistedBatchV3.js';
|
|
13
16
|
import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
|
|
14
17
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
15
|
-
import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
|
|
18
|
+
import { CustomCheckpointRequestDocumentV3, ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
|
|
16
19
|
import {
|
|
17
20
|
createNewSourceTable,
|
|
18
|
-
designateEventCarrier,
|
|
19
21
|
overlappingSourceTableFilter,
|
|
20
22
|
planSourceTableReconciliation,
|
|
21
23
|
sourceTableDesiredResolution,
|
|
@@ -27,6 +29,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
27
29
|
declare readonly db: VersionedPowerSyncMongoV3;
|
|
28
30
|
|
|
29
31
|
private readonly store: SourceRecordStore;
|
|
32
|
+
/** Avoid repeating idempotent createIndexes calls for every checkpoint flush. */
|
|
33
|
+
private readonly initializedCustomCheckpointEventIds = new Set<EventDefinitionId>();
|
|
34
|
+
// Used only to validate supplied checkpoint ids; it does not initialize collections.
|
|
35
|
+
private readonly knownEventIds: ReadonlySet<EventDefinitionId>;
|
|
30
36
|
private readonly syncConfigIds: bson.ObjectId[];
|
|
31
37
|
private needsActivationV3 = true;
|
|
32
38
|
private lastWaitingLogThrottledV3 = 0;
|
|
@@ -38,6 +44,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
38
44
|
throw new ReplicationAssertionError('Missing sync config id for v3 batch');
|
|
39
45
|
}
|
|
40
46
|
this.syncConfigIds = syncConfigIds;
|
|
47
|
+
this.knownEventIds = new Set(this.mapping.allEventDefinitionIds());
|
|
41
48
|
this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -45,7 +52,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
45
52
|
return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
|
|
46
53
|
logger: this.logger,
|
|
47
54
|
objectStorage: this.options.objectStorage,
|
|
48
|
-
inlineThresholdBytes: this.options.inlineThresholdBytes
|
|
55
|
+
inlineThresholdBytes: this.options.inlineThresholdBytes,
|
|
56
|
+
signal: this.options.signal,
|
|
57
|
+
objectStorageUsageWriterId: this.objectStorageUsageWriterId
|
|
49
58
|
});
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -53,14 +62,16 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
53
62
|
return this.store;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
protected override
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
protected override persistedOpHead(stream: SyncRuleDocumentBase): InternalOpId {
|
|
66
|
+
return (stream as ReplicationStreamDocumentV3).last_persisted_op ?? 0n;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected override onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: InternalOpId): void {
|
|
60
70
|
// Durably advance the stream-level head of persisted ops within the flush transaction.
|
|
61
71
|
// This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
|
|
62
72
|
// config that replicates nothing) covers all ops persisted before a potential crash.
|
|
63
|
-
|
|
73
|
+
writes.updateOne(
|
|
74
|
+
this.db.sync_rules,
|
|
64
75
|
{
|
|
65
76
|
_id: this.replicationStreamId
|
|
66
77
|
},
|
|
@@ -68,8 +79,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
68
79
|
$max: {
|
|
69
80
|
last_persisted_op: lastOp
|
|
70
81
|
}
|
|
71
|
-
}
|
|
72
|
-
{ session }
|
|
82
|
+
}
|
|
73
83
|
);
|
|
74
84
|
}
|
|
75
85
|
|
|
@@ -90,7 +100,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
90
100
|
this.syncConfigIds.map((id) => id.toHexString()),
|
|
91
101
|
table.ref,
|
|
92
102
|
bucketDataSourceIds,
|
|
93
|
-
parameterLookupSourceIds
|
|
103
|
+
parameterLookupSourceIds,
|
|
104
|
+
[...table.eventDefinitionIds!]
|
|
94
105
|
)
|
|
95
106
|
);
|
|
96
107
|
}
|
|
@@ -132,7 +143,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
132
143
|
.sourceRecords(this.replicationStreamId, mongoTableId(table.id))
|
|
133
144
|
.drop()
|
|
134
145
|
.catch((error) => {
|
|
135
|
-
if (lib_mongo.
|
|
146
|
+
if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
|
|
136
147
|
return;
|
|
137
148
|
}
|
|
138
149
|
throw error;
|
|
@@ -146,42 +157,58 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
146
157
|
const parsedOverride = options.parsedSyncConfig as MongoParsedSyncConfigSet | undefined;
|
|
147
158
|
const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
|
|
148
159
|
const mapping = parsedOverride?.mapping ?? this.mapping;
|
|
160
|
+
const eventById = (parsedOverride ?? this.options.parsedSyncConfig).eventById;
|
|
149
161
|
|
|
150
162
|
const { connection_id, source } = options;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
type_oid: column.typeId
|
|
162
|
-
}))
|
|
163
|
-
},
|
|
164
|
-
storeCurrentData: source.sendsCompleteRows !== true,
|
|
165
|
-
syncConfig,
|
|
166
|
-
mapping,
|
|
167
|
-
desired: sourceTableDesiredResolution(syncConfig, source, mapping)
|
|
163
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
164
|
+
const identity = {
|
|
165
|
+
schema: source.schema,
|
|
166
|
+
name: source.name,
|
|
167
|
+
objectId: source.objectId,
|
|
168
|
+
replicaIdColumns: source.replicaIdColumns.map((column) => ({
|
|
169
|
+
name: column.name,
|
|
170
|
+
type: column.type,
|
|
171
|
+
type_oid: column.typeId
|
|
172
|
+
}))
|
|
168
173
|
};
|
|
169
174
|
|
|
170
175
|
let result: storage.ResolveTablesResult | null = null;
|
|
171
176
|
const session = this.db.client.startSession();
|
|
172
177
|
await using _ = { [Symbol.asyncDispose]: () => session.endSession() };
|
|
173
178
|
|
|
174
|
-
await
|
|
179
|
+
await this.withTransaction(async () => {
|
|
175
180
|
const col = this.db.sourceTables(this.replicationStreamId);
|
|
176
181
|
|
|
177
|
-
//
|
|
178
|
-
// Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
|
|
182
|
+
// Find records that overlap by name or relation id.
|
|
179
183
|
const candidateDocs = await col
|
|
180
|
-
.find(overlappingSourceTableFilter(connection_id,
|
|
184
|
+
.find(overlappingSourceTableFilter(connection_id, identity), { session })
|
|
181
185
|
.toArray();
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
const candidateTables = candidateDocs.map((doc) =>
|
|
188
|
+
sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping, eventById)
|
|
189
|
+
);
|
|
190
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
191
|
+
const resolution = await reconcile({ source, candidates });
|
|
192
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
193
|
+
|
|
194
|
+
// Persist metadata from the reconciler without mutating the queried documents.
|
|
195
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
196
|
+
await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const context: SourceTableReconciliationContext = {
|
|
200
|
+
connectionId: connection_id,
|
|
201
|
+
connectionTag: source.connectionTag,
|
|
202
|
+
identity,
|
|
203
|
+
storeCurrentData: source.sendsCompleteRows !== true,
|
|
204
|
+
syncConfig,
|
|
205
|
+
mapping,
|
|
206
|
+
desired: sourceTableDesiredResolution(syncConfig, source, mapping, eventById),
|
|
207
|
+
sourceCompatibleTables: resolution.compatibleTables,
|
|
208
|
+
newTableSourceMetadata: resolution.newTableValues.sourceMetadata
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Plan record reuse, membership changes, creation, and removal.
|
|
185
212
|
const plan = planSourceTableReconciliation(candidateDocs, context);
|
|
186
213
|
|
|
187
214
|
// Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
|
|
@@ -194,7 +221,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
194
221
|
{
|
|
195
222
|
$set: {
|
|
196
223
|
bucket_data_source_ids: update.memberships.bucketDataSourceIds,
|
|
197
|
-
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
|
|
224
|
+
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds,
|
|
225
|
+
event_definition_ids: update.memberships.eventDefinitionIds
|
|
198
226
|
}
|
|
199
227
|
},
|
|
200
228
|
{ session }
|
|
@@ -212,14 +240,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
212
240
|
plan.tables.push(table);
|
|
213
241
|
}
|
|
214
242
|
|
|
215
|
-
// If memberships are split across multiple source tables, only one may fire events.
|
|
216
|
-
designateEventCarrier(plan.tables, context.desired.triggersEvent);
|
|
217
|
-
|
|
218
243
|
result = {
|
|
219
244
|
tables: plan.tables,
|
|
220
|
-
dropTables: plan.dropDocs.map((doc) =>
|
|
245
|
+
dropTables: plan.dropDocs.map((doc) =>
|
|
246
|
+
sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping, eventById)
|
|
247
|
+
)
|
|
221
248
|
};
|
|
222
|
-
});
|
|
249
|
+
}, session);
|
|
223
250
|
|
|
224
251
|
return result!;
|
|
225
252
|
}
|
|
@@ -232,167 +259,155 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
232
259
|
return null;
|
|
233
260
|
}
|
|
234
261
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
return sourceTableFromDocument(
|
|
263
|
+
doc,
|
|
264
|
+
table.ref.connectionTag,
|
|
265
|
+
this.sync_rules,
|
|
266
|
+
this.mapping,
|
|
267
|
+
this.options.parsedSyncConfig.eventById
|
|
268
|
+
);
|
|
241
269
|
}
|
|
242
270
|
|
|
243
271
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
244
272
|
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
245
273
|
|
|
246
|
-
await this.flush(options);
|
|
247
|
-
|
|
248
274
|
using _ = this.tracer.span('storage', 'commit');
|
|
249
275
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
$set: {
|
|
259
|
-
processed_at_lsn: lsn
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
session: this.session
|
|
264
|
-
}
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const preUpdateDocument = (await this.db.sync_rules.findOne(
|
|
268
|
-
{
|
|
269
|
-
_id: this.replicationStreamId,
|
|
270
|
-
'sync_configs._id': { $in: this.syncConfigIds }
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
session: this.session,
|
|
274
|
-
projection: {
|
|
275
|
-
sync_configs: 1,
|
|
276
|
-
last_persisted_op: 1
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
)) as ReplicationStreamDocumentV3 | null;
|
|
280
|
-
|
|
281
|
-
const states =
|
|
282
|
-
preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
|
|
283
|
-
if (states.length == 0) {
|
|
284
|
-
throw new ReplicationAssertionError(
|
|
285
|
-
`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
|
|
286
|
-
.map((id) => id.toHexString())
|
|
287
|
-
.join(',')}`
|
|
276
|
+
const checkpoint = async (stream: SyncRuleDocumentBase, lastOp?: InternalOpId) => {
|
|
277
|
+
const now = new Date();
|
|
278
|
+
const preUpdateDocument = stream as ReplicationStreamDocumentV3;
|
|
279
|
+
const writes = this.db.createWriteBatch(this.session, { ordered: false });
|
|
280
|
+
writes.updateMany(
|
|
281
|
+
this.db.write_checkpoints,
|
|
282
|
+
{ processed_at_lsn: null, 'lsns.1': { $lte: lsn } },
|
|
283
|
+
{ $set: { processed_at_lsn: lsn } }
|
|
288
284
|
);
|
|
289
|
-
}
|
|
290
|
-
// The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
|
|
291
|
-
// sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
|
|
292
|
-
// remain embedded in the document and are ignored here.
|
|
293
|
-
const missingSyncConfig = preUpdateDocument!.sync_configs.find(
|
|
294
|
-
(config) =>
|
|
295
|
-
[storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
|
|
296
|
-
!this.syncConfigIds.some((id) => id.equals(config._id))
|
|
297
|
-
);
|
|
298
|
-
if (missingSyncConfig != null) {
|
|
299
|
-
throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
|
|
300
|
-
}
|
|
301
285
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
preUpdateDocument?.last_persisted_op == null ? 0n : BigInt(preUpdateDocument.last_persisted_op);
|
|
307
|
-
|
|
308
|
-
let checkpointBlocked = false;
|
|
309
|
-
let checkpointCreated = false;
|
|
310
|
-
let checkpointLogState: unknown = null;
|
|
311
|
-
const unblockedConfigIds: bson.ObjectId[] = [];
|
|
312
|
-
|
|
313
|
-
for (const state of states) {
|
|
314
|
-
if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
|
|
315
|
-
// last_persisted_op is $max-advanced durably in the same transaction as every flush, and
|
|
316
|
-
// checkpoints are only ever created at that head, so a checkpoint past the head means the
|
|
317
|
-
// op sequence or the stored state is corrupt.
|
|
286
|
+
const states =
|
|
287
|
+
preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ??
|
|
288
|
+
[];
|
|
289
|
+
if (states.length == 0) {
|
|
318
290
|
throw new ReplicationAssertionError(
|
|
319
|
-
`
|
|
291
|
+
`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
|
|
292
|
+
.map((id) => id.toHexString())
|
|
293
|
+
.join(',')}`
|
|
320
294
|
);
|
|
321
295
|
}
|
|
296
|
+
// The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
|
|
297
|
+
// sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
|
|
298
|
+
// remain embedded in the document and are ignored here.
|
|
299
|
+
const missingSyncConfig = preUpdateDocument!.sync_configs.find(
|
|
300
|
+
(config) =>
|
|
301
|
+
[storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
|
|
302
|
+
!this.syncConfigIds.some((id) => id.equals(config._id))
|
|
303
|
+
);
|
|
304
|
+
if (missingSyncConfig != null) {
|
|
305
|
+
throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
|
|
306
|
+
}
|
|
322
307
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
noCheckpointBefore: state.no_checkpoint_before
|
|
327
|
-
});
|
|
308
|
+
// Effective head of the stream's op sequence.
|
|
309
|
+
// A combined flush supplies its new head; an empty commit uses the fenced persisted head.
|
|
310
|
+
const newCheckpoint = lastOp ?? this.persistedOpHead(preUpdateDocument);
|
|
328
311
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
snapshot_done: state.snapshot_done,
|
|
334
|
-
last_checkpoint_lsn: state.last_checkpoint_lsn,
|
|
335
|
-
no_checkpoint_before: state.no_checkpoint_before
|
|
336
|
-
};
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
312
|
+
let checkpointBlocked = false;
|
|
313
|
+
let checkpointCreated = false;
|
|
314
|
+
let checkpointLogState: unknown = null;
|
|
315
|
+
const unblockedConfigIds: bson.ObjectId[] = [];
|
|
339
316
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
317
|
+
for (const state of states) {
|
|
318
|
+
if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
|
|
319
|
+
// last_persisted_op is $max-advanced durably in the same transaction as every flush, and
|
|
320
|
+
// checkpoints are only ever created at that head, so a checkpoint past the head means the
|
|
321
|
+
// op sequence or the stored state is corrupt.
|
|
322
|
+
throw new ReplicationAssertionError(
|
|
323
|
+
`Invariant violation: sync config ${state._id} has last_checkpoint ${state.last_checkpoint} > stream head ${newCheckpoint}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
343
326
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
updateSet['last_checkpoint_ts'] = now;
|
|
327
|
+
const canCheckpoint = canCheckpointState(lsn, {
|
|
328
|
+
snapshotDone: state.snapshot_done === true,
|
|
329
|
+
lastCheckpointLsn: state.last_checkpoint_lsn,
|
|
330
|
+
noCheckpointBefore: state.no_checkpoint_before
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (!canCheckpoint) {
|
|
334
|
+
checkpointBlocked = true;
|
|
335
|
+
// Log the first blocked config's state.
|
|
336
|
+
checkpointLogState ??= {
|
|
337
|
+
snapshot_done: state.snapshot_done,
|
|
338
|
+
last_checkpoint_lsn: state.last_checkpoint_lsn,
|
|
339
|
+
no_checkpoint_before: state.no_checkpoint_before
|
|
340
|
+
};
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
checkpointCreated ||= createEmptyCheckpoints || state.last_checkpoint !== newCheckpoint;
|
|
345
|
+
unblockedConfigIds.push(state._id);
|
|
364
346
|
}
|
|
365
347
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
{
|
|
373
|
-
|
|
374
|
-
|
|
348
|
+
// Every commit advances the stream's resume position: commit() flushes first, so all
|
|
349
|
+
// source changes up to this lsn have been persisted, even when checkpoints are blocked.
|
|
350
|
+
// In the future we could also advance this on flush, when the connector provides the
|
|
351
|
+
// current position (see setResumeLsn, which connectors may already call after flushing).
|
|
352
|
+
const resumeLsnUpdate = {
|
|
353
|
+
resume_lsn: lsn,
|
|
354
|
+
...(lastOp == null ? {} : { last_persisted_op: lastOp })
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (unblockedConfigIds.length > 0) {
|
|
358
|
+
// All unblocked configs get the SAME new value, so we apply it with a single updateOne
|
|
359
|
+
// (single-document atomicity).
|
|
360
|
+
const updateSet: Record<string, any> = {
|
|
361
|
+
last_fatal_error: null,
|
|
362
|
+
last_fatal_error_ts: null
|
|
363
|
+
};
|
|
364
|
+
// Only advance checkpoint fields when an actual (non-empty) checkpoint is created, matching
|
|
365
|
+
// the previous per-config / v1 behaviour.
|
|
366
|
+
if (checkpointCreated) {
|
|
367
|
+
updateSet['sync_configs.$[config].last_checkpoint'] = newCheckpoint;
|
|
368
|
+
updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
|
|
369
|
+
updateSet['last_checkpoint_ts'] = now;
|
|
375
370
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
_id: this.replicationStreamId
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
$set: {
|
|
386
|
-
last_keepalive_ts: now,
|
|
387
|
-
last_fatal_error: null,
|
|
388
|
-
last_fatal_error_ts: null
|
|
371
|
+
|
|
372
|
+
writes.updateOne(
|
|
373
|
+
this.db.sync_rules,
|
|
374
|
+
{
|
|
375
|
+
_id: this.replicationStreamId,
|
|
376
|
+
'sync_configs._id': { $in: unblockedConfigIds }
|
|
389
377
|
},
|
|
390
|
-
$max: resumeLsnUpdate
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
378
|
+
{ $set: updateSet, $max: resumeLsnUpdate },
|
|
379
|
+
{
|
|
380
|
+
arrayFilters: checkpointCreated ? [{ 'config._id': { $in: unblockedConfigIds } }] : undefined
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
// All selected configs are blocked - only update keepalive/error tracking and the
|
|
385
|
+
// resume position.
|
|
386
|
+
writes.updateOne(
|
|
387
|
+
this.db.sync_rules,
|
|
388
|
+
{
|
|
389
|
+
_id: this.replicationStreamId
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
$set: {
|
|
393
|
+
last_fatal_error: null,
|
|
394
|
+
last_fatal_error_ts: null
|
|
395
|
+
},
|
|
396
|
+
$max: resumeLsnUpdate
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
}
|
|
395
400
|
|
|
401
|
+
await writes.execute();
|
|
402
|
+
return { checkpointBlocked, checkpointCreated, checkpointLogState, newCheckpoint };
|
|
403
|
+
};
|
|
404
|
+
const projection = { sync_configs: 1, last_persisted_op: 1 };
|
|
405
|
+
const { checkpointBlocked, checkpointCreated, checkpointLogState, newCheckpoint } = await this.flushAndCommit(
|
|
406
|
+
checkpoint,
|
|
407
|
+
() => this.withFencedTransaction(() => this.fence(this.session, projection), checkpoint),
|
|
408
|
+
options,
|
|
409
|
+
projection
|
|
410
|
+
);
|
|
396
411
|
if (checkpointBlocked) {
|
|
397
412
|
if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
|
|
398
413
|
this.logger.info(
|
|
@@ -423,23 +438,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
423
438
|
|
|
424
439
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
425
440
|
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
$set: {
|
|
432
|
-
resume_lsn: lsn
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
session: this.session,
|
|
437
|
-
// Losing occasional resume LSN is fine. That may mean reprocessing
|
|
438
|
-
// some source changes in some edge cases, which is not an issue since
|
|
439
|
-
// changes are processed in an idempotent way.
|
|
440
|
-
writeConcern: { w: 1 }
|
|
441
|
-
}
|
|
442
|
-
);
|
|
441
|
+
// Losing occasional resume LSN would only reprocess source changes.
|
|
442
|
+
// Keep the lease check atomic, but do not wait for majority replication of this resume hint.
|
|
443
|
+
await this.updateStreamMetadata({ resume_lsn: { $literal: lsn } }, {}, { w: 1 });
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
private async autoActivateV3(lsn: string): Promise<void> {
|
|
@@ -450,7 +451,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
450
451
|
const session = this.session;
|
|
451
452
|
let activated = false;
|
|
452
453
|
let needsFutureActivationCheck = true;
|
|
453
|
-
await
|
|
454
|
+
await this.withTransaction(async () => {
|
|
454
455
|
// Reset on transaction retries.
|
|
455
456
|
needsFutureActivationCheck = true;
|
|
456
457
|
activated = false;
|
|
@@ -504,7 +505,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
504
505
|
_id: { $ne: this.replicationStreamId },
|
|
505
506
|
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
506
507
|
},
|
|
507
|
-
|
|
508
|
+
stopReplicationStreamPipeline(),
|
|
508
509
|
{ session }
|
|
509
510
|
);
|
|
510
511
|
activated = true;
|
|
@@ -538,7 +539,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
538
539
|
} else if (doc.state == storage.SyncRuleState.ACTIVE && processingStates.length == 0) {
|
|
539
540
|
needsFutureActivationCheck = false;
|
|
540
541
|
}
|
|
541
|
-
});
|
|
542
|
+
}, session);
|
|
542
543
|
if (activated) {
|
|
543
544
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
544
545
|
await this.db.notifyCheckpoint();
|
|
@@ -548,26 +549,35 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
551
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
{
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
552
|
+
private syncConfigMetadataUpdate(syncConfigIds: bson.ObjectId[], set: lib_mongo.mongo.Document) {
|
|
553
|
+
return {
|
|
554
|
+
sync_configs: {
|
|
555
|
+
$map: {
|
|
556
|
+
input: '$sync_configs',
|
|
557
|
+
as: 'config',
|
|
558
|
+
in: {
|
|
559
|
+
$cond: [
|
|
560
|
+
{ $in: ['$$config._id', { $literal: syncConfigIds }] },
|
|
561
|
+
{ $mergeObjects: ['$$config', set] },
|
|
562
|
+
'$$config'
|
|
563
|
+
]
|
|
564
|
+
}
|
|
564
565
|
}
|
|
565
|
-
},
|
|
566
|
-
{
|
|
567
|
-
session: this.session,
|
|
568
|
-
arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
|
|
569
566
|
}
|
|
570
|
-
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private snapshotDoneUpdate(no_checkpoint_before_lsn: string) {
|
|
571
|
+
return this.syncConfigMetadataUpdate(this.syncConfigIds, {
|
|
572
|
+
snapshot_done: true,
|
|
573
|
+
no_checkpoint_before: { $max: ['$$config.no_checkpoint_before', { $literal: no_checkpoint_before_lsn }] }
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
578
|
+
await this.updateStreamMetadata(this.snapshotDoneUpdate(no_checkpoint_before_lsn), {
|
|
579
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
580
|
+
});
|
|
571
581
|
}
|
|
572
582
|
|
|
573
583
|
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
@@ -591,7 +601,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
591
601
|
}
|
|
592
602
|
}
|
|
593
603
|
|
|
594
|
-
await this.
|
|
604
|
+
await this.updateStreamMetadataInTransaction(this.snapshotDoneUpdate(no_checkpoint_before_lsn), {
|
|
605
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
606
|
+
});
|
|
595
607
|
});
|
|
596
608
|
}
|
|
597
609
|
|
|
@@ -600,22 +612,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
600
612
|
if (syncConfigIds.length == 0) {
|
|
601
613
|
return;
|
|
602
614
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
_id: this.replicationStreamId,
|
|
607
|
-
'sync_configs._id': { $in: syncConfigIds }
|
|
608
|
-
},
|
|
609
|
-
{
|
|
610
|
-
$set: {
|
|
611
|
-
'sync_configs.$[config].snapshot_done': false
|
|
612
|
-
}
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
session: this.session,
|
|
616
|
-
arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
|
|
617
|
-
}
|
|
618
|
-
);
|
|
615
|
+
await this.updateStreamMetadata(this.syncConfigMetadataUpdate(syncConfigIds, { snapshot_done: false }), {
|
|
616
|
+
'sync_configs._id': { $in: syncConfigIds }
|
|
617
|
+
});
|
|
619
618
|
}
|
|
620
619
|
|
|
621
620
|
async markTableSnapshotDone(
|
|
@@ -627,7 +626,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
627
626
|
const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
|
|
628
627
|
|
|
629
628
|
await this.withTransaction(async () => {
|
|
630
|
-
|
|
629
|
+
const writes = this.db.createWriteBatch(session, { ordered: false });
|
|
630
|
+
writes.updateMany(
|
|
631
|
+
this.db.sourceTables(this.replicationStreamId),
|
|
631
632
|
{ _id: { $in: ids } },
|
|
632
633
|
{
|
|
633
634
|
$set: {
|
|
@@ -636,31 +637,28 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
636
637
|
$unset: {
|
|
637
638
|
snapshot_status: 1
|
|
638
639
|
}
|
|
639
|
-
}
|
|
640
|
-
{ session }
|
|
640
|
+
}
|
|
641
641
|
);
|
|
642
642
|
|
|
643
643
|
if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
|
|
644
|
-
|
|
644
|
+
writes.updateOne(
|
|
645
|
+
this.db.sync_rules,
|
|
645
646
|
{
|
|
646
647
|
_id: this.replicationStreamId,
|
|
647
648
|
'sync_configs._id': { $in: syncConfigIds }
|
|
648
649
|
},
|
|
649
650
|
{
|
|
650
|
-
$set: {
|
|
651
|
-
last_keepalive_ts: new Date()
|
|
652
|
-
},
|
|
653
651
|
$max: {
|
|
654
652
|
'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
|
|
655
653
|
}
|
|
656
654
|
},
|
|
657
655
|
{
|
|
658
|
-
session: this.session,
|
|
659
656
|
// Only set for sync configs that use this table
|
|
660
657
|
arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
|
|
661
658
|
}
|
|
662
659
|
);
|
|
663
660
|
}
|
|
661
|
+
await writes.execute();
|
|
664
662
|
});
|
|
665
663
|
return tables.map((table) => {
|
|
666
664
|
const copy = table.clone();
|
|
@@ -668,4 +666,98 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
668
666
|
return copy;
|
|
669
667
|
});
|
|
670
668
|
}
|
|
669
|
+
|
|
670
|
+
protected override async batchCreateCustomWriteCheckpoints(
|
|
671
|
+
session: lib_mongo.mongo.ClientSession,
|
|
672
|
+
opId: InternalOpId
|
|
673
|
+
): Promise<void> {
|
|
674
|
+
if (this.write_checkpoint_batch.length == 0) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const checkpointsByEvent = Map.groupBy(this.write_checkpoint_batch, (checkpoint) =>
|
|
679
|
+
this.validateCustomCheckpointEventId(checkpoint.event_id)
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
const writes = this.db.createWriteBatch(session, { ordered: false });
|
|
683
|
+
for (const [eventId, checkpoints] of checkpointsByEvent) {
|
|
684
|
+
this.batchCreateEventCustomWriteCheckpoints(writes, opId, eventId, checkpoints);
|
|
685
|
+
}
|
|
686
|
+
await writes.execute();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
protected override async prepareCustomWriteCheckpoints(): Promise<void> {
|
|
690
|
+
// Most events never produce custom checkpoints, so create collections lazily
|
|
691
|
+
// only for the event ids present in this batch. This hook runs before the
|
|
692
|
+
// replication transaction because MongoDB cannot create indexes within it.
|
|
693
|
+
const eventIds = new Set(
|
|
694
|
+
this.write_checkpoint_batch.map((checkpoint) => this.validateCustomCheckpointEventId(checkpoint.event_id))
|
|
695
|
+
);
|
|
696
|
+
for (const eventId of eventIds) {
|
|
697
|
+
if (this.initializedCustomCheckpointEventIds.has(eventId)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
await this.db.initializeCustomCheckpointRequestsCollection({
|
|
701
|
+
replicationStreamId: this.replicationStreamId,
|
|
702
|
+
eventId
|
|
703
|
+
});
|
|
704
|
+
this.initializedCustomCheckpointEventIds.add(eventId);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private validateCustomCheckpointEventId(eventId: EventDefinitionId | undefined): EventDefinitionId {
|
|
709
|
+
if (eventId == null) {
|
|
710
|
+
throw new ServiceAssertionError('V3 custom checkpoints require an event definition id');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!this.knownEventIds.has(eventId)) {
|
|
714
|
+
throw new ServiceAssertionError(`Unknown custom checkpoint event definition ${eventId}`);
|
|
715
|
+
}
|
|
716
|
+
return eventId;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private batchCreateEventCustomWriteCheckpoints(
|
|
720
|
+
writes: MongoWriteBatch,
|
|
721
|
+
opId: InternalOpId,
|
|
722
|
+
eventId: EventDefinitionId,
|
|
723
|
+
checkpoints: storage.CustomWriteCheckpointOptions[]
|
|
724
|
+
): void {
|
|
725
|
+
// A repeated user within an event replaces the complete checkpoint state.
|
|
726
|
+
const uniqueCheckpoints = new Map(checkpoints.map((checkpoint) => [checkpoint.user_id, checkpoint]));
|
|
727
|
+
writes.bulkWriteUnordered(
|
|
728
|
+
this.db.customCheckpointRequests({
|
|
729
|
+
eventId,
|
|
730
|
+
replicationStreamId: this.replicationStreamId
|
|
731
|
+
}),
|
|
732
|
+
[...uniqueCheckpoints.values()].map((checkpointOptions) => {
|
|
733
|
+
const set: Partial<CustomCheckpointRequestDocumentV3> = {
|
|
734
|
+
user_id: checkpointOptions.user_id,
|
|
735
|
+
checkpoint: checkpointOptions.checkpoint,
|
|
736
|
+
op_id: opId
|
|
737
|
+
};
|
|
738
|
+
if (checkpointOptions.checkpoint_requested_at != null) {
|
|
739
|
+
set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
updateOne: {
|
|
744
|
+
filter: {
|
|
745
|
+
user_id: checkpointOptions.user_id
|
|
746
|
+
},
|
|
747
|
+
update: {
|
|
748
|
+
$set: set,
|
|
749
|
+
...(checkpointOptions.checkpoint_requested_at == null
|
|
750
|
+
? {
|
|
751
|
+
$unset: {
|
|
752
|
+
checkpoint_requested_at: 1
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
: {})
|
|
756
|
+
},
|
|
757
|
+
upsert: true
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
}
|
|
671
763
|
}
|