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