@powersync/service-module-mongodb-storage 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634
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 +119 -6
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +3 -3
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +5 -3
- package/dist/storage/MongoBucketStorage.js +53 -26
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/MongoReportStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +25 -21
- package/dist/storage/implementation/MongoBucketBatch.js +256 -210
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
- package/dist/storage/implementation/MongoChecksums.d.ts +30 -19
- package/dist/storage/implementation/MongoChecksums.js +13 -71
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +99 -53
- package/dist/storage/implementation/MongoCompactor.js +265 -303
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +12 -7
- package/dist/storage/implementation/MongoParameterCompactor.js +24 -9
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +11 -7
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +54 -30
- package/dist/storage/implementation/MongoSyncBucketStorage.js +118 -472
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +7 -5
- package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +3 -2
- package/dist/storage/implementation/OperationBatch.js.map +1 -1
- package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
- package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
- package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
- package/dist/storage/implementation/common/PersistedBatch.js +237 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
- package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
- package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
- package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
- package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
- package/dist/storage/implementation/db.d.ts +38 -4
- package/dist/storage/implementation/db.js +80 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +66 -25
- package/dist/storage/implementation/models.js +22 -2
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +269 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
- package/dist/storage/implementation/v1/models.d.ts +34 -0
- package/dist/storage/implementation/v1/models.js +37 -0
- package/dist/storage/implementation/v1/models.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +404 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +43 -0
- package/dist/storage/implementation/v3/models.js +34 -0
- package/dist/storage/implementation/v3/models.js.map +1 -0
- package/dist/storage/storage-index.d.ts +8 -5
- package/dist/storage/storage-index.js +8 -5
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -1
- package/dist/utils/test-utils.js +15 -12
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +13 -5
- package/dist/utils/util.js +40 -5
- package/dist/utils/util.js.map +1 -1
- package/package.json +9 -9
- package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
- package/src/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
- package/src/storage/MongoBucketStorage.ts +111 -55
- package/src/storage/MongoReportStorage.ts +2 -2
- package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
- package/src/storage/implementation/MongoBucketBatch.ts +322 -270
- package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
- package/src/storage/implementation/MongoChecksums.ts +54 -75
- package/src/storage/implementation/MongoCompactor.ts +411 -412
- package/src/storage/implementation/MongoParameterCompactor.ts +52 -26
- package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +19 -10
- package/src/storage/implementation/MongoStorageProvider.ts +1 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +206 -466
- package/src/storage/implementation/MongoSyncRulesLock.ts +14 -16
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +7 -5
- package/src/storage/implementation/OperationBatch.ts +3 -2
- package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
- package/src/storage/implementation/common/PersistedBatch.ts +364 -0
- package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
- package/src/storage/implementation/db.ts +115 -8
- package/src/storage/implementation/models.ts +88 -28
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +432 -0
- package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
- package/src/storage/implementation/v1/models.ts +84 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
- package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +545 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
- package/src/storage/implementation/v3/models.ts +96 -0
- package/src/storage/storage-index.ts +8 -5
- package/src/utils/test-utils.ts +15 -12
- package/src/utils/util.ts +53 -9
- package/test/src/__snapshots__/storage.test.ts.snap +201 -0
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1393 -16
- package/test/src/connection-report-storage.test.ts +3 -3
- package/test/src/setup.ts +1 -1
- package/test/src/storage.test.ts +10 -8
- package/test/src/storage_compacting.test.ts +146 -46
- package/test/src/storage_sync.test.ts +404 -56
- package/test/src/util.ts +3 -3
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +0 -64
- package/dist/storage/implementation/PersistedBatch.js +0 -323
- package/dist/storage/implementation/PersistedBatch.js.map +0 -1
- package/src/storage/implementation/PersistedBatch.ts +0 -401
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
1
|
+
import { isMongoServerError, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { logger as defaultLogger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { addChecksums, isPartialChecksum, utils } from '@powersync/service-core';
|
|
4
4
|
import { cacheKey } from './OperationBatch.js';
|
|
5
5
|
const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
|
|
@@ -25,19 +25,21 @@ export class MongoCompactor {
|
|
|
25
25
|
buckets;
|
|
26
26
|
signal;
|
|
27
27
|
group_id;
|
|
28
|
+
logger;
|
|
28
29
|
constructor(storage, db, options) {
|
|
29
30
|
this.storage = storage;
|
|
30
31
|
this.db = db;
|
|
31
32
|
this.group_id = storage.group_id;
|
|
32
|
-
this.idLimitBytes = (options
|
|
33
|
-
this.moveBatchLimit = options
|
|
34
|
-
this.moveBatchQueryLimit = options
|
|
35
|
-
this.clearBatchLimit = options
|
|
36
|
-
this.minBucketChanges = options
|
|
37
|
-
this.minChangeRatio = options
|
|
38
|
-
this.maxOpId = options
|
|
39
|
-
this.buckets = options
|
|
40
|
-
this.signal = options
|
|
33
|
+
this.idLimitBytes = (options.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
|
|
34
|
+
this.moveBatchLimit = options.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
|
|
35
|
+
this.moveBatchQueryLimit = options.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
|
|
36
|
+
this.clearBatchLimit = options.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
|
|
37
|
+
this.minBucketChanges = options.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
|
|
38
|
+
this.minChangeRatio = options.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
|
|
39
|
+
this.maxOpId = options.maxOpId ?? 0n;
|
|
40
|
+
this.buckets = options.compactBuckets;
|
|
41
|
+
this.signal = options.signal;
|
|
42
|
+
this.logger = options.logger ?? defaultLogger;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Compact buckets by converting operations into MOVE and/or CLEAR operations.
|
|
@@ -46,37 +48,178 @@ export class MongoCompactor {
|
|
|
46
48
|
*/
|
|
47
49
|
async compact() {
|
|
48
50
|
if (this.buckets) {
|
|
49
|
-
for (
|
|
50
|
-
// We can make this more efficient later on by iterating
|
|
51
|
-
// through the buckets in a single query.
|
|
51
|
+
for (const bucket of this.buckets) {
|
|
52
|
+
// We can make this more efficient later on by iterating through the buckets in a single query.
|
|
52
53
|
// That makes batching more tricky, so we leave for later.
|
|
53
|
-
await this.
|
|
54
|
+
await this.compactSingleBucketRetried(bucket);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
else {
|
|
57
58
|
await this.compactDirtyBuckets();
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Subset of compact, only populating checksums where relevant.
|
|
63
|
+
*/
|
|
64
|
+
async populateChecksums(options) {
|
|
65
|
+
let count = 0;
|
|
66
|
+
while (true) {
|
|
67
|
+
this.signal?.throwIfAborted();
|
|
68
|
+
const buckets = await this.dirtyBucketBatchForChecksums(options);
|
|
69
|
+
if (buckets.length == 0) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
this.signal?.throwIfAborted();
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
// Filter batch by estimated bucket size, to reduce possibility of timeouts.
|
|
75
|
+
const checkBuckets = [];
|
|
76
|
+
let totalCountEstimate = 0;
|
|
77
|
+
for (const bucket of buckets) {
|
|
78
|
+
checkBuckets.push(bucket);
|
|
79
|
+
totalCountEstimate += bucket.estimatedCount;
|
|
80
|
+
if (totalCountEstimate > 50_000) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.logger.info(`Calculating checksums for batch of ${buckets.length} buckets, estimated count of ${totalCountEstimate}`);
|
|
85
|
+
await this.updateChecksumsBatch(checkBuckets);
|
|
86
|
+
this.logger.info(`Updated checksums for batch of ${checkBuckets.length} buckets in ${Date.now() - start}ms`);
|
|
87
|
+
count += checkBuckets.length;
|
|
88
|
+
}
|
|
89
|
+
return { buckets: count };
|
|
90
|
+
}
|
|
91
|
+
async *dirtyBucketBatchesForCollection(collection, lastId, maxId, options, getDefinitionId) {
|
|
92
|
+
while (true) {
|
|
93
|
+
// To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, use an aggregation pipeline
|
|
94
|
+
// to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria.
|
|
95
|
+
const [result] = await collection
|
|
96
|
+
.aggregate([
|
|
97
|
+
{
|
|
98
|
+
$match: {
|
|
99
|
+
_id: { $gt: lastId, $lt: maxId }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
$sort: { _id: 1 }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// Scan a fixed number of docs each query so sparse matches don't block progress.
|
|
107
|
+
$limit: DIRTY_BUCKET_SCAN_BATCH_SIZE
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
$facet: {
|
|
111
|
+
buckets: [
|
|
112
|
+
{
|
|
113
|
+
$match: {
|
|
114
|
+
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
$project: {
|
|
119
|
+
_id: 1,
|
|
120
|
+
estimate_since_compact: 1,
|
|
121
|
+
compacted_state: 1
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
// This is used for the next query.
|
|
126
|
+
cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
], { maxTimeMS: MONGO_OPERATION_TIMEOUT_MS })
|
|
130
|
+
.toArray();
|
|
131
|
+
const cursor = result?.cursor?.[0];
|
|
132
|
+
if (cursor == null) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
lastId = cursor._id;
|
|
136
|
+
const mapped = (result?.buckets ?? []).map((bucketState) => {
|
|
137
|
+
// The numbers, specifically the bytes, could be a bigint. Convert to Number to allow calculating ratios.
|
|
138
|
+
// BigInt precision is not needed here since this is only an estimate.
|
|
139
|
+
const updatedCount = bucketState.estimate_since_compact?.count ?? 0;
|
|
140
|
+
const totalCount = (bucketState.compacted_state?.count ?? 0) + updatedCount;
|
|
141
|
+
const updatedBytes = Number(bucketState.estimate_since_compact?.bytes ?? 0);
|
|
142
|
+
const totalBytes = Number(bucketState.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
143
|
+
const dirtyChangeNumber = totalCount > 0 ? updatedCount / totalCount : 0;
|
|
144
|
+
const dirtyChangeBytes = totalBytes > 0 ? updatedBytes / totalBytes : 0;
|
|
145
|
+
return {
|
|
146
|
+
bucket: bucketState._id.b,
|
|
147
|
+
definitionId: getDefinitionId(bucketState),
|
|
148
|
+
estimatedCount: totalCount,
|
|
149
|
+
dirtyRatio: Math.max(dirtyChangeNumber, dirtyChangeBytes)
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
yield mapped.filter((bucket) => bucket.estimatedCount >= options.minBucketChanges && bucket.dirtyRatio >= options.minChangeRatio);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async dirtyBucketBatchForChecksumsForCollection(collection, filter, getDefinitionId) {
|
|
156
|
+
const dirtyBuckets = await collection
|
|
157
|
+
.find(filter, {
|
|
158
|
+
projection: {
|
|
159
|
+
_id: 1,
|
|
160
|
+
estimate_since_compact: 1,
|
|
161
|
+
compacted_state: 1
|
|
162
|
+
},
|
|
163
|
+
sort: {
|
|
164
|
+
'estimate_since_compact.count': -1
|
|
165
|
+
},
|
|
166
|
+
limit: 200,
|
|
167
|
+
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
168
|
+
})
|
|
169
|
+
.toArray();
|
|
170
|
+
return dirtyBuckets.map((bucket) => ({
|
|
171
|
+
bucket: bucket._id.b,
|
|
172
|
+
definitionId: getDefinitionId(bucket),
|
|
173
|
+
estimatedCount: Number(bucket.estimate_since_compact.count) + Number(bucket.compacted_state?.count ?? 0)
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
60
176
|
async compactDirtyBuckets() {
|
|
61
|
-
for await (
|
|
177
|
+
for await (const buckets of this.dirtyBucketBatches({
|
|
62
178
|
minBucketChanges: this.minBucketChanges,
|
|
63
179
|
minChangeRatio: this.minChangeRatio
|
|
64
180
|
})) {
|
|
65
|
-
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
181
|
+
this.signal?.throwIfAborted();
|
|
68
182
|
if (buckets.length == 0) {
|
|
69
183
|
continue;
|
|
70
184
|
}
|
|
71
|
-
for (
|
|
72
|
-
await this.
|
|
185
|
+
for (const { bucket, definitionId } of buckets) {
|
|
186
|
+
await this.compactSingleBucketRetried(bucket, definitionId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Compaction for a single bucket, with retries on failure.
|
|
192
|
+
*
|
|
193
|
+
* This covers against occasional network or other database errors during a long compact job.
|
|
194
|
+
*/
|
|
195
|
+
async compactSingleBucketRetried(bucket, definitionId = null) {
|
|
196
|
+
let retryCount = 0;
|
|
197
|
+
while (true) {
|
|
198
|
+
try {
|
|
199
|
+
await this.compactSingleBucket(bucket, definitionId);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
if (retryCount < 3 && isMongoServerError(e)) {
|
|
204
|
+
this.logger.warn(`Error compacting bucket ${bucket}, retrying...`, e);
|
|
205
|
+
retryCount++;
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
73
211
|
}
|
|
74
212
|
}
|
|
75
213
|
}
|
|
76
|
-
async compactSingleBucket(bucket) {
|
|
214
|
+
async compactSingleBucket(bucket, definitionId = null) {
|
|
77
215
|
const idLimitBytes = this.idLimitBytes;
|
|
78
|
-
|
|
216
|
+
const bucketContext = await this.getBucketDataContext(bucket, definitionId);
|
|
217
|
+
if (bucketContext == null) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const currentState = {
|
|
79
221
|
bucket,
|
|
222
|
+
definitionId: bucketContext.key.definitionId,
|
|
80
223
|
seen: new Map(),
|
|
81
224
|
trackingSize: 0,
|
|
82
225
|
lastNotPut: null,
|
|
@@ -85,27 +228,24 @@ export class MongoCompactor {
|
|
|
85
228
|
opCount: 0,
|
|
86
229
|
opBytes: 0
|
|
87
230
|
};
|
|
88
|
-
// Constant lower bound
|
|
89
|
-
const lowerBound =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
g: this.group_id,
|
|
97
|
-
b: bucket,
|
|
98
|
-
o: new mongo.MaxKey()
|
|
99
|
-
};
|
|
100
|
-
while (!this.signal?.aborted) {
|
|
101
|
-
// Query one batch at a time, to avoid cursor timeouts
|
|
102
|
-
const cursor = this.db.bucket_data.aggregate([
|
|
231
|
+
// Constant lower bound.
|
|
232
|
+
const lowerBound = bucketContext.minId;
|
|
233
|
+
// Upper bound is adjusted for each batch.
|
|
234
|
+
let upperBound = bucketContext.maxId;
|
|
235
|
+
while (true) {
|
|
236
|
+
this.signal?.throwIfAborted();
|
|
237
|
+
// Query one batch at a time, to avoid cursor timeouts.
|
|
238
|
+
const pipeline = [
|
|
103
239
|
{
|
|
104
240
|
$match: {
|
|
105
241
|
_id: {
|
|
106
242
|
$gte: lowerBound,
|
|
107
243
|
$lt: upperBound
|
|
108
|
-
}
|
|
244
|
+
},
|
|
245
|
+
// Workaround for a clustered collection bug where the $lt operator may include upperBound.
|
|
246
|
+
// Technically only needed for storage V3.
|
|
247
|
+
// https://jira.mongodb.org/browse/SERVER-121822
|
|
248
|
+
'_id.o': { $lt: upperBound.o }
|
|
109
249
|
}
|
|
110
250
|
},
|
|
111
251
|
{ $sort: { _id: -1 } },
|
|
@@ -122,39 +262,45 @@ export class MongoCompactor {
|
|
|
122
262
|
size: { $bsonSize: '$$ROOT' }
|
|
123
263
|
}
|
|
124
264
|
}
|
|
125
|
-
]
|
|
265
|
+
];
|
|
266
|
+
const cursor = bucketContext.collection.aggregate(pipeline, {
|
|
126
267
|
// batchSize is 1 more than limit to auto-close the cursor.
|
|
127
268
|
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
128
269
|
batchSize: this.moveBatchQueryLimit + 1
|
|
129
270
|
});
|
|
130
271
|
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
131
272
|
// Instead, we load up to the limit.
|
|
132
|
-
const
|
|
273
|
+
const rawBatch = await cursor.toArray();
|
|
274
|
+
const batch = rawBatch.map((document) => {
|
|
275
|
+
const { size, ...rest } = document;
|
|
276
|
+
return {
|
|
277
|
+
doc: bucketContext.fromPersistedDocument(rest),
|
|
278
|
+
size
|
|
279
|
+
};
|
|
280
|
+
});
|
|
133
281
|
if (batch.length == 0) {
|
|
134
|
-
// We've reached the end
|
|
282
|
+
// We've reached the end.
|
|
135
283
|
break;
|
|
136
284
|
}
|
|
137
|
-
//
|
|
138
|
-
upperBound =
|
|
139
|
-
for (
|
|
140
|
-
if (doc.
|
|
285
|
+
// Reuse the exact collection _id value from Mongo for the next bound.
|
|
286
|
+
upperBound = rawBatch[rawBatch.length - 1]._id;
|
|
287
|
+
for (const { doc, size } of batch) {
|
|
288
|
+
if (doc.o > this.maxOpId) {
|
|
141
289
|
continue;
|
|
142
290
|
}
|
|
143
291
|
currentState.checksum = addChecksums(currentState.checksum, Number(doc.checksum));
|
|
144
292
|
currentState.opCount += 1;
|
|
145
293
|
let isPersistentPut = doc.op == 'PUT';
|
|
146
|
-
currentState.opBytes += Number(
|
|
294
|
+
currentState.opBytes += Number(size);
|
|
147
295
|
if (doc.op == 'REMOVE' || doc.op == 'PUT') {
|
|
148
296
|
const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table, doc.source_key)}`;
|
|
149
297
|
const targetOp = currentState.seen.get(key);
|
|
150
298
|
if (targetOp) {
|
|
151
|
-
// Will convert to MOVE, so don't count as PUT
|
|
299
|
+
// Will convert to MOVE, so don't count as PUT.
|
|
152
300
|
isPersistentPut = false;
|
|
153
301
|
this.updates.push({
|
|
154
302
|
updateOne: {
|
|
155
|
-
filter: {
|
|
156
|
-
_id: doc._id
|
|
157
|
-
},
|
|
303
|
+
filter: { _id: bucketContext.docId(doc.o) },
|
|
158
304
|
update: {
|
|
159
305
|
$set: {
|
|
160
306
|
op: 'MOVE',
|
|
@@ -170,22 +316,17 @@ export class MongoCompactor {
|
|
|
170
316
|
}
|
|
171
317
|
}
|
|
172
318
|
});
|
|
173
|
-
|
|
319
|
+
// TODO: better estimate for this.
|
|
320
|
+
currentState.opBytes += 200 - Number(size);
|
|
174
321
|
}
|
|
175
|
-
else {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// length + 16 for the string
|
|
184
|
-
// 24 for the bigint
|
|
185
|
-
// 50 for map overhead
|
|
186
|
-
// 50 for additional overhead
|
|
187
|
-
currentState.trackingSize += key.length + 140;
|
|
188
|
-
}
|
|
322
|
+
else if (currentState.trackingSize < idLimitBytes) {
|
|
323
|
+
// flatstr reduces the memory usage by flattening the string.
|
|
324
|
+
currentState.seen.set(utils.flatstr(key), doc.o);
|
|
325
|
+
// length + 16 for the string
|
|
326
|
+
// 24 for the bigint
|
|
327
|
+
// 50 for map overhead
|
|
328
|
+
// 50 for additional overhead
|
|
329
|
+
currentState.trackingSize += key.length + 140;
|
|
189
330
|
}
|
|
190
331
|
}
|
|
191
332
|
if (isPersistentPut) {
|
|
@@ -194,44 +335,36 @@ export class MongoCompactor {
|
|
|
194
335
|
}
|
|
195
336
|
else if (doc.op != 'CLEAR') {
|
|
196
337
|
if (currentState.lastNotPut == null) {
|
|
197
|
-
currentState.lastNotPut = doc.
|
|
338
|
+
currentState.lastNotPut = doc.o;
|
|
198
339
|
}
|
|
199
340
|
currentState.opsSincePut += 1;
|
|
200
341
|
}
|
|
201
342
|
if (this.updates.length + this.bucketStateUpdates.length >= this.moveBatchLimit) {
|
|
202
|
-
await this.flush();
|
|
343
|
+
await this.flush(bucketContext);
|
|
203
344
|
}
|
|
204
345
|
}
|
|
205
|
-
logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
346
|
+
this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
206
347
|
}
|
|
207
|
-
// Free memory before clearing bucket
|
|
348
|
+
// Free memory before clearing the bucket.
|
|
208
349
|
currentState.seen.clear();
|
|
209
350
|
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
210
|
-
logger.info(`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`);
|
|
211
|
-
// Need flush() before clear()
|
|
212
|
-
await this.flush();
|
|
213
|
-
await this.clearBucket(currentState);
|
|
351
|
+
this.logger.info(`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`);
|
|
352
|
+
// Need flush() before clear().
|
|
353
|
+
await this.flush(bucketContext);
|
|
354
|
+
await this.clearBucket(currentState, bucketContext);
|
|
214
355
|
}
|
|
215
|
-
// Do this
|
|
356
|
+
// Do this after clearBucket so we have accurate counts.
|
|
216
357
|
this.updateBucketChecksums(currentState);
|
|
217
|
-
// Need another flush after updateBucketChecksums()
|
|
218
|
-
await this.flush();
|
|
358
|
+
// Need another flush after updateBucketChecksums().
|
|
359
|
+
await this.flush(bucketContext);
|
|
219
360
|
}
|
|
220
|
-
/**
|
|
221
|
-
* Call when done with a bucket.
|
|
222
|
-
*/
|
|
223
361
|
updateBucketChecksums(state) {
|
|
224
362
|
if (state.opCount < 0) {
|
|
225
363
|
throw new ServiceAssertionError(`Invalid opCount: ${state.opCount} checksum ${state.checksum} opsSincePut: ${state.opsSincePut} maxOpId: ${this.maxOpId}`);
|
|
226
364
|
}
|
|
227
365
|
this.bucketStateUpdates.push({
|
|
228
366
|
updateOne: {
|
|
229
|
-
filter:
|
|
230
|
-
_id: {
|
|
231
|
-
g: this.group_id,
|
|
232
|
-
b: state.bucket
|
|
233
|
-
}
|
|
234
|
-
},
|
|
367
|
+
filter: this.bucketStateFilter(state.bucket, state.definitionId),
|
|
235
368
|
update: {
|
|
236
369
|
$set: {
|
|
237
370
|
compacted_state: {
|
|
@@ -241,9 +374,8 @@ export class MongoCompactor {
|
|
|
241
374
|
bytes: state.opBytes
|
|
242
375
|
},
|
|
243
376
|
estimate_since_compact: {
|
|
244
|
-
//
|
|
245
|
-
// which we don't currently cater for.
|
|
246
|
-
// We could potentially query for that, but that could add overhead.
|
|
377
|
+
// There could have been a whole bunch of new operations added to the bucket while compacting,
|
|
378
|
+
// which we don't currently cater for. We could potentially query for that, but that adds overhead.
|
|
247
379
|
count: 0,
|
|
248
380
|
bytes: 0
|
|
249
381
|
}
|
|
@@ -255,60 +387,49 @@ export class MongoCompactor {
|
|
|
255
387
|
}
|
|
256
388
|
});
|
|
257
389
|
}
|
|
258
|
-
async flush() {
|
|
390
|
+
async flush(col) {
|
|
259
391
|
if (this.updates.length > 0) {
|
|
260
|
-
logger.info(`Compacting ${this.updates.length} ops`);
|
|
261
|
-
await
|
|
262
|
-
// Order is not important.
|
|
263
|
-
//
|
|
264
|
-
// and it's fine if the operations are partially applied.
|
|
265
|
-
// Each individual operation is atomic.
|
|
392
|
+
this.logger.info(`Compacting ${this.updates.length} ops`);
|
|
393
|
+
await col.collection.bulkWrite(this.updates, {
|
|
394
|
+
// Order is not important. Since checksums are not affected, these operations can happen in any order,
|
|
395
|
+
// and it's fine if the operations are partially applied. Each individual operation is atomic.
|
|
266
396
|
ordered: false
|
|
267
397
|
});
|
|
268
398
|
this.updates = [];
|
|
269
399
|
}
|
|
400
|
+
await this.flushBucketStateUpdates();
|
|
401
|
+
}
|
|
402
|
+
async flushBucketStateUpdates() {
|
|
270
403
|
if (this.bucketStateUpdates.length > 0) {
|
|
271
|
-
logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
272
|
-
await this.
|
|
273
|
-
ordered: false
|
|
274
|
-
});
|
|
404
|
+
this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
405
|
+
await this.writeBucketStateUpdates();
|
|
275
406
|
this.bucketStateUpdates = [];
|
|
276
407
|
}
|
|
277
408
|
}
|
|
278
409
|
/**
|
|
279
410
|
* Perform a CLEAR compact for a bucket.
|
|
280
411
|
*
|
|
281
|
-
*
|
|
282
|
-
* @param bucket bucket name
|
|
283
|
-
* @param op op_id of the last non-PUT operation, which will be converted to CLEAR.
|
|
412
|
+
* @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
|
|
284
413
|
*/
|
|
285
|
-
async clearBucket(currentState) {
|
|
286
|
-
const bucket = currentState.bucket;
|
|
414
|
+
async clearBucket(currentState, col) {
|
|
287
415
|
const clearOp = currentState.lastNotPut;
|
|
288
416
|
const opFilter = {
|
|
289
417
|
_id: {
|
|
290
|
-
$gte:
|
|
291
|
-
|
|
292
|
-
b: bucket,
|
|
293
|
-
o: new mongo.MinKey()
|
|
294
|
-
},
|
|
295
|
-
$lte: {
|
|
296
|
-
g: this.group_id,
|
|
297
|
-
b: bucket,
|
|
298
|
-
o: clearOp
|
|
299
|
-
}
|
|
418
|
+
$gte: col.minId,
|
|
419
|
+
$lte: col.docId(clearOp)
|
|
300
420
|
}
|
|
301
421
|
};
|
|
302
422
|
const session = this.db.client.startSession();
|
|
303
423
|
try {
|
|
304
424
|
let done = false;
|
|
305
|
-
while (!done
|
|
425
|
+
while (!done) {
|
|
426
|
+
this.signal?.throwIfAborted();
|
|
306
427
|
let opCountDiff = 0;
|
|
307
428
|
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
308
429
|
// The state after each batch is fully consistent.
|
|
309
430
|
// We need a transaction per batch to make sure checksums stay consistent.
|
|
310
431
|
await session.withTransaction(async () => {
|
|
311
|
-
const query =
|
|
432
|
+
const query = col.collection.find(opFilter, {
|
|
312
433
|
session,
|
|
313
434
|
sort: { _id: 1 },
|
|
314
435
|
projection: {
|
|
@@ -320,56 +441,52 @@ export class MongoCompactor {
|
|
|
320
441
|
limit: this.clearBatchLimit
|
|
321
442
|
});
|
|
322
443
|
let checksum = 0;
|
|
323
|
-
let
|
|
444
|
+
let lastOp = null;
|
|
324
445
|
let targetOp = null;
|
|
325
446
|
let gotAnOp = false;
|
|
326
447
|
let numberOfOpsToClear = 0;
|
|
327
|
-
for await (
|
|
448
|
+
for await (const rawOp of query.stream()) {
|
|
449
|
+
const op = col.fromPartialPersistedDocument(rawOp);
|
|
328
450
|
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
329
451
|
checksum = utils.addChecksums(checksum, Number(op.checksum));
|
|
330
|
-
|
|
452
|
+
lastOp = op;
|
|
331
453
|
numberOfOpsToClear += 1;
|
|
332
454
|
if (op.op != 'CLEAR') {
|
|
333
455
|
gotAnOp = true;
|
|
334
456
|
}
|
|
335
|
-
if (op.target_op != null) {
|
|
336
|
-
|
|
337
|
-
targetOp = op.target_op;
|
|
338
|
-
}
|
|
457
|
+
if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
|
|
458
|
+
targetOp = op.target_op;
|
|
339
459
|
}
|
|
340
460
|
}
|
|
341
461
|
else {
|
|
342
|
-
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${
|
|
462
|
+
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
|
|
343
463
|
}
|
|
344
464
|
}
|
|
345
465
|
if (!gotAnOp) {
|
|
346
466
|
done = true;
|
|
347
467
|
return;
|
|
348
468
|
}
|
|
349
|
-
logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${
|
|
350
|
-
await
|
|
469
|
+
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
470
|
+
await col.collection.deleteMany({
|
|
351
471
|
_id: {
|
|
352
|
-
$gte:
|
|
353
|
-
|
|
354
|
-
b: bucket,
|
|
355
|
-
o: new mongo.MinKey()
|
|
356
|
-
},
|
|
357
|
-
$lte: lastOpId
|
|
472
|
+
$gte: col.minId,
|
|
473
|
+
$lte: col.docId(lastOp.o)
|
|
358
474
|
}
|
|
359
475
|
}, { session });
|
|
360
|
-
|
|
361
|
-
|
|
476
|
+
const op = col.toPersistedDocument({
|
|
477
|
+
o: lastOp.o,
|
|
362
478
|
op: 'CLEAR',
|
|
363
479
|
checksum: BigInt(checksum),
|
|
364
480
|
data: null,
|
|
365
481
|
target_op: targetOp
|
|
366
|
-
}
|
|
482
|
+
});
|
|
483
|
+
await col.collection.insertOne(op, { session });
|
|
367
484
|
opCountDiff = -numberOfOpsToClear + 1;
|
|
368
485
|
}, {
|
|
369
486
|
writeConcern: { w: 'majority' },
|
|
370
487
|
readConcern: { level: 'snapshot' }
|
|
371
488
|
});
|
|
372
|
-
// Update
|
|
489
|
+
// Update outside the transaction, since the transaction can be retried multiple times.
|
|
373
490
|
currentState.opCount += opCountDiff;
|
|
374
491
|
}
|
|
375
492
|
}
|
|
@@ -377,175 +494,17 @@ export class MongoCompactor {
|
|
|
377
494
|
await session.endSession();
|
|
378
495
|
}
|
|
379
496
|
}
|
|
380
|
-
/**
|
|
381
|
-
* Subset of compact, only populating checksums where relevant.
|
|
382
|
-
*/
|
|
383
|
-
async populateChecksums(options) {
|
|
384
|
-
let count = 0;
|
|
385
|
-
while (!this.signal?.aborted) {
|
|
386
|
-
const buckets = await this.dirtyBucketBatchForChecksums(options);
|
|
387
|
-
if (buckets.length == 0 || this.signal?.aborted) {
|
|
388
|
-
// All done
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
const start = Date.now();
|
|
392
|
-
// Filter batch by estimated bucket size, to reduce possibility of timeouts
|
|
393
|
-
let checkBuckets = [];
|
|
394
|
-
let totalCountEstimate = 0;
|
|
395
|
-
for (let bucket of buckets) {
|
|
396
|
-
checkBuckets.push(bucket);
|
|
397
|
-
totalCountEstimate += bucket.estimatedCount;
|
|
398
|
-
if (totalCountEstimate > 50_000) {
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
logger.info(`Calculating checksums for batch of ${buckets.length} buckets, estimated count of ${totalCountEstimate}`);
|
|
403
|
-
await this.updateChecksumsBatch(checkBuckets.map((b) => b.bucket));
|
|
404
|
-
logger.info(`Updated checksums for batch of ${checkBuckets.length} buckets in ${Date.now() - start}ms`);
|
|
405
|
-
count += checkBuckets.length;
|
|
406
|
-
}
|
|
407
|
-
return { buckets: count };
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Return batches of dirty buckets.
|
|
411
|
-
*
|
|
412
|
-
* Can be used to iterate through all buckets.
|
|
413
|
-
*
|
|
414
|
-
* minBucketChanges: minimum number of changes for a bucket to be included in the results.
|
|
415
|
-
* minChangeRatio: minimum ratio of changes to total ops for a bucket to be included in the results, number between 0 and 1.
|
|
416
|
-
*/
|
|
417
|
-
async *dirtyBucketBatches(options) {
|
|
418
|
-
// Previously, we used an index on {_id.g: 1, estimate_since_compact.count: 1} to only buckets with changes.
|
|
419
|
-
// This works well if there are only a small number of buckets with changes.
|
|
420
|
-
// However, if buckets are continuosly modified while we are compacting, we get the same buckets over and over again.
|
|
421
|
-
// This has caused the compact process to re-read the same collection around 5x times in total, which is very inefficient.
|
|
422
|
-
// To solve this, we now just iterate through all buckets, and filter out the ones with low changes.
|
|
423
|
-
if (options.minBucketChanges <= 0) {
|
|
424
|
-
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
425
|
-
}
|
|
426
|
-
let lastId = { g: this.group_id, b: new mongo.MinKey() };
|
|
427
|
-
const maxId = { g: this.group_id, b: new mongo.MaxKey() };
|
|
428
|
-
while (true) {
|
|
429
|
-
// To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, we use an aggregation pipeline
|
|
430
|
-
// to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria, rather than limiting
|
|
431
|
-
// on the output number.
|
|
432
|
-
const [result] = await this.db.bucket_state
|
|
433
|
-
.aggregate([
|
|
434
|
-
{
|
|
435
|
-
$match: {
|
|
436
|
-
_id: { $gt: lastId, $lt: maxId }
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
$sort: { _id: 1 }
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
// Scan a fixed number of docs each query so sparse matches don't block progress.
|
|
444
|
-
$limit: DIRTY_BUCKET_SCAN_BATCH_SIZE
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
$facet: {
|
|
448
|
-
// This is the results for the batch
|
|
449
|
-
buckets: [
|
|
450
|
-
{
|
|
451
|
-
$match: {
|
|
452
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
$project: {
|
|
457
|
-
_id: 1,
|
|
458
|
-
estimate_since_compact: 1,
|
|
459
|
-
compacted_state: 1
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
],
|
|
463
|
-
// This is used for the next query.
|
|
464
|
-
cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
], { maxTimeMS: MONGO_OPERATION_TIMEOUT_MS })
|
|
468
|
-
.toArray();
|
|
469
|
-
const cursor = result?.cursor?.[0];
|
|
470
|
-
if (cursor == null) {
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
lastId = cursor._id;
|
|
474
|
-
const mapped = (result?.buckets ?? []).map((b) => {
|
|
475
|
-
const updatedCount = b.estimate_since_compact?.count ?? 0;
|
|
476
|
-
const totalCount = (b.compacted_state?.count ?? 0) + updatedCount;
|
|
477
|
-
const updatedBytes = b.estimate_since_compact?.bytes ?? 0;
|
|
478
|
-
const totalBytes = (b.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
479
|
-
const dirtyChangeNumber = totalCount > 0 ? updatedCount / totalCount : 0;
|
|
480
|
-
const dirtyChangeBytes = totalBytes > 0 ? updatedBytes / totalBytes : 0;
|
|
481
|
-
return {
|
|
482
|
-
bucket: b._id.b,
|
|
483
|
-
estimatedCount: totalCount,
|
|
484
|
-
dirtyRatio: Math.max(dirtyChangeNumber, dirtyChangeBytes)
|
|
485
|
-
};
|
|
486
|
-
});
|
|
487
|
-
const filtered = mapped.filter((b) => b.estimatedCount >= options.minBucketChanges && b.dirtyRatio >= options.minChangeRatio);
|
|
488
|
-
yield filtered;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Returns a batch of dirty buckets - buckets with most changes first.
|
|
493
|
-
*
|
|
494
|
-
* This cannot be used to iterate on its own - the client is expected to process these buckets and
|
|
495
|
-
* set estimate_since_compact.count: 0 when done, before fetching the next batch.
|
|
496
|
-
*
|
|
497
|
-
* Unlike dirtyBucketBatches, used for compacting, this is specifically designed to be resuamble after a restart,
|
|
498
|
-
* since it is used as the last step for initial replication.
|
|
499
|
-
*
|
|
500
|
-
* We currently don't get new data while doing populateChecksums, so we don't need to worry about buckets changing while processing.
|
|
501
|
-
*/
|
|
502
|
-
async dirtyBucketBatchForChecksums(options) {
|
|
503
|
-
if (options.minBucketChanges <= 0) {
|
|
504
|
-
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
505
|
-
}
|
|
506
|
-
// We make use of an index on {_id.g: 1, 'estimate_since_compact.count': -1}
|
|
507
|
-
const dirtyBuckets = await this.db.bucket_state
|
|
508
|
-
.find({
|
|
509
|
-
'_id.g': this.group_id,
|
|
510
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
511
|
-
}, {
|
|
512
|
-
projection: {
|
|
513
|
-
_id: 1,
|
|
514
|
-
estimate_since_compact: 1,
|
|
515
|
-
compacted_state: 1
|
|
516
|
-
},
|
|
517
|
-
sort: {
|
|
518
|
-
'estimate_since_compact.count': -1
|
|
519
|
-
},
|
|
520
|
-
limit: 200,
|
|
521
|
-
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
522
|
-
})
|
|
523
|
-
.toArray();
|
|
524
|
-
return dirtyBuckets.map((bucket) => ({
|
|
525
|
-
bucket: bucket._id.b,
|
|
526
|
-
estimatedCount: bucket.estimate_since_compact.count + (bucket.compacted_state?.count ?? 0)
|
|
527
|
-
}));
|
|
528
|
-
}
|
|
529
497
|
async updateChecksumsBatch(buckets) {
|
|
530
|
-
const checksums = await this.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
end: this.maxOpId
|
|
534
|
-
};
|
|
535
|
-
}));
|
|
536
|
-
for (let bucketChecksum of checksums.values()) {
|
|
498
|
+
const checksums = await this.computeChecksumsForBuckets(buckets);
|
|
499
|
+
const definitionIdByBucket = new Map(buckets.map((bucket) => [bucket.bucket, bucket.definitionId]));
|
|
500
|
+
for (const bucketChecksum of checksums.values()) {
|
|
537
501
|
if (isPartialChecksum(bucketChecksum)) {
|
|
538
|
-
// Should never happen since we don't specify `start
|
|
502
|
+
// Should never happen since we don't specify `start`.
|
|
539
503
|
throw new ServiceAssertionError(`Full checksum expected, got ${JSON.stringify(bucketChecksum)}`);
|
|
540
504
|
}
|
|
541
505
|
this.bucketStateUpdates.push({
|
|
542
506
|
updateOne: {
|
|
543
|
-
filter:
|
|
544
|
-
_id: {
|
|
545
|
-
g: this.group_id,
|
|
546
|
-
b: bucketChecksum.bucket
|
|
547
|
-
}
|
|
548
|
-
},
|
|
507
|
+
filter: this.bucketStateFilter(bucketChecksum.bucket, definitionIdByBucket.get(bucketChecksum.bucket) ?? null),
|
|
549
508
|
update: {
|
|
550
509
|
$set: {
|
|
551
510
|
compacted_state: {
|
|
@@ -560,13 +519,16 @@ export class MongoCompactor {
|
|
|
560
519
|
}
|
|
561
520
|
}
|
|
562
521
|
},
|
|
563
|
-
// We don't create new ones here - it gets tricky to get the last_op right with the unique index on
|
|
564
|
-
// bucket_updates
|
|
522
|
+
// We don't create new ones here - it gets tricky to get the last_op right with the unique index on
|
|
523
|
+
// bucket_updates.
|
|
565
524
|
upsert: false
|
|
566
525
|
}
|
|
567
526
|
});
|
|
568
527
|
}
|
|
569
|
-
await this.
|
|
528
|
+
await this.flushBucketStateUpdates();
|
|
529
|
+
}
|
|
530
|
+
formatBucketDataKey(doc) {
|
|
531
|
+
return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
|
|
570
532
|
}
|
|
571
533
|
}
|
|
572
534
|
//# sourceMappingURL=MongoCompactor.js.map
|