@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260803144812
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 +101 -5
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +41 -16
- package/dist/storage/MongoBucketStorage.js +226 -71
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +56 -25
- package/dist/storage/implementation/MongoBucketBatch.js +110 -62
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +41 -13
- package/dist/storage/implementation/MongoChecksums.js +36 -121
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +17 -19
- package/dist/storage/implementation/MongoCompactor.js +80 -263
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
- package/dist/storage/implementation/MongoHydrationState.js +49 -0
- package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +7 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +25 -2
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +123 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/implementation/MongoStorageProvider.js +39 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +57 -29
- package/dist/storage/implementation/MongoSyncBucketStorage.js +87 -61
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +3 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +171 -24
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +8 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +7 -2
- package/dist/storage/implementation/common/PersistedBatch.js +12 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/db.js +17 -15
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -13
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +61 -29
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +251 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -19
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +82 -58
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.d.ts +10 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +309 -259
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +40 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +647 -23
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +67 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +351 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +45 -30
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +272 -140
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +6 -3
- package/dist/storage/implementation/v3/PersistedBatchV3.js +84 -21
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +15 -13
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
- package/dist/storage/implementation/v3/bucket-format.js +77 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
- package/dist/storage/implementation/v3/chunking.d.ts +10 -0
- package/dist/storage/implementation/v3/chunking.js +30 -0
- package/dist/storage/implementation/v3/chunking.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +101 -21
- package/dist/storage/implementation/v3/models.js +11 -28
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
- package/dist/storage/implementation/v3/source-table-utils.js +214 -0
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
- package/dist/storage/storage-index.d.ts +4 -4
- package/dist/storage/storage-index.js +4 -4
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +49 -3
- package/dist/types/types.js +48 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +1 -2
- package/dist/utils/test-utils.js +7 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +9 -2
- package/dist/utils/util.js +128 -4
- package/dist/utils/util.js.map +1 -1
- package/package.json +12 -19
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +347 -88
- package/src/storage/implementation/MongoBucketBatch.ts +147 -82
- package/src/storage/implementation/MongoChecksums.ts +93 -153
- package/src/storage/implementation/MongoCompactor.ts +102 -316
- package/src/storage/implementation/MongoHydrationState.ts +61 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +25 -6
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
- package/src/storage/implementation/MongoStorageProvider.ts +46 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +121 -87
- package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +202 -28
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +9 -0
- package/src/storage/implementation/common/PersistedBatch.ts +19 -3
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
- package/src/storage/implementation/db.ts +19 -20
- package/src/storage/implementation/models.ts +20 -14
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +75 -37
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
- package/src/storage/implementation/v1/MongoCompactorV1.ts +294 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +142 -105
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
- package/src/storage/implementation/v1/models.ts +11 -1
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +382 -321
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
- package/src/storage/implementation/v3/MongoCompactorV3.ts +847 -32
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +482 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +359 -187
- package/src/storage/implementation/v3/PersistedBatchV3.ts +118 -37
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +46 -36
- package/src/storage/implementation/v3/bucket-format.ts +97 -0
- package/src/storage/implementation/v3/chunking.ts +40 -0
- package/src/storage/implementation/v3/models.ts +132 -55
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/storage/implementation/v3/source-table-utils.ts +392 -0
- package/src/storage/storage-index.ts +12 -4
- package/src/types/types.ts +58 -1
- package/src/utils/test-utils.ts +8 -3
- package/src/utils/util.ts +185 -6
- package/test/src/__snapshots__/storage.test.ts.snap +0 -75
- package/test/src/__snapshots__/storage_sync.test.ts.snap +35 -617
- package/test/src/cleanup-stopped-sync-configs.test.ts +671 -0
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +227 -13
- package/test/src/storage_compacting.test.ts +1951 -12
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +1180 -73
- package/test/src/storeCurrentData.test.ts +227 -0
- package/test/src/util.ts +2 -1
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
- package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
- package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
- package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import {
|
|
4
|
-
import { HydratedSyncConfig, MatchingSources } from '@powersync/service-sync-rules';
|
|
3
|
+
import { storage } from '@powersync/service-core';
|
|
5
4
|
import * as bson from 'bson';
|
|
6
5
|
import { mongoTableId } from '../../../utils/util.js';
|
|
7
|
-
import {
|
|
6
|
+
import { canCheckpointState } from '../CheckpointState.js';
|
|
8
7
|
import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
|
|
8
|
+
import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
|
|
9
9
|
import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
|
|
10
10
|
import { PersistedBatch } from '../common/PersistedBatch.js';
|
|
11
11
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
@@ -13,31 +13,39 @@ import { PersistedBatchV3 } from './PersistedBatchV3.js';
|
|
|
13
13
|
import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
|
|
14
14
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
15
15
|
import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import {
|
|
17
|
+
createNewSourceTable,
|
|
18
|
+
designateEventCarrier,
|
|
19
|
+
overlappingSourceTableFilter,
|
|
20
|
+
planSourceTableReconciliation,
|
|
21
|
+
sourceTableDesiredResolution,
|
|
22
|
+
sourceTableFromDocument,
|
|
23
|
+
SourceTableReconciliationContext
|
|
24
|
+
} from './source-table-utils.js';
|
|
20
25
|
|
|
21
26
|
export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
22
|
-
declare
|
|
27
|
+
declare readonly db: VersionedPowerSyncMongoV3;
|
|
23
28
|
|
|
24
29
|
private readonly store: SourceRecordStore;
|
|
25
|
-
private readonly
|
|
30
|
+
private readonly syncConfigIds: bson.ObjectId[];
|
|
26
31
|
private needsActivationV3 = true;
|
|
27
32
|
private lastWaitingLogThrottledV3 = 0;
|
|
28
33
|
|
|
29
34
|
constructor(options: MongoBucketBatchOptions) {
|
|
30
35
|
super(options);
|
|
31
|
-
|
|
36
|
+
const syncConfigIds = options.syncConfigIds ?? [];
|
|
37
|
+
if (syncConfigIds.length == 0) {
|
|
32
38
|
throw new ReplicationAssertionError('Missing sync config id for v3 batch');
|
|
33
39
|
}
|
|
34
|
-
this.
|
|
35
|
-
this.store = new SourceRecordStoreV3(this.db, this.
|
|
40
|
+
this.syncConfigIds = syncConfigIds;
|
|
41
|
+
this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
protected createPersistedBatch(writtenSize: number): PersistedBatch {
|
|
39
|
-
return new PersistedBatchV3(this.db, this.
|
|
40
|
-
logger: this.logger
|
|
45
|
+
return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
|
|
46
|
+
logger: this.logger,
|
|
47
|
+
objectStorage: this.options.objectStorage,
|
|
48
|
+
inlineThresholdBytes: this.options.inlineThresholdBytes
|
|
41
49
|
});
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -45,10 +53,83 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
45
53
|
return this.store;
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
protected override async onReplicationTransactionFlush(
|
|
57
|
+
session: lib_mongo.mongo.ClientSession,
|
|
58
|
+
lastOp: bigint
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
// Durably advance the stream-level head of persisted ops within the flush transaction.
|
|
61
|
+
// This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
|
|
62
|
+
// config that replicates nothing) covers all ops persisted before a potential crash.
|
|
63
|
+
await this.db.sync_rules.updateOne(
|
|
64
|
+
{
|
|
65
|
+
_id: this.replicationStreamId
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
$max: {
|
|
69
|
+
last_persisted_op: lastOp
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{ session }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private selectedSyncConfigObjectIds(syncConfigIds: string[]): bson.ObjectId[] {
|
|
77
|
+
const selectedIds = new Set(this.syncConfigIds.map((id) => id.toHexString()));
|
|
78
|
+
return syncConfigIds
|
|
79
|
+
.filter((id) => selectedIds.has(id) && bson.ObjectId.isValid(id))
|
|
80
|
+
.map((id) => new bson.ObjectId(id));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private relevantSyncConfigIds(table: storage.SourceTable): bson.ObjectId[] {
|
|
84
|
+
const bucketDataSourceIds = table.bucketDataSources.map((source) => this.mapping.bucketSourceId(source));
|
|
85
|
+
const parameterLookupSourceIds = table.parameterLookupSources.map((source) =>
|
|
86
|
+
this.mapping.parameterLookupId(source)
|
|
87
|
+
);
|
|
88
|
+
return this.selectedSyncConfigObjectIds(
|
|
89
|
+
this.mapping.syncConfigIdsForSourceTable(
|
|
90
|
+
this.syncConfigIds.map((id) => id.toHexString()),
|
|
91
|
+
table.ref,
|
|
92
|
+
bucketDataSourceIds,
|
|
93
|
+
parameterLookupSourceIds
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private relevantSyncConfigIdsForTables(tables: storage.SourceTable[]): bson.ObjectId[] {
|
|
99
|
+
const ids = new Map<string, bson.ObjectId>();
|
|
100
|
+
for (const table of tables) {
|
|
101
|
+
for (const id of this.relevantSyncConfigIds(table)) {
|
|
102
|
+
ids.set(id.toHexString(), id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...ids.values()];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private snapshotBlockingSourceTablesFilter(): Record<string, unknown> {
|
|
109
|
+
const clauses = this.syncConfigIds.flatMap((syncConfigId) => {
|
|
110
|
+
const filter = this.mapping.snapshotBlockingSourceTablesFilter(syncConfigId.toHexString()) as {
|
|
111
|
+
$or?: Record<string, unknown>[];
|
|
112
|
+
};
|
|
113
|
+
return filter.$or ?? [];
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (clauses.length == 0) {
|
|
117
|
+
return {
|
|
118
|
+
snapshot_done: false,
|
|
119
|
+
_id: { $exists: false }
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
snapshot_done: false,
|
|
125
|
+
$or: clauses
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
48
129
|
protected async cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]) {
|
|
49
130
|
for (const table of sourceTables) {
|
|
50
131
|
await this.db
|
|
51
|
-
.
|
|
132
|
+
.sourceRecords(this.replicationStreamId, mongoTableId(table.id))
|
|
52
133
|
.drop()
|
|
53
134
|
.catch((error) => {
|
|
54
135
|
if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
|
|
@@ -60,235 +141,103 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
60
141
|
}
|
|
61
142
|
|
|
62
143
|
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
144
|
+
// The test-only override is a whole parsed set, so the sync rules and the mapping
|
|
145
|
+
// used below always come from the same parse.
|
|
146
|
+
const parsedOverride = options.parsedSyncConfig as MongoParsedSyncConfigSet | undefined;
|
|
147
|
+
const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
|
|
148
|
+
const mapping = parsedOverride?.mapping ?? this.mapping;
|
|
66
149
|
|
|
67
150
|
const { connection_id, source } = options;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
151
|
+
const context: SourceTableReconciliationContext = {
|
|
152
|
+
connectionId: connection_id,
|
|
153
|
+
connectionTag: source.connectionTag,
|
|
154
|
+
identity: {
|
|
155
|
+
schema: source.schema,
|
|
156
|
+
name: source.name,
|
|
157
|
+
objectId: source.objectId,
|
|
158
|
+
replicaIdColumns: source.replicaIdColumns.map((column) => ({
|
|
159
|
+
name: column.name,
|
|
160
|
+
type: column.type,
|
|
161
|
+
type_oid: column.typeId
|
|
162
|
+
}))
|
|
163
|
+
},
|
|
164
|
+
storeCurrentData: source.sendsCompleteRows !== true,
|
|
165
|
+
syncConfig,
|
|
166
|
+
mapping,
|
|
167
|
+
desired: sourceTableDesiredResolution(syncConfig, source, mapping)
|
|
168
|
+
};
|
|
74
169
|
|
|
75
170
|
let result: storage.ResolveTablesResult | null = null;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
await this.db.client.withSession(async (session) => {
|
|
79
|
-
const col = this.db.commonSourceTables(this.group_id);
|
|
80
|
-
const exactFilter: Record<string, unknown> = {
|
|
81
|
-
connection_id,
|
|
82
|
-
schema_name: schema,
|
|
83
|
-
table_name: name,
|
|
84
|
-
replica_id_columns2: normalizedReplicaIdColumns
|
|
85
|
-
};
|
|
86
|
-
if (objectId != null) {
|
|
87
|
-
exactFilter.relation_id = objectId;
|
|
88
|
-
}
|
|
171
|
+
const session = this.db.client.startSession();
|
|
172
|
+
await using _ = { [Symbol.asyncDispose]: () => session.endSession() };
|
|
89
173
|
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
matchingSources.bucketDataSources.map((source) => [this.mapping.bucketSourceId(source), source] as const)
|
|
93
|
-
);
|
|
94
|
-
const parameterLookupSourceById = new Map(
|
|
95
|
-
matchingSources.parameterLookupSources.map(
|
|
96
|
-
(source) => [this.mapping.parameterLookupId(source), source] as const
|
|
97
|
-
)
|
|
98
|
-
);
|
|
99
|
-
const desiredBucketIds = new Set(bucketSourceById.keys());
|
|
100
|
-
const desiredLookupIds = new Set(parameterLookupSourceById.keys());
|
|
101
|
-
const desiredHasMembership = desiredBucketIds.size > 0 || desiredLookupIds.size > 0;
|
|
102
|
-
const triggersEvent = syncRules.tableTriggersEvent(ref);
|
|
103
|
-
|
|
104
|
-
const coveredBucketIds = new Set<string>();
|
|
105
|
-
const coveredLookupIds = new Set<string>();
|
|
106
|
-
const retainedDocIds: bson.ObjectId[] = [];
|
|
107
|
-
const tables: storage.SourceTable[] = [];
|
|
108
|
-
let retainedEventOnlyTable = false;
|
|
109
|
-
|
|
110
|
-
for (const doc of exactDocs) {
|
|
111
|
-
const bucketDataSourceIds = doc.bucket_data_source_ids.filter((id) => desiredBucketIds.has(id));
|
|
112
|
-
const parameterLookupSourceIds = doc.parameter_lookup_source_ids.filter((id) => desiredLookupIds.has(id));
|
|
113
|
-
const coversDesiredMembership = bucketDataSourceIds.length > 0 || parameterLookupSourceIds.length > 0;
|
|
114
|
-
const coversEventOnlyTable = !desiredHasMembership && triggersEvent && !retainedEventOnlyTable;
|
|
115
|
-
|
|
116
|
-
for (const id of bucketDataSourceIds) {
|
|
117
|
-
coveredBucketIds.add(id);
|
|
118
|
-
}
|
|
119
|
-
for (const id of parameterLookupSourceIds) {
|
|
120
|
-
coveredLookupIds.add(id);
|
|
121
|
-
}
|
|
174
|
+
await session.withTransaction(async () => {
|
|
175
|
+
const col = this.db.sourceTables(this.replicationStreamId);
|
|
122
176
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
) {
|
|
127
|
-
|
|
128
|
-
{ _id: doc._id },
|
|
129
|
-
{
|
|
130
|
-
$set: {
|
|
131
|
-
bucket_data_source_ids: bucketDataSourceIds,
|
|
132
|
-
parameter_lookup_source_ids: parameterLookupSourceIds
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
{ session }
|
|
136
|
-
);
|
|
137
|
-
}
|
|
177
|
+
// Fetch every persisted source-table doc that can overlap this physical table.
|
|
178
|
+
// Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
|
|
179
|
+
const candidateDocs = await col
|
|
180
|
+
.find(overlappingSourceTableFilter(connection_id, context.identity), { session })
|
|
181
|
+
.toArray();
|
|
138
182
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
159
|
-
}
|
|
183
|
+
// Pure planning: which docs to retain (and narrow), whether a new doc is needed for
|
|
184
|
+
// uncovered memberships, and which docs conflict with the current identity.
|
|
185
|
+
const plan = planSourceTableReconciliation(candidateDocs, context);
|
|
186
|
+
|
|
187
|
+
// Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
|
|
188
|
+
// coverage ids so compatible future configs can reuse already-snapshotted data.
|
|
189
|
+
// Narrowing occurs after removing a sync config, meaning we don't process those
|
|
190
|
+
// definitions anymore.
|
|
191
|
+
for (const update of plan.narrowingUpdates) {
|
|
192
|
+
await col.updateOne(
|
|
193
|
+
{ _id: update.id },
|
|
194
|
+
{
|
|
195
|
+
$set: {
|
|
196
|
+
bucket_data_source_ids: update.memberships.bucketDataSourceIds,
|
|
197
|
+
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{ session }
|
|
201
|
+
);
|
|
160
202
|
}
|
|
161
203
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (uncoveredBucketIds.length > 0 || uncoveredLookupIds.length > 0 || (triggersEvent && tables.length == 0)) {
|
|
204
|
+
// Any desired membership not covered by an existing doc gets a new source table.
|
|
205
|
+
// That table snapshots only the uncovered memberships.
|
|
206
|
+
if (plan.newTableMemberships != null) {
|
|
166
207
|
const id = options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId();
|
|
167
|
-
const
|
|
168
|
-
id,
|
|
169
|
-
ref,
|
|
170
|
-
objectId,
|
|
171
|
-
replicaIdColumns,
|
|
172
|
-
snapshotComplete: false,
|
|
173
|
-
bucketDataSources: uncoveredBucketIds.map((id) => bucketSourceById.get(id)!),
|
|
174
|
-
parameterLookupSources: uncoveredLookupIds.map((id) => parameterLookupSourceById.get(id)!)
|
|
175
|
-
});
|
|
176
|
-
sourceTable.syncData = uncoveredBucketIds.length > 0;
|
|
177
|
-
sourceTable.syncParameters = uncoveredLookupIds.length > 0;
|
|
178
|
-
sourceTable.syncEvent = triggersEvent;
|
|
179
|
-
|
|
180
|
-
const createDoc: SourceTableDocumentV3 = {
|
|
181
|
-
_id: id,
|
|
182
|
-
connection_id,
|
|
183
|
-
relation_id: objectId,
|
|
184
|
-
schema_name: schema,
|
|
185
|
-
table_name: name,
|
|
186
|
-
replica_id_columns: null,
|
|
187
|
-
replica_id_columns2: normalizedReplicaIdColumns,
|
|
188
|
-
snapshot_done: false,
|
|
189
|
-
snapshot_status: undefined,
|
|
190
|
-
bucket_data_source_ids: uncoveredBucketIds,
|
|
191
|
-
parameter_lookup_source_ids: uncoveredLookupIds
|
|
192
|
-
};
|
|
208
|
+
const { doc, table } = createNewSourceTable(id, plan.newTableMemberships, context);
|
|
193
209
|
|
|
194
|
-
await col.insertOne(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
tables.push(sourceTable);
|
|
210
|
+
await col.insertOne(doc, { session });
|
|
211
|
+
await this.db.initializeSourceRecordsCollection(this.replicationStreamId, doc._id, session);
|
|
212
|
+
plan.tables.push(table);
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
conflictFilter.push({ relation_id: objectId });
|
|
203
|
-
}
|
|
204
|
-
const dropTables = await col
|
|
205
|
-
.find(
|
|
206
|
-
{
|
|
207
|
-
connection_id,
|
|
208
|
-
_id: { $nin: retainedDocIds },
|
|
209
|
-
$or: conflictFilter
|
|
210
|
-
},
|
|
211
|
-
{ session }
|
|
212
|
-
)
|
|
213
|
-
.toArray();
|
|
215
|
+
// If memberships are split across multiple source tables, only one may fire events.
|
|
216
|
+
designateEventCarrier(plan.tables, context.desired.triggersEvent);
|
|
214
217
|
|
|
215
218
|
result = {
|
|
216
|
-
tables,
|
|
217
|
-
dropTables:
|
|
218
|
-
this.sourceTableFromDocument(doc as SourceTableDocumentV3, connectionTag, syncRules)
|
|
219
|
-
)
|
|
219
|
+
tables: plan.tables,
|
|
220
|
+
dropTables: plan.dropDocs.map((doc) => sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping))
|
|
220
221
|
};
|
|
221
222
|
});
|
|
222
223
|
|
|
223
|
-
for (const sourceTableId of initializeSourceRecordsFor) {
|
|
224
|
-
await this.db.initializeSourceRecordsCollection(this.group_id, sourceTableId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
224
|
return result!;
|
|
228
225
|
}
|
|
229
226
|
|
|
230
|
-
private sourceTableFromDocument(
|
|
231
|
-
doc: SourceTableDocumentV3,
|
|
232
|
-
connectionTag: string,
|
|
233
|
-
syncRules: HydratedSyncConfig,
|
|
234
|
-
memberships?: MatchingSources
|
|
235
|
-
): storage.SourceTable {
|
|
236
|
-
const resolvedMemberships = memberships ?? this.sourceTableMembershipsFromDocument(doc, syncRules);
|
|
237
|
-
const table = new storage.SourceTable({
|
|
238
|
-
id: doc._id,
|
|
239
|
-
ref: {
|
|
240
|
-
connectionTag,
|
|
241
|
-
schema: doc.schema_name,
|
|
242
|
-
name: doc.table_name
|
|
243
|
-
},
|
|
244
|
-
objectId: doc.relation_id,
|
|
245
|
-
replicaIdColumns: doc.replica_id_columns2!.map(
|
|
246
|
-
(c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
|
|
247
|
-
),
|
|
248
|
-
snapshotComplete: doc.snapshot_done ?? true,
|
|
249
|
-
bucketDataSources: resolvedMemberships.bucketDataSources,
|
|
250
|
-
parameterLookupSources: resolvedMemberships.parameterLookupSources
|
|
251
|
-
});
|
|
252
|
-
table.syncData = table.bucketDataSources.length > 0;
|
|
253
|
-
table.syncParameters = table.parameterLookupSources.length > 0;
|
|
254
|
-
table.syncEvent = syncRules.tableTriggersEvent(table.ref);
|
|
255
|
-
table.snapshotStatus =
|
|
256
|
-
doc.snapshot_status == null
|
|
257
|
-
? undefined
|
|
258
|
-
: {
|
|
259
|
-
lastKey: doc.snapshot_status.last_key?.buffer ?? null,
|
|
260
|
-
totalEstimatedCount: doc.snapshot_status.total_estimated_count,
|
|
261
|
-
replicatedCount: doc.snapshot_status.replicated_count
|
|
262
|
-
};
|
|
263
|
-
return table;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private sourceTableMembershipsFromDocument(
|
|
267
|
-
doc: SourceTableDocumentV3,
|
|
268
|
-
syncRules: HydratedSyncConfig
|
|
269
|
-
): MatchingSources {
|
|
270
|
-
const bucketDataSourceIds = new Set(doc.bucket_data_source_ids);
|
|
271
|
-
const parameterLookupSourceIds = new Set(doc.parameter_lookup_source_ids);
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
bucketDataSources: syncRules.bucketDataSources.filter((source) =>
|
|
275
|
-
bucketDataSourceIds.has(this.mapping.bucketSourceId(source))
|
|
276
|
-
),
|
|
277
|
-
parameterLookupSources: syncRules.bucketParameterLookupSources.filter((source) =>
|
|
278
|
-
parameterLookupSourceIds.has(this.mapping.parameterLookupId(source))
|
|
279
|
-
)
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
227
|
async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
|
|
284
228
|
const doc = (await this.db
|
|
285
|
-
.
|
|
229
|
+
.sourceTables(this.replicationStreamId)
|
|
286
230
|
.findOne({ _id: mongoTableId(table.id) }, { session: this.session })) as SourceTableDocumentV3 | null;
|
|
287
231
|
if (doc == null) {
|
|
288
232
|
return null;
|
|
289
233
|
}
|
|
290
234
|
|
|
291
|
-
|
|
235
|
+
const refreshed = sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules, this.mapping);
|
|
236
|
+
// The event-carrier designation is decided per resolveTables result and not persisted -
|
|
237
|
+
// preserve the caller's designation instead of recomputing it from the ref, so that
|
|
238
|
+
// refreshing a non-carrier table does not make it fire events.
|
|
239
|
+
refreshed.syncEvent = table.syncEvent;
|
|
240
|
+
return refreshed;
|
|
292
241
|
}
|
|
293
242
|
|
|
294
243
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
@@ -296,6 +245,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
296
245
|
|
|
297
246
|
await this.flush(options);
|
|
298
247
|
|
|
248
|
+
using _ = this.tracer.span('storage', 'commit');
|
|
249
|
+
|
|
299
250
|
const now = new Date();
|
|
300
251
|
|
|
301
252
|
await this.db.write_checkpoints.updateMany(
|
|
@@ -313,97 +264,156 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
313
264
|
}
|
|
314
265
|
);
|
|
315
266
|
|
|
316
|
-
const preUpdateDocument = await this.db.sync_rules.findOne(
|
|
267
|
+
const preUpdateDocument = (await this.db.sync_rules.findOne(
|
|
317
268
|
{
|
|
318
|
-
_id: this.
|
|
319
|
-
'sync_configs._id': this.
|
|
269
|
+
_id: this.replicationStreamId,
|
|
270
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
320
271
|
},
|
|
321
272
|
{
|
|
322
273
|
session: this.session,
|
|
323
274
|
projection: {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
$elemMatch: {
|
|
327
|
-
_id: this.syncConfigId
|
|
328
|
-
}
|
|
329
|
-
}
|
|
275
|
+
sync_configs: 1,
|
|
276
|
+
last_persisted_op: 1
|
|
330
277
|
}
|
|
331
278
|
}
|
|
332
|
-
);
|
|
279
|
+
)) as ReplicationStreamDocumentV3 | null;
|
|
333
280
|
|
|
334
|
-
const
|
|
335
|
-
|
|
281
|
+
const states =
|
|
282
|
+
preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
|
|
283
|
+
if (states.length == 0) {
|
|
336
284
|
throw new ReplicationAssertionError(
|
|
337
|
-
`Failed to update checkpoint - no matching sync_config for _id: ${this.
|
|
285
|
+
`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
|
|
286
|
+
.map((id) => id.toHexString())
|
|
287
|
+
.join(',')}`
|
|
338
288
|
);
|
|
339
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
|
+
}
|
|
340
301
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
302
|
+
// Effective head of the stream's op sequence.
|
|
303
|
+
// last_persisted_op is $max-advanced durably in the same transaction as every flush, and this
|
|
304
|
+
// read uses the same session, so it covers all ops persisted by this batch.
|
|
305
|
+
const newCheckpoint =
|
|
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.
|
|
318
|
+
throw new ReplicationAssertionError(
|
|
319
|
+
`Invariant violation: sync config ${state._id} has last_checkpoint ${state.last_checkpoint} > stream head ${newCheckpoint}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
351
322
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
323
|
+
const canCheckpoint = canCheckpointState(lsn, {
|
|
324
|
+
snapshotDone: state.snapshot_done === true,
|
|
325
|
+
lastCheckpointLsn: state.last_checkpoint_lsn,
|
|
326
|
+
noCheckpointBefore: state.no_checkpoint_before
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (!canCheckpoint) {
|
|
330
|
+
checkpointBlocked = true;
|
|
331
|
+
// Log the first blocked config's state.
|
|
332
|
+
checkpointLogState ??= {
|
|
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
|
+
}
|
|
339
|
+
|
|
340
|
+
checkpointCreated ||= createEmptyCheckpoints || state.last_checkpoint !== newCheckpoint;
|
|
341
|
+
unblockedConfigIds.push(state._id);
|
|
363
342
|
}
|
|
364
343
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
344
|
+
// Every commit advances the stream's resume position: commit() flushes first, so all
|
|
345
|
+
// source changes up to this lsn have been persisted, even when checkpoints are blocked.
|
|
346
|
+
// In the future we could also advance this on flush, when the connector provides the
|
|
347
|
+
// current position (see setResumeLsn, which connectors may already call after flushing).
|
|
348
|
+
const resumeLsnUpdate = { resume_lsn: lsn };
|
|
349
|
+
|
|
350
|
+
if (unblockedConfigIds.length > 0) {
|
|
351
|
+
// All unblocked configs get the SAME new value, so we apply it with a single updateOne
|
|
352
|
+
// (single-document atomicity).
|
|
353
|
+
const updateSet: Record<string, any> = {
|
|
354
|
+
last_keepalive_ts: now,
|
|
355
|
+
last_fatal_error: null,
|
|
356
|
+
last_fatal_error_ts: null
|
|
357
|
+
};
|
|
358
|
+
// Only advance checkpoint fields when an actual (non-empty) checkpoint is created, matching
|
|
359
|
+
// the previous per-config / v1 behaviour.
|
|
360
|
+
if (checkpointCreated) {
|
|
361
|
+
updateSet['sync_configs.$[config].last_checkpoint'] = newCheckpoint;
|
|
362
|
+
updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
|
|
363
|
+
updateSet['last_checkpoint_ts'] = now;
|
|
376
364
|
}
|
|
377
|
-
);
|
|
378
365
|
|
|
379
|
-
|
|
366
|
+
await this.db.sync_rules.updateOne(
|
|
367
|
+
{
|
|
368
|
+
_id: this.replicationStreamId,
|
|
369
|
+
'sync_configs._id': { $in: unblockedConfigIds }
|
|
370
|
+
},
|
|
371
|
+
{ $set: updateSet, $max: resumeLsnUpdate },
|
|
372
|
+
{
|
|
373
|
+
session: this.session,
|
|
374
|
+
arrayFilters: checkpointCreated ? [{ 'config._id': { $in: unblockedConfigIds } }] : undefined
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
// All selected configs are blocked - only update keepalive/error tracking and the
|
|
379
|
+
// resume position.
|
|
380
|
+
await this.db.sync_rules.updateOne(
|
|
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
|
|
389
|
+
},
|
|
390
|
+
$max: resumeLsnUpdate
|
|
391
|
+
},
|
|
392
|
+
{ session: this.session }
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (checkpointBlocked) {
|
|
380
397
|
if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
|
|
381
398
|
this.logger.info(
|
|
382
|
-
`Waiting before creating checkpoint, currently at ${lsn}
|
|
383
|
-
{
|
|
384
|
-
snapshot_done: state.snapshot_done,
|
|
385
|
-
last_checkpoint_lsn: state.last_checkpoint_lsn,
|
|
386
|
-
no_checkpoint_before: state.no_checkpoint_before
|
|
387
|
-
}
|
|
388
|
-
)}`
|
|
399
|
+
`Waiting before creating checkpoint, currently at ${lsn}. Current state: ${JSON.stringify(checkpointLogState)}`
|
|
389
400
|
);
|
|
390
401
|
this.lastWaitingLogThrottledV3 = Date.now();
|
|
391
402
|
}
|
|
392
403
|
} else {
|
|
393
|
-
if (
|
|
394
|
-
this.logger.debug(`Created checkpoint at ${lsn}
|
|
404
|
+
if (checkpointCreated) {
|
|
405
|
+
this.logger.debug(`Created checkpoint at ${lsn}`);
|
|
395
406
|
}
|
|
396
407
|
await this.autoActivateV3(lsn);
|
|
408
|
+
// All configs are now checkpointed at newCheckpoint (the stream head).
|
|
409
|
+
await this.sourceRecordStore.postCommitCleanup(newCheckpoint, this.logger);
|
|
410
|
+
}
|
|
411
|
+
if (checkpointCreated) {
|
|
397
412
|
await this.db.notifyCheckpoint();
|
|
398
|
-
this.persisted_op = null;
|
|
399
|
-
this.last_checkpoint_lsn = lsn;
|
|
400
|
-
if (checkpointState.newLastCheckpoint != null) {
|
|
401
|
-
await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
|
|
402
|
-
}
|
|
403
413
|
}
|
|
404
414
|
return {
|
|
405
|
-
checkpointBlocked
|
|
406
|
-
checkpointCreated
|
|
415
|
+
checkpointBlocked,
|
|
416
|
+
checkpointCreated
|
|
407
417
|
};
|
|
408
418
|
}
|
|
409
419
|
|
|
@@ -412,17 +422,23 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
412
422
|
}
|
|
413
423
|
|
|
414
424
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
425
|
+
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
415
426
|
await this.db.sync_rules.updateOne(
|
|
416
427
|
{
|
|
417
|
-
_id: this.
|
|
418
|
-
'sync_configs._id': this.syncConfigId
|
|
428
|
+
_id: this.replicationStreamId
|
|
419
429
|
},
|
|
420
430
|
{
|
|
421
431
|
$set: {
|
|
422
|
-
|
|
432
|
+
resume_lsn: lsn
|
|
423
433
|
}
|
|
424
434
|
},
|
|
425
|
-
{
|
|
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
|
+
}
|
|
426
442
|
);
|
|
427
443
|
}
|
|
428
444
|
|
|
@@ -433,36 +449,43 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
433
449
|
|
|
434
450
|
const session = this.session;
|
|
435
451
|
let activated = false;
|
|
452
|
+
let needsFutureActivationCheck = true;
|
|
436
453
|
await session.withTransaction(async () => {
|
|
454
|
+
// Reset on transaction retries.
|
|
455
|
+
needsFutureActivationCheck = true;
|
|
456
|
+
activated = false;
|
|
457
|
+
|
|
437
458
|
const doc = await this.db.sync_rules.findOne(
|
|
438
459
|
{
|
|
439
|
-
_id: this.
|
|
440
|
-
'sync_configs._id': this.
|
|
460
|
+
_id: this.replicationStreamId,
|
|
461
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
441
462
|
},
|
|
442
463
|
{
|
|
443
464
|
session,
|
|
444
465
|
projection: {
|
|
445
466
|
state: 1,
|
|
446
|
-
sync_configs:
|
|
447
|
-
$elemMatch: {
|
|
448
|
-
_id: this.syncConfigId
|
|
449
|
-
}
|
|
450
|
-
}
|
|
467
|
+
sync_configs: 1
|
|
451
468
|
}
|
|
452
469
|
}
|
|
453
470
|
);
|
|
454
|
-
const
|
|
471
|
+
const states =
|
|
472
|
+
(doc as ReplicationStreamDocumentV3)?.sync_configs?.filter((config) =>
|
|
473
|
+
this.syncConfigIds.some((id) => id.equals(config._id))
|
|
474
|
+
) ?? [];
|
|
475
|
+
if (doc == null || states.length == 0) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const processingStates = states.filter((state) => state.state == storage.SyncRuleState.PROCESSING);
|
|
455
480
|
if (
|
|
456
|
-
doc &&
|
|
457
481
|
doc.state == storage.SyncRuleState.PROCESSING &&
|
|
458
|
-
|
|
459
|
-
state.snapshot_done &&
|
|
460
|
-
state.last_checkpoint != null
|
|
482
|
+
processingStates.length == states.length &&
|
|
483
|
+
states.every((state) => state.snapshot_done && state.last_checkpoint != null)
|
|
461
484
|
) {
|
|
462
485
|
await this.db.sync_rules.updateOne(
|
|
463
486
|
{
|
|
464
|
-
_id: this.
|
|
465
|
-
'sync_configs._id': this.
|
|
487
|
+
_id: this.replicationStreamId,
|
|
488
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
466
489
|
},
|
|
467
490
|
{
|
|
468
491
|
$set: {
|
|
@@ -472,35 +495,64 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
472
495
|
},
|
|
473
496
|
{
|
|
474
497
|
session,
|
|
475
|
-
arrayFilters: [{ 'config._id': this.
|
|
498
|
+
arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
|
|
476
499
|
}
|
|
477
500
|
);
|
|
478
501
|
|
|
479
502
|
await this.db.sync_rules.updateMany(
|
|
480
503
|
{
|
|
481
|
-
_id: { $ne: this.
|
|
504
|
+
_id: { $ne: this.replicationStreamId },
|
|
482
505
|
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
483
506
|
},
|
|
484
507
|
syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
|
|
485
508
|
{ session }
|
|
486
509
|
);
|
|
487
510
|
activated = true;
|
|
488
|
-
} else if (
|
|
489
|
-
|
|
511
|
+
} else if (
|
|
512
|
+
doc.state == storage.SyncRuleState.ACTIVE &&
|
|
513
|
+
processingStates.length > 0 &&
|
|
514
|
+
processingStates.every((state) => state.snapshot_done && state.last_checkpoint != null)
|
|
515
|
+
) {
|
|
516
|
+
await this.db.sync_rules.updateOne(
|
|
517
|
+
{
|
|
518
|
+
_id: this.replicationStreamId,
|
|
519
|
+
'sync_configs._id': { $in: processingStates.map((state) => state._id) }
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
$set: {
|
|
523
|
+
'sync_configs.$[activeConfig].state': storage.SyncRuleState.STOP,
|
|
524
|
+
'sync_configs.$[processingConfig].state': storage.SyncRuleState.ACTIVE
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
session,
|
|
529
|
+
arrayFilters: [
|
|
530
|
+
{ 'activeConfig.state': storage.SyncRuleState.ACTIVE },
|
|
531
|
+
{ 'processingConfig._id': { $in: processingStates.map((state) => state._id) } }
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
activated = true;
|
|
536
|
+
} else if (doc.state != storage.SyncRuleState.PROCESSING && doc.state != storage.SyncRuleState.ACTIVE) {
|
|
537
|
+
needsFutureActivationCheck = false;
|
|
538
|
+
} else if (doc.state == storage.SyncRuleState.ACTIVE && processingStates.length == 0) {
|
|
539
|
+
needsFutureActivationCheck = false;
|
|
490
540
|
}
|
|
491
541
|
});
|
|
492
542
|
if (activated) {
|
|
493
543
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
494
544
|
await this.db.notifyCheckpoint();
|
|
495
545
|
this.needsActivationV3 = false;
|
|
546
|
+
} else if (!needsFutureActivationCheck) {
|
|
547
|
+
this.needsActivationV3 = false;
|
|
496
548
|
}
|
|
497
549
|
}
|
|
498
550
|
|
|
499
551
|
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
500
552
|
await this.db.sync_rules.updateOne(
|
|
501
553
|
{
|
|
502
|
-
_id: this.
|
|
503
|
-
'sync_configs._id': this.
|
|
554
|
+
_id: this.replicationStreamId,
|
|
555
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
504
556
|
},
|
|
505
557
|
{
|
|
506
558
|
$set: {
|
|
@@ -513,7 +565,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
513
565
|
},
|
|
514
566
|
{
|
|
515
567
|
session: this.session,
|
|
516
|
-
arrayFilters: [{ 'config._id': this.
|
|
568
|
+
arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
|
|
517
569
|
}
|
|
518
570
|
);
|
|
519
571
|
}
|
|
@@ -521,16 +573,18 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
521
573
|
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
522
574
|
await this.withTransaction(async () => {
|
|
523
575
|
// Protect against race conditions
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
576
|
+
const blockingTables = await this.db
|
|
577
|
+
.sourceTables(this.replicationStreamId)
|
|
578
|
+
.find(this.snapshotBlockingSourceTablesFilter(), {
|
|
579
|
+
session: this.session,
|
|
580
|
+
projection: { schema_name: 1, table_name: 1 }
|
|
581
|
+
})
|
|
582
|
+
.toArray();
|
|
583
|
+
|
|
584
|
+
if (blockingTables.length > 0) {
|
|
531
585
|
if (options?.throwOnConflict ?? true) {
|
|
532
586
|
throw new ReplicationAssertionError(
|
|
533
|
-
`Cannot mark snapshot done while
|
|
587
|
+
`Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`
|
|
534
588
|
);
|
|
535
589
|
} else {
|
|
536
590
|
return;
|
|
@@ -541,11 +595,16 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
541
595
|
});
|
|
542
596
|
}
|
|
543
597
|
|
|
544
|
-
async markTableSnapshotRequired(
|
|
598
|
+
async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
|
|
599
|
+
const syncConfigIds = this.relevantSyncConfigIds(table);
|
|
600
|
+
if (syncConfigIds.length == 0) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
545
604
|
await this.db.sync_rules.updateOne(
|
|
546
605
|
{
|
|
547
|
-
_id: this.
|
|
548
|
-
'sync_configs._id':
|
|
606
|
+
_id: this.replicationStreamId,
|
|
607
|
+
'sync_configs._id': { $in: syncConfigIds }
|
|
549
608
|
},
|
|
550
609
|
{
|
|
551
610
|
$set: {
|
|
@@ -554,7 +613,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
554
613
|
},
|
|
555
614
|
{
|
|
556
615
|
session: this.session,
|
|
557
|
-
arrayFilters: [{ 'config._id':
|
|
616
|
+
arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
|
|
558
617
|
}
|
|
559
618
|
);
|
|
560
619
|
}
|
|
@@ -565,9 +624,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
565
624
|
): Promise<storage.SourceTable[]> {
|
|
566
625
|
const session = this.session;
|
|
567
626
|
const ids = tables.map((table) => mongoTableId(table.id));
|
|
627
|
+
const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
|
|
568
628
|
|
|
569
629
|
await this.withTransaction(async () => {
|
|
570
|
-
await this.db.
|
|
630
|
+
await this.db.sourceTables(this.replicationStreamId).updateMany(
|
|
571
631
|
{ _id: { $in: ids } },
|
|
572
632
|
{
|
|
573
633
|
$set: {
|
|
@@ -580,11 +640,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
580
640
|
{ session }
|
|
581
641
|
);
|
|
582
642
|
|
|
583
|
-
if (no_checkpoint_before_lsn != null) {
|
|
643
|
+
if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
|
|
584
644
|
await this.db.sync_rules.updateOne(
|
|
585
645
|
{
|
|
586
|
-
_id: this.
|
|
587
|
-
'sync_configs._id':
|
|
646
|
+
_id: this.replicationStreamId,
|
|
647
|
+
'sync_configs._id': { $in: syncConfigIds }
|
|
588
648
|
},
|
|
589
649
|
{
|
|
590
650
|
$set: {
|
|
@@ -596,7 +656,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
596
656
|
},
|
|
597
657
|
{
|
|
598
658
|
session: this.session,
|
|
599
|
-
|
|
659
|
+
// Only set for sync configs that use this table
|
|
660
|
+
arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
|
|
600
661
|
}
|
|
601
662
|
);
|
|
602
663
|
}
|