@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,16 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { storage } from '@powersync/service-core';
|
|
2
|
+
import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { addChecksums, storage, utils } from '@powersync/service-core';
|
|
4
4
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
|
+
import { BucketDataDocumentGeneric } from '../common/SingleBucketStore.js';
|
|
7
|
+
import { BucketDataKey, BucketStateDocumentBase } from '../models.js';
|
|
7
8
|
import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
|
|
8
|
-
import {
|
|
9
|
+
import { cacheKey } from '../OperationBatch.js';
|
|
10
|
+
import { loadBucketDataDocument, serializeBucketData } from './bucket-format.js';
|
|
11
|
+
import { chunkBucketData } from './chunking.js';
|
|
12
|
+
import { BucketDataDocumentV3, BucketStateDocumentV3 } from './models.js';
|
|
13
|
+
import { DefinitionChecksumOperations, MongoChecksumsV3 } from './MongoChecksumsV3.js';
|
|
9
14
|
import type { MongoSyncBucketStorageV3 } from './MongoSyncBucketStorageV3.js';
|
|
10
15
|
import { SingleBucketStoreV3 } from './SingleBucketStoreV3.js';
|
|
11
16
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
@@ -21,11 +26,11 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
21
26
|
if (options.minBucketChanges <= 0) {
|
|
22
27
|
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
23
28
|
}
|
|
24
|
-
|
|
29
|
+
const collection = this.db.bucketState(this.group_id) as unknown as mongo.Collection<BucketStateDocumentBase>;
|
|
25
30
|
yield* this.dirtyBucketBatchesForCollection(
|
|
26
|
-
|
|
27
|
-
{ d: new mongo.MinKey()
|
|
28
|
-
{ d: new mongo.MaxKey()
|
|
31
|
+
collection,
|
|
32
|
+
{ d: new mongo.MinKey(), b: new mongo.MinKey() } as unknown as BucketStateDocumentV3['_id'],
|
|
33
|
+
{ d: new mongo.MaxKey(), b: new mongo.MaxKey() } as unknown as BucketStateDocumentV3['_id'],
|
|
29
34
|
options,
|
|
30
35
|
(bucketState) => (bucketState as BucketStateDocumentV3)._id.d
|
|
31
36
|
);
|
|
@@ -36,27 +41,37 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
36
41
|
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
37
42
|
}
|
|
38
43
|
return this.dirtyBucketBatchForChecksumsForCollection(
|
|
39
|
-
this.db.
|
|
44
|
+
this.db.bucketState(this.group_id) as unknown as mongo.Collection<BucketStateDocumentBase>,
|
|
40
45
|
{
|
|
41
46
|
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
42
|
-
}
|
|
47
|
+
} as unknown as mongo.Filter<BucketStateDocumentBase>,
|
|
43
48
|
(bucketState) => (bucketState as BucketStateDocumentV3)._id.d
|
|
44
49
|
);
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
protected async writeBucketStateUpdates(): Promise<void> {
|
|
48
53
|
await this.db
|
|
49
|
-
.
|
|
50
|
-
.bulkWrite(this.bucketStateUpdates as mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[], {
|
|
54
|
+
.bucketState(this.group_id)
|
|
55
|
+
.bulkWrite(this.bucketStateUpdates as mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[], {
|
|
56
|
+
ordered: false
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The compactor operates on persisted definition ids only - never on parsed sources.
|
|
62
|
+
* This narrowed view makes the source-resolving checksum methods unreachable here.
|
|
63
|
+
*/
|
|
64
|
+
private get definitionChecksums(): DefinitionChecksumOperations {
|
|
65
|
+
return this.storage.checksums as MongoChecksumsV3;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
protected async computeChecksumsForBuckets(
|
|
54
69
|
buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]
|
|
55
70
|
): Promise<storage.PartialChecksumMap> {
|
|
56
|
-
return this.
|
|
71
|
+
return this.definitionChecksums.computePartialChecksumsDirectByDefinition(
|
|
57
72
|
buckets.map(({ bucket, definitionId }) => {
|
|
58
73
|
if (definitionId == null) {
|
|
59
|
-
throw new ServiceAssertionError(`Missing definitionId for
|
|
74
|
+
throw new ServiceAssertionError(`Missing definitionId for bucket checksum update on bucket ${bucket}`);
|
|
60
75
|
}
|
|
61
76
|
return {
|
|
62
77
|
bucket,
|
|
@@ -85,23 +100,625 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
85
100
|
protected async getBucketDataContext(
|
|
86
101
|
bucket: string,
|
|
87
102
|
definitionId: BucketDefinitionId | null
|
|
88
|
-
): Promise<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
): Promise<SingleBucketStoreV3 | null> {
|
|
104
|
+
let resolvedDefinitionId = definitionId;
|
|
105
|
+
|
|
106
|
+
if (resolvedDefinitionId == null) {
|
|
107
|
+
const allDefinitionIds = this.storage.storageIds.bucketDefinitionIds;
|
|
108
|
+
if (allDefinitionIds.length > 0) {
|
|
109
|
+
const potentialIds = allDefinitionIds.map((id) => ({ d: id, b: bucket }));
|
|
110
|
+
const bucketState = await this.db.bucketState(this.group_id).findOne({
|
|
111
|
+
_id: { $in: potentialIds }
|
|
112
|
+
});
|
|
113
|
+
if (bucketState != null) {
|
|
114
|
+
resolvedDefinitionId = bucketState._id.d;
|
|
115
|
+
}
|
|
94
116
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (resolvedDefinitionId == null) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return new SingleBucketStoreV3(this.db, {
|
|
124
|
+
bucket,
|
|
125
|
+
definitionId: resolvedDefinitionId,
|
|
126
|
+
replicationStreamId: this.group_id
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected override async compactSingleBucket(bucket: string, definitionId: BucketDefinitionId | null = null) {
|
|
131
|
+
const bucketContext = await this.getBucketDataContext(bucket, definitionId);
|
|
132
|
+
if (bucketContext == null) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolvedDefinitionId = bucketContext.key.definitionId;
|
|
137
|
+
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
138
|
+
const context = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
139
|
+
|
|
140
|
+
const lowerBound = bucketContext.minId;
|
|
141
|
+
let upperBound = bucketContext.maxId;
|
|
142
|
+
|
|
143
|
+
let totalChecksum = 0;
|
|
144
|
+
let totalOpCount = 0;
|
|
145
|
+
let totalOpBytes = 0;
|
|
146
|
+
|
|
147
|
+
let lastNotPut: bigint | null = null;
|
|
148
|
+
let opsSincePut = 0;
|
|
149
|
+
let clearBoundaryDocId: BucketDataKey | null = null;
|
|
150
|
+
|
|
151
|
+
const seen = new Map<string, bigint>();
|
|
152
|
+
let trackingSize = 0;
|
|
153
|
+
|
|
154
|
+
// --- Read batch from MongoDB ---
|
|
155
|
+
while (true) {
|
|
156
|
+
this.signal?.throwIfAborted();
|
|
157
|
+
|
|
158
|
+
const pipeline: mongo.Document[] = [
|
|
159
|
+
{
|
|
160
|
+
$match: {
|
|
161
|
+
'_id.b': bucket,
|
|
162
|
+
_id: {
|
|
163
|
+
$gte: lowerBound,
|
|
164
|
+
$lt: upperBound
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{ $sort: { _id: -1 } },
|
|
169
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
170
|
+
{
|
|
171
|
+
$project: {
|
|
172
|
+
_id: 1,
|
|
173
|
+
min_op: 1,
|
|
174
|
+
checksum: 1,
|
|
175
|
+
count: 1,
|
|
176
|
+
size: 1,
|
|
177
|
+
target_op: 1,
|
|
178
|
+
ops: 1,
|
|
179
|
+
bsonSize: { $bsonSize: '$$ROOT' }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const rawBatch = await collection
|
|
185
|
+
.aggregate<BucketDataDocumentV3 & { bsonSize: number | bigint }>(pipeline, {
|
|
186
|
+
batchSize: this.moveBatchQueryLimit + 1
|
|
187
|
+
})
|
|
188
|
+
.toArray();
|
|
189
|
+
|
|
190
|
+
if (rawBatch.length == 0) {
|
|
191
|
+
// No more documents in this bucket — compaction complete.
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Cut batch to byte limit ---
|
|
196
|
+
let cumulativeBytes = 0;
|
|
197
|
+
let batchCutIndex = rawBatch.length;
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < rawBatch.length; i++) {
|
|
200
|
+
cumulativeBytes += Number(rawBatch[i].bsonSize);
|
|
201
|
+
if (cumulativeBytes > this.moveBatchByteLimit && i > 0) {
|
|
202
|
+
// Byte limit exceeded; cut batch at current index. Always include
|
|
203
|
+
// at least one document (i > 0 guard) to guarantee forward progress.
|
|
204
|
+
batchCutIndex = i;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const batchDocs = rawBatch.slice(0, batchCutIndex);
|
|
210
|
+
|
|
211
|
+
// --- Decode documents into individual ops ---
|
|
212
|
+
// Processable: document has at least one op <= maxOpId.
|
|
213
|
+
// Only processable docs are deleted and recreated; the rest survive untouched.
|
|
214
|
+
const batchOps: BucketDataDoc[] = [];
|
|
215
|
+
const processableDocs: (BucketDataDocumentV3 & { bsonSize: number | bigint })[] = [];
|
|
216
|
+
|
|
217
|
+
for (const doc of batchDocs) {
|
|
218
|
+
let hasRelevantOp = false;
|
|
219
|
+
const candidateOps: BucketDataDoc[] = [];
|
|
220
|
+
for (const op of loadBucketDataDocument(context, doc as unknown as BucketDataDocumentV3)) {
|
|
221
|
+
candidateOps.push(op);
|
|
222
|
+
if (op.o <= this.maxOpId) {
|
|
223
|
+
hasRelevantOp = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (hasRelevantOp) {
|
|
227
|
+
processableDocs.push(doc);
|
|
228
|
+
batchOps.push(...candidateOps);
|
|
229
|
+
} // else: candidateOps discarded — document has no ops <= maxOpId
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (processableDocs.length == 0) {
|
|
233
|
+
// No documents with relevant ops in this batch; paginate to next batch
|
|
234
|
+
// without performing any writes. This handles batches where all documents
|
|
235
|
+
// contain only ops above maxOpId.
|
|
236
|
+
upperBound = batchDocs[batchDocs.length - 1]._id as typeof upperBound;
|
|
237
|
+
if (batchCutIndex >= rawBatch.length && rawBatch.length < this.moveBatchQueryLimit) {
|
|
238
|
+
// Entire remaining bucket is non-processable — compaction complete.
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
// Skip dedup, rechunking, and transaction for this batch.
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Scoped replace in a bounded transaction.
|
|
246
|
+
// Delete by individual _id values instead of a continuous range.
|
|
247
|
+
// A continuous range could catch non-processable documents (all ops > maxOpId)
|
|
248
|
+
// that happen to fall between processable documents in _id.o sort order.
|
|
249
|
+
const idsToDelete = processableDocs.map((d) => d._id);
|
|
250
|
+
const expectedDocCount = processableDocs.length;
|
|
251
|
+
const expectedChecksum = processableDocs.reduce((sum, doc) => sum + doc.checksum, 0n);
|
|
252
|
+
const expectedOpCount = processableDocs.reduce((sum, doc) => sum + doc.count, 0);
|
|
253
|
+
|
|
254
|
+
// Sort ops by o descending for newest-first dedup
|
|
255
|
+
batchOps.sort((a, b) => (b.o > a.o ? 1 : b.o < a.o ? -1 : 0));
|
|
256
|
+
|
|
257
|
+
// --- Dedup: newest-first, superseded → MOVE ---
|
|
258
|
+
const surviving: BucketDataDoc[] = [];
|
|
259
|
+
|
|
260
|
+
for (const op of batchOps) {
|
|
261
|
+
if (op.op == 'PUT' || op.op == 'REMOVE') {
|
|
262
|
+
if (op.o > this.maxOpId) {
|
|
263
|
+
surviving.push(op);
|
|
264
|
+
continue; // Do not dedup ops above compaction horizon
|
|
265
|
+
}
|
|
266
|
+
const key = `${op.table}/${op.row_id}/${cacheKey(op.source_table!, op.source_key!)}`;
|
|
267
|
+
const targetOp = seen.get(key);
|
|
268
|
+
if (targetOp != null) {
|
|
269
|
+
surviving.push({
|
|
270
|
+
...op,
|
|
271
|
+
op: 'MOVE',
|
|
272
|
+
target_op: targetOp,
|
|
273
|
+
table: undefined,
|
|
274
|
+
row_id: undefined,
|
|
275
|
+
source_table: undefined,
|
|
276
|
+
source_key: undefined,
|
|
277
|
+
data: null
|
|
278
|
+
});
|
|
279
|
+
if (lastNotPut == null) {
|
|
280
|
+
lastNotPut = op.o;
|
|
281
|
+
}
|
|
282
|
+
opsSincePut += 1;
|
|
283
|
+
} else {
|
|
284
|
+
if (trackingSize < this.idLimitBytes) {
|
|
285
|
+
seen.set(utils.flatstr(key), op.o);
|
|
286
|
+
trackingSize += key.length + 140;
|
|
287
|
+
}
|
|
288
|
+
surviving.push(op);
|
|
289
|
+
if (op.op == 'PUT') {
|
|
290
|
+
lastNotPut = null;
|
|
291
|
+
opsSincePut = 0;
|
|
292
|
+
} else {
|
|
293
|
+
if (lastNotPut == null) {
|
|
294
|
+
lastNotPut = op.o;
|
|
295
|
+
}
|
|
296
|
+
opsSincePut += 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
surviving.push(op);
|
|
301
|
+
if (op.op != 'CLEAR') {
|
|
302
|
+
if (lastNotPut == null) {
|
|
303
|
+
lastNotPut = op.o;
|
|
304
|
+
}
|
|
305
|
+
opsSincePut += 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Reverse back to ascending order for rechunking
|
|
311
|
+
surviving.reverse();
|
|
312
|
+
|
|
313
|
+
// --- Rechunk survivors into new V3 documents ---
|
|
314
|
+
const chunks = chunkBucketData(surviving);
|
|
315
|
+
const newDocs = chunks.map((chunk) => serializeBucketData(bucket, chunk));
|
|
316
|
+
|
|
317
|
+
if (lastNotPut == null) {
|
|
318
|
+
clearBoundaryDocId = null;
|
|
319
|
+
} else {
|
|
320
|
+
const boundaryOp = lastNotPut;
|
|
321
|
+
const boundaryDoc = newDocs.find((doc) => doc.min_op <= boundaryOp && doc._id.o >= boundaryOp);
|
|
322
|
+
if (boundaryDoc != null) {
|
|
323
|
+
clearBoundaryDocId = boundaryDoc._id;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Commit: scoped delete + insert in transaction ---
|
|
328
|
+
const session = this.db.client.startSession();
|
|
329
|
+
try {
|
|
330
|
+
await session.withTransaction(
|
|
331
|
+
async () => {
|
|
332
|
+
// Verify documents haven't been modified since we read them.
|
|
333
|
+
// This aggregate anchors the transaction snapshot and catches
|
|
334
|
+
// concurrent compaction jobs that modified the same documents.
|
|
335
|
+
const verification = await bucketContext.collection
|
|
336
|
+
.aggregate<{ docCount: number; checksumSum: bigint | null; opCountSum: number | null }>(
|
|
337
|
+
[
|
|
338
|
+
{ $match: { _id: { $in: idsToDelete } } },
|
|
339
|
+
{
|
|
340
|
+
$group: {
|
|
341
|
+
_id: null,
|
|
342
|
+
docCount: { $sum: 1 },
|
|
343
|
+
checksumSum: { $sum: '$checksum' },
|
|
344
|
+
opCountSum: { $sum: '$count' }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
{ session }
|
|
349
|
+
)
|
|
350
|
+
.next();
|
|
351
|
+
|
|
352
|
+
if (
|
|
353
|
+
verification == null || // all docs deleted
|
|
354
|
+
verification.docCount !== expectedDocCount || // some docs deleted
|
|
355
|
+
verification.checksumSum !== expectedChecksum || // docs modified in-place
|
|
356
|
+
verification.opCountSum !== expectedOpCount // ops added/removed within docs
|
|
357
|
+
) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Concurrent modification detected in bucket ${bucket}. Aborting compaction for this batch.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await bucketContext.collection.deleteMany(
|
|
364
|
+
{
|
|
365
|
+
_id: { $in: idsToDelete }
|
|
366
|
+
} as any,
|
|
367
|
+
{ session }
|
|
368
|
+
);
|
|
369
|
+
if (newDocs.length > 0) {
|
|
370
|
+
await bucketContext.collection.insertMany(newDocs as unknown as BucketDataDocumentGeneric[], { session });
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
writeConcern: { w: 'majority' },
|
|
375
|
+
readConcern: { level: 'snapshot' }
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
} finally {
|
|
379
|
+
await session.endSession();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --- Accumulate bucket state ---
|
|
383
|
+
for (const chunk of chunks) {
|
|
384
|
+
for (const op of chunk) {
|
|
385
|
+
if (op.o <= this.maxOpId) {
|
|
386
|
+
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
387
|
+
totalOpBytes += op.data?.length ?? 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
101
390
|
}
|
|
102
|
-
|
|
391
|
+
totalOpCount += surviving.filter((op) => op.o <= this.maxOpId).length;
|
|
392
|
+
|
|
393
|
+
// --- Advance to next batch ---
|
|
394
|
+
upperBound = (newDocs.length > 0 ? newDocs[0]._id : rawBatch[batchCutIndex - 1]._id) as typeof upperBound;
|
|
395
|
+
|
|
396
|
+
if (batchCutIndex < rawBatch.length) {
|
|
397
|
+
// We cut the batch short due to byte limit — don't advance past cut point
|
|
398
|
+
// The upperBound is already set to the last doc we processed
|
|
399
|
+
} else {
|
|
400
|
+
// Processed all docs in the raw batch. If we got fewer than the query
|
|
401
|
+
// limit, there are no more documents in this bucket — compaction complete.
|
|
402
|
+
if (rawBatch.length < this.moveBatchQueryLimit) {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.logger.info(`Compacted batch of ${batchDocs.length} documents for bucket ${bucket}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// --- Clear: collapse leading MOVE/REMOVE/CLEAR sequence ---
|
|
411
|
+
if (lastNotPut != null && opsSincePut >= 2) {
|
|
412
|
+
if (clearBoundaryDocId == null) {
|
|
413
|
+
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
totalOpCount += await this.clearBucketLeading(lastNotPut, clearBoundaryDocId, bucketContext, collection, context);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// --- Finalize: update bucket checksums and state ---
|
|
420
|
+
this.updateBucketChecksums({
|
|
421
|
+
bucket,
|
|
422
|
+
definitionId: resolvedDefinitionId,
|
|
423
|
+
seen: new Map(),
|
|
424
|
+
trackingSize: 0,
|
|
425
|
+
lastNotPut: lastNotPut,
|
|
426
|
+
opsSincePut: opsSincePut,
|
|
427
|
+
checksum: totalChecksum,
|
|
428
|
+
opCount: totalOpCount,
|
|
429
|
+
opBytes: totalOpBytes
|
|
430
|
+
});
|
|
431
|
+
if (this.bucketStateUpdates.length > 0) {
|
|
432
|
+
await this.writeBucketStateUpdates();
|
|
433
|
+
this.bucketStateUpdates = [];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
logger.info(`Compacted bucket ${bucket}: ${totalOpCount} surviving ops`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Collapse the leading sequence of MOVE/REMOVE/CLEAR ops at the start
|
|
441
|
+
* of the bucket into a single CLEAR op. Reads whole clearable documents
|
|
442
|
+
* before the known boundary document, then splits that boundary document
|
|
443
|
+
* if it contains ops on both sides of lastNotPut.
|
|
444
|
+
*
|
|
445
|
+
* Returns the op count diff after replacing cleared ops with CLEAR ops.
|
|
446
|
+
*/
|
|
447
|
+
private async clearBucketLeading(
|
|
448
|
+
lastNotPut: bigint,
|
|
449
|
+
boundaryDocId: BucketDataKey,
|
|
450
|
+
bucketContext: SingleBucketStoreV3,
|
|
451
|
+
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
452
|
+
context: { replicationStreamId: number; definitionId: string }
|
|
453
|
+
): Promise<number> {
|
|
454
|
+
let opCountDiff = 0;
|
|
455
|
+
const session = this.db.client.startSession();
|
|
456
|
+
try {
|
|
457
|
+
let done = false;
|
|
458
|
+
// First step is to clear full chunks that contain only CLEAR/MOVE/REMOVE operations.
|
|
459
|
+
// There can be many of them, so we do one batch at a time.
|
|
460
|
+
while (!done) {
|
|
461
|
+
const batch = await this.clearLeadingFullDocuments(
|
|
462
|
+
session,
|
|
463
|
+
lastNotPut,
|
|
464
|
+
boundaryDocId,
|
|
465
|
+
bucketContext,
|
|
466
|
+
collection,
|
|
467
|
+
context
|
|
468
|
+
);
|
|
469
|
+
done = batch.done;
|
|
470
|
+
opCountDiff += batch.opCountDiff;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
474
|
+
// potentially followed by PUT operations. This is only a single document, so no need for batching.
|
|
475
|
+
opCountDiff += await this.clearBoundaryDocument(
|
|
476
|
+
session,
|
|
477
|
+
lastNotPut,
|
|
478
|
+
boundaryDocId,
|
|
479
|
+
bucketContext,
|
|
480
|
+
collection,
|
|
481
|
+
context
|
|
482
|
+
);
|
|
483
|
+
} finally {
|
|
484
|
+
await session.endSession();
|
|
103
485
|
}
|
|
104
486
|
|
|
105
|
-
return
|
|
487
|
+
return opCountDiff;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private async clearLeadingFullDocuments(
|
|
491
|
+
session: mongo.ClientSession,
|
|
492
|
+
lastNotPut: bigint,
|
|
493
|
+
boundaryDocId: BucketDataKey,
|
|
494
|
+
bucketContext: SingleBucketStoreV3,
|
|
495
|
+
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
496
|
+
context: { replicationStreamId: number; definitionId: string }
|
|
497
|
+
): Promise<{ done: boolean; opCountDiff: number }> {
|
|
498
|
+
const bucket = bucketContext.key.bucket;
|
|
499
|
+
let done = false;
|
|
500
|
+
let opCountDiff = 0;
|
|
501
|
+
|
|
502
|
+
this.signal?.throwIfAborted();
|
|
503
|
+
await session.withTransaction(
|
|
504
|
+
async () => {
|
|
505
|
+
const query = collection.find(
|
|
506
|
+
{
|
|
507
|
+
_id: {
|
|
508
|
+
$gte: bucketContext.minId,
|
|
509
|
+
$lt: boundaryDocId
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
session,
|
|
514
|
+
sort: { _id: 1 },
|
|
515
|
+
projection: {
|
|
516
|
+
_id: 1,
|
|
517
|
+
min_op: 1,
|
|
518
|
+
checksum: 1,
|
|
519
|
+
count: 1,
|
|
520
|
+
target_op: 1,
|
|
521
|
+
ops: 1
|
|
522
|
+
},
|
|
523
|
+
limit: this.clearBatchLimit
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
let combinedChecksum = 0;
|
|
528
|
+
let clearedOpCount = 0;
|
|
529
|
+
let maxTargetOp: bigint | null = null;
|
|
530
|
+
let lastDocId: BucketDataKey | null = null;
|
|
531
|
+
let clearOpCount = 0;
|
|
532
|
+
let gotNonClearOp = false;
|
|
533
|
+
|
|
534
|
+
for await (const doc of query.stream()) {
|
|
535
|
+
if (doc.min_op > lastNotPut) {
|
|
536
|
+
throw new ReplicationAssertionError(
|
|
537
|
+
`Unexpected document before CLEAR boundary with min_op ${doc.min_op} > ${lastNotPut} in bucket ${bucket}`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
lastDocId = doc._id;
|
|
542
|
+
for (const op of loadBucketDataDocument(context, doc)) {
|
|
543
|
+
if (op.o > lastNotPut) {
|
|
544
|
+
throw new ReplicationAssertionError(
|
|
545
|
+
`Unexpected op ${op.o} after CLEAR boundary ${lastNotPut} in bucket ${bucket}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (op.op == 'PUT') {
|
|
549
|
+
throw new ReplicationAssertionError(`Unexpected PUT at op ${op.o} in CLEAR region for bucket ${bucket}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (op.op == 'CLEAR') {
|
|
553
|
+
clearOpCount++;
|
|
554
|
+
if (clearOpCount > 1) {
|
|
555
|
+
throw new ReplicationAssertionError(`Unexpected multiple CLEAR operations in bucket ${bucket}`);
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
gotNonClearOp = true;
|
|
559
|
+
}
|
|
560
|
+
combinedChecksum = addChecksums(combinedChecksum, Number(op.checksum));
|
|
561
|
+
clearedOpCount++;
|
|
562
|
+
if (op.target_op != null && (maxTargetOp == null || op.target_op > maxTargetOp)) {
|
|
563
|
+
maxTargetOp = op.target_op;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!gotNonClearOp) {
|
|
569
|
+
done = true;
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
574
|
+
await collection.deleteMany(
|
|
575
|
+
{
|
|
576
|
+
_id: {
|
|
577
|
+
$gte: bucketContext.minId,
|
|
578
|
+
$lte: lastDocId!
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
{ session }
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
const clearOp = {
|
|
585
|
+
bucketKey: { ...context, bucket },
|
|
586
|
+
o: lastDocId!.o,
|
|
587
|
+
op: 'CLEAR' as const,
|
|
588
|
+
checksum: BigInt(combinedChecksum),
|
|
589
|
+
data: null,
|
|
590
|
+
target_op: maxTargetOp
|
|
591
|
+
} satisfies BucketDataDoc;
|
|
592
|
+
await collection.insertOne(serializeBucketData(bucket, [clearOp]), { session });
|
|
593
|
+
|
|
594
|
+
opCountDiff = -clearedOpCount + 1;
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
writeConcern: { w: 'majority' },
|
|
598
|
+
readConcern: { level: 'snapshot' }
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
return { done, opCountDiff };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private async clearBoundaryDocument(
|
|
606
|
+
session: mongo.ClientSession,
|
|
607
|
+
lastNotPut: bigint,
|
|
608
|
+
boundaryDocId: BucketDataKey,
|
|
609
|
+
bucketContext: SingleBucketStoreV3,
|
|
610
|
+
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
611
|
+
context: { replicationStreamId: number; definitionId: string }
|
|
612
|
+
): Promise<number> {
|
|
613
|
+
const bucket = bucketContext.key.bucket;
|
|
614
|
+
let opCountDiff = 0;
|
|
615
|
+
|
|
616
|
+
await session.withTransaction(
|
|
617
|
+
async () => {
|
|
618
|
+
const query = collection.find(
|
|
619
|
+
{
|
|
620
|
+
// This is a range query, but should only ever return two documents:
|
|
621
|
+
// 1. The CLEAR op from the previous clearLeadingFullDocuments.
|
|
622
|
+
// 2. The boundary document.
|
|
623
|
+
_id: {
|
|
624
|
+
$gte: bucketContext.minId,
|
|
625
|
+
$lte: boundaryDocId
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
session,
|
|
630
|
+
sort: { _id: 1 },
|
|
631
|
+
projection: {
|
|
632
|
+
_id: 1,
|
|
633
|
+
min_op: 1,
|
|
634
|
+
checksum: 1,
|
|
635
|
+
count: 1,
|
|
636
|
+
target_op: 1,
|
|
637
|
+
ops: 1
|
|
638
|
+
},
|
|
639
|
+
limit: 3
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
let docsRead = 0;
|
|
644
|
+
let combinedChecksum = 0;
|
|
645
|
+
let clearedOpCount = 0;
|
|
646
|
+
let maxTargetOp: bigint | null = null;
|
|
647
|
+
const boundarySurvivors: BucketDataDoc[] = [];
|
|
648
|
+
|
|
649
|
+
for await (const doc of query.stream()) {
|
|
650
|
+
docsRead++;
|
|
651
|
+
if (docsRead > 2) {
|
|
652
|
+
throw new ReplicationAssertionError(`Unexpected extra document before CLEAR boundary in bucket ${bucket}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const isBoundaryDoc = doc._id.o == boundaryDocId.o;
|
|
656
|
+
for (const op of loadBucketDataDocument(context, doc)) {
|
|
657
|
+
if (!isBoundaryDoc && op.op != 'CLEAR') {
|
|
658
|
+
throw new ReplicationAssertionError(
|
|
659
|
+
`Unexpected ${op.op} operation before CLEAR boundary in bucket ${bucket}`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (op.o <= lastNotPut) {
|
|
664
|
+
if (op.op == 'PUT') {
|
|
665
|
+
throw new ReplicationAssertionError(
|
|
666
|
+
`Unexpected PUT at op ${op.o} in CLEAR region for bucket ${bucket}`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
combinedChecksum = addChecksums(combinedChecksum, Number(op.checksum));
|
|
670
|
+
clearedOpCount++;
|
|
671
|
+
if (op.target_op != null && (maxTargetOp == null || op.target_op > maxTargetOp)) {
|
|
672
|
+
maxTargetOp = op.target_op;
|
|
673
|
+
}
|
|
674
|
+
} else if (isBoundaryDoc) {
|
|
675
|
+
boundarySurvivors.push(op);
|
|
676
|
+
} else {
|
|
677
|
+
throw new ReplicationAssertionError(
|
|
678
|
+
`Unexpected op ${op.o} after CLEAR boundary ${lastNotPut} in bucket ${bucket}`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (clearedOpCount == 0) {
|
|
685
|
+
throw new Error(`CLEAR boundary document not found for bucket ${bucket}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastNotPut}`);
|
|
689
|
+
await collection.deleteMany(
|
|
690
|
+
{
|
|
691
|
+
_id: {
|
|
692
|
+
$gte: bucketContext.minId,
|
|
693
|
+
$lte: boundaryDocId
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
{ session }
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const clearOp = {
|
|
700
|
+
bucketKey: { ...context, bucket },
|
|
701
|
+
o: lastNotPut,
|
|
702
|
+
op: 'CLEAR' as const,
|
|
703
|
+
checksum: BigInt(combinedChecksum),
|
|
704
|
+
data: null,
|
|
705
|
+
target_op: maxTargetOp
|
|
706
|
+
} satisfies BucketDataDoc;
|
|
707
|
+
await collection.insertOne(serializeBucketData(bucket, [clearOp]), { session });
|
|
708
|
+
|
|
709
|
+
if (boundarySurvivors.length > 0) {
|
|
710
|
+
const survivingDocs = chunkBucketData(boundarySurvivors).map((chunk) => serializeBucketData(bucket, chunk));
|
|
711
|
+
await collection.insertMany(survivingDocs, { session });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
opCountDiff = -clearedOpCount + 1;
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
writeConcern: { w: 'majority' },
|
|
718
|
+
readConcern: { level: 'snapshot' }
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
return opCountDiff;
|
|
106
723
|
}
|
|
107
724
|
}
|