@powersync/service-module-mongodb-storage 0.20.0 → 0.22.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 +62 -0
- package/dist/storage/MongoBucketStorage.d.ts +36 -3
- package/dist/storage/MongoBucketStorage.js +372 -207
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +17 -5
- package/dist/storage/implementation/MongoBucketBatch.js +44 -17
- 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 +7 -3
- 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/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 +49 -6
- 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 +27 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +31 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +33 -4
- 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 +65 -16
- 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 +10 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +100 -26
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +82 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +698 -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 +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 +14 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +76 -42
- 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 +84 -4
- 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 +6 -6
- package/dist/storage/implementation/v3/source-table-utils.js +30 -37
- 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.js +1 -0
- 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 +507 -269
- package/src/storage/implementation/MongoBucketBatch.ts +67 -24
- 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 +15 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- 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 +66 -10
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +35 -2
- package/src/storage/implementation/models.ts +34 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +46 -6
- 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 +87 -27
- 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 +138 -34
- package/src/storage/implementation/v3/MongoCompactorV3.ts +958 -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 +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +93 -48
- 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 +91 -3
- 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 +39 -41
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +1 -0
- 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/chunk_compaction_config.test.ts +38 -0
- 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 +267 -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 +968 -246
- 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 +290 -2
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +336 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,236 +1,866 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import {
|
|
4
|
+
acquireSemaphoreAbortable,
|
|
5
|
+
addChecksums,
|
|
6
|
+
formatBytes,
|
|
7
|
+
InternalOpId,
|
|
8
|
+
storage,
|
|
9
|
+
utils
|
|
10
|
+
} from '@powersync/service-core';
|
|
4
11
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
12
|
+
import { setImmediate } from 'node:timers/promises';
|
|
5
13
|
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
|
-
import { BucketDataKey
|
|
7
|
-
import { ConcurrentCompactionError,
|
|
14
|
+
import { BucketDataKey } from '../models.js';
|
|
15
|
+
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
16
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
8
17
|
import { cacheKey } from '../OperationBatch.js';
|
|
9
18
|
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
10
19
|
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
11
20
|
import { DEFAULT_MAX_DOC_SIZE_BYTES } from './chunking.js';
|
|
21
|
+
import {
|
|
22
|
+
applyStatsReplacement,
|
|
23
|
+
bucketStats,
|
|
24
|
+
BucketStatsWithChecksum,
|
|
25
|
+
chooseCompactionKind,
|
|
26
|
+
combineAdjacentStats,
|
|
27
|
+
combineChunkStats,
|
|
28
|
+
CompactIntervalConfig,
|
|
29
|
+
CompactionContext,
|
|
30
|
+
CompactionDecision,
|
|
31
|
+
CompactionKind,
|
|
32
|
+
CompactionResult,
|
|
33
|
+
CompactTargetConfig,
|
|
34
|
+
emptyBucketStats,
|
|
35
|
+
firstUncompactedWrite,
|
|
36
|
+
forcedCompactionKind,
|
|
37
|
+
PendingCompactionGroup,
|
|
38
|
+
readCompactionBatch,
|
|
39
|
+
ScheduledCompactionOptions,
|
|
40
|
+
statsForDocument,
|
|
41
|
+
statsForDocuments,
|
|
42
|
+
unclaimedSnapshotFilter
|
|
43
|
+
} from './compact-utils.js';
|
|
44
|
+
import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
|
|
45
|
+
import { AVAILABLE_LEASE_EXPR, CompactionLease } from './CompactionLease.js';
|
|
12
46
|
import { BucketDataDocumentV3, BucketStateDocumentV3 } from './models.js';
|
|
13
|
-
import { DefinitionChecksumOperations, MongoChecksumsV3 } from './MongoChecksumsV3.js';
|
|
14
47
|
import type { MongoSyncBucketStorageV3 } from './MongoSyncBucketStorageV3.js';
|
|
15
48
|
import { BucketDataObjectStorage, hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
16
49
|
import { ObjectStorageLifecycle, PreparedObjectStorageUpload } from './object-storage/ObjectStorageLifecycle.js';
|
|
50
|
+
import { createObjectStorageUsageWriterId, ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
|
|
17
51
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
18
52
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*/
|
|
24
|
-
inputs: BucketDataDocumentV3[];
|
|
25
|
-
ops: BucketDataDoc[];
|
|
26
|
-
changed: boolean;
|
|
27
|
-
targetOp: InternalOpId | null;
|
|
28
|
-
}
|
|
53
|
+
const DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS = 2 * 60 * 60 * 1000;
|
|
54
|
+
const DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
55
|
+
const DEFAULT_COMPACT_LEASE_DURATION_MS = 10 * 60 * 1000;
|
|
56
|
+
const SCHEDULED_COMPACTION_BATCH_SIZE = 100;
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* pagination resumes below the last returned `_id`, so that document remains
|
|
35
|
-
* eligible for the next query. The first document is always accepted to ensure
|
|
36
|
-
* progress when a single document exceeds the configured byte limit.
|
|
37
|
-
*
|
|
38
|
-
* `hasMore` is conservative when the document limit is reached. An extra empty
|
|
39
|
-
* query is preferable to exhausting the cursor just to determine whether the
|
|
40
|
-
* limited MongoDB query contained another document.
|
|
41
|
-
*/
|
|
42
|
-
async function readCompactionBatch(
|
|
43
|
-
cursor: mongo.AggregationCursor<BucketDataDocumentV3>,
|
|
44
|
-
options: { byteLimit: number; documentLimit: number }
|
|
45
|
-
): Promise<{ documents: BucketDataDocumentV3[]; hasMore: boolean }> {
|
|
46
|
-
const documents: BucketDataDocumentV3[] = [];
|
|
47
|
-
let cumulativeBytes = 0;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
for await (const document of cursor) {
|
|
51
|
-
if (documents.length > 0 && cumulativeBytes + document.size > options.byteLimit) {
|
|
52
|
-
return { documents, hasMore: true };
|
|
53
|
-
}
|
|
58
|
+
interface CompactionGroupResult {
|
|
59
|
+
documentId: BucketDataKey;
|
|
60
|
+
stats: BucketStatsWithChecksum;
|
|
61
|
+
}
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
interface CompactionStatsReplacement {
|
|
64
|
+
before: BucketStatsWithChecksum;
|
|
65
|
+
after: BucketStatsWithChecksum;
|
|
66
|
+
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return { documents, hasMore: false };
|
|
63
|
-
} finally {
|
|
64
|
-
await cursor.close();
|
|
65
|
-
}
|
|
68
|
+
interface ClearCompactionResult extends CompactionStatsReplacement {
|
|
69
|
+
opCountDiff: number;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
export class MongoCompactorV3 extends MongoCompactor {
|
|
72
|
+
export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalConfig, CompactTargetConfig {
|
|
69
73
|
declare protected readonly db: VersionedPowerSyncMongoV3;
|
|
70
74
|
declare protected readonly storage: MongoSyncBucketStorageV3;
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
readonly minCompactChunkIntervalMs: number;
|
|
77
|
+
readonly minCompactFullIntervalMs: number;
|
|
78
|
+
readonly maxCompactFullIntervalMs: number;
|
|
79
|
+
readonly compactLeaseDurationMs: number;
|
|
80
|
+
readonly maxOpIdCap: InternalOpId | undefined;
|
|
81
|
+
private readonly objectStorageUsage: ObjectStorageUsage;
|
|
82
|
+
|
|
83
|
+
constructor(bucketStorage: MongoSyncBucketStorageV3, db: VersionedPowerSyncMongoV3, options: storage.CompactOptions) {
|
|
84
|
+
super(bucketStorage, db, options);
|
|
85
|
+
this.minCompactChunkIntervalMs = options.minCompactChunkIntervalMs ?? DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS;
|
|
86
|
+
this.minCompactFullIntervalMs = options.minCompactFullIntervalMs ?? DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS;
|
|
87
|
+
this.maxCompactFullIntervalMs = options.maxCompactFullIntervalMs ?? DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS;
|
|
88
|
+
this.compactLeaseDurationMs = options.compactLeaseDurationMs ?? DEFAULT_COMPACT_LEASE_DURATION_MS;
|
|
89
|
+
this.maxOpIdCap = options.maxOpId;
|
|
90
|
+
this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override async compact(): Promise<number> {
|
|
73
94
|
if (this.storage.objectStorage) {
|
|
74
95
|
// Clean these before compacting - should be quick in most cases.
|
|
75
96
|
try {
|
|
76
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
97
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
77
98
|
} catch (e) {
|
|
99
|
+
if (this.signal?.aborted) {
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
78
102
|
// In this case, still continue normal compact process
|
|
79
103
|
this.logger.error(`Failed to clean up object storage deletion markers before compaction`, e);
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
|
-
await
|
|
106
|
+
await this.deleteOldCheckpointRequests();
|
|
107
|
+
|
|
108
|
+
if (this.buckets != null) {
|
|
109
|
+
await this.compactExplicitBuckets(this.buckets);
|
|
110
|
+
} else if (this.compactChunksOnly) {
|
|
111
|
+
// Writers defer their first chunk-compaction check by this fixed default.
|
|
112
|
+
// Include that interval so this synchronous initial-replication pass
|
|
113
|
+
// processes the work that existed when it started.
|
|
114
|
+
await this.compactScheduledBuckets({
|
|
115
|
+
dueAheadMs: DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS,
|
|
116
|
+
forceKind: CompactionKind.Chunks
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
await this.compactScheduledBuckets();
|
|
120
|
+
}
|
|
83
121
|
if (this.storage.objectStorage) {
|
|
84
122
|
// Cleanup for any produced during compacting.
|
|
85
123
|
// Note that markers only expire after a delay, so this may skip many produced during this compact
|
|
86
124
|
// run. However, during long compact runs, this may also have many ones it can clean up.
|
|
87
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
125
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
126
|
+
await this.objectStorageUsage.foldStaleWriterDeltas();
|
|
88
127
|
}
|
|
128
|
+
return this.compactedBucketCount;
|
|
89
129
|
}
|
|
90
130
|
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
131
|
+
protected override async deleteOldCustomCheckpointRequests() {
|
|
132
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const collection of await this.db.listCustomCheckpointRequestCollections(this.group_id)) {
|
|
137
|
+
this.signal?.throwIfAborted();
|
|
138
|
+
await collection.deleteMany({
|
|
139
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore }
|
|
140
|
+
});
|
|
94
141
|
}
|
|
95
|
-
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
96
142
|
}
|
|
97
143
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
/** An explicit compact request always runs a full compact for its buckets. */
|
|
145
|
+
private async compactExplicitBuckets(buckets: string[]) {
|
|
146
|
+
for (const bucket of buckets) {
|
|
147
|
+
// This is not a super efficient query, but this is not a common use case.
|
|
148
|
+
// May be optimized later.
|
|
149
|
+
const states = await this.db
|
|
150
|
+
.bucketState(this.group_id)
|
|
151
|
+
.find({ '_id.b': bucket }, { projection: { _id: 1 } })
|
|
152
|
+
.toArray();
|
|
153
|
+
for (const state of states) {
|
|
154
|
+
await using lease = await this.claimBucket({ _id: state._id });
|
|
155
|
+
if (lease == null || lease.state.first_uncompacted_write == null) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (this.isCompactionTargetCovered(lease.state, CompactionKind.Full)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const decision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
162
|
+
await this.compactClaimedBucket(lease, CompactionKind.Full, decision);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Process scheduled work in bounded batches.
|
|
169
|
+
*
|
|
170
|
+
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
171
|
+
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
172
|
+
*
|
|
173
|
+
* Chunk merges overlap a bounded number of buckets. Full compaction stays
|
|
174
|
+
* sequential because its working set includes operation deduplication state.
|
|
175
|
+
*
|
|
176
|
+
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
177
|
+
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
178
|
+
* filtered out when claiming a compaction lease.
|
|
179
|
+
*
|
|
180
|
+
* We filter scheduled jobs by the job start date, so that the same bucket is not compacted
|
|
181
|
+
* multiple times in one run. Reschedules fall beyond the fixed boundary. For the run after
|
|
182
|
+
* initial replication, dueAheadMs extends that boundary to include the first deferred interval.
|
|
183
|
+
*/
|
|
184
|
+
private async compactScheduledBuckets(options: ScheduledCompactionOptions = {}) {
|
|
185
|
+
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
186
|
+
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
187
|
+
// the exact initial-replication interval.
|
|
188
|
+
const jobStartedAt = await this.readCompactionTime();
|
|
189
|
+
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
190
|
+
const forceKind = options.forceKind;
|
|
191
|
+
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
192
|
+
// Keep accounting documents bounded by workers, not buckets or scan batches.
|
|
193
|
+
const workerUsage = Array.from(
|
|
194
|
+
{ length: this.storage.factory.chunkCompactionConcurrency },
|
|
195
|
+
() => new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId())
|
|
112
196
|
);
|
|
197
|
+
while (true) {
|
|
198
|
+
this.signal?.throwIfAborted();
|
|
199
|
+
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
200
|
+
if (states.length == 0) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
// Keep eligibility bounded by dueBefore, but classify with the current
|
|
204
|
+
// server time so buckets that age into full compaction can advance.
|
|
205
|
+
const batchStartedAt = await this.readCompactionTime();
|
|
206
|
+
|
|
207
|
+
const scheduled: {
|
|
208
|
+
state: BucketStateDocumentV3;
|
|
209
|
+
decision: CompactionDecision;
|
|
210
|
+
forcedKind: CompactionKind | null;
|
|
211
|
+
}[] = [];
|
|
212
|
+
for (const state of states) {
|
|
213
|
+
try {
|
|
214
|
+
scheduled.push({
|
|
215
|
+
state,
|
|
216
|
+
decision: chooseCompactionKind(state, batchStartedAt, this),
|
|
217
|
+
forcedKind: forcedCompactionKind(state, forceKind, this)
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const noOpStates = scheduled.filter(
|
|
224
|
+
({ state, decision, forcedKind }) =>
|
|
225
|
+
state.compact_lease == null && (forceKind == null ? decision.kind : forcedKind) == null
|
|
226
|
+
);
|
|
227
|
+
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
228
|
+
|
|
229
|
+
const processBucket = async (
|
|
230
|
+
{ state, decision, forcedKind }: (typeof scheduled)[number],
|
|
231
|
+
objectStorageUsage: ObjectStorageUsage,
|
|
232
|
+
chunksOnly = false
|
|
233
|
+
) => {
|
|
234
|
+
const kind = forceKind == null ? decision.kind : forcedKind;
|
|
235
|
+
if (state.compact_lease == null && kind == null) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
241
|
+
if (lease == null) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
245
|
+
const claimedKind =
|
|
246
|
+
forceKind == null ? claimedDecision.kind : forcedCompactionKind(lease.state, forceKind, this);
|
|
247
|
+
if (chunksOnly && claimedKind === CompactionKind.Full) {
|
|
248
|
+
// The decision changed after scanning. Release the lease without
|
|
249
|
+
// rescheduling; the next batch will classify it with a fresh timestamp.
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (claimedKind == null) {
|
|
253
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
254
|
+
} else if (this.isCompactionTargetCovered(lease.state, claimedKind)) {
|
|
255
|
+
// The run cannot advance this kind's watermark without regressing
|
|
256
|
+
// already-published progress. Keep any newer work scheduled.
|
|
257
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
258
|
+
} else {
|
|
259
|
+
await this.compactClaimedBucket(
|
|
260
|
+
lease,
|
|
261
|
+
claimedKind,
|
|
262
|
+
claimedDecision,
|
|
263
|
+
rescheduleNotBefore,
|
|
264
|
+
objectStorageUsage
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (this.signal?.aborted) {
|
|
269
|
+
// When aborted, stop completely, rather than logging and re-scheduling individual buckets.
|
|
270
|
+
// The lease on the current bucket is still released automatically.
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const chunkBuckets = scheduled.filter(
|
|
278
|
+
({ decision, forcedKind }) => (forceKind == null ? decision.kind : forcedKind) === CompactionKind.Chunks
|
|
279
|
+
);
|
|
280
|
+
const sequentialBuckets = scheduled.filter(
|
|
281
|
+
({ decision, forcedKind }) => (forceKind == null ? decision.kind : forcedKind) !== CompactionKind.Chunks
|
|
282
|
+
);
|
|
283
|
+
await this.runChunkCompactionWorkers(chunkBuckets, workerUsage, (entry, usage) =>
|
|
284
|
+
processBucket(entry, usage, true)
|
|
285
|
+
);
|
|
286
|
+
// Full compaction cannot overlap chunk workers from this job, and only
|
|
287
|
+
// one full bucket is processed at a time.
|
|
288
|
+
for (const entry of sequentialBuckets) {
|
|
289
|
+
await processBucket(entry, this.objectStorageUsage);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Use MongoDB's clock, matching scheduling and lease timestamps. */
|
|
295
|
+
private async readCompactionTime(): Promise<Date> {
|
|
296
|
+
const [{ now }] = await this.db.db
|
|
297
|
+
.aggregate<{ now: Date }>([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
298
|
+
.toArray();
|
|
299
|
+
return now;
|
|
113
300
|
}
|
|
114
301
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
302
|
+
/** Process one scheduled batch with a fixed pool of workers. */
|
|
303
|
+
private async runChunkCompactionWorkers<T>(
|
|
304
|
+
buckets: readonly T[],
|
|
305
|
+
workerUsage: readonly ObjectStorageUsage[],
|
|
306
|
+
processBucket: (bucket: T, usage: ObjectStorageUsage) => Promise<void>
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
const signal = this.signal;
|
|
309
|
+
let nextBucket = 0;
|
|
310
|
+
let failed = false;
|
|
311
|
+
|
|
312
|
+
const runWorker = async (usage: ObjectStorageUsage) => {
|
|
313
|
+
try {
|
|
314
|
+
while (!failed && nextBucket < buckets.length) {
|
|
315
|
+
// Taking an entry has no await, so each worker gets a different bucket.
|
|
316
|
+
// A worker takes another only after finishing its current bucket.
|
|
317
|
+
const bucket = buckets[nextBucket++];
|
|
318
|
+
|
|
319
|
+
// This pool bounds one job; the factory semaphore bounds all jobs together.
|
|
320
|
+
// Acquire before claiming the bucket lease, and hold until it is released.
|
|
321
|
+
const acquired = await acquireSemaphoreAbortable(this.storage.factory.chunkCompactionSlots, signal);
|
|
322
|
+
if (acquired === 'aborted') {
|
|
323
|
+
signal?.throwIfAborted();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const [, releaseSlot] = acquired;
|
|
327
|
+
try {
|
|
328
|
+
// A sibling may have failed while this worker waited for a slot.
|
|
329
|
+
if (failed) return;
|
|
330
|
+
signal?.throwIfAborted();
|
|
331
|
+
await processBucket(bucket, usage);
|
|
332
|
+
} finally {
|
|
333
|
+
releaseSlot();
|
|
334
|
+
}
|
|
335
|
+
// Let replication and other event-loop work run between buckets.
|
|
336
|
+
await setImmediate();
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// Drain work already started, but do not let siblings start new buckets.
|
|
340
|
+
failed = true;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Concurrent transactions must not all increment the same usage document.
|
|
346
|
+
// Reuse one writer for each worker instead of creating one per bucket.
|
|
347
|
+
const workers = workerUsage.map(runWorker);
|
|
348
|
+
|
|
349
|
+
// Do not release the caller's replication lock or run cleanup while a
|
|
350
|
+
// sibling worker still owns a bucket lease or is finishing a replacement.
|
|
351
|
+
// Wait for every worker even on failure, then propagate the first error.
|
|
352
|
+
const results = await Promise.allSettled(workers);
|
|
353
|
+
for (const result of results) {
|
|
354
|
+
if (result.status === 'rejected') {
|
|
355
|
+
throw result.reason;
|
|
356
|
+
}
|
|
118
357
|
}
|
|
119
|
-
return this.dirtyBucketBatchForChecksumsForCollection(
|
|
120
|
-
this.db.bucketState(this.group_id) as unknown as mongo.Collection<BucketStateDocumentBase>,
|
|
121
|
-
{
|
|
122
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
123
|
-
} as unknown as mongo.Filter<BucketStateDocumentBase>,
|
|
124
|
-
(bucketState) => (bucketState as BucketStateDocumentV3)._id.d
|
|
125
|
-
);
|
|
126
358
|
}
|
|
127
359
|
|
|
128
|
-
|
|
129
|
-
|
|
360
|
+
/** Read a bounded, priority-ordered snapshot of currently claimable scheduled work. */
|
|
361
|
+
private async findScheduledBucketBatch(dueBefore: Date): Promise<BucketStateDocumentV3[]> {
|
|
362
|
+
return this.db
|
|
130
363
|
.bucketState(this.group_id)
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
364
|
+
.find({
|
|
365
|
+
next_compact_check: { $lte: dueBefore },
|
|
366
|
+
...AVAILABLE_LEASE_EXPR
|
|
367
|
+
})
|
|
368
|
+
.sort({ next_compact_check: 1 })
|
|
369
|
+
.limit(SCHEDULED_COMPACTION_BATCH_SIZE)
|
|
370
|
+
.toArray();
|
|
134
371
|
}
|
|
135
372
|
|
|
136
373
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
374
|
+
* Reschedule snapshots that were already known to be no-ops without first
|
|
375
|
+
* taking a lease. Every decision input is compared so a concurrent writer
|
|
376
|
+
* or compactor simply makes the update a no-op instead of losing work. A
|
|
377
|
+
* successful reschedule moves beyond this run's fixed selection boundary.
|
|
139
378
|
*/
|
|
140
|
-
private
|
|
141
|
-
|
|
379
|
+
private async rescheduleUnclaimedBuckets(
|
|
380
|
+
states: { state: BucketStateDocumentV3; decision: CompactionDecision }[],
|
|
381
|
+
notBefore: Date
|
|
382
|
+
) {
|
|
383
|
+
if (states.length == 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
await this.db.bucketState(this.group_id).bulkWrite(
|
|
387
|
+
states.map(({ state, decision }) => ({
|
|
388
|
+
updateOne: {
|
|
389
|
+
filter: unclaimedSnapshotFilter(state),
|
|
390
|
+
update: [{ $set: { next_compact_check: this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore) } }]
|
|
391
|
+
}
|
|
392
|
+
})),
|
|
393
|
+
{ ordered: false }
|
|
394
|
+
);
|
|
142
395
|
}
|
|
143
396
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Isolate a malformed bucket so it cannot prevent other scheduled buckets
|
|
399
|
+
* from compacting. The snapshot filter preserves any concurrent write or
|
|
400
|
+
* compactor result instead of overwriting its next check.
|
|
401
|
+
*/
|
|
402
|
+
private async rescheduleFailedBucket(state: BucketStateDocumentV3, notBefore: Date, error: unknown) {
|
|
403
|
+
this.logger.error(`Failed to compact scheduled bucket ${state._id.b}; rescheduling it`, error);
|
|
404
|
+
try {
|
|
405
|
+
await this.db
|
|
406
|
+
.bucketState(this.group_id)
|
|
407
|
+
.updateOne(unclaimedSnapshotFilter(state), [{ $set: { next_compact_check: notBefore } }]);
|
|
408
|
+
} catch (rescheduleError) {
|
|
409
|
+
this.logger.error(`Failed to reschedule bucket ${state._id.b} after a compaction error`, rescheduleError);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Given a bucket filter, claim a lease on the bucket. The filter should include a filter on _id.
|
|
415
|
+
*
|
|
416
|
+
* Resolves to null if the bucket is already claimed, not found, or filtered out.
|
|
417
|
+
*/
|
|
418
|
+
private async claimBucket(
|
|
419
|
+
filter: mongo.Filter<BucketStateDocumentV3>,
|
|
420
|
+
sort?: mongo.Sort
|
|
421
|
+
): Promise<CompactionLease | null> {
|
|
422
|
+
return CompactionLease.claim(this.db.bucketState(this.group_id), filter, sort, this.compactLeaseDurationMs);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private async compactClaimedBucket(
|
|
426
|
+
lease: CompactionLease,
|
|
427
|
+
kind: CompactionKind,
|
|
428
|
+
decision: CompactionDecision,
|
|
429
|
+
rescheduleNotBefore?: Date,
|
|
430
|
+
objectStorageUsage = this.objectStorageUsage
|
|
431
|
+
) {
|
|
432
|
+
const context = new CompactionContext(
|
|
433
|
+
lease,
|
|
434
|
+
kind,
|
|
435
|
+
decision,
|
|
436
|
+
rescheduleNotBefore,
|
|
437
|
+
this.compactionTarget(lease.state)
|
|
158
438
|
);
|
|
439
|
+
lease.startRenewal();
|
|
440
|
+
await this.compactSingleBucket(context, objectStorageUsage);
|
|
159
441
|
}
|
|
160
442
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
443
|
+
private async rescheduleClaimedBucket(lease: CompactionLease, decision: CompactionDecision, notBefore?: Date) {
|
|
444
|
+
await lease.reschedule(this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private rescheduleAtOrAfter(nextCompactCheck: mongo.Document, notBefore: Date | undefined): mongo.Document {
|
|
448
|
+
return notBefore == null ? nextCompactCheck : { $max: [nextCompactCheck, notBefore] };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private compactionTarget(state: BucketStateDocumentV3): InternalOpId {
|
|
452
|
+
return this.maxOpIdCap == null || state.last_op < this.maxOpIdCap ? state.last_op : this.maxOpIdCap;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private isCompactionTargetCovered(state: BucketStateDocumentV3, kind: CompactionKind): boolean {
|
|
456
|
+
const target = this.compactionTarget(state);
|
|
457
|
+
if (kind == CompactionKind.Chunks) {
|
|
458
|
+
return state.compacted_state != null && state.compacted_state.op_id >= target;
|
|
167
459
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
460
|
+
if (state.last_full_compact != null && state.last_full_compact.op_id >= target) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
// A full compact may change counts before the checksum-cache boundary.
|
|
464
|
+
// Wait for the safe target to catch up instead of publishing an older or
|
|
465
|
+
// stale cache. At the same boundary, full coverage can still advance.
|
|
466
|
+
return state.compacted_state != null && state.compacted_state.op_id > target;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private get objectStorageLifecycle(): ObjectStorageLifecycle {
|
|
470
|
+
if (!this.storage.objectStorage) {
|
|
471
|
+
throw new Error('Object storage is not configured');
|
|
472
|
+
}
|
|
473
|
+
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async compactSingleBucket(context: CompactionContext, objectStorageUsage: ObjectStorageUsage) {
|
|
477
|
+
if (context.kind == CompactionKind.Chunks) {
|
|
478
|
+
return this.compactSingleBucketChunks(context, objectStorageUsage);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return this.compactSingleBucketFully(context, objectStorageUsage);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Merge adjacent bucket-data chunks without inspecting their operations
|
|
486
|
+
* unless a merge is possible. The metadata contains enough information to
|
|
487
|
+
* update the persisted checksum state and to decide whether a group can fit
|
|
488
|
+
* in one chunk.
|
|
489
|
+
*/
|
|
490
|
+
private async compactSingleBucketChunks(context: CompactionContext, objectStorageUsage: ObjectStorageUsage) {
|
|
491
|
+
const bucket = context.state._id.b;
|
|
492
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
493
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
494
|
+
bucket,
|
|
495
|
+
definitionId: resolvedDefinitionId,
|
|
496
|
+
replicationStreamId: this.group_id
|
|
497
|
+
});
|
|
498
|
+
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
499
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
500
|
+
let previousCompactedState = context.state.compacted_state;
|
|
501
|
+
// A zero boundary represents an empty prefix, so there is no stored chunk
|
|
502
|
+
// whose statistics need to be carried into this pass.
|
|
503
|
+
if (previousCompactedState?.op_id === 0n) {
|
|
504
|
+
previousCompactedState = undefined;
|
|
505
|
+
}
|
|
506
|
+
// Include the last previously compacted chunk as well as new chunks. It
|
|
507
|
+
// is the only old chunk which can become mergeable with the new tail.
|
|
508
|
+
let lowerBound =
|
|
509
|
+
previousCompactedState != null ? bucketContext.docId(previousCompactedState.op_id - 1n) : bucketContext.minId;
|
|
510
|
+
const upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
511
|
+
let cachedBoundaryToVerify = previousCompactedState?.op_id;
|
|
512
|
+
|
|
513
|
+
let compactedOpId: bigint | null = null;
|
|
514
|
+
let overlappingCompactedChunk: BucketStatsWithChecksum | undefined;
|
|
515
|
+
let compactedTail = emptyBucketStats();
|
|
516
|
+
let pendingChunks: BucketDataDocumentV3[] = [];
|
|
517
|
+
let pendingSize = 0;
|
|
518
|
+
|
|
519
|
+
while (true) {
|
|
520
|
+
this.signal?.throwIfAborted();
|
|
521
|
+
await context.lease.throwIfLost();
|
|
522
|
+
|
|
523
|
+
const batch = await readCompactionBatch(
|
|
524
|
+
collection.aggregate<BucketDataDocumentV3>(
|
|
525
|
+
[
|
|
526
|
+
{
|
|
527
|
+
$match: {
|
|
528
|
+
_id: {
|
|
529
|
+
$gt: lowerBound,
|
|
530
|
+
$lt: upperBound
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
{ $sort: { _id: 1 } },
|
|
535
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
536
|
+
{
|
|
537
|
+
$project: {
|
|
538
|
+
_id: 1,
|
|
539
|
+
min_op: 1,
|
|
540
|
+
checksum: 1,
|
|
541
|
+
count: 1,
|
|
542
|
+
size: 1,
|
|
543
|
+
target_op: 1,
|
|
544
|
+
storage_ref: 1
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
{ batchSize: this.moveBatchQueryLimit + 1 }
|
|
549
|
+
),
|
|
550
|
+
{
|
|
551
|
+
byteLimit: this.moveBatchByteLimit,
|
|
552
|
+
documentLimit: this.moveBatchQueryLimit
|
|
553
|
+
}
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
if (cachedBoundaryToVerify != null) {
|
|
557
|
+
const cachedBoundary = cachedBoundaryToVerify;
|
|
558
|
+
cachedBoundaryToVerify = undefined;
|
|
559
|
+
if (batch.documents[0]?._id.o !== cachedBoundary) {
|
|
560
|
+
// A previous attempt may have replaced the cached boundary before
|
|
561
|
+
// finalizing bucket state. Keep the persisted cache available to
|
|
562
|
+
// readers, but ignore it in this attempt and calculate its
|
|
563
|
+
// replacement through the normal scan from the bucket beginning.
|
|
564
|
+
previousCompactedState = undefined;
|
|
565
|
+
lowerBound = bucketContext.minId;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (batch.documents.length == 0) {
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const doc of batch.documents) {
|
|
575
|
+
compactedOpId = maxOpId(compactedOpId, doc._id.o);
|
|
576
|
+
const documentStats = statsForDocument(doc);
|
|
577
|
+
if (previousCompactedState?.op_id === doc._id.o) {
|
|
578
|
+
overlappingCompactedChunk = documentStats;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const nextSize = pendingSize + doc.size;
|
|
582
|
+
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
583
|
+
const groupStats = await this.flushChunkMerge(
|
|
584
|
+
bucket,
|
|
585
|
+
pendingChunks,
|
|
586
|
+
collection,
|
|
587
|
+
dataContext,
|
|
588
|
+
bucketContext,
|
|
589
|
+
objectStorageUsage
|
|
590
|
+
);
|
|
591
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
592
|
+
pendingChunks = [];
|
|
593
|
+
pendingSize = 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
pendingChunks.push(doc);
|
|
597
|
+
pendingSize += doc.size;
|
|
172
598
|
}
|
|
599
|
+
|
|
600
|
+
lowerBound = batch.documents[batch.documents.length - 1]._id;
|
|
601
|
+
if (!batch.hasMore) {
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (pendingChunks.length > 0) {
|
|
607
|
+
const groupStats = await this.flushChunkMerge(
|
|
608
|
+
bucket,
|
|
609
|
+
pendingChunks,
|
|
610
|
+
collection,
|
|
611
|
+
dataContext,
|
|
612
|
+
bucketContext,
|
|
613
|
+
objectStorageUsage
|
|
614
|
+
);
|
|
615
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (compactedOpId == null) {
|
|
619
|
+
await this.finalizeSkippedBucket(context);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const compactedState =
|
|
624
|
+
previousCompactedState == null
|
|
625
|
+
? compactedTail
|
|
626
|
+
: combineChunkStats(previousCompactedState, compactedTail, overlappingCompactedChunk!);
|
|
627
|
+
const tailStats =
|
|
628
|
+
compactedOpId == context.lastOp
|
|
629
|
+
? undefined
|
|
630
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
631
|
+
const result: CompactionResult = {
|
|
632
|
+
compactedState,
|
|
633
|
+
bucketStats: tailStats == null ? compactedState : combineAdjacentStats(compactedState, tailStats)
|
|
173
634
|
};
|
|
635
|
+
|
|
636
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: 0 });
|
|
637
|
+
this.compactedBucketCount++;
|
|
638
|
+
this.logger.info(
|
|
639
|
+
`Compacted bucket chunks ${bucket}: ${result.bucketStats.count} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`
|
|
640
|
+
);
|
|
174
641
|
}
|
|
175
642
|
|
|
176
|
-
private async
|
|
643
|
+
private async flushChunkMerge(
|
|
177
644
|
bucket: string,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
645
|
+
inputs: BucketDataDocumentV3[],
|
|
646
|
+
collection: mongo.Collection<BucketDataDocumentV3>,
|
|
647
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
648
|
+
bucketContext: BucketDataContextV3,
|
|
649
|
+
objectStorageUsage: ObjectStorageUsage
|
|
650
|
+
): Promise<BucketStatsWithChecksum> {
|
|
651
|
+
if (inputs.length == 1) {
|
|
652
|
+
return statsForDocument(inputs[0]);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
this.signal?.throwIfAborted();
|
|
657
|
+
|
|
658
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
659
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
660
|
+
// the same rule.
|
|
661
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
662
|
+
if (inlineInputs.length > 0) {
|
|
663
|
+
const inlineDocuments = await collection
|
|
664
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
665
|
+
.toArray();
|
|
666
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
667
|
+
for (const input of inlineInputs) {
|
|
668
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
191
669
|
}
|
|
192
670
|
}
|
|
671
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
672
|
+
|
|
673
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
674
|
+
const targetOp = inputs.reduce<InternalOpId | null>(
|
|
675
|
+
(maxTarget, input) => maxOpId(maxTarget, input.target_op),
|
|
676
|
+
null
|
|
677
|
+
);
|
|
678
|
+
const result = await this.flushCompactionGroup(
|
|
679
|
+
bucket,
|
|
680
|
+
{
|
|
681
|
+
inputs,
|
|
682
|
+
ops: operations,
|
|
683
|
+
changed: true,
|
|
684
|
+
targetOp
|
|
685
|
+
},
|
|
686
|
+
bucketContext,
|
|
687
|
+
context,
|
|
688
|
+
objectStorageUsage
|
|
689
|
+
);
|
|
690
|
+
return result.stats;
|
|
691
|
+
} finally {
|
|
692
|
+
// The scan batch also references these documents. Do not retain hydrated
|
|
693
|
+
// operations after finishing this merge group.
|
|
694
|
+
for (const input of inputs) {
|
|
695
|
+
delete input.ops;
|
|
696
|
+
}
|
|
697
|
+
await setImmediate();
|
|
193
698
|
}
|
|
699
|
+
}
|
|
194
700
|
|
|
195
|
-
|
|
196
|
-
|
|
701
|
+
private async finalizeCompactedBucket({
|
|
702
|
+
context,
|
|
703
|
+
compactedOpId,
|
|
704
|
+
compactionResult,
|
|
705
|
+
puts
|
|
706
|
+
}: {
|
|
707
|
+
context: CompactionContext;
|
|
708
|
+
compactedOpId: InternalOpId;
|
|
709
|
+
compactionResult: CompactionResult;
|
|
710
|
+
puts: number;
|
|
711
|
+
}) {
|
|
712
|
+
await context.lease.throwIfLost();
|
|
713
|
+
const startedStats = bucketStats(context.state);
|
|
714
|
+
const delta = {
|
|
715
|
+
count: compactionResult.bucketStats.count - startedStats.count,
|
|
716
|
+
bytes: compactionResult.bucketStats.bytes - startedStats.bytes,
|
|
717
|
+
chunks: compactionResult.bucketStats.chunks - startedStats.chunks
|
|
718
|
+
};
|
|
719
|
+
const coveredClaimedHead = compactedOpId >= context.lastOp;
|
|
720
|
+
const concurrentWriteCheck = { $gt: ['$last_op', context.lastOp] };
|
|
721
|
+
const remainingFullWorkCheck = coveredClaimedHead ? concurrentWriteCheck : true;
|
|
722
|
+
const nextAfterPartialFullCompact = this.rescheduleAtOrAfter(
|
|
723
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } },
|
|
724
|
+
context.rescheduleNotBefore
|
|
725
|
+
);
|
|
726
|
+
const nextCheckForUncompactedWork = this.rescheduleAtOrAfter(
|
|
727
|
+
{
|
|
728
|
+
$min: [
|
|
729
|
+
new Date(firstUncompactedWrite(context.state).getTime() + this.maxCompactFullIntervalMs),
|
|
730
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
731
|
+
]
|
|
732
|
+
},
|
|
733
|
+
context.rescheduleNotBefore
|
|
734
|
+
);
|
|
735
|
+
const update: mongo.Document = {
|
|
736
|
+
compacted_state: {
|
|
737
|
+
op_id: compactedOpId,
|
|
738
|
+
checksum: BigInt(compactionResult.compactedState.checksum),
|
|
739
|
+
count: compactionResult.compactedState.count,
|
|
740
|
+
bytes: compactionResult.compactedState.bytes,
|
|
741
|
+
chunks: compactionResult.compactedState.chunks,
|
|
742
|
+
at: '$$NOW'
|
|
743
|
+
},
|
|
744
|
+
bucket_stats: {
|
|
745
|
+
count: { $add: ['$bucket_stats.count', delta.count] },
|
|
746
|
+
bytes: { $add: ['$bucket_stats.bytes', delta.bytes] },
|
|
747
|
+
chunks: { $add: ['$bucket_stats.chunks', delta.chunks] }
|
|
748
|
+
},
|
|
749
|
+
first_uncompacted_write:
|
|
750
|
+
context.kind == CompactionKind.Full
|
|
751
|
+
? { $cond: [remainingFullWorkCheck, '$$NOW', '$$REMOVE'] }
|
|
752
|
+
: '$first_uncompacted_write',
|
|
753
|
+
next_compact_check:
|
|
754
|
+
context.kind == CompactionKind.Full
|
|
755
|
+
? { $cond: [remainingFullWorkCheck, nextAfterPartialFullCompact, '$$REMOVE'] }
|
|
756
|
+
: nextCheckForUncompactedWork
|
|
757
|
+
};
|
|
758
|
+
if (context.kind == CompactionKind.Full) {
|
|
759
|
+
update.last_full_compact = {
|
|
760
|
+
op_id: compactedOpId,
|
|
761
|
+
count: compactionResult.compactedState.count,
|
|
762
|
+
puts,
|
|
763
|
+
at: '$$NOW'
|
|
764
|
+
};
|
|
197
765
|
}
|
|
198
766
|
|
|
199
|
-
|
|
767
|
+
await context.lease.finalize(update);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private async finalizeSkippedBucket(context: CompactionContext) {
|
|
771
|
+
// A maxOpId cap can exclude the first remaining document entirely. Avoid
|
|
772
|
+
// immediately claiming the same no-progress bucket again in this run.
|
|
773
|
+
await this.rescheduleClaimedBucket(
|
|
774
|
+
context.lease,
|
|
775
|
+
{
|
|
776
|
+
...context.decision,
|
|
777
|
+
nextCompactCheck: {
|
|
778
|
+
$max: [
|
|
779
|
+
context.decision.nextCompactCheck,
|
|
780
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
781
|
+
]
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
context.rescheduleNotBefore
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Read bucket stats directly from bucket_data documents.
|
|
790
|
+
*/
|
|
791
|
+
private async readBucketStats(
|
|
792
|
+
bucket: string,
|
|
793
|
+
definitionId: BucketDefinitionId,
|
|
794
|
+
maxOp: InternalOpId,
|
|
795
|
+
lowerBound?: BucketDataKey
|
|
796
|
+
): Promise<BucketStatsWithChecksum> {
|
|
797
|
+
const context = new BucketDataContextV3(this.db, {
|
|
200
798
|
bucket,
|
|
201
|
-
definitionId
|
|
799
|
+
definitionId,
|
|
202
800
|
replicationStreamId: this.group_id
|
|
203
801
|
});
|
|
802
|
+
const [stats] = await this.db
|
|
803
|
+
.bucketData(this.group_id, definitionId)
|
|
804
|
+
.aggregate<{ count: number; bytes: number | bigint; chunks: number; checksum: bigint }>([
|
|
805
|
+
{
|
|
806
|
+
$match: {
|
|
807
|
+
_id:
|
|
808
|
+
lowerBound == null
|
|
809
|
+
? { $gte: context.minId, $lte: context.docId(maxOp) }
|
|
810
|
+
: { $gt: lowerBound, $lte: context.docId(maxOp) }
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
$group: {
|
|
815
|
+
_id: null,
|
|
816
|
+
count: { $sum: '$count' },
|
|
817
|
+
bytes: { $sum: '$size' },
|
|
818
|
+
chunks: { $sum: 1 },
|
|
819
|
+
checksum: { $sum: '$checksum' }
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
])
|
|
823
|
+
.toArray();
|
|
824
|
+
return {
|
|
825
|
+
count: Number(stats?.count ?? 0),
|
|
826
|
+
bytes: BigInt(stats?.bytes ?? 0),
|
|
827
|
+
chunks: Number(stats?.chunks ?? 0),
|
|
828
|
+
checksum:
|
|
829
|
+
typeof stats?.checksum == 'bigint'
|
|
830
|
+
? Number(BigInt.asIntN(32, stats.checksum))
|
|
831
|
+
: addChecksums(0, Number(stats?.checksum ?? 0))
|
|
832
|
+
};
|
|
204
833
|
}
|
|
205
834
|
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
835
|
+
private async compactSingleBucketFully(context: CompactionContext, objectStorageUsage: ObjectStorageUsage) {
|
|
836
|
+
const bucket = context.state._id.b;
|
|
837
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
838
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
839
|
+
bucket,
|
|
840
|
+
definitionId: resolvedDefinitionId,
|
|
841
|
+
replicationStreamId: this.group_id
|
|
842
|
+
});
|
|
213
843
|
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
214
|
-
const
|
|
215
|
-
|
|
844
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
216
845
|
const lowerBound = bucketContext.minId;
|
|
217
|
-
let upperBound = bucketContext.docId(
|
|
846
|
+
let upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
218
847
|
|
|
219
|
-
let totalChecksum = 0;
|
|
220
848
|
let totalOpCount = 0;
|
|
221
|
-
let totalOpBytes = 0;
|
|
222
849
|
|
|
223
850
|
let lastNotPut: bigint | null = null;
|
|
224
851
|
let opsSincePut = 0;
|
|
225
852
|
let compactedOpId: bigint | null = null;
|
|
226
853
|
let clearBoundary: { opId: bigint; documentId: BucketDataKey } | null = null;
|
|
854
|
+
let compactedStats = emptyBucketStats();
|
|
227
855
|
const seen = new Map<string, bigint>();
|
|
228
856
|
let trackingSize = 0;
|
|
857
|
+
let putCount = 0;
|
|
229
858
|
let pendingGroup: PendingCompactionGroup | null = null;
|
|
230
859
|
|
|
231
860
|
// --- Read batch from MongoDB ---
|
|
232
861
|
while (true) {
|
|
233
862
|
this.signal?.throwIfAborted();
|
|
863
|
+
await context.lease.throwIfLost();
|
|
234
864
|
|
|
235
865
|
const pipeline: mongo.Document[] = [
|
|
236
866
|
{
|
|
@@ -280,7 +910,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
280
910
|
// merging is useful, and writes each final object at most once.
|
|
281
911
|
for (const doc of batchDocs) {
|
|
282
912
|
compactedOpId ??= doc._id.o;
|
|
283
|
-
const originalOps = Array.from(loadBucketDataDocument(
|
|
913
|
+
const originalOps = Array.from(loadBucketDataDocument(dataContext, doc));
|
|
284
914
|
|
|
285
915
|
let changed = false;
|
|
286
916
|
const compactedOps: BucketDataDoc[] = [];
|
|
@@ -299,6 +929,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
299
929
|
row_id: undefined,
|
|
300
930
|
source_table: undefined,
|
|
301
931
|
source_key: undefined,
|
|
932
|
+
subkey: undefined,
|
|
302
933
|
data: null
|
|
303
934
|
});
|
|
304
935
|
changed = true;
|
|
@@ -313,6 +944,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
313
944
|
}
|
|
314
945
|
compactedOps.push(op);
|
|
315
946
|
if (op.op == 'PUT') {
|
|
947
|
+
putCount++;
|
|
316
948
|
lastNotPut = null;
|
|
317
949
|
opsSincePut = 0;
|
|
318
950
|
} else {
|
|
@@ -334,10 +966,6 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
334
966
|
}
|
|
335
967
|
compactedOps.reverse();
|
|
336
968
|
|
|
337
|
-
for (const op of compactedOps) {
|
|
338
|
-
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
339
|
-
totalOpBytes += op.data?.length ?? 0;
|
|
340
|
-
}
|
|
341
969
|
totalOpCount += compactedOps.length;
|
|
342
970
|
|
|
343
971
|
const candidate: PendingCompactionGroup = {
|
|
@@ -361,13 +989,20 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
361
989
|
};
|
|
362
990
|
} else {
|
|
363
991
|
const flushedGroup = pendingGroup;
|
|
364
|
-
const
|
|
992
|
+
const result = await this.flushCompactionGroup(
|
|
993
|
+
bucket,
|
|
994
|
+
flushedGroup,
|
|
995
|
+
bucketContext,
|
|
996
|
+
dataContext,
|
|
997
|
+
objectStorageUsage
|
|
998
|
+
);
|
|
999
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
365
1000
|
if (
|
|
366
1001
|
lastNotPut != null &&
|
|
367
1002
|
flushedGroup.ops[0].o <= lastNotPut &&
|
|
368
1003
|
flushedGroup.ops[flushedGroup.ops.length - 1].o >= lastNotPut
|
|
369
1004
|
) {
|
|
370
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
1005
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
371
1006
|
}
|
|
372
1007
|
pendingGroup = candidate;
|
|
373
1008
|
}
|
|
@@ -385,16 +1020,24 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
385
1020
|
}
|
|
386
1021
|
|
|
387
1022
|
if (pendingGroup != null) {
|
|
388
|
-
const
|
|
1023
|
+
const result = await this.flushCompactionGroup(
|
|
1024
|
+
bucket,
|
|
1025
|
+
pendingGroup,
|
|
1026
|
+
bucketContext,
|
|
1027
|
+
dataContext,
|
|
1028
|
+
objectStorageUsage
|
|
1029
|
+
);
|
|
1030
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
389
1031
|
if (
|
|
390
1032
|
lastNotPut != null &&
|
|
391
1033
|
pendingGroup.ops[0].o <= lastNotPut &&
|
|
392
1034
|
pendingGroup.ops[pendingGroup.ops.length - 1].o >= lastNotPut
|
|
393
1035
|
) {
|
|
394
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
1036
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
395
1037
|
}
|
|
396
1038
|
}
|
|
397
1039
|
if (compactedOpId == null) {
|
|
1040
|
+
await this.finalizeSkippedBucket(context);
|
|
398
1041
|
return;
|
|
399
1042
|
}
|
|
400
1043
|
|
|
@@ -404,36 +1047,34 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
404
1047
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
405
1048
|
}
|
|
406
1049
|
|
|
407
|
-
|
|
1050
|
+
const clearResult = await this.clearBucketLeading(
|
|
408
1051
|
lastNotPut,
|
|
409
1052
|
clearBoundary.documentId,
|
|
410
1053
|
bucketContext,
|
|
411
1054
|
collection,
|
|
412
|
-
|
|
1055
|
+
dataContext,
|
|
1056
|
+
objectStorageUsage
|
|
413
1057
|
);
|
|
1058
|
+
totalOpCount += clearResult.opCountDiff;
|
|
1059
|
+
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
414
1060
|
}
|
|
415
1061
|
|
|
1062
|
+
const tailStats =
|
|
1063
|
+
compactedOpId == context.lastOp
|
|
1064
|
+
? undefined
|
|
1065
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
1066
|
+
const result: CompactionResult = {
|
|
1067
|
+
compactedState: compactedStats,
|
|
1068
|
+
bucketStats: tailStats == null ? compactedStats : combineAdjacentStats(compactedStats, tailStats)
|
|
1069
|
+
};
|
|
1070
|
+
|
|
416
1071
|
// --- Finalize: update bucket checksums and state ---
|
|
417
|
-
this.
|
|
418
|
-
{
|
|
419
|
-
bucket,
|
|
420
|
-
definitionId: resolvedDefinitionId,
|
|
421
|
-
seen: new Map(),
|
|
422
|
-
trackingSize: 0,
|
|
423
|
-
lastNotPut: lastNotPut,
|
|
424
|
-
opsSincePut: opsSincePut,
|
|
425
|
-
checksum: totalChecksum,
|
|
426
|
-
opCount: totalOpCount,
|
|
427
|
-
opBytes: totalOpBytes
|
|
428
|
-
},
|
|
429
|
-
compactedOpId
|
|
430
|
-
);
|
|
431
|
-
if (this.bucketStateUpdates.length > 0) {
|
|
432
|
-
await this.writeBucketStateUpdates();
|
|
433
|
-
this.bucketStateUpdates = [];
|
|
434
|
-
}
|
|
1072
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: putCount });
|
|
435
1073
|
|
|
436
|
-
|
|
1074
|
+
this.compactedBucketCount++;
|
|
1075
|
+
this.logger.info(
|
|
1076
|
+
`Compacted bucket ${bucket}: ${totalOpCount} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`
|
|
1077
|
+
);
|
|
437
1078
|
}
|
|
438
1079
|
|
|
439
1080
|
/**
|
|
@@ -446,10 +1087,14 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
446
1087
|
bucket: string,
|
|
447
1088
|
group: PendingCompactionGroup,
|
|
448
1089
|
bucketContext: BucketDataContextV3,
|
|
449
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
450
|
-
|
|
1090
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1091
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1092
|
+
): Promise<CompactionGroupResult> {
|
|
451
1093
|
if (group.inputs.length == 1 && !group.changed) {
|
|
452
|
-
return
|
|
1094
|
+
return {
|
|
1095
|
+
documentId: group.inputs[0]._id,
|
|
1096
|
+
stats: statsForDocument(group.inputs[0])
|
|
1097
|
+
};
|
|
453
1098
|
}
|
|
454
1099
|
|
|
455
1100
|
const inputs = group.inputs;
|
|
@@ -458,6 +1103,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
458
1103
|
const expectedChecksum = inputs.reduce((sum, doc) => sum + doc.checksum, 0n);
|
|
459
1104
|
const expectedOpCount = inputs.reduce((sum, doc) => sum + doc.count, 0);
|
|
460
1105
|
const oldStoragePaths = inputs.flatMap((doc) => (doc.storage_ref ? [doc.storage_ref.path] : []));
|
|
1106
|
+
const oldStorageBytes = inputs.reduce((sum, document) => sum + ObjectStorageUsage.bytes(document), 0n);
|
|
461
1107
|
const {
|
|
462
1108
|
documents,
|
|
463
1109
|
storagePaths: newStoragePaths,
|
|
@@ -495,9 +1141,19 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
495
1141
|
);
|
|
496
1142
|
}
|
|
497
1143
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1144
|
+
// Replacement documents can reuse deleted IDs, so retain delete-before-insert ordering.
|
|
1145
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1146
|
+
writes.deleteMany(bucketContext.collection, { _id: { $in: idsToDelete } });
|
|
1147
|
+
writes.insertMany(bucketContext.collection, documents);
|
|
1148
|
+
this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes);
|
|
1149
|
+
this.recordObjectStorageReplacement(
|
|
1150
|
+
oldStorageBytes,
|
|
1151
|
+
documents,
|
|
1152
|
+
context.definitionId,
|
|
1153
|
+
writes,
|
|
1154
|
+
objectStorageUsage
|
|
1155
|
+
);
|
|
1156
|
+
await writes.execute();
|
|
501
1157
|
},
|
|
502
1158
|
{
|
|
503
1159
|
writeConcern: { w: 'majority' },
|
|
@@ -507,7 +1163,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
507
1163
|
} finally {
|
|
508
1164
|
await session.endSession();
|
|
509
1165
|
}
|
|
510
|
-
return
|
|
1166
|
+
return {
|
|
1167
|
+
documentId: documents[0]._id,
|
|
1168
|
+
stats: statsForDocuments(documents)
|
|
1169
|
+
};
|
|
511
1170
|
}
|
|
512
1171
|
|
|
513
1172
|
/**
|
|
@@ -516,16 +1175,20 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
516
1175
|
* before the known boundary document, then splits that boundary document
|
|
517
1176
|
* if it contains ops on both sides of lastNotPut.
|
|
518
1177
|
*
|
|
519
|
-
* Returns the op count
|
|
1178
|
+
* Returns the op count and stored-stat changes after replacing cleared ops
|
|
1179
|
+
* with CLEAR ops.
|
|
520
1180
|
*/
|
|
521
1181
|
private async clearBucketLeading(
|
|
522
1182
|
lastNotPut: bigint,
|
|
523
1183
|
boundaryDocId: BucketDataKey,
|
|
524
1184
|
bucketContext: BucketDataContextV3,
|
|
525
1185
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
526
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
527
|
-
|
|
1186
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1187
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1188
|
+
): Promise<ClearCompactionResult> {
|
|
528
1189
|
let opCountDiff = 0;
|
|
1190
|
+
let before = emptyBucketStats();
|
|
1191
|
+
let after = emptyBucketStats();
|
|
529
1192
|
const session = this.db.client.startSession();
|
|
530
1193
|
try {
|
|
531
1194
|
let done = false;
|
|
@@ -538,27 +1201,34 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
538
1201
|
boundaryDocId,
|
|
539
1202
|
bucketContext,
|
|
540
1203
|
collection,
|
|
541
|
-
context
|
|
1204
|
+
context,
|
|
1205
|
+
objectStorageUsage
|
|
542
1206
|
);
|
|
543
1207
|
done = batch.done;
|
|
544
1208
|
opCountDiff += batch.opCountDiff;
|
|
1209
|
+
before = combineAdjacentStats(before, batch.before);
|
|
1210
|
+
after = combineAdjacentStats(after, batch.after);
|
|
545
1211
|
}
|
|
546
1212
|
|
|
547
1213
|
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
548
1214
|
// potentially followed by PUT operations. This is only a single document, so no need for batching.
|
|
549
|
-
|
|
1215
|
+
const boundaryResult = await this.clearBoundaryDocument(
|
|
550
1216
|
session,
|
|
551
1217
|
lastNotPut,
|
|
552
1218
|
boundaryDocId,
|
|
553
1219
|
bucketContext,
|
|
554
1220
|
collection,
|
|
555
|
-
context
|
|
1221
|
+
context,
|
|
1222
|
+
objectStorageUsage
|
|
556
1223
|
);
|
|
1224
|
+
opCountDiff += boundaryResult.opCountDiff;
|
|
1225
|
+
before = combineAdjacentStats(before, boundaryResult.before);
|
|
1226
|
+
after = combineAdjacentStats(after, boundaryResult.after);
|
|
557
1227
|
} finally {
|
|
558
1228
|
await session.endSession();
|
|
559
1229
|
}
|
|
560
1230
|
|
|
561
|
-
return opCountDiff;
|
|
1231
|
+
return { opCountDiff, before, after };
|
|
562
1232
|
}
|
|
563
1233
|
|
|
564
1234
|
private async clearLeadingFullDocuments(
|
|
@@ -567,19 +1237,25 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
567
1237
|
boundaryDocId: BucketDataKey,
|
|
568
1238
|
bucketContext: BucketDataContextV3,
|
|
569
1239
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
570
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
571
|
-
|
|
1240
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1241
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1242
|
+
): Promise<{ done: boolean; opCountDiff: number } & CompactionStatsReplacement> {
|
|
572
1243
|
const bucket = bucketContext.key.bucket;
|
|
573
1244
|
this.signal?.throwIfAborted();
|
|
574
|
-
|
|
1245
|
+
let prepared: PreparedObjectStorageUpload[] | undefined;
|
|
575
1246
|
let done = false;
|
|
576
1247
|
let opCountDiff = 0;
|
|
1248
|
+
let before = emptyBucketStats();
|
|
1249
|
+
let after = emptyBucketStats();
|
|
577
1250
|
|
|
578
1251
|
await session.withTransaction(
|
|
579
1252
|
async () => {
|
|
580
1253
|
done = false;
|
|
581
1254
|
opCountDiff = 0;
|
|
1255
|
+
before = emptyBucketStats();
|
|
1256
|
+
after = emptyBucketStats();
|
|
582
1257
|
const oldStoragePaths: string[] = [];
|
|
1258
|
+
let oldStorageBytes = 0n;
|
|
583
1259
|
const query = collection.find(
|
|
584
1260
|
{
|
|
585
1261
|
_id: {
|
|
@@ -595,6 +1271,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
595
1271
|
min_op: 1,
|
|
596
1272
|
checksum: 1,
|
|
597
1273
|
count: 1,
|
|
1274
|
+
size: 1,
|
|
598
1275
|
target_op: 1,
|
|
599
1276
|
has_clear_op: 1,
|
|
600
1277
|
storage_ref: 1
|
|
@@ -609,6 +1286,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
609
1286
|
let lastDocId: BucketDataKey | null = null;
|
|
610
1287
|
let clearOpCount = 0;
|
|
611
1288
|
let gotNonClearOp = false;
|
|
1289
|
+
const inputStats = emptyBucketStats();
|
|
612
1290
|
|
|
613
1291
|
for await (const doc of query.stream()) {
|
|
614
1292
|
if (doc.min_op > lastNotPut) {
|
|
@@ -618,9 +1296,15 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
618
1296
|
}
|
|
619
1297
|
|
|
620
1298
|
lastDocId = doc._id;
|
|
1299
|
+
const documentStats = statsForDocument(doc);
|
|
1300
|
+
inputStats.count += documentStats.count;
|
|
1301
|
+
inputStats.bytes += documentStats.bytes;
|
|
1302
|
+
inputStats.chunks += documentStats.chunks;
|
|
1303
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
621
1304
|
if (doc.storage_ref) {
|
|
622
1305
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
623
1306
|
}
|
|
1307
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
624
1308
|
|
|
625
1309
|
// The compaction scan established that every operation before the
|
|
626
1310
|
// boundary is MOVE/REMOVE/CLEAR. Root metadata is sufficient to fold
|
|
@@ -646,16 +1330,15 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
646
1330
|
return;
|
|
647
1331
|
}
|
|
648
1332
|
|
|
1333
|
+
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
649
1334
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
{ session }
|
|
658
|
-
);
|
|
1335
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1336
|
+
writes.deleteMany(collection, {
|
|
1337
|
+
_id: {
|
|
1338
|
+
$gte: bucketContext.minId,
|
|
1339
|
+
$lte: lastDocId!
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
659
1342
|
|
|
660
1343
|
const clearOp = {
|
|
661
1344
|
bucketKey: { ...context, bucket },
|
|
@@ -667,10 +1350,20 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
667
1350
|
const persisted = await this.persistBucketData(bucket, [[clearOp]], context, prepared, {
|
|
668
1351
|
targetOp: maxTargetOp
|
|
669
1352
|
});
|
|
670
|
-
|
|
671
|
-
|
|
1353
|
+
writes.insertOne(collection, persisted.documents[0]);
|
|
1354
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1355
|
+
this.recordObjectStorageReplacement(
|
|
1356
|
+
oldStorageBytes,
|
|
1357
|
+
persisted.documents,
|
|
1358
|
+
context.definitionId,
|
|
1359
|
+
writes,
|
|
1360
|
+
objectStorageUsage
|
|
1361
|
+
);
|
|
1362
|
+
await writes.execute();
|
|
672
1363
|
|
|
673
1364
|
opCountDiff = -clearedOpCount + 1;
|
|
1365
|
+
before = inputStats;
|
|
1366
|
+
after = statsForDocuments(persisted.documents);
|
|
674
1367
|
},
|
|
675
1368
|
{
|
|
676
1369
|
writeConcern: { w: 'majority' },
|
|
@@ -678,7 +1371,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
678
1371
|
}
|
|
679
1372
|
);
|
|
680
1373
|
|
|
681
|
-
return { done, opCountDiff };
|
|
1374
|
+
return { done, opCountDiff, before, after };
|
|
682
1375
|
}
|
|
683
1376
|
|
|
684
1377
|
private async clearBoundaryDocument(
|
|
@@ -687,17 +1380,23 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
687
1380
|
boundaryDocId: BucketDataKey,
|
|
688
1381
|
bucketContext: BucketDataContextV3,
|
|
689
1382
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
690
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
691
|
-
|
|
1383
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1384
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1385
|
+
): Promise<ClearCompactionResult> {
|
|
692
1386
|
const bucket = bucketContext.key.bucket;
|
|
693
1387
|
this.signal?.throwIfAborted();
|
|
694
1388
|
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
695
1389
|
let opCountDiff = 0;
|
|
1390
|
+
let before = emptyBucketStats();
|
|
1391
|
+
let after = emptyBucketStats();
|
|
696
1392
|
|
|
697
1393
|
await session.withTransaction(
|
|
698
1394
|
async () => {
|
|
699
1395
|
opCountDiff = 0;
|
|
1396
|
+
before = emptyBucketStats();
|
|
1397
|
+
after = emptyBucketStats();
|
|
700
1398
|
const oldStoragePaths: string[] = [];
|
|
1399
|
+
let oldStorageBytes = 0n;
|
|
701
1400
|
const query = collection.find(
|
|
702
1401
|
{
|
|
703
1402
|
// This is a range query, but should only ever return two documents:
|
|
@@ -716,6 +1415,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
716
1415
|
min_op: 1,
|
|
717
1416
|
checksum: 1,
|
|
718
1417
|
count: 1,
|
|
1418
|
+
size: 1,
|
|
719
1419
|
target_op: 1,
|
|
720
1420
|
ops: 1,
|
|
721
1421
|
storage_ref: 1
|
|
@@ -729,6 +1429,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
729
1429
|
let clearedOpCount = 0;
|
|
730
1430
|
let maxTargetOp: bigint | null = null;
|
|
731
1431
|
const boundarySurvivors: BucketDataDoc[] = [];
|
|
1432
|
+
const inputStats = emptyBucketStats();
|
|
732
1433
|
|
|
733
1434
|
for await (const doc of query.stream()) {
|
|
734
1435
|
docsRead++;
|
|
@@ -736,10 +1437,17 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
736
1437
|
throw new ReplicationAssertionError(`Unexpected extra document before CLEAR boundary in bucket ${bucket}`);
|
|
737
1438
|
}
|
|
738
1439
|
|
|
1440
|
+
const documentStats = statsForDocument(doc);
|
|
1441
|
+
inputStats.count += documentStats.count;
|
|
1442
|
+
inputStats.bytes += documentStats.bytes;
|
|
1443
|
+
inputStats.chunks += documentStats.chunks;
|
|
1444
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
1445
|
+
|
|
739
1446
|
const isBoundaryDoc = doc._id.o == boundaryDocId.o;
|
|
740
1447
|
if (doc.storage_ref) {
|
|
741
1448
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
742
1449
|
}
|
|
1450
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
743
1451
|
await hydrateBucketDataDocuments([doc], this.storage.objectStorage, { signal: this.signal });
|
|
744
1452
|
maxTargetOp = maxOpId(maxTargetOp, doc.target_op);
|
|
745
1453
|
for (const op of loadBucketDataDocument(context, doc)) {
|
|
@@ -772,15 +1480,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
772
1480
|
}
|
|
773
1481
|
|
|
774
1482
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastNotPut}`);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
{ session }
|
|
783
|
-
);
|
|
1483
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1484
|
+
writes.deleteMany(collection, {
|
|
1485
|
+
_id: {
|
|
1486
|
+
$gte: bucketContext.minId,
|
|
1487
|
+
$lte: boundaryDocId
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
784
1490
|
|
|
785
1491
|
const clearOp = {
|
|
786
1492
|
bucketKey: { ...context, bucket },
|
|
@@ -798,10 +1504,20 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
798
1504
|
const persisted = await this.persistBucketData(bucket, chunks, context, prepared, {
|
|
799
1505
|
targetOp: maxTargetOp ?? undefined
|
|
800
1506
|
});
|
|
801
|
-
|
|
802
|
-
|
|
1507
|
+
writes.insertMany(collection, persisted.documents);
|
|
1508
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1509
|
+
this.recordObjectStorageReplacement(
|
|
1510
|
+
oldStorageBytes,
|
|
1511
|
+
persisted.documents,
|
|
1512
|
+
context.definitionId,
|
|
1513
|
+
writes,
|
|
1514
|
+
objectStorageUsage
|
|
1515
|
+
);
|
|
1516
|
+
await writes.execute();
|
|
803
1517
|
|
|
804
1518
|
opCountDiff = -clearedOpCount + 1;
|
|
1519
|
+
before = inputStats;
|
|
1520
|
+
after = statsForDocuments(persisted.documents);
|
|
805
1521
|
},
|
|
806
1522
|
{
|
|
807
1523
|
writeConcern: { w: 'majority' },
|
|
@@ -809,7 +1525,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
809
1525
|
}
|
|
810
1526
|
);
|
|
811
1527
|
|
|
812
|
-
return opCountDiff;
|
|
1528
|
+
return { opCountDiff, before, after };
|
|
813
1529
|
}
|
|
814
1530
|
|
|
815
1531
|
/**
|
|
@@ -836,22 +1552,39 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
836
1552
|
}
|
|
837
1553
|
|
|
838
1554
|
/** Publish replacement uploads and retire superseded objects in the same transaction. */
|
|
839
|
-
private
|
|
1555
|
+
private finishObjectStorageReplacement(
|
|
840
1556
|
oldStoragePaths: Iterable<string>,
|
|
841
1557
|
newStoragePaths: Set<string>,
|
|
842
1558
|
uploads: PreparedObjectStorageUpload[],
|
|
843
|
-
|
|
844
|
-
):
|
|
1559
|
+
writes: MongoWriteBatch
|
|
1560
|
+
): void {
|
|
845
1561
|
if (!this.storage.objectStorage) {
|
|
846
1562
|
return;
|
|
847
1563
|
}
|
|
848
|
-
|
|
849
|
-
|
|
1564
|
+
this.objectStorageLifecycle.publishUploads(uploads, writes);
|
|
1565
|
+
this.objectStorageLifecycle.retire(
|
|
850
1566
|
Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)),
|
|
851
|
-
|
|
1567
|
+
writes
|
|
852
1568
|
);
|
|
853
1569
|
}
|
|
854
1570
|
|
|
1571
|
+
private recordObjectStorageReplacement(
|
|
1572
|
+
oldBytes: bigint,
|
|
1573
|
+
newDocuments: Iterable<Pick<BucketDataDocumentV3, 'storage_ref'>>,
|
|
1574
|
+
definitionId: BucketDefinitionId,
|
|
1575
|
+
writes: MongoWriteBatch,
|
|
1576
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1577
|
+
): void {
|
|
1578
|
+
if (!this.storage.objectStorage) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
let newBytes = 0n;
|
|
1582
|
+
for (const document of newDocuments) {
|
|
1583
|
+
newBytes += ObjectStorageUsage.bytes(document);
|
|
1584
|
+
}
|
|
1585
|
+
objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, writes);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
855
1588
|
private async persistBucketData(
|
|
856
1589
|
bucket: string,
|
|
857
1590
|
chunks: BucketDataDoc[][],
|
|
@@ -908,7 +1641,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
908
1641
|
}
|
|
909
1642
|
|
|
910
1643
|
const { ops, ...metadata } = serialized;
|
|
911
|
-
const { fileSize } = await store.store(upload.path, ops
|
|
1644
|
+
const { fileSize } = await store.store(upload.path, ops!, { signal: this.signal });
|
|
912
1645
|
storagePaths.add(upload.path);
|
|
913
1646
|
return {
|
|
914
1647
|
...metadata,
|