@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716
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 +129 -7
- package/dist/storage/MongoBucketStorage.d.ts +42 -5
- package/dist/storage/MongoBucketStorage.js +404 -215
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
- package/dist/storage/implementation/MongoBucketBatch.js +169 -98
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
- package/dist/storage/implementation/MongoIdSequence.js +14 -1
- package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
- 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 +35 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +8 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
- 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 +32 -4
- package/dist/storage/implementation/common/PersistedBatch.js +57 -14
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +9 -1
- package/dist/storage/implementation/db.js +28 -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 +11 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
- package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
- 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 +66 -17
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.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 +14 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
- 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 +92 -32
- 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 +14 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
- 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 +108 -0
- package/dist/storage/implementation/v3/compact-utils.js +223 -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 +90 -6
- 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 +16 -5
- 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 +6 -8
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -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 +197 -25
- 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 +28 -1
- package/dist/types/types.js +28 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -2
- package/dist/utils/test-utils.js +7 -4
- package/dist/utils/test-utils.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 +545 -277
- package/src/storage/implementation/MongoBucketBatch.ts +258 -124
- package/src/storage/implementation/MongoCompactor.ts +37 -346
- package/src/storage/implementation/MongoIdSequence.ts +21 -1
- package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
- package/src/storage/implementation/MongoStorageProvider.ts +16 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +76 -20
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +36 -2
- package/src/storage/implementation/models.ts +39 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
- package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- 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 +339 -247
- package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
- 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 +112 -42
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
- 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 +329 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +97 -4
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +9 -5
- 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 +794 -205
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
- package/test/src/compact-utils.test.ts +251 -0
- package/test/src/concurrent_writers.test.ts +599 -0
- package/test/src/connection_config.test.ts +159 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/helpers/concurrentWriter.mjs +29 -0
- package/test/src/helpers/s3TestFactory.ts +12 -12
- package/test/src/object_storage_usage.test.ts +268 -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 +280 -24
- package/test/src/storage_compacting.test.ts +1097 -253
- package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
- package/test/src/storage_s3_checksums.test.ts +6 -7
- package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
- package/test/src/storage_s3_reading.test.ts +294 -6
- package/test/src/storage_s3_writing.test.ts +4 -3
- package/test/src/storage_sync.test.ts +412 -80
- package/test/src/storeCurrentData.test.ts +5 -5
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,169 +1,649 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
+
import { acquireSemaphoreAbortable, addChecksums, formatBytes, utils } from '@powersync/service-core';
|
|
3
|
+
import { setImmediate } from 'node:timers/promises';
|
|
4
4
|
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
5
5
|
import { cacheKey } from '../OperationBatch.js';
|
|
6
6
|
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
7
7
|
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
8
8
|
import { DEFAULT_MAX_DOC_SIZE_BYTES } from './chunking.js';
|
|
9
|
+
import { applyStatsReplacement, bucketStats, chooseCompactionKind, chooseRequestedCompactionKind, combineAdjacentStats, combineChunkStats, CompactionContext, CompactionKind, emptyBucketStats, firstUncompactedWrite, readCompactionBatch, statsForDocument, statsForDocuments, unclaimedSnapshotFilter } from './compact-utils.js';
|
|
10
|
+
import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
|
|
11
|
+
import { AVAILABLE_LEASE_EXPR, CompactionLease } from './CompactionLease.js';
|
|
9
12
|
import { BucketDataObjectStorage, hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
10
13
|
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
|
-
}
|
|
14
|
+
import { createObjectStorageUsageWriterId, ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
|
|
15
|
+
const DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS = 2 * 60 * 60 * 1000;
|
|
16
|
+
const DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const DEFAULT_COMPACT_LEASE_DURATION_MS = 10 * 60 * 1000;
|
|
18
|
+
const SCHEDULED_COMPACTION_BATCH_SIZE = 100;
|
|
43
19
|
export class MongoCompactorV3 extends MongoCompactor {
|
|
20
|
+
minCompactChunkIntervalMs;
|
|
21
|
+
minCompactFullIntervalMs;
|
|
22
|
+
maxCompactFullIntervalMs;
|
|
23
|
+
compactLeaseDurationMs;
|
|
24
|
+
maxOpIdCap;
|
|
25
|
+
forceChunkCompaction;
|
|
26
|
+
objectStorageUsage;
|
|
27
|
+
constructor(bucketStorage, db, options) {
|
|
28
|
+
super(bucketStorage, db, options);
|
|
29
|
+
this.minCompactChunkIntervalMs = options.minCompactChunkIntervalMs ?? DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS;
|
|
30
|
+
this.minCompactFullIntervalMs = options.minCompactFullIntervalMs ?? DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS;
|
|
31
|
+
this.maxCompactFullIntervalMs = options.maxCompactFullIntervalMs ?? DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS;
|
|
32
|
+
this.compactLeaseDurationMs = options.compactLeaseDurationMs ?? DEFAULT_COMPACT_LEASE_DURATION_MS;
|
|
33
|
+
this.maxOpIdCap = options.maxOpId;
|
|
34
|
+
this.forceChunkCompaction = options.forceChunkCompaction ?? false;
|
|
35
|
+
this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId());
|
|
36
|
+
}
|
|
44
37
|
async compact() {
|
|
45
38
|
if (this.storage.objectStorage) {
|
|
46
39
|
// Clean these before compacting - should be quick in most cases.
|
|
47
40
|
try {
|
|
48
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
41
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
49
42
|
}
|
|
50
43
|
catch (e) {
|
|
44
|
+
if (this.signal?.aborted) {
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
51
47
|
// In this case, still continue normal compact process
|
|
52
48
|
this.logger.error(`Failed to clean up object storage deletion markers before compaction`, e);
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
|
-
await
|
|
51
|
+
await this.deleteOldCheckpointRequests();
|
|
52
|
+
if (this.buckets != null) {
|
|
53
|
+
await this.compactExplicitBuckets(this.buckets);
|
|
54
|
+
}
|
|
55
|
+
else if (this.compactChunksOnly) {
|
|
56
|
+
// Writers defer their first chunk-compaction check by this fixed default.
|
|
57
|
+
// Include that interval so this synchronous initial-replication pass
|
|
58
|
+
// processes the work that existed when it started.
|
|
59
|
+
await this.compactScheduledBuckets({
|
|
60
|
+
dueAheadMs: DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS,
|
|
61
|
+
requestedKind: CompactionKind.Chunks
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await this.compactScheduledBuckets();
|
|
66
|
+
}
|
|
56
67
|
if (this.storage.objectStorage) {
|
|
57
68
|
// Cleanup for any produced during compacting.
|
|
58
69
|
// Note that markers only expire after a delay, so this may skip many produced during this compact
|
|
59
70
|
// run. However, during long compact runs, this may also have many ones it can clean up.
|
|
60
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
71
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
72
|
+
await this.objectStorageUsage.foldStaleWriterDeltas();
|
|
61
73
|
}
|
|
74
|
+
return this.compactedBucketCount;
|
|
62
75
|
}
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
76
|
+
async deleteOldCustomCheckpointRequests() {
|
|
77
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const collection of await this.db.listCustomCheckpointRequestCollections(this.group_id)) {
|
|
81
|
+
this.signal?.throwIfAborted();
|
|
82
|
+
await collection.deleteMany({
|
|
83
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore }
|
|
84
|
+
});
|
|
66
85
|
}
|
|
67
|
-
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
68
86
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
/** An explicit compact request always runs a full compact for its buckets. */
|
|
88
|
+
async compactExplicitBuckets(buckets) {
|
|
89
|
+
for (const bucket of buckets) {
|
|
90
|
+
// This is not a super efficient query, but this is not a common use case.
|
|
91
|
+
// May be optimized later.
|
|
92
|
+
const states = await this.db
|
|
93
|
+
.bucketState(this.group_id)
|
|
94
|
+
.find({ '_id.b': bucket }, { projection: { _id: 1 } })
|
|
95
|
+
.toArray();
|
|
96
|
+
for (const state of states) {
|
|
97
|
+
await using lease = await this.claimBucket({ _id: state._id });
|
|
98
|
+
if (lease == null || lease.state.first_uncompacted_write == null) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (this.isCompactionTargetCovered(lease.state, CompactionKind.Full)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const decision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
105
|
+
await this.compactClaimedBucket(lease, CompactionKind.Full, decision);
|
|
106
|
+
}
|
|
72
107
|
}
|
|
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
108
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Process scheduled work in bounded batches.
|
|
111
|
+
*
|
|
112
|
+
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
113
|
+
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
114
|
+
*
|
|
115
|
+
* Chunk merges overlap a bounded number of buckets. Full compaction stays
|
|
116
|
+
* sequential because its working set includes operation deduplication state.
|
|
117
|
+
*
|
|
118
|
+
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
119
|
+
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
120
|
+
* filtered out when claiming a compaction lease.
|
|
121
|
+
*
|
|
122
|
+
* We filter scheduled jobs by the job start date, so that the same bucket is not compacted
|
|
123
|
+
* multiple times in one run. Reschedules fall beyond the fixed boundary. For the run after
|
|
124
|
+
* initial replication, dueAheadMs extends that boundary to include the first deferred interval.
|
|
125
|
+
*/
|
|
126
|
+
async compactScheduledBuckets(options = {}) {
|
|
127
|
+
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
128
|
+
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
129
|
+
// the exact initial-replication interval.
|
|
130
|
+
const jobStartedAt = await this.readCompactionTime();
|
|
131
|
+
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
132
|
+
const requestedKind = options.requestedKind;
|
|
133
|
+
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
134
|
+
// Keep accounting documents bounded by workers, not buckets or scan batches.
|
|
135
|
+
const workerUsage = Array.from({ length: this.storage.factory.chunkCompactionConcurrency }, () => new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId()));
|
|
136
|
+
while (true) {
|
|
137
|
+
this.signal?.throwIfAborted();
|
|
138
|
+
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
139
|
+
if (states.length == 0) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
// Keep eligibility bounded by dueBefore, but classify with the current
|
|
143
|
+
// server time so buckets that age into full compaction can advance.
|
|
144
|
+
const batchStartedAt = await this.readCompactionTime();
|
|
145
|
+
const scheduled = [];
|
|
146
|
+
for (const state of states) {
|
|
147
|
+
try {
|
|
148
|
+
scheduled.push({
|
|
149
|
+
state,
|
|
150
|
+
decision: chooseCompactionKind(state, batchStartedAt, this),
|
|
151
|
+
selectedKind: chooseRequestedCompactionKind(state, requestedKind, this)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const noOpStates = scheduled.filter(({ state, decision, selectedKind }) => state.compact_lease == null && (requestedKind == null ? decision.kind : selectedKind) == null);
|
|
159
|
+
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
160
|
+
const processBucket = async ({ state, decision, selectedKind }, objectStorageUsage, chunksOnly = false) => {
|
|
161
|
+
const kind = requestedKind == null ? decision.kind : selectedKind;
|
|
162
|
+
if (state.compact_lease == null && kind == null) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
167
|
+
if (lease == null) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
171
|
+
const claimedKind = requestedKind == null
|
|
172
|
+
? claimedDecision.kind
|
|
173
|
+
: chooseRequestedCompactionKind(lease.state, requestedKind, this);
|
|
174
|
+
if (chunksOnly && claimedKind === CompactionKind.Full) {
|
|
175
|
+
// The decision changed after scanning. Release the lease without
|
|
176
|
+
// rescheduling; the next batch will classify it with a fresh timestamp.
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (claimedKind == null) {
|
|
180
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
181
|
+
}
|
|
182
|
+
else if (this.isCompactionTargetCovered(lease.state, claimedKind)) {
|
|
183
|
+
// The run cannot advance this kind's watermark without regressing
|
|
184
|
+
// already-published progress. Keep any newer work scheduled.
|
|
185
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore, objectStorageUsage);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (this.signal?.aborted) {
|
|
193
|
+
// When aborted, stop completely, rather than logging and re-scheduling individual buckets.
|
|
194
|
+
// The lease on the current bucket is still released automatically.
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const chunkBuckets = scheduled.filter(({ decision, selectedKind }) => (requestedKind == null ? decision.kind : selectedKind) === CompactionKind.Chunks);
|
|
201
|
+
const sequentialBuckets = scheduled.filter(({ decision, selectedKind }) => (requestedKind == null ? decision.kind : selectedKind) !== CompactionKind.Chunks);
|
|
202
|
+
await this.runChunkCompactionWorkers(chunkBuckets, workerUsage, (entry, usage) => processBucket(entry, usage, true));
|
|
203
|
+
// Full compaction cannot overlap chunk workers from this job, and only
|
|
204
|
+
// one full bucket is processed at a time.
|
|
205
|
+
for (const entry of sequentialBuckets) {
|
|
206
|
+
await processBucket(entry, this.objectStorageUsage);
|
|
207
|
+
}
|
|
79
208
|
}
|
|
80
|
-
return this.dirtyBucketBatchForChecksumsForCollection(this.db.bucketState(this.group_id), {
|
|
81
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
82
|
-
}, (bucketState) => bucketState._id.d);
|
|
83
209
|
}
|
|
84
|
-
|
|
85
|
-
|
|
210
|
+
/** Use MongoDB's clock, matching scheduling and lease timestamps. */
|
|
211
|
+
async readCompactionTime() {
|
|
212
|
+
const [{ now }] = await this.db.db
|
|
213
|
+
.aggregate([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
214
|
+
.toArray();
|
|
215
|
+
return now;
|
|
216
|
+
}
|
|
217
|
+
/** Process one scheduled batch with a fixed pool of workers. */
|
|
218
|
+
async runChunkCompactionWorkers(buckets, workerUsage, processBucket) {
|
|
219
|
+
const signal = this.signal;
|
|
220
|
+
let nextBucket = 0;
|
|
221
|
+
let failed = false;
|
|
222
|
+
const runWorker = async (usage) => {
|
|
223
|
+
try {
|
|
224
|
+
while (!failed && nextBucket < buckets.length) {
|
|
225
|
+
// Taking an entry has no await, so each worker gets a different bucket.
|
|
226
|
+
// A worker takes another only after finishing its current bucket.
|
|
227
|
+
const bucket = buckets[nextBucket++];
|
|
228
|
+
// This pool bounds one job; the factory semaphore bounds all jobs together.
|
|
229
|
+
// Acquire before claiming the bucket lease, and hold until it is released.
|
|
230
|
+
const acquired = await acquireSemaphoreAbortable(this.storage.factory.chunkCompactionSlots, signal);
|
|
231
|
+
if (acquired === 'aborted') {
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const [, releaseSlot] = acquired;
|
|
236
|
+
try {
|
|
237
|
+
// A sibling may have failed while this worker waited for a slot.
|
|
238
|
+
if (failed)
|
|
239
|
+
return;
|
|
240
|
+
signal?.throwIfAborted();
|
|
241
|
+
await processBucket(bucket, usage);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
releaseSlot();
|
|
245
|
+
}
|
|
246
|
+
// Let replication and other event-loop work run between buckets.
|
|
247
|
+
await setImmediate();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
// Drain work already started, but do not let siblings start new buckets.
|
|
252
|
+
failed = true;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
// Concurrent transactions must not all increment the same usage document.
|
|
257
|
+
// Reuse one writer for each worker instead of creating one per bucket.
|
|
258
|
+
const workers = workerUsage.map(runWorker);
|
|
259
|
+
// Do not release the caller's replication lock or run cleanup while a
|
|
260
|
+
// sibling worker still owns a bucket lease or is finishing a replacement.
|
|
261
|
+
// Wait for every worker even on failure, then propagate the first error.
|
|
262
|
+
const results = await Promise.allSettled(workers);
|
|
263
|
+
for (const result of results) {
|
|
264
|
+
if (result.status === 'rejected') {
|
|
265
|
+
throw result.reason;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Read a bounded, priority-ordered snapshot of currently claimable scheduled work. */
|
|
270
|
+
async findScheduledBucketBatch(dueBefore) {
|
|
271
|
+
return this.db
|
|
86
272
|
.bucketState(this.group_id)
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
273
|
+
.find({
|
|
274
|
+
next_compact_check: { $lte: dueBefore },
|
|
275
|
+
...AVAILABLE_LEASE_EXPR
|
|
276
|
+
})
|
|
277
|
+
.sort({ next_compact_check: 1 })
|
|
278
|
+
.limit(SCHEDULED_COMPACTION_BATCH_SIZE)
|
|
279
|
+
.toArray();
|
|
90
280
|
}
|
|
91
281
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
282
|
+
* Reschedule snapshots that were already known to be no-ops without first
|
|
283
|
+
* taking a lease. Every decision input is compared so a concurrent writer
|
|
284
|
+
* or compactor simply makes the update a no-op instead of losing work. A
|
|
285
|
+
* successful reschedule moves beyond this run's fixed selection boundary.
|
|
94
286
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
287
|
+
async rescheduleUnclaimedBuckets(states, notBefore) {
|
|
288
|
+
if (states.length == 0) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await this.db.bucketState(this.group_id).bulkWrite(states.map(({ state, decision }) => ({
|
|
292
|
+
updateOne: {
|
|
293
|
+
filter: unclaimedSnapshotFilter(state),
|
|
294
|
+
update: [{ $set: { next_compact_check: this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore) } }]
|
|
102
295
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
296
|
+
})), { ordered: false });
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Isolate a malformed bucket so it cannot prevent other scheduled buckets
|
|
300
|
+
* from compacting. The snapshot filter preserves any concurrent write or
|
|
301
|
+
* compactor result instead of overwriting its next check.
|
|
302
|
+
*/
|
|
303
|
+
async rescheduleFailedBucket(state, notBefore, error) {
|
|
304
|
+
this.logger.error(`Failed to compact scheduled bucket ${state._id.b}; rescheduling it`, error);
|
|
305
|
+
try {
|
|
306
|
+
await this.db
|
|
307
|
+
.bucketState(this.group_id)
|
|
308
|
+
.updateOne(unclaimedSnapshotFilter(state), [{ $set: { next_compact_check: notBefore } }]);
|
|
309
|
+
}
|
|
310
|
+
catch (rescheduleError) {
|
|
311
|
+
this.logger.error(`Failed to reschedule bucket ${state._id.b} after a compaction error`, rescheduleError);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Given a bucket filter, claim a lease on the bucket. The filter should include a filter on _id.
|
|
316
|
+
*
|
|
317
|
+
* Resolves to null if the bucket is already claimed, not found, or filtered out.
|
|
318
|
+
*/
|
|
319
|
+
async claimBucket(filter, sort) {
|
|
320
|
+
return CompactionLease.claim(this.db.bucketState(this.group_id), filter, sort, this.compactLeaseDurationMs);
|
|
321
|
+
}
|
|
322
|
+
async compactClaimedBucket(lease, kind, decision, rescheduleNotBefore, objectStorageUsage = this.objectStorageUsage) {
|
|
323
|
+
const context = new CompactionContext(lease, kind, decision, rescheduleNotBefore, this.compactionTarget(lease.state));
|
|
324
|
+
lease.startRenewal();
|
|
325
|
+
await this.compactSingleBucket(context, objectStorageUsage);
|
|
326
|
+
}
|
|
327
|
+
async rescheduleClaimedBucket(lease, decision, notBefore) {
|
|
328
|
+
await lease.reschedule(this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore));
|
|
329
|
+
}
|
|
330
|
+
rescheduleAtOrAfter(nextCompactCheck, notBefore) {
|
|
331
|
+
return notBefore == null ? nextCompactCheck : { $max: [nextCompactCheck, notBefore] };
|
|
109
332
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
333
|
+
compactionTarget(state) {
|
|
334
|
+
return this.maxOpIdCap == null || state.last_op < this.maxOpIdCap ? state.last_op : this.maxOpIdCap;
|
|
335
|
+
}
|
|
336
|
+
isCompactionTargetCovered(state, kind) {
|
|
337
|
+
const target = this.compactionTarget(state);
|
|
338
|
+
if (kind == CompactionKind.Chunks) {
|
|
339
|
+
return state.compacted_state != null && state.compacted_state.op_id >= target;
|
|
113
340
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
341
|
+
if (state.last_full_compact != null && state.last_full_compact.op_id >= target) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
// A full compact may change counts before the checksum-cache boundary.
|
|
345
|
+
// Wait for the safe target to catch up instead of publishing an older or
|
|
346
|
+
// stale cache. At the same boundary, full coverage can still advance.
|
|
347
|
+
return state.compacted_state != null && state.compacted_state.op_id > target;
|
|
348
|
+
}
|
|
349
|
+
get objectStorageLifecycle() {
|
|
350
|
+
if (!this.storage.objectStorage) {
|
|
351
|
+
throw new Error('Object storage is not configured');
|
|
352
|
+
}
|
|
353
|
+
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
354
|
+
}
|
|
355
|
+
async compactSingleBucket(context, objectStorageUsage) {
|
|
356
|
+
if (context.kind == CompactionKind.Chunks) {
|
|
357
|
+
return this.compactSingleBucketChunks(context, objectStorageUsage);
|
|
358
|
+
}
|
|
359
|
+
return this.compactSingleBucketFully(context, objectStorageUsage);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Merge adjacent bucket-data chunks without inspecting their operations
|
|
363
|
+
* unless a merge is possible. The metadata contains enough information to
|
|
364
|
+
* update the persisted checksum state and to decide whether a group can fit
|
|
365
|
+
* in one chunk.
|
|
366
|
+
*/
|
|
367
|
+
async compactSingleBucketChunks(context, objectStorageUsage) {
|
|
368
|
+
const bucket = context.state._id.b;
|
|
369
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
370
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
371
|
+
bucket,
|
|
372
|
+
definitionId: resolvedDefinitionId,
|
|
373
|
+
replicationStreamId: this.group_id
|
|
374
|
+
});
|
|
375
|
+
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
376
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
377
|
+
let previousCompactedState = context.state.compacted_state;
|
|
378
|
+
// A zero boundary represents an empty prefix, so there is no stored chunk
|
|
379
|
+
// whose statistics need to be carried into this pass.
|
|
380
|
+
if (previousCompactedState?.op_id === 0n) {
|
|
381
|
+
previousCompactedState = undefined;
|
|
382
|
+
}
|
|
383
|
+
// Include the last previously compacted chunk as well as new chunks. It
|
|
384
|
+
// is the only old chunk which can become mergeable with the new tail.
|
|
385
|
+
let lowerBound = previousCompactedState != null ? bucketContext.docId(previousCompactedState.op_id - 1n) : bucketContext.minId;
|
|
386
|
+
const upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
387
|
+
let cachedBoundaryToVerify = previousCompactedState?.op_id;
|
|
388
|
+
let compactedOpId = null;
|
|
389
|
+
let overlappingCompactedChunk;
|
|
390
|
+
let compactedTail = emptyBucketStats();
|
|
391
|
+
let pendingChunks = [];
|
|
392
|
+
let pendingSize = 0;
|
|
393
|
+
while (true) {
|
|
394
|
+
this.signal?.throwIfAborted();
|
|
395
|
+
await context.lease.throwIfLost();
|
|
396
|
+
const batch = await readCompactionBatch(collection.aggregate([
|
|
397
|
+
{
|
|
398
|
+
$match: {
|
|
399
|
+
_id: {
|
|
400
|
+
$gt: lowerBound,
|
|
401
|
+
$lt: upperBound
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
{ $sort: { _id: 1 } },
|
|
406
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
407
|
+
{
|
|
408
|
+
$project: {
|
|
409
|
+
_id: 1,
|
|
410
|
+
min_op: 1,
|
|
411
|
+
checksum: 1,
|
|
412
|
+
count: 1,
|
|
413
|
+
size: 1,
|
|
414
|
+
target_op: 1,
|
|
415
|
+
storage_ref: 1
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
], { batchSize: this.moveBatchQueryLimit + 1 }), {
|
|
419
|
+
byteLimit: this.moveBatchByteLimit,
|
|
420
|
+
documentLimit: this.moveBatchQueryLimit
|
|
421
|
+
});
|
|
422
|
+
if (cachedBoundaryToVerify != null) {
|
|
423
|
+
const cachedBoundary = cachedBoundaryToVerify;
|
|
424
|
+
cachedBoundaryToVerify = undefined;
|
|
425
|
+
if (batch.documents[0]?._id.o !== cachedBoundary) {
|
|
426
|
+
// A previous attempt may have replaced the cached boundary before
|
|
427
|
+
// finalizing bucket state. Keep the persisted cache available to
|
|
428
|
+
// readers, but ignore it in this attempt and calculate its
|
|
429
|
+
// replacement through the normal scan from the bucket beginning.
|
|
430
|
+
previousCompactedState = undefined;
|
|
431
|
+
lowerBound = bucketContext.minId;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
118
434
|
}
|
|
435
|
+
if (batch.documents.length == 0) {
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
for (const doc of batch.documents) {
|
|
439
|
+
compactedOpId = maxOpId(compactedOpId, doc._id.o);
|
|
440
|
+
const documentStats = statsForDocument(doc);
|
|
441
|
+
if (previousCompactedState?.op_id === doc._id.o) {
|
|
442
|
+
overlappingCompactedChunk = documentStats;
|
|
443
|
+
}
|
|
444
|
+
const nextSize = pendingSize + doc.size;
|
|
445
|
+
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
446
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext, objectStorageUsage);
|
|
447
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
448
|
+
pendingChunks = [];
|
|
449
|
+
pendingSize = 0;
|
|
450
|
+
}
|
|
451
|
+
pendingChunks.push(doc);
|
|
452
|
+
pendingSize += doc.size;
|
|
453
|
+
}
|
|
454
|
+
lowerBound = batch.documents[batch.documents.length - 1]._id;
|
|
455
|
+
if (!batch.hasMore) {
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (pendingChunks.length > 0) {
|
|
460
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext, objectStorageUsage);
|
|
461
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
462
|
+
}
|
|
463
|
+
if (compactedOpId == null) {
|
|
464
|
+
await this.finalizeSkippedBucket(context);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const compactedState = previousCompactedState == null
|
|
468
|
+
? compactedTail
|
|
469
|
+
: combineChunkStats(previousCompactedState, compactedTail, overlappingCompactedChunk);
|
|
470
|
+
const tailStats = compactedOpId == context.lastOp
|
|
471
|
+
? undefined
|
|
472
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
473
|
+
const result = {
|
|
474
|
+
compactedState,
|
|
475
|
+
bucketStats: tailStats == null ? compactedState : combineAdjacentStats(compactedState, tailStats)
|
|
119
476
|
};
|
|
477
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: 0 });
|
|
478
|
+
this.compactedBucketCount++;
|
|
479
|
+
this.logger.info(`Compacted bucket chunks ${bucket}: ${result.bucketStats.count} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`);
|
|
120
480
|
}
|
|
121
|
-
async
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
481
|
+
async flushChunkMerge(bucket, inputs, collection, context, bucketContext, objectStorageUsage) {
|
|
482
|
+
if (inputs.length == 1) {
|
|
483
|
+
return statsForDocument(inputs[0]);
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
this.signal?.throwIfAborted();
|
|
487
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
488
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
489
|
+
// the same rule.
|
|
490
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
491
|
+
if (inlineInputs.length > 0) {
|
|
492
|
+
const inlineDocuments = await collection
|
|
493
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
494
|
+
.toArray();
|
|
495
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
496
|
+
for (const input of inlineInputs) {
|
|
497
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
132
498
|
}
|
|
133
499
|
}
|
|
500
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
501
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
502
|
+
const targetOp = inputs.reduce((maxTarget, input) => maxOpId(maxTarget, input.target_op), null);
|
|
503
|
+
const result = await this.flushCompactionGroup(bucket, {
|
|
504
|
+
inputs,
|
|
505
|
+
ops: operations,
|
|
506
|
+
changed: true,
|
|
507
|
+
targetOp
|
|
508
|
+
}, bucketContext, context, objectStorageUsage);
|
|
509
|
+
return result.stats;
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
// The scan batch also references these documents. Do not retain hydrated
|
|
513
|
+
// operations after finishing this merge group.
|
|
514
|
+
for (const input of inputs) {
|
|
515
|
+
delete input.ops;
|
|
516
|
+
}
|
|
517
|
+
await setImmediate();
|
|
134
518
|
}
|
|
135
|
-
|
|
136
|
-
|
|
519
|
+
}
|
|
520
|
+
async finalizeCompactedBucket({ context, compactedOpId, compactionResult, puts }) {
|
|
521
|
+
await context.lease.throwIfLost();
|
|
522
|
+
const startedStats = bucketStats(context.state);
|
|
523
|
+
const delta = {
|
|
524
|
+
count: compactionResult.bucketStats.count - startedStats.count,
|
|
525
|
+
bytes: compactionResult.bucketStats.bytes - startedStats.bytes,
|
|
526
|
+
chunks: compactionResult.bucketStats.chunks - startedStats.chunks
|
|
527
|
+
};
|
|
528
|
+
const coveredClaimedHead = compactedOpId >= context.lastOp;
|
|
529
|
+
const concurrentWriteCheck = { $gt: ['$last_op', context.lastOp] };
|
|
530
|
+
const remainingFullWorkCheck = coveredClaimedHead ? concurrentWriteCheck : true;
|
|
531
|
+
const nextAfterPartialFullCompact = this.rescheduleAtOrAfter({ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }, context.rescheduleNotBefore);
|
|
532
|
+
const nextCheckForUncompactedWork = this.rescheduleAtOrAfter({
|
|
533
|
+
$min: [
|
|
534
|
+
new Date(firstUncompactedWrite(context.state).getTime() + this.maxCompactFullIntervalMs),
|
|
535
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
536
|
+
]
|
|
537
|
+
}, context.rescheduleNotBefore);
|
|
538
|
+
const update = {
|
|
539
|
+
compacted_state: {
|
|
540
|
+
op_id: compactedOpId,
|
|
541
|
+
checksum: BigInt(compactionResult.compactedState.checksum),
|
|
542
|
+
count: compactionResult.compactedState.count,
|
|
543
|
+
bytes: compactionResult.compactedState.bytes,
|
|
544
|
+
chunks: compactionResult.compactedState.chunks,
|
|
545
|
+
at: '$$NOW'
|
|
546
|
+
},
|
|
547
|
+
bucket_stats: {
|
|
548
|
+
count: { $add: ['$bucket_stats.count', delta.count] },
|
|
549
|
+
bytes: { $add: ['$bucket_stats.bytes', delta.bytes] },
|
|
550
|
+
chunks: { $add: ['$bucket_stats.chunks', delta.chunks] }
|
|
551
|
+
},
|
|
552
|
+
first_uncompacted_write: context.kind == CompactionKind.Full
|
|
553
|
+
? { $cond: [remainingFullWorkCheck, '$$NOW', '$$REMOVE'] }
|
|
554
|
+
: '$first_uncompacted_write',
|
|
555
|
+
next_compact_check: context.kind == CompactionKind.Full
|
|
556
|
+
? { $cond: [remainingFullWorkCheck, nextAfterPartialFullCompact, '$$REMOVE'] }
|
|
557
|
+
: nextCheckForUncompactedWork
|
|
558
|
+
};
|
|
559
|
+
if (context.kind == CompactionKind.Full) {
|
|
560
|
+
update.last_full_compact = {
|
|
561
|
+
op_id: compactedOpId,
|
|
562
|
+
count: compactionResult.compactedState.count,
|
|
563
|
+
puts,
|
|
564
|
+
at: '$$NOW'
|
|
565
|
+
};
|
|
137
566
|
}
|
|
138
|
-
|
|
567
|
+
await context.lease.finalize(update);
|
|
568
|
+
}
|
|
569
|
+
async finalizeSkippedBucket(context) {
|
|
570
|
+
// A maxOpId cap can exclude the first remaining document entirely. Avoid
|
|
571
|
+
// immediately claiming the same no-progress bucket again in this run.
|
|
572
|
+
await this.rescheduleClaimedBucket(context.lease, {
|
|
573
|
+
...context.decision,
|
|
574
|
+
nextCompactCheck: {
|
|
575
|
+
$max: [
|
|
576
|
+
context.decision.nextCompactCheck,
|
|
577
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
}, context.rescheduleNotBefore);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Read bucket stats directly from bucket_data documents.
|
|
584
|
+
*/
|
|
585
|
+
async readBucketStats(bucket, definitionId, maxOp, lowerBound) {
|
|
586
|
+
const context = new BucketDataContextV3(this.db, {
|
|
139
587
|
bucket,
|
|
140
|
-
definitionId
|
|
588
|
+
definitionId,
|
|
141
589
|
replicationStreamId: this.group_id
|
|
142
590
|
});
|
|
591
|
+
const [stats] = await this.db
|
|
592
|
+
.bucketData(this.group_id, definitionId)
|
|
593
|
+
.aggregate([
|
|
594
|
+
{
|
|
595
|
+
$match: {
|
|
596
|
+
_id: lowerBound == null
|
|
597
|
+
? { $gte: context.minId, $lte: context.docId(maxOp) }
|
|
598
|
+
: { $gt: lowerBound, $lte: context.docId(maxOp) }
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
$group: {
|
|
603
|
+
_id: null,
|
|
604
|
+
count: { $sum: '$count' },
|
|
605
|
+
bytes: { $sum: '$size' },
|
|
606
|
+
chunks: { $sum: 1 },
|
|
607
|
+
checksum: { $sum: '$checksum' }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
])
|
|
611
|
+
.toArray();
|
|
612
|
+
return {
|
|
613
|
+
count: Number(stats?.count ?? 0),
|
|
614
|
+
bytes: BigInt(stats?.bytes ?? 0),
|
|
615
|
+
chunks: Number(stats?.chunks ?? 0),
|
|
616
|
+
checksum: typeof stats?.checksum == 'bigint'
|
|
617
|
+
? Number(BigInt.asIntN(32, stats.checksum))
|
|
618
|
+
: addChecksums(0, Number(stats?.checksum ?? 0))
|
|
619
|
+
};
|
|
143
620
|
}
|
|
144
|
-
async
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
621
|
+
async compactSingleBucketFully(context, objectStorageUsage) {
|
|
622
|
+
const bucket = context.state._id.b;
|
|
623
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
624
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
625
|
+
bucket,
|
|
626
|
+
definitionId: resolvedDefinitionId,
|
|
627
|
+
replicationStreamId: this.group_id
|
|
628
|
+
});
|
|
150
629
|
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
151
|
-
const
|
|
630
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
152
631
|
const lowerBound = bucketContext.minId;
|
|
153
|
-
let upperBound = bucketContext.docId(
|
|
154
|
-
let totalChecksum = 0;
|
|
632
|
+
let upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
155
633
|
let totalOpCount = 0;
|
|
156
|
-
let totalOpBytes = 0;
|
|
157
634
|
let lastNotPut = null;
|
|
158
635
|
let opsSincePut = 0;
|
|
159
636
|
let compactedOpId = null;
|
|
160
637
|
let clearBoundary = null;
|
|
638
|
+
let compactedStats = emptyBucketStats();
|
|
161
639
|
const seen = new Map();
|
|
162
640
|
let trackingSize = 0;
|
|
641
|
+
let putCount = 0;
|
|
163
642
|
let pendingGroup = null;
|
|
164
643
|
// --- Read batch from MongoDB ---
|
|
165
644
|
while (true) {
|
|
166
645
|
this.signal?.throwIfAborted();
|
|
646
|
+
await context.lease.throwIfLost();
|
|
167
647
|
const pipeline = [
|
|
168
648
|
{
|
|
169
649
|
$match: {
|
|
@@ -205,7 +685,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
205
685
|
// merging is useful, and writes each final object at most once.
|
|
206
686
|
for (const doc of batchDocs) {
|
|
207
687
|
compactedOpId ??= doc._id.o;
|
|
208
|
-
const originalOps = Array.from(loadBucketDataDocument(
|
|
688
|
+
const originalOps = Array.from(loadBucketDataDocument(dataContext, doc));
|
|
209
689
|
let changed = false;
|
|
210
690
|
const compactedOps = [];
|
|
211
691
|
let maxTargetOp = doc.target_op ?? null;
|
|
@@ -223,6 +703,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
223
703
|
row_id: undefined,
|
|
224
704
|
source_table: undefined,
|
|
225
705
|
source_key: undefined,
|
|
706
|
+
subkey: undefined,
|
|
226
707
|
data: null
|
|
227
708
|
});
|
|
228
709
|
changed = true;
|
|
@@ -238,6 +719,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
238
719
|
}
|
|
239
720
|
compactedOps.push(op);
|
|
240
721
|
if (op.op == 'PUT') {
|
|
722
|
+
putCount++;
|
|
241
723
|
lastNotPut = null;
|
|
242
724
|
opsSincePut = 0;
|
|
243
725
|
}
|
|
@@ -260,10 +742,6 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
260
742
|
}
|
|
261
743
|
}
|
|
262
744
|
compactedOps.reverse();
|
|
263
|
-
for (const op of compactedOps) {
|
|
264
|
-
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
265
|
-
totalOpBytes += op.data?.length ?? 0;
|
|
266
|
-
}
|
|
267
745
|
totalOpCount += compactedOps.length;
|
|
268
746
|
const candidate = {
|
|
269
747
|
inputs: [doc],
|
|
@@ -287,11 +765,12 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
287
765
|
}
|
|
288
766
|
else {
|
|
289
767
|
const flushedGroup = pendingGroup;
|
|
290
|
-
const
|
|
768
|
+
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext, objectStorageUsage);
|
|
769
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
291
770
|
if (lastNotPut != null &&
|
|
292
771
|
flushedGroup.ops[0].o <= lastNotPut &&
|
|
293
772
|
flushedGroup.ops[flushedGroup.ops.length - 1].o >= lastNotPut) {
|
|
294
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
773
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
295
774
|
}
|
|
296
775
|
pendingGroup = candidate;
|
|
297
776
|
}
|
|
@@ -305,14 +784,16 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
305
784
|
this.logger.info(`Compacted batch of ${batchDocs.length} documents for bucket ${bucket}`);
|
|
306
785
|
}
|
|
307
786
|
if (pendingGroup != null) {
|
|
308
|
-
const
|
|
787
|
+
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext, objectStorageUsage);
|
|
788
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
309
789
|
if (lastNotPut != null &&
|
|
310
790
|
pendingGroup.ops[0].o <= lastNotPut &&
|
|
311
791
|
pendingGroup.ops[pendingGroup.ops.length - 1].o >= lastNotPut) {
|
|
312
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
792
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
313
793
|
}
|
|
314
794
|
}
|
|
315
795
|
if (compactedOpId == null) {
|
|
796
|
+
await this.finalizeSkippedBucket(context);
|
|
316
797
|
return;
|
|
317
798
|
}
|
|
318
799
|
// --- Clear: collapse leading MOVE/REMOVE/CLEAR sequence ---
|
|
@@ -320,25 +801,21 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
320
801
|
if (clearBoundary == null || clearBoundary.opId != lastNotPut) {
|
|
321
802
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
322
803
|
}
|
|
323
|
-
|
|
804
|
+
const clearResult = await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, dataContext, objectStorageUsage);
|
|
805
|
+
totalOpCount += clearResult.opCountDiff;
|
|
806
|
+
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
324
807
|
}
|
|
808
|
+
const tailStats = compactedOpId == context.lastOp
|
|
809
|
+
? undefined
|
|
810
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
811
|
+
const result = {
|
|
812
|
+
compactedState: compactedStats,
|
|
813
|
+
bucketStats: tailStats == null ? compactedStats : combineAdjacentStats(compactedStats, tailStats)
|
|
814
|
+
};
|
|
325
815
|
// --- 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`);
|
|
816
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: putCount });
|
|
817
|
+
this.compactedBucketCount++;
|
|
818
|
+
this.logger.info(`Compacted bucket ${bucket}: ${totalOpCount} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`);
|
|
342
819
|
}
|
|
343
820
|
/**
|
|
344
821
|
* Persist replacement objects before starting the transaction, then atomically
|
|
@@ -346,9 +823,12 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
346
823
|
* If verification or the transaction fails, the prepared markers retain enough
|
|
347
824
|
* information for the uploaded objects to be cleaned up later.
|
|
348
825
|
*/
|
|
349
|
-
async flushCompactionGroup(bucket, group, bucketContext, context) {
|
|
826
|
+
async flushCompactionGroup(bucket, group, bucketContext, context, objectStorageUsage) {
|
|
350
827
|
if (group.inputs.length == 1 && !group.changed) {
|
|
351
|
-
return
|
|
828
|
+
return {
|
|
829
|
+
documentId: group.inputs[0]._id,
|
|
830
|
+
stats: statsForDocument(group.inputs[0])
|
|
831
|
+
};
|
|
352
832
|
}
|
|
353
833
|
const inputs = group.inputs;
|
|
354
834
|
const idsToDelete = inputs.map((doc) => doc._id);
|
|
@@ -356,6 +836,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
356
836
|
const expectedChecksum = inputs.reduce((sum, doc) => sum + doc.checksum, 0n);
|
|
357
837
|
const expectedOpCount = inputs.reduce((sum, doc) => sum + doc.count, 0);
|
|
358
838
|
const oldStoragePaths = inputs.flatMap((doc) => (doc.storage_ref ? [doc.storage_ref.path] : []));
|
|
839
|
+
const oldStorageBytes = inputs.reduce((sum, document) => sum + ObjectStorageUsage.bytes(document), 0n);
|
|
359
840
|
const { documents, storagePaths: newStoragePaths, uploads } = await this.persistBucketData(bucket, [group.ops], context, undefined, { targetOp: group.targetOp });
|
|
360
841
|
const session = this.db.client.startSession();
|
|
361
842
|
try {
|
|
@@ -379,9 +860,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
379
860
|
verification.opCountSum !== expectedOpCount) {
|
|
380
861
|
throw new ConcurrentCompactionError(`Inputs changed while compacting bucket ${bucket}; restarting from the latest bucket state`);
|
|
381
862
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
863
|
+
// Replacement documents can reuse deleted IDs, so retain delete-before-insert ordering.
|
|
864
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
865
|
+
writes.deleteMany(bucketContext.collection, { _id: { $in: idsToDelete } });
|
|
866
|
+
writes.insertMany(bucketContext.collection, documents);
|
|
867
|
+
this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes);
|
|
868
|
+
this.recordObjectStorageReplacement(oldStorageBytes, documents, context.definitionId, writes, objectStorageUsage);
|
|
869
|
+
await writes.execute();
|
|
385
870
|
}, {
|
|
386
871
|
writeConcern: { w: 'majority' },
|
|
387
872
|
readConcern: { level: 'snapshot' }
|
|
@@ -390,7 +875,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
390
875
|
finally {
|
|
391
876
|
await session.endSession();
|
|
392
877
|
}
|
|
393
|
-
return
|
|
878
|
+
return {
|
|
879
|
+
documentId: documents[0]._id,
|
|
880
|
+
stats: statsForDocuments(documents)
|
|
881
|
+
};
|
|
394
882
|
}
|
|
395
883
|
/**
|
|
396
884
|
* Collapse the leading sequence of MOVE/REMOVE/CLEAR ops at the start
|
|
@@ -398,39 +886,52 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
398
886
|
* before the known boundary document, then splits that boundary document
|
|
399
887
|
* if it contains ops on both sides of lastNotPut.
|
|
400
888
|
*
|
|
401
|
-
* Returns the op count
|
|
889
|
+
* Returns the op count and stored-stat changes after replacing cleared ops
|
|
890
|
+
* with CLEAR ops.
|
|
402
891
|
*/
|
|
403
|
-
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
892
|
+
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
404
893
|
let opCountDiff = 0;
|
|
894
|
+
let before = emptyBucketStats();
|
|
895
|
+
let after = emptyBucketStats();
|
|
405
896
|
const session = this.db.client.startSession();
|
|
406
897
|
try {
|
|
407
898
|
let done = false;
|
|
408
899
|
// First step is to clear full chunks that contain only CLEAR/MOVE/REMOVE operations.
|
|
409
900
|
// There can be many of them, so we do one batch at a time.
|
|
410
901
|
while (!done) {
|
|
411
|
-
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context);
|
|
902
|
+
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage);
|
|
412
903
|
done = batch.done;
|
|
413
904
|
opCountDiff += batch.opCountDiff;
|
|
905
|
+
before = combineAdjacentStats(before, batch.before);
|
|
906
|
+
after = combineAdjacentStats(after, batch.after);
|
|
414
907
|
}
|
|
415
908
|
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
416
909
|
// potentially followed by PUT operations. This is only a single document, so no need for batching.
|
|
417
|
-
|
|
910
|
+
const boundaryResult = await this.clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage);
|
|
911
|
+
opCountDiff += boundaryResult.opCountDiff;
|
|
912
|
+
before = combineAdjacentStats(before, boundaryResult.before);
|
|
913
|
+
after = combineAdjacentStats(after, boundaryResult.after);
|
|
418
914
|
}
|
|
419
915
|
finally {
|
|
420
916
|
await session.endSession();
|
|
421
917
|
}
|
|
422
|
-
return opCountDiff;
|
|
918
|
+
return { opCountDiff, before, after };
|
|
423
919
|
}
|
|
424
|
-
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
920
|
+
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
425
921
|
const bucket = bucketContext.key.bucket;
|
|
426
922
|
this.signal?.throwIfAborted();
|
|
427
|
-
|
|
923
|
+
let prepared;
|
|
428
924
|
let done = false;
|
|
429
925
|
let opCountDiff = 0;
|
|
926
|
+
let before = emptyBucketStats();
|
|
927
|
+
let after = emptyBucketStats();
|
|
430
928
|
await session.withTransaction(async () => {
|
|
431
929
|
done = false;
|
|
432
930
|
opCountDiff = 0;
|
|
931
|
+
before = emptyBucketStats();
|
|
932
|
+
after = emptyBucketStats();
|
|
433
933
|
const oldStoragePaths = [];
|
|
934
|
+
let oldStorageBytes = 0n;
|
|
434
935
|
const query = collection.find({
|
|
435
936
|
_id: {
|
|
436
937
|
$gte: bucketContext.minId,
|
|
@@ -444,6 +945,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
444
945
|
min_op: 1,
|
|
445
946
|
checksum: 1,
|
|
446
947
|
count: 1,
|
|
948
|
+
size: 1,
|
|
447
949
|
target_op: 1,
|
|
448
950
|
has_clear_op: 1,
|
|
449
951
|
storage_ref: 1
|
|
@@ -456,14 +958,21 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
456
958
|
let lastDocId = null;
|
|
457
959
|
let clearOpCount = 0;
|
|
458
960
|
let gotNonClearOp = false;
|
|
961
|
+
const inputStats = emptyBucketStats();
|
|
459
962
|
for await (const doc of query.stream()) {
|
|
460
963
|
if (doc.min_op > lastNotPut) {
|
|
461
964
|
throw new ReplicationAssertionError(`Unexpected document before CLEAR boundary with min_op ${doc.min_op} > ${lastNotPut} in bucket ${bucket}`);
|
|
462
965
|
}
|
|
463
966
|
lastDocId = doc._id;
|
|
967
|
+
const documentStats = statsForDocument(doc);
|
|
968
|
+
inputStats.count += documentStats.count;
|
|
969
|
+
inputStats.bytes += documentStats.bytes;
|
|
970
|
+
inputStats.chunks += documentStats.chunks;
|
|
971
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
464
972
|
if (doc.storage_ref) {
|
|
465
973
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
466
974
|
}
|
|
975
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
467
976
|
// The compaction scan established that every operation before the
|
|
468
977
|
// boundary is MOVE/REMOVE/CLEAR. Root metadata is sufficient to fold
|
|
469
978
|
// whole documents into one CLEAR, so avoid downloading their payloads.
|
|
@@ -486,13 +995,15 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
486
995
|
done = true;
|
|
487
996
|
return;
|
|
488
997
|
}
|
|
998
|
+
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
489
999
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
490
|
-
|
|
1000
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1001
|
+
writes.deleteMany(collection, {
|
|
491
1002
|
_id: {
|
|
492
1003
|
$gte: bucketContext.minId,
|
|
493
1004
|
$lte: lastDocId
|
|
494
1005
|
}
|
|
495
|
-
}
|
|
1006
|
+
});
|
|
496
1007
|
const clearOp = {
|
|
497
1008
|
bucketKey: { ...context, bucket },
|
|
498
1009
|
o: lastDocId.o,
|
|
@@ -503,23 +1014,32 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
503
1014
|
const persisted = await this.persistBucketData(bucket, [[clearOp]], context, prepared, {
|
|
504
1015
|
targetOp: maxTargetOp
|
|
505
1016
|
});
|
|
506
|
-
|
|
507
|
-
|
|
1017
|
+
writes.insertOne(collection, persisted.documents[0]);
|
|
1018
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1019
|
+
this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, writes, objectStorageUsage);
|
|
1020
|
+
await writes.execute();
|
|
508
1021
|
opCountDiff = -clearedOpCount + 1;
|
|
1022
|
+
before = inputStats;
|
|
1023
|
+
after = statsForDocuments(persisted.documents);
|
|
509
1024
|
}, {
|
|
510
1025
|
writeConcern: { w: 'majority' },
|
|
511
1026
|
readConcern: { level: 'snapshot' }
|
|
512
1027
|
});
|
|
513
|
-
return { done, opCountDiff };
|
|
1028
|
+
return { done, opCountDiff, before, after };
|
|
514
1029
|
}
|
|
515
|
-
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
1030
|
+
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
516
1031
|
const bucket = bucketContext.key.bucket;
|
|
517
1032
|
this.signal?.throwIfAborted();
|
|
518
1033
|
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
519
1034
|
let opCountDiff = 0;
|
|
1035
|
+
let before = emptyBucketStats();
|
|
1036
|
+
let after = emptyBucketStats();
|
|
520
1037
|
await session.withTransaction(async () => {
|
|
521
1038
|
opCountDiff = 0;
|
|
1039
|
+
before = emptyBucketStats();
|
|
1040
|
+
after = emptyBucketStats();
|
|
522
1041
|
const oldStoragePaths = [];
|
|
1042
|
+
let oldStorageBytes = 0n;
|
|
523
1043
|
const query = collection.find({
|
|
524
1044
|
// This is a range query, but should only ever return two documents:
|
|
525
1045
|
// 1. The CLEAR op from the previous clearLeadingFullDocuments.
|
|
@@ -536,6 +1056,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
536
1056
|
min_op: 1,
|
|
537
1057
|
checksum: 1,
|
|
538
1058
|
count: 1,
|
|
1059
|
+
size: 1,
|
|
539
1060
|
target_op: 1,
|
|
540
1061
|
ops: 1,
|
|
541
1062
|
storage_ref: 1
|
|
@@ -547,15 +1068,22 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
547
1068
|
let clearedOpCount = 0;
|
|
548
1069
|
let maxTargetOp = null;
|
|
549
1070
|
const boundarySurvivors = [];
|
|
1071
|
+
const inputStats = emptyBucketStats();
|
|
550
1072
|
for await (const doc of query.stream()) {
|
|
551
1073
|
docsRead++;
|
|
552
1074
|
if (docsRead > 2) {
|
|
553
1075
|
throw new ReplicationAssertionError(`Unexpected extra document before CLEAR boundary in bucket ${bucket}`);
|
|
554
1076
|
}
|
|
1077
|
+
const documentStats = statsForDocument(doc);
|
|
1078
|
+
inputStats.count += documentStats.count;
|
|
1079
|
+
inputStats.bytes += documentStats.bytes;
|
|
1080
|
+
inputStats.chunks += documentStats.chunks;
|
|
1081
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
555
1082
|
const isBoundaryDoc = doc._id.o == boundaryDocId.o;
|
|
556
1083
|
if (doc.storage_ref) {
|
|
557
1084
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
558
1085
|
}
|
|
1086
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
559
1087
|
await hydrateBucketDataDocuments([doc], this.storage.objectStorage, { signal: this.signal });
|
|
560
1088
|
maxTargetOp = maxOpId(maxTargetOp, doc.target_op);
|
|
561
1089
|
for (const op of loadBucketDataDocument(context, doc)) {
|
|
@@ -581,12 +1109,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
581
1109
|
throw new Error(`CLEAR boundary document not found for bucket ${bucket}`);
|
|
582
1110
|
}
|
|
583
1111
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastNotPut}`);
|
|
584
|
-
|
|
1112
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1113
|
+
writes.deleteMany(collection, {
|
|
585
1114
|
_id: {
|
|
586
1115
|
$gte: bucketContext.minId,
|
|
587
1116
|
$lte: boundaryDocId
|
|
588
1117
|
}
|
|
589
|
-
}
|
|
1118
|
+
});
|
|
590
1119
|
const clearOp = {
|
|
591
1120
|
bucketKey: { ...context, bucket },
|
|
592
1121
|
o: lastNotPut,
|
|
@@ -603,14 +1132,18 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
603
1132
|
const persisted = await this.persistBucketData(bucket, chunks, context, prepared, {
|
|
604
1133
|
targetOp: maxTargetOp ?? undefined
|
|
605
1134
|
});
|
|
606
|
-
|
|
607
|
-
|
|
1135
|
+
writes.insertMany(collection, persisted.documents);
|
|
1136
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1137
|
+
this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, writes, objectStorageUsage);
|
|
1138
|
+
await writes.execute();
|
|
608
1139
|
opCountDiff = -clearedOpCount + 1;
|
|
1140
|
+
before = inputStats;
|
|
1141
|
+
after = statsForDocuments(persisted.documents);
|
|
609
1142
|
}, {
|
|
610
1143
|
writeConcern: { w: 'majority' },
|
|
611
1144
|
readConcern: { level: 'snapshot' }
|
|
612
1145
|
});
|
|
613
|
-
return opCountDiff;
|
|
1146
|
+
return { opCountDiff, before, after };
|
|
614
1147
|
}
|
|
615
1148
|
/**
|
|
616
1149
|
* Reserve stable object paths before starting a retryable MongoDB transaction.
|
|
@@ -630,12 +1163,22 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
630
1163
|
return lifecycle.prepareUploads(paths);
|
|
631
1164
|
}
|
|
632
1165
|
/** Publish replacement uploads and retire superseded objects in the same transaction. */
|
|
633
|
-
|
|
1166
|
+
finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes) {
|
|
634
1167
|
if (!this.storage.objectStorage) {
|
|
635
1168
|
return;
|
|
636
1169
|
}
|
|
637
|
-
|
|
638
|
-
|
|
1170
|
+
this.objectStorageLifecycle.publishUploads(uploads, writes);
|
|
1171
|
+
this.objectStorageLifecycle.retire(Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)), writes);
|
|
1172
|
+
}
|
|
1173
|
+
recordObjectStorageReplacement(oldBytes, newDocuments, definitionId, writes, objectStorageUsage) {
|
|
1174
|
+
if (!this.storage.objectStorage) {
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
let newBytes = 0n;
|
|
1178
|
+
for (const document of newDocuments) {
|
|
1179
|
+
newBytes += ObjectStorageUsage.bytes(document);
|
|
1180
|
+
}
|
|
1181
|
+
objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, writes);
|
|
639
1182
|
}
|
|
640
1183
|
async persistBucketData(bucket, chunks, context, preparedUploads, options) {
|
|
641
1184
|
const serializedChunks = chunks.map((chunk) => serializeBucketData(bucket, chunk, options));
|
|
@@ -679,7 +1222,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
679
1222
|
return serialized;
|
|
680
1223
|
}
|
|
681
1224
|
const { ops, ...metadata } = serialized;
|
|
682
|
-
const { fileSize } = await store.store(upload.path, ops);
|
|
1225
|
+
const { fileSize } = await store.store(upload.path, ops, { signal: this.signal });
|
|
683
1226
|
storagePaths.add(upload.path);
|
|
684
1227
|
return {
|
|
685
1228
|
...metadata,
|