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