@powersync/service-module-mongodb-storage 0.19.0 → 0.21.0
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 +64 -0
- package/dist/storage/MongoBucketStorage.d.ts +31 -3
- package/dist/storage/MongoBucketStorage.js +365 -206
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
- package/dist/storage/implementation/MongoBucketBatch.js +35 -9
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +3 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
- package/dist/storage/implementation/common/PersistedBatch.js +43 -2
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -1
- package/dist/storage/implementation/db.js +14 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +35 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
- package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +89 -5
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
- package/dist/storage/implementation/v3/source-table-utils.js +49 -57
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +23 -1
- package/dist/types/types.js +16 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +495 -268
- package/src/storage/implementation/MongoBucketBatch.ts +54 -14
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +3 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +54 -2
- package/src/storage/implementation/db.ts +21 -2
- package/src/storage/implementation/models.ts +38 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
- package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +96 -3
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +24 -7
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +255 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +273 -22
- package/test/src/storage_compacting.test.ts +714 -244
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +243 -1
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +365 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,169 +1,557 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { addChecksums, utils } from '@powersync/service-core';
|
|
1
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
+
import { addChecksums, formatBytes, utils } from '@powersync/service-core';
|
|
4
3
|
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
5
4
|
import { cacheKey } from '../OperationBatch.js';
|
|
6
5
|
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
7
6
|
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
8
7
|
import { DEFAULT_MAX_DOC_SIZE_BYTES } from './chunking.js';
|
|
8
|
+
import { applyStatsReplacement, bucketStats, chooseCompactionKind, combineAdjacentStats, combineChunkStats, CompactionContext, CompactionKind, emptyBucketStats, firstUncompactedWrite, forcedCompactionKind, readCompactionBatch, statsForDocument, statsForDocuments, unclaimedSnapshotFilter } from './compact-utils.js';
|
|
9
|
+
import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
|
|
10
|
+
import { AVAILABLE_LEASE_EXPR, CompactionLease } from './CompactionLease.js';
|
|
9
11
|
import { BucketDataObjectStorage, hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
10
12
|
import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* eligible for the next query. The first document is always accepted to ensure
|
|
17
|
-
* progress when a single document exceeds the configured byte limit.
|
|
18
|
-
*
|
|
19
|
-
* `hasMore` is conservative when the document limit is reached. An extra empty
|
|
20
|
-
* query is preferable to exhausting the cursor just to determine whether the
|
|
21
|
-
* limited MongoDB query contained another document.
|
|
22
|
-
*/
|
|
23
|
-
async function readCompactionBatch(cursor, options) {
|
|
24
|
-
const documents = [];
|
|
25
|
-
let cumulativeBytes = 0;
|
|
26
|
-
try {
|
|
27
|
-
for await (const document of cursor) {
|
|
28
|
-
if (documents.length > 0 && cumulativeBytes + document.size > options.byteLimit) {
|
|
29
|
-
return { documents, hasMore: true };
|
|
30
|
-
}
|
|
31
|
-
documents.push(document);
|
|
32
|
-
cumulativeBytes += document.size;
|
|
33
|
-
if (documents.length >= options.documentLimit) {
|
|
34
|
-
return { documents, hasMore: true };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { documents, hasMore: false };
|
|
38
|
-
}
|
|
39
|
-
finally {
|
|
40
|
-
await cursor.close();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
13
|
+
import { createObjectStorageUsageWriterId, ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
|
|
14
|
+
const DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS = 2 * 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
|
+
const DEFAULT_COMPACT_LEASE_DURATION_MS = 10 * 60 * 1000;
|
|
17
|
+
const SCHEDULED_COMPACTION_BATCH_SIZE = 100;
|
|
43
18
|
export class MongoCompactorV3 extends MongoCompactor {
|
|
19
|
+
minCompactChunkIntervalMs;
|
|
20
|
+
minCompactFullIntervalMs;
|
|
21
|
+
maxCompactFullIntervalMs;
|
|
22
|
+
compactLeaseDurationMs;
|
|
23
|
+
maxOpIdCap;
|
|
24
|
+
objectStorageUsage;
|
|
25
|
+
constructor(bucketStorage, db, options) {
|
|
26
|
+
super(bucketStorage, db, options);
|
|
27
|
+
this.minCompactChunkIntervalMs = options.minCompactChunkIntervalMs ?? DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS;
|
|
28
|
+
this.minCompactFullIntervalMs = options.minCompactFullIntervalMs ?? DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS;
|
|
29
|
+
this.maxCompactFullIntervalMs = options.maxCompactFullIntervalMs ?? DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS;
|
|
30
|
+
this.compactLeaseDurationMs = options.compactLeaseDurationMs ?? DEFAULT_COMPACT_LEASE_DURATION_MS;
|
|
31
|
+
this.maxOpIdCap = options.maxOpId;
|
|
32
|
+
this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId());
|
|
33
|
+
}
|
|
44
34
|
async compact() {
|
|
45
35
|
if (this.storage.objectStorage) {
|
|
46
36
|
// Clean these before compacting - should be quick in most cases.
|
|
47
37
|
try {
|
|
48
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
38
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
49
39
|
}
|
|
50
40
|
catch (e) {
|
|
41
|
+
if (this.signal?.aborted) {
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
51
44
|
// In this case, still continue normal compact process
|
|
52
45
|
this.logger.error(`Failed to clean up object storage deletion markers before compaction`, e);
|
|
53
46
|
}
|
|
54
47
|
}
|
|
55
|
-
await
|
|
48
|
+
await this.deleteOldCheckpointRequests();
|
|
49
|
+
if (this.buckets != null) {
|
|
50
|
+
await this.compactExplicitBuckets(this.buckets);
|
|
51
|
+
}
|
|
52
|
+
else if (this.compactChunksOnly) {
|
|
53
|
+
// Writers defer their first chunk-compaction check by this fixed default.
|
|
54
|
+
// Include that interval so this synchronous initial-replication pass
|
|
55
|
+
// processes the work that existed when it started.
|
|
56
|
+
await this.compactScheduledBuckets({
|
|
57
|
+
dueAheadMs: DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS,
|
|
58
|
+
forceKind: CompactionKind.Chunks
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await this.compactScheduledBuckets();
|
|
63
|
+
}
|
|
56
64
|
if (this.storage.objectStorage) {
|
|
57
65
|
// Cleanup for any produced during compacting.
|
|
58
66
|
// Note that markers only expire after a delay, so this may skip many produced during this compact
|
|
59
67
|
// run. However, during long compact runs, this may also have many ones it can clean up.
|
|
60
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
68
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
69
|
+
await this.objectStorageUsage.foldStaleWriterDeltas();
|
|
61
70
|
}
|
|
71
|
+
return this.compactedBucketCount;
|
|
62
72
|
}
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
73
|
+
async deleteOldCustomCheckpointRequests() {
|
|
74
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const collection of await this.db.listCustomCheckpointRequestCollections(this.group_id)) {
|
|
78
|
+
this.signal?.throwIfAborted();
|
|
79
|
+
await collection.deleteMany({
|
|
80
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore }
|
|
81
|
+
});
|
|
66
82
|
}
|
|
67
|
-
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
68
83
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
/** An explicit compact request always runs a full compact for its buckets. */
|
|
85
|
+
async compactExplicitBuckets(buckets) {
|
|
86
|
+
for (const bucket of buckets) {
|
|
87
|
+
// This is not a super efficient query, but this is not a common use case.
|
|
88
|
+
// May be optimized later.
|
|
89
|
+
const states = await this.db
|
|
90
|
+
.bucketState(this.group_id)
|
|
91
|
+
.find({ '_id.b': bucket }, { projection: { _id: 1 } })
|
|
92
|
+
.toArray();
|
|
93
|
+
for (const state of states) {
|
|
94
|
+
await using lease = await this.claimBucket({ _id: state._id });
|
|
95
|
+
if (lease == null || lease.state.first_uncompacted_write == null) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (this.isCompactionTargetCovered(lease.state, CompactionKind.Full)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const decision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
102
|
+
await this.compactClaimedBucket(lease, CompactionKind.Full, decision);
|
|
103
|
+
}
|
|
72
104
|
}
|
|
73
|
-
const collection = this.db.bucketState(this.group_id);
|
|
74
|
-
yield* this.dirtyBucketBatchesForCollection(collection, { d: new mongo.MinKey(), b: new mongo.MinKey() }, { d: new mongo.MaxKey(), b: new mongo.MaxKey() }, options, (bucketState) => bucketState._id.d);
|
|
75
105
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Process scheduled work in bounded batches.
|
|
108
|
+
*
|
|
109
|
+
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
110
|
+
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
111
|
+
*
|
|
112
|
+
* Buckets that do need compaction are still claimed and processed sequentially.
|
|
113
|
+
*
|
|
114
|
+
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
115
|
+
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
116
|
+
* filtered out when claiming a compaction lease.
|
|
117
|
+
*
|
|
118
|
+
* We filter scheduled jobs by the job start date, so that the same bucket is not compacted
|
|
119
|
+
* multiple times in one run. Reschedules fall beyond the fixed boundary. For the run after
|
|
120
|
+
* initial replication, dueAheadMs extends that boundary to include the first deferred interval.
|
|
121
|
+
*/
|
|
122
|
+
async compactScheduledBuckets(options = {}) {
|
|
123
|
+
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
124
|
+
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
125
|
+
// the exact initial-replication interval.
|
|
126
|
+
const [{ now: jobStartedAt }] = await this.db.db
|
|
127
|
+
.aggregate([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
128
|
+
.toArray();
|
|
129
|
+
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
130
|
+
const forceKind = options.forceKind;
|
|
131
|
+
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
132
|
+
while (true) {
|
|
133
|
+
this.signal?.throwIfAborted();
|
|
134
|
+
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
135
|
+
if (states.length == 0) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const scheduled = [];
|
|
139
|
+
for (const state of states) {
|
|
140
|
+
try {
|
|
141
|
+
scheduled.push({
|
|
142
|
+
state,
|
|
143
|
+
decision: chooseCompactionKind(state, jobStartedAt, this),
|
|
144
|
+
forcedKind: forcedCompactionKind(state, forceKind, this)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const noOpStates = scheduled.filter(({ state, decision, forcedKind }) => state.compact_lease == null && (forceKind == null ? decision.kind : forcedKind) == null);
|
|
152
|
+
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
153
|
+
for (const { state, decision, forcedKind } of scheduled) {
|
|
154
|
+
const kind = forceKind == null ? decision.kind : forcedKind;
|
|
155
|
+
if (state.compact_lease == null && kind == null) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
160
|
+
if (lease == null) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
164
|
+
const claimedKind = forceKind == null ? claimedDecision.kind : forcedCompactionKind(lease.state, forceKind, this);
|
|
165
|
+
if (claimedKind == null) {
|
|
166
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
167
|
+
}
|
|
168
|
+
else if (this.isCompactionTargetCovered(lease.state, claimedKind)) {
|
|
169
|
+
// The run cannot advance this kind's watermark without regressing
|
|
170
|
+
// already-published progress. Keep any newer work scheduled.
|
|
171
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (this.signal?.aborted) {
|
|
179
|
+
// When aborted, stop completely, rather than logging and re-scheduling individual buckets.
|
|
180
|
+
// The lease on the current bucket is still released automatically.
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
79
186
|
}
|
|
80
|
-
return this.dirtyBucketBatchForChecksumsForCollection(this.db.bucketState(this.group_id), {
|
|
81
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
82
|
-
}, (bucketState) => bucketState._id.d);
|
|
83
187
|
}
|
|
84
|
-
|
|
85
|
-
|
|
188
|
+
/** Read a bounded, priority-ordered snapshot of currently claimable scheduled work. */
|
|
189
|
+
async findScheduledBucketBatch(dueBefore) {
|
|
190
|
+
return this.db
|
|
86
191
|
.bucketState(this.group_id)
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
192
|
+
.find({
|
|
193
|
+
next_compact_check: { $lte: dueBefore },
|
|
194
|
+
...AVAILABLE_LEASE_EXPR
|
|
195
|
+
})
|
|
196
|
+
.sort({ next_compact_check: 1 })
|
|
197
|
+
.limit(SCHEDULED_COMPACTION_BATCH_SIZE)
|
|
198
|
+
.toArray();
|
|
90
199
|
}
|
|
91
200
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
201
|
+
* Reschedule snapshots that were already known to be no-ops without first
|
|
202
|
+
* taking a lease. Every decision input is compared so a concurrent writer
|
|
203
|
+
* or compactor simply makes the update a no-op instead of losing work. A
|
|
204
|
+
* successful reschedule moves beyond this run's fixed selection boundary.
|
|
94
205
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
206
|
+
async rescheduleUnclaimedBuckets(states, notBefore) {
|
|
207
|
+
if (states.length == 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await this.db.bucketState(this.group_id).bulkWrite(states.map(({ state, decision }) => ({
|
|
211
|
+
updateOne: {
|
|
212
|
+
filter: unclaimedSnapshotFilter(state),
|
|
213
|
+
update: [{ $set: { next_compact_check: this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore) } }]
|
|
102
214
|
}
|
|
103
|
-
|
|
104
|
-
bucket,
|
|
105
|
-
definitionId,
|
|
106
|
-
end: this.maxOpId
|
|
107
|
-
};
|
|
108
|
-
}));
|
|
215
|
+
})), { ordered: false });
|
|
109
216
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Isolate a malformed bucket so it cannot prevent other scheduled buckets
|
|
219
|
+
* from compacting. The snapshot filter preserves any concurrent write or
|
|
220
|
+
* compactor result instead of overwriting its next check.
|
|
221
|
+
*/
|
|
222
|
+
async rescheduleFailedBucket(state, notBefore, error) {
|
|
223
|
+
this.logger.error(`Failed to compact scheduled bucket ${state._id.b}; rescheduling it`, error);
|
|
224
|
+
try {
|
|
225
|
+
await this.db
|
|
226
|
+
.bucketState(this.group_id)
|
|
227
|
+
.updateOne(unclaimedSnapshotFilter(state), [{ $set: { next_compact_check: notBefore } }]);
|
|
113
228
|
}
|
|
114
|
-
|
|
115
|
-
_id
|
|
116
|
-
|
|
117
|
-
|
|
229
|
+
catch (rescheduleError) {
|
|
230
|
+
this.logger.error(`Failed to reschedule bucket ${state._id.b} after a compaction error`, rescheduleError);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Given a bucket filter, claim a lease on the bucket. The filter should include a filter on _id.
|
|
235
|
+
*
|
|
236
|
+
* Resolves to null if the bucket is already claimed, not found, or filtered out.
|
|
237
|
+
*/
|
|
238
|
+
async claimBucket(filter, sort) {
|
|
239
|
+
return CompactionLease.claim(this.db.bucketState(this.group_id), filter, sort, this.compactLeaseDurationMs);
|
|
240
|
+
}
|
|
241
|
+
async compactClaimedBucket(lease, kind, decision, rescheduleNotBefore) {
|
|
242
|
+
const context = new CompactionContext(lease, kind, decision, rescheduleNotBefore, this.compactionTarget(lease.state));
|
|
243
|
+
lease.startRenewal();
|
|
244
|
+
await this.compactSingleBucket(context);
|
|
245
|
+
}
|
|
246
|
+
async rescheduleClaimedBucket(lease, decision, notBefore) {
|
|
247
|
+
await lease.reschedule(this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore));
|
|
248
|
+
}
|
|
249
|
+
rescheduleAtOrAfter(nextCompactCheck, notBefore) {
|
|
250
|
+
return notBefore == null ? nextCompactCheck : { $max: [nextCompactCheck, notBefore] };
|
|
251
|
+
}
|
|
252
|
+
compactionTarget(state) {
|
|
253
|
+
return this.maxOpIdCap == null || state.last_op < this.maxOpIdCap ? state.last_op : this.maxOpIdCap;
|
|
254
|
+
}
|
|
255
|
+
isCompactionTargetCovered(state, kind) {
|
|
256
|
+
const target = this.compactionTarget(state);
|
|
257
|
+
if (kind == CompactionKind.Chunks) {
|
|
258
|
+
return state.compacted_state != null && state.compacted_state.op_id >= target;
|
|
259
|
+
}
|
|
260
|
+
if (state.last_full_compact != null && state.last_full_compact.op_id >= target) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
// A full compact may change counts before the checksum-cache boundary.
|
|
264
|
+
// Wait for the safe target to catch up instead of publishing an older or
|
|
265
|
+
// stale cache. At the same boundary, full coverage can still advance.
|
|
266
|
+
return state.compacted_state != null && state.compacted_state.op_id > target;
|
|
267
|
+
}
|
|
268
|
+
get objectStorageLifecycle() {
|
|
269
|
+
if (!this.storage.objectStorage) {
|
|
270
|
+
throw new Error('Object storage is not configured');
|
|
271
|
+
}
|
|
272
|
+
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
273
|
+
}
|
|
274
|
+
async compactSingleBucket(context) {
|
|
275
|
+
if (context.kind == CompactionKind.Chunks) {
|
|
276
|
+
return this.compactSingleBucketChunks(context);
|
|
277
|
+
}
|
|
278
|
+
return this.compactSingleBucketFully(context);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Merge adjacent bucket-data chunks without inspecting their operations
|
|
282
|
+
* unless a merge is possible. The metadata contains enough information to
|
|
283
|
+
* update the persisted checksum state and to decide whether a group can fit
|
|
284
|
+
* in one chunk.
|
|
285
|
+
*/
|
|
286
|
+
async compactSingleBucketChunks(context) {
|
|
287
|
+
const bucket = context.state._id.b;
|
|
288
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
289
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
290
|
+
bucket,
|
|
291
|
+
definitionId: resolvedDefinitionId,
|
|
292
|
+
replicationStreamId: this.group_id
|
|
293
|
+
});
|
|
294
|
+
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
295
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
296
|
+
let previousCompactedState = context.state.compacted_state;
|
|
297
|
+
// A zero boundary represents an empty prefix, so there is no stored chunk
|
|
298
|
+
// whose statistics need to be carried into this pass.
|
|
299
|
+
if (previousCompactedState?.op_id === 0n) {
|
|
300
|
+
previousCompactedState = undefined;
|
|
301
|
+
}
|
|
302
|
+
// Include the last previously compacted chunk as well as new chunks. It
|
|
303
|
+
// is the only old chunk which can become mergeable with the new tail.
|
|
304
|
+
let lowerBound = previousCompactedState != null ? bucketContext.docId(previousCompactedState.op_id - 1n) : bucketContext.minId;
|
|
305
|
+
const upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
306
|
+
let cachedBoundaryToVerify = previousCompactedState?.op_id;
|
|
307
|
+
let compactedOpId = null;
|
|
308
|
+
let overlappingCompactedChunk;
|
|
309
|
+
let compactedTail = emptyBucketStats();
|
|
310
|
+
let pendingChunks = [];
|
|
311
|
+
let pendingSize = 0;
|
|
312
|
+
while (true) {
|
|
313
|
+
this.signal?.throwIfAborted();
|
|
314
|
+
await context.lease.throwIfLost();
|
|
315
|
+
const batch = await readCompactionBatch(collection.aggregate([
|
|
316
|
+
{
|
|
317
|
+
$match: {
|
|
318
|
+
_id: {
|
|
319
|
+
$gt: lowerBound,
|
|
320
|
+
$lt: upperBound
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{ $sort: { _id: 1 } },
|
|
325
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
326
|
+
{
|
|
327
|
+
$project: {
|
|
328
|
+
_id: 1,
|
|
329
|
+
min_op: 1,
|
|
330
|
+
checksum: 1,
|
|
331
|
+
count: 1,
|
|
332
|
+
size: 1,
|
|
333
|
+
target_op: 1,
|
|
334
|
+
storage_ref: 1
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
], { batchSize: this.moveBatchQueryLimit + 1 }), {
|
|
338
|
+
byteLimit: this.moveBatchByteLimit,
|
|
339
|
+
documentLimit: this.moveBatchQueryLimit
|
|
340
|
+
});
|
|
341
|
+
if (cachedBoundaryToVerify != null) {
|
|
342
|
+
const cachedBoundary = cachedBoundaryToVerify;
|
|
343
|
+
cachedBoundaryToVerify = undefined;
|
|
344
|
+
if (batch.documents[0]?._id.o !== cachedBoundary) {
|
|
345
|
+
// A previous attempt may have replaced the cached boundary before
|
|
346
|
+
// finalizing bucket state. Keep the persisted cache available to
|
|
347
|
+
// readers, but ignore it in this attempt and calculate its
|
|
348
|
+
// replacement through the normal scan from the bucket beginning.
|
|
349
|
+
previousCompactedState = undefined;
|
|
350
|
+
lowerBound = bucketContext.minId;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (batch.documents.length == 0) {
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
for (const doc of batch.documents) {
|
|
358
|
+
compactedOpId = maxOpId(compactedOpId, doc._id.o);
|
|
359
|
+
const documentStats = statsForDocument(doc);
|
|
360
|
+
if (previousCompactedState?.op_id === doc._id.o) {
|
|
361
|
+
overlappingCompactedChunk = documentStats;
|
|
362
|
+
}
|
|
363
|
+
const nextSize = pendingSize + doc.size;
|
|
364
|
+
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
365
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
366
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
367
|
+
pendingChunks = [];
|
|
368
|
+
pendingSize = 0;
|
|
369
|
+
}
|
|
370
|
+
pendingChunks.push(doc);
|
|
371
|
+
pendingSize += doc.size;
|
|
118
372
|
}
|
|
373
|
+
lowerBound = batch.documents[batch.documents.length - 1]._id;
|
|
374
|
+
if (!batch.hasMore) {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (pendingChunks.length > 0) {
|
|
379
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
380
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
381
|
+
}
|
|
382
|
+
if (compactedOpId == null) {
|
|
383
|
+
await this.finalizeSkippedBucket(context);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const compactedState = previousCompactedState == null
|
|
387
|
+
? compactedTail
|
|
388
|
+
: combineChunkStats(previousCompactedState, compactedTail, overlappingCompactedChunk);
|
|
389
|
+
const tailStats = compactedOpId == context.lastOp
|
|
390
|
+
? undefined
|
|
391
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
392
|
+
const result = {
|
|
393
|
+
compactedState,
|
|
394
|
+
bucketStats: tailStats == null ? compactedState : combineAdjacentStats(compactedState, tailStats)
|
|
119
395
|
};
|
|
396
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: 0 });
|
|
397
|
+
this.compactedBucketCount++;
|
|
398
|
+
this.logger.info(`Compacted bucket chunks ${bucket}: ${result.bucketStats.count} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`);
|
|
120
399
|
}
|
|
121
|
-
async
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
400
|
+
async flushChunkMerge(bucket, inputs, collection, context, bucketContext) {
|
|
401
|
+
if (inputs.length == 1) {
|
|
402
|
+
return statsForDocument(inputs[0]);
|
|
403
|
+
}
|
|
404
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
405
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
406
|
+
// the same rule.
|
|
407
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
408
|
+
if (inlineInputs.length > 0) {
|
|
409
|
+
const inlineDocuments = await collection
|
|
410
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
411
|
+
.toArray();
|
|
412
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
413
|
+
for (const input of inlineInputs) {
|
|
414
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
133
415
|
}
|
|
134
416
|
}
|
|
135
|
-
|
|
136
|
-
|
|
417
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
418
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
419
|
+
const targetOp = inputs.reduce((maxTarget, input) => maxOpId(maxTarget, input.target_op), null);
|
|
420
|
+
const result = await this.flushCompactionGroup(bucket, {
|
|
421
|
+
inputs,
|
|
422
|
+
ops: operations,
|
|
423
|
+
changed: true,
|
|
424
|
+
targetOp
|
|
425
|
+
}, bucketContext, context);
|
|
426
|
+
return result.stats;
|
|
427
|
+
}
|
|
428
|
+
async finalizeCompactedBucket({ context, compactedOpId, compactionResult, puts }) {
|
|
429
|
+
await context.lease.throwIfLost();
|
|
430
|
+
const startedStats = bucketStats(context.state);
|
|
431
|
+
const delta = {
|
|
432
|
+
count: compactionResult.bucketStats.count - startedStats.count,
|
|
433
|
+
bytes: compactionResult.bucketStats.bytes - startedStats.bytes,
|
|
434
|
+
chunks: compactionResult.bucketStats.chunks - startedStats.chunks
|
|
435
|
+
};
|
|
436
|
+
const coveredClaimedHead = compactedOpId >= context.lastOp;
|
|
437
|
+
const concurrentWriteCheck = { $gt: ['$last_op', context.lastOp] };
|
|
438
|
+
const remainingFullWorkCheck = coveredClaimedHead ? concurrentWriteCheck : true;
|
|
439
|
+
const nextAfterPartialFullCompact = this.rescheduleAtOrAfter({ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }, context.rescheduleNotBefore);
|
|
440
|
+
const nextCheckForUncompactedWork = this.rescheduleAtOrAfter({
|
|
441
|
+
$min: [
|
|
442
|
+
new Date(firstUncompactedWrite(context.state).getTime() + this.maxCompactFullIntervalMs),
|
|
443
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
444
|
+
]
|
|
445
|
+
}, context.rescheduleNotBefore);
|
|
446
|
+
const update = {
|
|
447
|
+
compacted_state: {
|
|
448
|
+
op_id: compactedOpId,
|
|
449
|
+
checksum: BigInt(compactionResult.compactedState.checksum),
|
|
450
|
+
count: compactionResult.compactedState.count,
|
|
451
|
+
bytes: compactionResult.compactedState.bytes,
|
|
452
|
+
chunks: compactionResult.compactedState.chunks,
|
|
453
|
+
at: '$$NOW'
|
|
454
|
+
},
|
|
455
|
+
bucket_stats: {
|
|
456
|
+
count: { $add: ['$bucket_stats.count', delta.count] },
|
|
457
|
+
bytes: { $add: ['$bucket_stats.bytes', delta.bytes] },
|
|
458
|
+
chunks: { $add: ['$bucket_stats.chunks', delta.chunks] }
|
|
459
|
+
},
|
|
460
|
+
first_uncompacted_write: context.kind == CompactionKind.Full
|
|
461
|
+
? { $cond: [remainingFullWorkCheck, '$$NOW', '$$REMOVE'] }
|
|
462
|
+
: '$first_uncompacted_write',
|
|
463
|
+
next_compact_check: context.kind == CompactionKind.Full
|
|
464
|
+
? { $cond: [remainingFullWorkCheck, nextAfterPartialFullCompact, '$$REMOVE'] }
|
|
465
|
+
: nextCheckForUncompactedWork
|
|
466
|
+
};
|
|
467
|
+
if (context.kind == CompactionKind.Full) {
|
|
468
|
+
update.last_full_compact = {
|
|
469
|
+
op_id: compactedOpId,
|
|
470
|
+
count: compactionResult.compactedState.count,
|
|
471
|
+
puts,
|
|
472
|
+
at: '$$NOW'
|
|
473
|
+
};
|
|
137
474
|
}
|
|
138
|
-
|
|
475
|
+
await context.lease.finalize(update);
|
|
476
|
+
}
|
|
477
|
+
async finalizeSkippedBucket(context) {
|
|
478
|
+
// A maxOpId cap can exclude the first remaining document entirely. Avoid
|
|
479
|
+
// immediately claiming the same no-progress bucket again in this run.
|
|
480
|
+
await this.rescheduleClaimedBucket(context.lease, {
|
|
481
|
+
...context.decision,
|
|
482
|
+
nextCompactCheck: {
|
|
483
|
+
$max: [
|
|
484
|
+
context.decision.nextCompactCheck,
|
|
485
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
}, context.rescheduleNotBefore);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Read bucket stats directly from bucket_data documents.
|
|
492
|
+
*/
|
|
493
|
+
async readBucketStats(bucket, definitionId, maxOp, lowerBound) {
|
|
494
|
+
const context = new BucketDataContextV3(this.db, {
|
|
139
495
|
bucket,
|
|
140
|
-
definitionId
|
|
496
|
+
definitionId,
|
|
141
497
|
replicationStreamId: this.group_id
|
|
142
498
|
});
|
|
499
|
+
const [stats] = await this.db
|
|
500
|
+
.bucketData(this.group_id, definitionId)
|
|
501
|
+
.aggregate([
|
|
502
|
+
{
|
|
503
|
+
$match: {
|
|
504
|
+
_id: lowerBound == null
|
|
505
|
+
? { $gte: context.minId, $lte: context.docId(maxOp) }
|
|
506
|
+
: { $gt: lowerBound, $lte: context.docId(maxOp) }
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
$group: {
|
|
511
|
+
_id: null,
|
|
512
|
+
count: { $sum: '$count' },
|
|
513
|
+
bytes: { $sum: '$size' },
|
|
514
|
+
chunks: { $sum: 1 },
|
|
515
|
+
checksum: { $sum: '$checksum' }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
])
|
|
519
|
+
.toArray();
|
|
520
|
+
return {
|
|
521
|
+
count: Number(stats?.count ?? 0),
|
|
522
|
+
bytes: BigInt(stats?.bytes ?? 0),
|
|
523
|
+
chunks: Number(stats?.chunks ?? 0),
|
|
524
|
+
checksum: typeof stats?.checksum == 'bigint'
|
|
525
|
+
? Number(BigInt.asIntN(32, stats.checksum))
|
|
526
|
+
: addChecksums(0, Number(stats?.checksum ?? 0))
|
|
527
|
+
};
|
|
143
528
|
}
|
|
144
|
-
async
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
529
|
+
async compactSingleBucketFully(context) {
|
|
530
|
+
const bucket = context.state._id.b;
|
|
531
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
532
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
533
|
+
bucket,
|
|
534
|
+
definitionId: resolvedDefinitionId,
|
|
535
|
+
replicationStreamId: this.group_id
|
|
536
|
+
});
|
|
150
537
|
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
151
|
-
const
|
|
538
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
152
539
|
const lowerBound = bucketContext.minId;
|
|
153
|
-
let upperBound = bucketContext.docId(
|
|
154
|
-
let totalChecksum = 0;
|
|
540
|
+
let upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
155
541
|
let totalOpCount = 0;
|
|
156
|
-
let totalOpBytes = 0;
|
|
157
542
|
let lastNotPut = null;
|
|
158
543
|
let opsSincePut = 0;
|
|
159
544
|
let compactedOpId = null;
|
|
160
545
|
let clearBoundary = null;
|
|
546
|
+
let compactedStats = emptyBucketStats();
|
|
161
547
|
const seen = new Map();
|
|
162
548
|
let trackingSize = 0;
|
|
549
|
+
let putCount = 0;
|
|
163
550
|
let pendingGroup = null;
|
|
164
551
|
// --- Read batch from MongoDB ---
|
|
165
552
|
while (true) {
|
|
166
553
|
this.signal?.throwIfAborted();
|
|
554
|
+
await context.lease.throwIfLost();
|
|
167
555
|
const pipeline = [
|
|
168
556
|
{
|
|
169
557
|
$match: {
|
|
@@ -205,7 +593,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
205
593
|
// merging is useful, and writes each final object at most once.
|
|
206
594
|
for (const doc of batchDocs) {
|
|
207
595
|
compactedOpId ??= doc._id.o;
|
|
208
|
-
const originalOps = Array.from(loadBucketDataDocument(
|
|
596
|
+
const originalOps = Array.from(loadBucketDataDocument(dataContext, doc));
|
|
209
597
|
let changed = false;
|
|
210
598
|
const compactedOps = [];
|
|
211
599
|
let maxTargetOp = doc.target_op ?? null;
|
|
@@ -223,6 +611,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
223
611
|
row_id: undefined,
|
|
224
612
|
source_table: undefined,
|
|
225
613
|
source_key: undefined,
|
|
614
|
+
subkey: undefined,
|
|
226
615
|
data: null
|
|
227
616
|
});
|
|
228
617
|
changed = true;
|
|
@@ -238,6 +627,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
238
627
|
}
|
|
239
628
|
compactedOps.push(op);
|
|
240
629
|
if (op.op == 'PUT') {
|
|
630
|
+
putCount++;
|
|
241
631
|
lastNotPut = null;
|
|
242
632
|
opsSincePut = 0;
|
|
243
633
|
}
|
|
@@ -260,10 +650,6 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
260
650
|
}
|
|
261
651
|
}
|
|
262
652
|
compactedOps.reverse();
|
|
263
|
-
for (const op of compactedOps) {
|
|
264
|
-
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
265
|
-
totalOpBytes += op.data?.length ?? 0;
|
|
266
|
-
}
|
|
267
653
|
totalOpCount += compactedOps.length;
|
|
268
654
|
const candidate = {
|
|
269
655
|
inputs: [doc],
|
|
@@ -287,11 +673,12 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
287
673
|
}
|
|
288
674
|
else {
|
|
289
675
|
const flushedGroup = pendingGroup;
|
|
290
|
-
const
|
|
676
|
+
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext);
|
|
677
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
291
678
|
if (lastNotPut != null &&
|
|
292
679
|
flushedGroup.ops[0].o <= lastNotPut &&
|
|
293
680
|
flushedGroup.ops[flushedGroup.ops.length - 1].o >= lastNotPut) {
|
|
294
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
681
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
295
682
|
}
|
|
296
683
|
pendingGroup = candidate;
|
|
297
684
|
}
|
|
@@ -305,14 +692,16 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
305
692
|
this.logger.info(`Compacted batch of ${batchDocs.length} documents for bucket ${bucket}`);
|
|
306
693
|
}
|
|
307
694
|
if (pendingGroup != null) {
|
|
308
|
-
const
|
|
695
|
+
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext);
|
|
696
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
309
697
|
if (lastNotPut != null &&
|
|
310
698
|
pendingGroup.ops[0].o <= lastNotPut &&
|
|
311
699
|
pendingGroup.ops[pendingGroup.ops.length - 1].o >= lastNotPut) {
|
|
312
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
700
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
313
701
|
}
|
|
314
702
|
}
|
|
315
703
|
if (compactedOpId == null) {
|
|
704
|
+
await this.finalizeSkippedBucket(context);
|
|
316
705
|
return;
|
|
317
706
|
}
|
|
318
707
|
// --- Clear: collapse leading MOVE/REMOVE/CLEAR sequence ---
|
|
@@ -320,25 +709,21 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
320
709
|
if (clearBoundary == null || clearBoundary.opId != lastNotPut) {
|
|
321
710
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
322
711
|
}
|
|
323
|
-
|
|
712
|
+
const clearResult = await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, dataContext);
|
|
713
|
+
totalOpCount += clearResult.opCountDiff;
|
|
714
|
+
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
324
715
|
}
|
|
716
|
+
const tailStats = compactedOpId == context.lastOp
|
|
717
|
+
? undefined
|
|
718
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
719
|
+
const result = {
|
|
720
|
+
compactedState: compactedStats,
|
|
721
|
+
bucketStats: tailStats == null ? compactedStats : combineAdjacentStats(compactedStats, tailStats)
|
|
722
|
+
};
|
|
325
723
|
// --- Finalize: update bucket checksums and state ---
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
seen: new Map(),
|
|
330
|
-
trackingSize: 0,
|
|
331
|
-
lastNotPut: lastNotPut,
|
|
332
|
-
opsSincePut: opsSincePut,
|
|
333
|
-
checksum: totalChecksum,
|
|
334
|
-
opCount: totalOpCount,
|
|
335
|
-
opBytes: totalOpBytes
|
|
336
|
-
}, compactedOpId);
|
|
337
|
-
if (this.bucketStateUpdates.length > 0) {
|
|
338
|
-
await this.writeBucketStateUpdates();
|
|
339
|
-
this.bucketStateUpdates = [];
|
|
340
|
-
}
|
|
341
|
-
logger.info(`Compacted bucket ${bucket}: ${totalOpCount} surviving ops`);
|
|
724
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: putCount });
|
|
725
|
+
this.compactedBucketCount++;
|
|
726
|
+
this.logger.info(`Compacted bucket ${bucket}: ${totalOpCount} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`);
|
|
342
727
|
}
|
|
343
728
|
/**
|
|
344
729
|
* Persist replacement objects before starting the transaction, then atomically
|
|
@@ -348,7 +733,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
348
733
|
*/
|
|
349
734
|
async flushCompactionGroup(bucket, group, bucketContext, context) {
|
|
350
735
|
if (group.inputs.length == 1 && !group.changed) {
|
|
351
|
-
return
|
|
736
|
+
return {
|
|
737
|
+
documentId: group.inputs[0]._id,
|
|
738
|
+
stats: statsForDocument(group.inputs[0])
|
|
739
|
+
};
|
|
352
740
|
}
|
|
353
741
|
const inputs = group.inputs;
|
|
354
742
|
const idsToDelete = inputs.map((doc) => doc._id);
|
|
@@ -356,6 +744,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
356
744
|
const expectedChecksum = inputs.reduce((sum, doc) => sum + doc.checksum, 0n);
|
|
357
745
|
const expectedOpCount = inputs.reduce((sum, doc) => sum + doc.count, 0);
|
|
358
746
|
const oldStoragePaths = inputs.flatMap((doc) => (doc.storage_ref ? [doc.storage_ref.path] : []));
|
|
747
|
+
const oldStorageBytes = inputs.reduce((sum, document) => sum + ObjectStorageUsage.bytes(document), 0n);
|
|
359
748
|
const { documents, storagePaths: newStoragePaths, uploads } = await this.persistBucketData(bucket, [group.ops], context, undefined, { targetOp: group.targetOp });
|
|
360
749
|
const session = this.db.client.startSession();
|
|
361
750
|
try {
|
|
@@ -382,6 +771,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
382
771
|
await bucketContext.collection.deleteMany({ _id: { $in: idsToDelete } }, { session });
|
|
383
772
|
await bucketContext.collection.insertMany(documents, { session });
|
|
384
773
|
await this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, session);
|
|
774
|
+
await this.recordObjectStorageReplacement(oldStorageBytes, documents, context.definitionId, session);
|
|
385
775
|
}, {
|
|
386
776
|
writeConcern: { w: 'majority' },
|
|
387
777
|
readConcern: { level: 'snapshot' }
|
|
@@ -390,7 +780,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
390
780
|
finally {
|
|
391
781
|
await session.endSession();
|
|
392
782
|
}
|
|
393
|
-
return
|
|
783
|
+
return {
|
|
784
|
+
documentId: documents[0]._id,
|
|
785
|
+
stats: statsForDocuments(documents)
|
|
786
|
+
};
|
|
394
787
|
}
|
|
395
788
|
/**
|
|
396
789
|
* Collapse the leading sequence of MOVE/REMOVE/CLEAR ops at the start
|
|
@@ -398,10 +791,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
398
791
|
* before the known boundary document, then splits that boundary document
|
|
399
792
|
* if it contains ops on both sides of lastNotPut.
|
|
400
793
|
*
|
|
401
|
-
* Returns the op count
|
|
794
|
+
* Returns the op count and stored-stat changes after replacing cleared ops
|
|
795
|
+
* with CLEAR ops.
|
|
402
796
|
*/
|
|
403
797
|
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
404
798
|
let opCountDiff = 0;
|
|
799
|
+
let before = emptyBucketStats();
|
|
800
|
+
let after = emptyBucketStats();
|
|
405
801
|
const session = this.db.client.startSession();
|
|
406
802
|
try {
|
|
407
803
|
let done = false;
|
|
@@ -411,26 +807,36 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
411
807
|
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context);
|
|
412
808
|
done = batch.done;
|
|
413
809
|
opCountDiff += batch.opCountDiff;
|
|
810
|
+
before = combineAdjacentStats(before, batch.before);
|
|
811
|
+
after = combineAdjacentStats(after, batch.after);
|
|
414
812
|
}
|
|
415
813
|
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
416
814
|
// potentially followed by PUT operations. This is only a single document, so no need for batching.
|
|
417
|
-
|
|
815
|
+
const boundaryResult = await this.clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context);
|
|
816
|
+
opCountDiff += boundaryResult.opCountDiff;
|
|
817
|
+
before = combineAdjacentStats(before, boundaryResult.before);
|
|
818
|
+
after = combineAdjacentStats(after, boundaryResult.after);
|
|
418
819
|
}
|
|
419
820
|
finally {
|
|
420
821
|
await session.endSession();
|
|
421
822
|
}
|
|
422
|
-
return opCountDiff;
|
|
823
|
+
return { opCountDiff, before, after };
|
|
423
824
|
}
|
|
424
825
|
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
425
826
|
const bucket = bucketContext.key.bucket;
|
|
426
827
|
this.signal?.throwIfAborted();
|
|
427
|
-
|
|
828
|
+
let prepared;
|
|
428
829
|
let done = false;
|
|
429
830
|
let opCountDiff = 0;
|
|
831
|
+
let before = emptyBucketStats();
|
|
832
|
+
let after = emptyBucketStats();
|
|
430
833
|
await session.withTransaction(async () => {
|
|
431
834
|
done = false;
|
|
432
835
|
opCountDiff = 0;
|
|
836
|
+
before = emptyBucketStats();
|
|
837
|
+
after = emptyBucketStats();
|
|
433
838
|
const oldStoragePaths = [];
|
|
839
|
+
let oldStorageBytes = 0n;
|
|
434
840
|
const query = collection.find({
|
|
435
841
|
_id: {
|
|
436
842
|
$gte: bucketContext.minId,
|
|
@@ -444,6 +850,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
444
850
|
min_op: 1,
|
|
445
851
|
checksum: 1,
|
|
446
852
|
count: 1,
|
|
853
|
+
size: 1,
|
|
447
854
|
target_op: 1,
|
|
448
855
|
has_clear_op: 1,
|
|
449
856
|
storage_ref: 1
|
|
@@ -456,14 +863,21 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
456
863
|
let lastDocId = null;
|
|
457
864
|
let clearOpCount = 0;
|
|
458
865
|
let gotNonClearOp = false;
|
|
866
|
+
const inputStats = emptyBucketStats();
|
|
459
867
|
for await (const doc of query.stream()) {
|
|
460
868
|
if (doc.min_op > lastNotPut) {
|
|
461
869
|
throw new ReplicationAssertionError(`Unexpected document before CLEAR boundary with min_op ${doc.min_op} > ${lastNotPut} in bucket ${bucket}`);
|
|
462
870
|
}
|
|
463
871
|
lastDocId = doc._id;
|
|
872
|
+
const documentStats = statsForDocument(doc);
|
|
873
|
+
inputStats.count += documentStats.count;
|
|
874
|
+
inputStats.bytes += documentStats.bytes;
|
|
875
|
+
inputStats.chunks += documentStats.chunks;
|
|
876
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
464
877
|
if (doc.storage_ref) {
|
|
465
878
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
466
879
|
}
|
|
880
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
467
881
|
// The compaction scan established that every operation before the
|
|
468
882
|
// boundary is MOVE/REMOVE/CLEAR. Root metadata is sufficient to fold
|
|
469
883
|
// whole documents into one CLEAR, so avoid downloading their payloads.
|
|
@@ -486,6 +900,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
486
900
|
done = true;
|
|
487
901
|
return;
|
|
488
902
|
}
|
|
903
|
+
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
489
904
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
490
905
|
await collection.deleteMany({
|
|
491
906
|
_id: {
|
|
@@ -505,21 +920,29 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
505
920
|
});
|
|
506
921
|
await collection.insertOne(persisted.documents[0], { session });
|
|
507
922
|
await this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, session);
|
|
923
|
+
await this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, session);
|
|
508
924
|
opCountDiff = -clearedOpCount + 1;
|
|
925
|
+
before = inputStats;
|
|
926
|
+
after = statsForDocuments(persisted.documents);
|
|
509
927
|
}, {
|
|
510
928
|
writeConcern: { w: 'majority' },
|
|
511
929
|
readConcern: { level: 'snapshot' }
|
|
512
930
|
});
|
|
513
|
-
return { done, opCountDiff };
|
|
931
|
+
return { done, opCountDiff, before, after };
|
|
514
932
|
}
|
|
515
933
|
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
516
934
|
const bucket = bucketContext.key.bucket;
|
|
517
935
|
this.signal?.throwIfAborted();
|
|
518
936
|
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
519
937
|
let opCountDiff = 0;
|
|
938
|
+
let before = emptyBucketStats();
|
|
939
|
+
let after = emptyBucketStats();
|
|
520
940
|
await session.withTransaction(async () => {
|
|
521
941
|
opCountDiff = 0;
|
|
942
|
+
before = emptyBucketStats();
|
|
943
|
+
after = emptyBucketStats();
|
|
522
944
|
const oldStoragePaths = [];
|
|
945
|
+
let oldStorageBytes = 0n;
|
|
523
946
|
const query = collection.find({
|
|
524
947
|
// This is a range query, but should only ever return two documents:
|
|
525
948
|
// 1. The CLEAR op from the previous clearLeadingFullDocuments.
|
|
@@ -536,6 +959,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
536
959
|
min_op: 1,
|
|
537
960
|
checksum: 1,
|
|
538
961
|
count: 1,
|
|
962
|
+
size: 1,
|
|
539
963
|
target_op: 1,
|
|
540
964
|
ops: 1,
|
|
541
965
|
storage_ref: 1
|
|
@@ -547,15 +971,22 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
547
971
|
let clearedOpCount = 0;
|
|
548
972
|
let maxTargetOp = null;
|
|
549
973
|
const boundarySurvivors = [];
|
|
974
|
+
const inputStats = emptyBucketStats();
|
|
550
975
|
for await (const doc of query.stream()) {
|
|
551
976
|
docsRead++;
|
|
552
977
|
if (docsRead > 2) {
|
|
553
978
|
throw new ReplicationAssertionError(`Unexpected extra document before CLEAR boundary in bucket ${bucket}`);
|
|
554
979
|
}
|
|
980
|
+
const documentStats = statsForDocument(doc);
|
|
981
|
+
inputStats.count += documentStats.count;
|
|
982
|
+
inputStats.bytes += documentStats.bytes;
|
|
983
|
+
inputStats.chunks += documentStats.chunks;
|
|
984
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
555
985
|
const isBoundaryDoc = doc._id.o == boundaryDocId.o;
|
|
556
986
|
if (doc.storage_ref) {
|
|
557
987
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
558
988
|
}
|
|
989
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
559
990
|
await hydrateBucketDataDocuments([doc], this.storage.objectStorage, { signal: this.signal });
|
|
560
991
|
maxTargetOp = maxOpId(maxTargetOp, doc.target_op);
|
|
561
992
|
for (const op of loadBucketDataDocument(context, doc)) {
|
|
@@ -605,12 +1036,15 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
605
1036
|
});
|
|
606
1037
|
await collection.insertMany(persisted.documents, { session });
|
|
607
1038
|
await this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, session);
|
|
1039
|
+
await this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, session);
|
|
608
1040
|
opCountDiff = -clearedOpCount + 1;
|
|
1041
|
+
before = inputStats;
|
|
1042
|
+
after = statsForDocuments(persisted.documents);
|
|
609
1043
|
}, {
|
|
610
1044
|
writeConcern: { w: 'majority' },
|
|
611
1045
|
readConcern: { level: 'snapshot' }
|
|
612
1046
|
});
|
|
613
|
-
return opCountDiff;
|
|
1047
|
+
return { opCountDiff, before, after };
|
|
614
1048
|
}
|
|
615
1049
|
/**
|
|
616
1050
|
* Reserve stable object paths before starting a retryable MongoDB transaction.
|
|
@@ -637,6 +1071,16 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
637
1071
|
await this.objectStorageLifecycle.publishUploads(uploads, session);
|
|
638
1072
|
await this.objectStorageLifecycle.retire(Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)), session);
|
|
639
1073
|
}
|
|
1074
|
+
async recordObjectStorageReplacement(oldBytes, newDocuments, definitionId, session) {
|
|
1075
|
+
if (!this.storage.objectStorage) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
let newBytes = 0n;
|
|
1079
|
+
for (const document of newDocuments) {
|
|
1080
|
+
newBytes += ObjectStorageUsage.bytes(document);
|
|
1081
|
+
}
|
|
1082
|
+
await this.objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, session);
|
|
1083
|
+
}
|
|
640
1084
|
async persistBucketData(bucket, chunks, context, preparedUploads, options) {
|
|
641
1085
|
const serializedChunks = chunks.map((chunk) => serializeBucketData(bucket, chunk, options));
|
|
642
1086
|
if (!this.storage.objectStorage) {
|
|
@@ -679,7 +1123,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
679
1123
|
return serialized;
|
|
680
1124
|
}
|
|
681
1125
|
const { ops, ...metadata } = serialized;
|
|
682
|
-
const { fileSize } = await store.store(upload.path, ops);
|
|
1126
|
+
const { fileSize } = await store.store(upload.path, ops, { signal: this.signal });
|
|
683
1127
|
storagePaths.add(upload.path);
|
|
684
1128
|
return {
|
|
685
1129
|
...metadata,
|