@powersync/service-module-mongodb-storage 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/dist/storage/MongoBucketStorage.d.ts +31 -3
- package/dist/storage/MongoBucketStorage.js +365 -206
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
- package/dist/storage/implementation/MongoBucketBatch.js +35 -9
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +3 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
- package/dist/storage/implementation/common/PersistedBatch.js +43 -2
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -1
- package/dist/storage/implementation/db.js +14 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +35 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
- package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +89 -5
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
- package/dist/storage/implementation/v3/source-table-utils.js +49 -57
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +23 -1
- package/dist/types/types.js +16 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +495 -268
- package/src/storage/implementation/MongoBucketBatch.ts +54 -14
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +3 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +54 -2
- package/src/storage/implementation/db.ts +21 -2
- package/src/storage/implementation/models.ts +38 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
- package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +96 -3
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +24 -7
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +255 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +273 -22
- package/test/src/storage_compacting.test.ts +714 -244
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +243 -1
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +365 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,236 +1,726 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
3
|
-
import { addChecksums, InternalOpId, storage, utils } from '@powersync/service-core';
|
|
2
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { addChecksums, formatBytes, InternalOpId, storage, utils } from '@powersync/service-core';
|
|
4
4
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
5
|
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
|
-
import { BucketDataKey
|
|
7
|
-
import { ConcurrentCompactionError,
|
|
6
|
+
import { BucketDataKey } from '../models.js';
|
|
7
|
+
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
8
8
|
import { cacheKey } from '../OperationBatch.js';
|
|
9
9
|
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
10
10
|
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
11
11
|
import { DEFAULT_MAX_DOC_SIZE_BYTES } from './chunking.js';
|
|
12
|
+
import {
|
|
13
|
+
applyStatsReplacement,
|
|
14
|
+
bucketStats,
|
|
15
|
+
BucketStatsWithChecksum,
|
|
16
|
+
chooseCompactionKind,
|
|
17
|
+
combineAdjacentStats,
|
|
18
|
+
combineChunkStats,
|
|
19
|
+
CompactIntervalConfig,
|
|
20
|
+
CompactionContext,
|
|
21
|
+
CompactionDecision,
|
|
22
|
+
CompactionKind,
|
|
23
|
+
CompactionResult,
|
|
24
|
+
CompactTargetConfig,
|
|
25
|
+
emptyBucketStats,
|
|
26
|
+
firstUncompactedWrite,
|
|
27
|
+
forcedCompactionKind,
|
|
28
|
+
PendingCompactionGroup,
|
|
29
|
+
readCompactionBatch,
|
|
30
|
+
ScheduledCompactionOptions,
|
|
31
|
+
statsForDocument,
|
|
32
|
+
statsForDocuments,
|
|
33
|
+
unclaimedSnapshotFilter
|
|
34
|
+
} from './compact-utils.js';
|
|
35
|
+
import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
|
|
36
|
+
import { AVAILABLE_LEASE_EXPR, CompactionLease } from './CompactionLease.js';
|
|
12
37
|
import { BucketDataDocumentV3, BucketStateDocumentV3 } from './models.js';
|
|
13
|
-
import { DefinitionChecksumOperations, MongoChecksumsV3 } from './MongoChecksumsV3.js';
|
|
14
38
|
import type { MongoSyncBucketStorageV3 } from './MongoSyncBucketStorageV3.js';
|
|
15
39
|
import { BucketDataObjectStorage, hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
16
40
|
import { ObjectStorageLifecycle, PreparedObjectStorageUpload } from './object-storage/ObjectStorageLifecycle.js';
|
|
41
|
+
import { createObjectStorageUsageWriterId, ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
|
|
17
42
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*/
|
|
24
|
-
inputs: BucketDataDocumentV3[];
|
|
25
|
-
ops: BucketDataDoc[];
|
|
26
|
-
changed: boolean;
|
|
27
|
-
targetOp: InternalOpId | null;
|
|
28
|
-
}
|
|
44
|
+
const DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS = 2 * 60 * 60 * 1000;
|
|
45
|
+
const DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
46
|
+
const DEFAULT_COMPACT_LEASE_DURATION_MS = 10 * 60 * 1000;
|
|
47
|
+
const SCHEDULED_COMPACTION_BATCH_SIZE = 100;
|
|
29
48
|
|
|
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
|
-
}
|
|
49
|
+
interface CompactionGroupResult {
|
|
50
|
+
documentId: BucketDataKey;
|
|
51
|
+
stats: BucketStatsWithChecksum;
|
|
52
|
+
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
interface CompactionStatsReplacement {
|
|
55
|
+
before: BucketStatsWithChecksum;
|
|
56
|
+
after: BucketStatsWithChecksum;
|
|
57
|
+
}
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return { documents, hasMore: false };
|
|
63
|
-
} finally {
|
|
64
|
-
await cursor.close();
|
|
65
|
-
}
|
|
59
|
+
interface ClearCompactionResult extends CompactionStatsReplacement {
|
|
60
|
+
opCountDiff: number;
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
export class MongoCompactorV3 extends MongoCompactor {
|
|
63
|
+
export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalConfig, CompactTargetConfig {
|
|
69
64
|
declare protected readonly db: VersionedPowerSyncMongoV3;
|
|
70
65
|
declare protected readonly storage: MongoSyncBucketStorageV3;
|
|
71
66
|
|
|
72
|
-
|
|
67
|
+
readonly minCompactChunkIntervalMs: number;
|
|
68
|
+
readonly minCompactFullIntervalMs: number;
|
|
69
|
+
readonly maxCompactFullIntervalMs: number;
|
|
70
|
+
readonly compactLeaseDurationMs: number;
|
|
71
|
+
readonly maxOpIdCap: InternalOpId | undefined;
|
|
72
|
+
private readonly objectStorageUsage: ObjectStorageUsage;
|
|
73
|
+
|
|
74
|
+
constructor(bucketStorage: MongoSyncBucketStorageV3, db: VersionedPowerSyncMongoV3, options: storage.CompactOptions) {
|
|
75
|
+
super(bucketStorage, db, options);
|
|
76
|
+
this.minCompactChunkIntervalMs = options.minCompactChunkIntervalMs ?? DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS;
|
|
77
|
+
this.minCompactFullIntervalMs = options.minCompactFullIntervalMs ?? DEFAULT_MIN_COMPACT_FULL_INTERVAL_MS;
|
|
78
|
+
this.maxCompactFullIntervalMs = options.maxCompactFullIntervalMs ?? DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS;
|
|
79
|
+
this.compactLeaseDurationMs = options.compactLeaseDurationMs ?? DEFAULT_COMPACT_LEASE_DURATION_MS;
|
|
80
|
+
this.maxOpIdCap = options.maxOpId;
|
|
81
|
+
this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override async compact(): Promise<number> {
|
|
73
85
|
if (this.storage.objectStorage) {
|
|
74
86
|
// Clean these before compacting - should be quick in most cases.
|
|
75
87
|
try {
|
|
76
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
88
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
77
89
|
} catch (e) {
|
|
90
|
+
if (this.signal?.aborted) {
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
78
93
|
// In this case, still continue normal compact process
|
|
79
94
|
this.logger.error(`Failed to clean up object storage deletion markers before compaction`, e);
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
|
-
await
|
|
97
|
+
await this.deleteOldCheckpointRequests();
|
|
98
|
+
|
|
99
|
+
if (this.buckets != null) {
|
|
100
|
+
await this.compactExplicitBuckets(this.buckets);
|
|
101
|
+
} else if (this.compactChunksOnly) {
|
|
102
|
+
// Writers defer their first chunk-compaction check by this fixed default.
|
|
103
|
+
// Include that interval so this synchronous initial-replication pass
|
|
104
|
+
// processes the work that existed when it started.
|
|
105
|
+
await this.compactScheduledBuckets({
|
|
106
|
+
dueAheadMs: DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS,
|
|
107
|
+
forceKind: CompactionKind.Chunks
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
await this.compactScheduledBuckets();
|
|
111
|
+
}
|
|
83
112
|
if (this.storage.objectStorage) {
|
|
84
113
|
// Cleanup for any produced during compacting.
|
|
85
114
|
// Note that markers only expire after a delay, so this may skip many produced during this compact
|
|
86
115
|
// run. However, during long compact runs, this may also have many ones it can clean up.
|
|
87
|
-
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
116
|
+
await this.objectStorageLifecycle.cleanup(this.logger, { signal: this.signal });
|
|
117
|
+
await this.objectStorageUsage.foldStaleWriterDeltas();
|
|
88
118
|
}
|
|
119
|
+
return this.compactedBucketCount;
|
|
89
120
|
}
|
|
90
121
|
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
122
|
+
protected override async deleteOldCustomCheckpointRequests() {
|
|
123
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const collection of await this.db.listCustomCheckpointRequestCollections(this.group_id)) {
|
|
128
|
+
this.signal?.throwIfAborted();
|
|
129
|
+
await collection.deleteMany({
|
|
130
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore }
|
|
131
|
+
});
|
|
94
132
|
}
|
|
95
|
-
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
96
133
|
}
|
|
97
134
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
/** An explicit compact request always runs a full compact for its buckets. */
|
|
136
|
+
private async compactExplicitBuckets(buckets: string[]) {
|
|
137
|
+
for (const bucket of buckets) {
|
|
138
|
+
// This is not a super efficient query, but this is not a common use case.
|
|
139
|
+
// May be optimized later.
|
|
140
|
+
const states = await this.db
|
|
141
|
+
.bucketState(this.group_id)
|
|
142
|
+
.find({ '_id.b': bucket }, { projection: { _id: 1 } })
|
|
143
|
+
.toArray();
|
|
144
|
+
for (const state of states) {
|
|
145
|
+
await using lease = await this.claimBucket({ _id: state._id });
|
|
146
|
+
if (lease == null || lease.state.first_uncompacted_write == null) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (this.isCompactionTargetCovered(lease.state, CompactionKind.Full)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const decision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
153
|
+
await this.compactClaimedBucket(lease, CompactionKind.Full, decision);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
113
156
|
}
|
|
114
157
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Process scheduled work in bounded batches.
|
|
160
|
+
*
|
|
161
|
+
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
162
|
+
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
163
|
+
*
|
|
164
|
+
* Buckets that do need compaction are still claimed and processed sequentially.
|
|
165
|
+
*
|
|
166
|
+
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
167
|
+
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
168
|
+
* filtered out when claiming a compaction lease.
|
|
169
|
+
*
|
|
170
|
+
* We filter scheduled jobs by the job start date, so that the same bucket is not compacted
|
|
171
|
+
* multiple times in one run. Reschedules fall beyond the fixed boundary. For the run after
|
|
172
|
+
* initial replication, dueAheadMs extends that boundary to include the first deferred interval.
|
|
173
|
+
*/
|
|
174
|
+
private async compactScheduledBuckets(options: ScheduledCompactionOptions = {}) {
|
|
175
|
+
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
176
|
+
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
177
|
+
// the exact initial-replication interval.
|
|
178
|
+
const [{ now: jobStartedAt }] = await this.db.db
|
|
179
|
+
.aggregate<{ now: Date }>([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
180
|
+
.toArray();
|
|
181
|
+
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
182
|
+
const forceKind = options.forceKind;
|
|
183
|
+
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
184
|
+
while (true) {
|
|
185
|
+
this.signal?.throwIfAborted();
|
|
186
|
+
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
187
|
+
if (states.length == 0) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const scheduled: {
|
|
192
|
+
state: BucketStateDocumentV3;
|
|
193
|
+
decision: CompactionDecision;
|
|
194
|
+
forcedKind: CompactionKind | null;
|
|
195
|
+
}[] = [];
|
|
196
|
+
for (const state of states) {
|
|
197
|
+
try {
|
|
198
|
+
scheduled.push({
|
|
199
|
+
state,
|
|
200
|
+
decision: chooseCompactionKind(state, jobStartedAt, this),
|
|
201
|
+
forcedKind: forcedCompactionKind(state, forceKind, this)
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const noOpStates = scheduled.filter(
|
|
208
|
+
({ state, decision, forcedKind }) =>
|
|
209
|
+
state.compact_lease == null && (forceKind == null ? decision.kind : forcedKind) == null
|
|
210
|
+
);
|
|
211
|
+
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
212
|
+
|
|
213
|
+
for (const { state, decision, forcedKind } of scheduled) {
|
|
214
|
+
const kind = forceKind == null ? decision.kind : forcedKind;
|
|
215
|
+
if (state.compact_lease == null && kind == null) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
221
|
+
if (lease == null) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
225
|
+
const claimedKind =
|
|
226
|
+
forceKind == null ? claimedDecision.kind : forcedCompactionKind(lease.state, forceKind, this);
|
|
227
|
+
if (claimedKind == null) {
|
|
228
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
229
|
+
} else if (this.isCompactionTargetCovered(lease.state, claimedKind)) {
|
|
230
|
+
// The run cannot advance this kind's watermark without regressing
|
|
231
|
+
// already-published progress. Keep any newer work scheduled.
|
|
232
|
+
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
233
|
+
} else {
|
|
234
|
+
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore);
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (this.signal?.aborted) {
|
|
238
|
+
// When aborted, stop completely, rather than logging and re-scheduling individual buckets.
|
|
239
|
+
// The lease on the current bucket is still released automatically.
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
118
245
|
}
|
|
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
246
|
}
|
|
127
247
|
|
|
128
|
-
|
|
129
|
-
|
|
248
|
+
/** Read a bounded, priority-ordered snapshot of currently claimable scheduled work. */
|
|
249
|
+
private async findScheduledBucketBatch(dueBefore: Date): Promise<BucketStateDocumentV3[]> {
|
|
250
|
+
return this.db
|
|
130
251
|
.bucketState(this.group_id)
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
252
|
+
.find({
|
|
253
|
+
next_compact_check: { $lte: dueBefore },
|
|
254
|
+
...AVAILABLE_LEASE_EXPR
|
|
255
|
+
})
|
|
256
|
+
.sort({ next_compact_check: 1 })
|
|
257
|
+
.limit(SCHEDULED_COMPACTION_BATCH_SIZE)
|
|
258
|
+
.toArray();
|
|
134
259
|
}
|
|
135
260
|
|
|
136
261
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
262
|
+
* Reschedule snapshots that were already known to be no-ops without first
|
|
263
|
+
* taking a lease. Every decision input is compared so a concurrent writer
|
|
264
|
+
* or compactor simply makes the update a no-op instead of losing work. A
|
|
265
|
+
* successful reschedule moves beyond this run's fixed selection boundary.
|
|
139
266
|
*/
|
|
140
|
-
private
|
|
141
|
-
|
|
267
|
+
private async rescheduleUnclaimedBuckets(
|
|
268
|
+
states: { state: BucketStateDocumentV3; decision: CompactionDecision }[],
|
|
269
|
+
notBefore: Date
|
|
270
|
+
) {
|
|
271
|
+
if (states.length == 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await this.db.bucketState(this.group_id).bulkWrite(
|
|
275
|
+
states.map(({ state, decision }) => ({
|
|
276
|
+
updateOne: {
|
|
277
|
+
filter: unclaimedSnapshotFilter(state),
|
|
278
|
+
update: [{ $set: { next_compact_check: this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore) } }]
|
|
279
|
+
}
|
|
280
|
+
})),
|
|
281
|
+
{ ordered: false }
|
|
282
|
+
);
|
|
142
283
|
}
|
|
143
284
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Isolate a malformed bucket so it cannot prevent other scheduled buckets
|
|
287
|
+
* from compacting. The snapshot filter preserves any concurrent write or
|
|
288
|
+
* compactor result instead of overwriting its next check.
|
|
289
|
+
*/
|
|
290
|
+
private async rescheduleFailedBucket(state: BucketStateDocumentV3, notBefore: Date, error: unknown) {
|
|
291
|
+
this.logger.error(`Failed to compact scheduled bucket ${state._id.b}; rescheduling it`, error);
|
|
292
|
+
try {
|
|
293
|
+
await this.db
|
|
294
|
+
.bucketState(this.group_id)
|
|
295
|
+
.updateOne(unclaimedSnapshotFilter(state), [{ $set: { next_compact_check: notBefore } }]);
|
|
296
|
+
} catch (rescheduleError) {
|
|
297
|
+
this.logger.error(`Failed to reschedule bucket ${state._id.b} after a compaction error`, rescheduleError);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Given a bucket filter, claim a lease on the bucket. The filter should include a filter on _id.
|
|
303
|
+
*
|
|
304
|
+
* Resolves to null if the bucket is already claimed, not found, or filtered out.
|
|
305
|
+
*/
|
|
306
|
+
private async claimBucket(
|
|
307
|
+
filter: mongo.Filter<BucketStateDocumentV3>,
|
|
308
|
+
sort?: mongo.Sort
|
|
309
|
+
): Promise<CompactionLease | null> {
|
|
310
|
+
return CompactionLease.claim(this.db.bucketState(this.group_id), filter, sort, this.compactLeaseDurationMs);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async compactClaimedBucket(
|
|
314
|
+
lease: CompactionLease,
|
|
315
|
+
kind: CompactionKind,
|
|
316
|
+
decision: CompactionDecision,
|
|
317
|
+
rescheduleNotBefore?: Date
|
|
318
|
+
) {
|
|
319
|
+
const context = new CompactionContext(
|
|
320
|
+
lease,
|
|
321
|
+
kind,
|
|
322
|
+
decision,
|
|
323
|
+
rescheduleNotBefore,
|
|
324
|
+
this.compactionTarget(lease.state)
|
|
158
325
|
);
|
|
326
|
+
lease.startRenewal();
|
|
327
|
+
await this.compactSingleBucket(context);
|
|
159
328
|
}
|
|
160
329
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
330
|
+
private async rescheduleClaimedBucket(lease: CompactionLease, decision: CompactionDecision, notBefore?: Date) {
|
|
331
|
+
await lease.reschedule(this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private rescheduleAtOrAfter(nextCompactCheck: mongo.Document, notBefore: Date | undefined): mongo.Document {
|
|
335
|
+
return notBefore == null ? nextCompactCheck : { $max: [nextCompactCheck, notBefore] };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private compactionTarget(state: BucketStateDocumentV3): InternalOpId {
|
|
339
|
+
return this.maxOpIdCap == null || state.last_op < this.maxOpIdCap ? state.last_op : this.maxOpIdCap;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private isCompactionTargetCovered(state: BucketStateDocumentV3, kind: CompactionKind): boolean {
|
|
343
|
+
const target = this.compactionTarget(state);
|
|
344
|
+
if (kind == CompactionKind.Chunks) {
|
|
345
|
+
return state.compacted_state != null && state.compacted_state.op_id >= target;
|
|
167
346
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
347
|
+
if (state.last_full_compact != null && state.last_full_compact.op_id >= target) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
// A full compact may change counts before the checksum-cache boundary.
|
|
351
|
+
// Wait for the safe target to catch up instead of publishing an older or
|
|
352
|
+
// stale cache. At the same boundary, full coverage can still advance.
|
|
353
|
+
return state.compacted_state != null && state.compacted_state.op_id > target;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private get objectStorageLifecycle(): ObjectStorageLifecycle {
|
|
357
|
+
if (!this.storage.objectStorage) {
|
|
358
|
+
throw new Error('Object storage is not configured');
|
|
359
|
+
}
|
|
360
|
+
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async compactSingleBucket(context: CompactionContext) {
|
|
364
|
+
if (context.kind == CompactionKind.Chunks) {
|
|
365
|
+
return this.compactSingleBucketChunks(context);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return this.compactSingleBucketFully(context);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Merge adjacent bucket-data chunks without inspecting their operations
|
|
373
|
+
* unless a merge is possible. The metadata contains enough information to
|
|
374
|
+
* update the persisted checksum state and to decide whether a group can fit
|
|
375
|
+
* in one chunk.
|
|
376
|
+
*/
|
|
377
|
+
private async compactSingleBucketChunks(context: CompactionContext) {
|
|
378
|
+
const bucket = context.state._id.b;
|
|
379
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
380
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
381
|
+
bucket,
|
|
382
|
+
definitionId: resolvedDefinitionId,
|
|
383
|
+
replicationStreamId: this.group_id
|
|
384
|
+
});
|
|
385
|
+
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
386
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
387
|
+
let previousCompactedState = context.state.compacted_state;
|
|
388
|
+
// A zero boundary represents an empty prefix, so there is no stored chunk
|
|
389
|
+
// whose statistics need to be carried into this pass.
|
|
390
|
+
if (previousCompactedState?.op_id === 0n) {
|
|
391
|
+
previousCompactedState = undefined;
|
|
392
|
+
}
|
|
393
|
+
// Include the last previously compacted chunk as well as new chunks. It
|
|
394
|
+
// is the only old chunk which can become mergeable with the new tail.
|
|
395
|
+
let lowerBound =
|
|
396
|
+
previousCompactedState != null ? bucketContext.docId(previousCompactedState.op_id - 1n) : bucketContext.minId;
|
|
397
|
+
const upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
398
|
+
let cachedBoundaryToVerify = previousCompactedState?.op_id;
|
|
399
|
+
|
|
400
|
+
let compactedOpId: bigint | null = null;
|
|
401
|
+
let overlappingCompactedChunk: BucketStatsWithChecksum | undefined;
|
|
402
|
+
let compactedTail = emptyBucketStats();
|
|
403
|
+
let pendingChunks: BucketDataDocumentV3[] = [];
|
|
404
|
+
let pendingSize = 0;
|
|
405
|
+
|
|
406
|
+
while (true) {
|
|
407
|
+
this.signal?.throwIfAborted();
|
|
408
|
+
await context.lease.throwIfLost();
|
|
409
|
+
|
|
410
|
+
const batch = await readCompactionBatch(
|
|
411
|
+
collection.aggregate<BucketDataDocumentV3>(
|
|
412
|
+
[
|
|
413
|
+
{
|
|
414
|
+
$match: {
|
|
415
|
+
_id: {
|
|
416
|
+
$gt: lowerBound,
|
|
417
|
+
$lt: upperBound
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{ $sort: { _id: 1 } },
|
|
422
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
423
|
+
{
|
|
424
|
+
$project: {
|
|
425
|
+
_id: 1,
|
|
426
|
+
min_op: 1,
|
|
427
|
+
checksum: 1,
|
|
428
|
+
count: 1,
|
|
429
|
+
size: 1,
|
|
430
|
+
target_op: 1,
|
|
431
|
+
storage_ref: 1
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
],
|
|
435
|
+
{ batchSize: this.moveBatchQueryLimit + 1 }
|
|
436
|
+
),
|
|
437
|
+
{
|
|
438
|
+
byteLimit: this.moveBatchByteLimit,
|
|
439
|
+
documentLimit: this.moveBatchQueryLimit
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (cachedBoundaryToVerify != null) {
|
|
444
|
+
const cachedBoundary = cachedBoundaryToVerify;
|
|
445
|
+
cachedBoundaryToVerify = undefined;
|
|
446
|
+
if (batch.documents[0]?._id.o !== cachedBoundary) {
|
|
447
|
+
// A previous attempt may have replaced the cached boundary before
|
|
448
|
+
// finalizing bucket state. Keep the persisted cache available to
|
|
449
|
+
// readers, but ignore it in this attempt and calculate its
|
|
450
|
+
// replacement through the normal scan from the bucket beginning.
|
|
451
|
+
previousCompactedState = undefined;
|
|
452
|
+
lowerBound = bucketContext.minId;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
172
455
|
}
|
|
456
|
+
|
|
457
|
+
if (batch.documents.length == 0) {
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const doc of batch.documents) {
|
|
462
|
+
compactedOpId = maxOpId(compactedOpId, doc._id.o);
|
|
463
|
+
const documentStats = statsForDocument(doc);
|
|
464
|
+
if (previousCompactedState?.op_id === doc._id.o) {
|
|
465
|
+
overlappingCompactedChunk = documentStats;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const nextSize = pendingSize + doc.size;
|
|
469
|
+
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
470
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
471
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
472
|
+
pendingChunks = [];
|
|
473
|
+
pendingSize = 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
pendingChunks.push(doc);
|
|
477
|
+
pendingSize += doc.size;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
lowerBound = batch.documents[batch.documents.length - 1]._id;
|
|
481
|
+
if (!batch.hasMore) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (pendingChunks.length > 0) {
|
|
487
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
488
|
+
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (compactedOpId == null) {
|
|
492
|
+
await this.finalizeSkippedBucket(context);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const compactedState =
|
|
497
|
+
previousCompactedState == null
|
|
498
|
+
? compactedTail
|
|
499
|
+
: combineChunkStats(previousCompactedState, compactedTail, overlappingCompactedChunk!);
|
|
500
|
+
const tailStats =
|
|
501
|
+
compactedOpId == context.lastOp
|
|
502
|
+
? undefined
|
|
503
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
504
|
+
const result: CompactionResult = {
|
|
505
|
+
compactedState,
|
|
506
|
+
bucketStats: tailStats == null ? compactedState : combineAdjacentStats(compactedState, tailStats)
|
|
173
507
|
};
|
|
508
|
+
|
|
509
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: 0 });
|
|
510
|
+
this.compactedBucketCount++;
|
|
511
|
+
this.logger.info(
|
|
512
|
+
`Compacted bucket chunks ${bucket}: ${result.bucketStats.count} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`
|
|
513
|
+
);
|
|
174
514
|
}
|
|
175
515
|
|
|
176
|
-
private async
|
|
516
|
+
private async flushChunkMerge(
|
|
177
517
|
bucket: string,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
518
|
+
inputs: BucketDataDocumentV3[],
|
|
519
|
+
collection: mongo.Collection<BucketDataDocumentV3>,
|
|
520
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
521
|
+
bucketContext: BucketDataContextV3
|
|
522
|
+
): Promise<BucketStatsWithChecksum> {
|
|
523
|
+
if (inputs.length == 1) {
|
|
524
|
+
return statsForDocument(inputs[0]);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
528
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
529
|
+
// the same rule.
|
|
530
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
531
|
+
if (inlineInputs.length > 0) {
|
|
532
|
+
const inlineDocuments = await collection
|
|
533
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
534
|
+
.toArray();
|
|
535
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
536
|
+
for (const input of inlineInputs) {
|
|
537
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
192
538
|
}
|
|
193
539
|
}
|
|
540
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
194
541
|
|
|
195
|
-
|
|
196
|
-
|
|
542
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
543
|
+
const targetOp = inputs.reduce<InternalOpId | null>(
|
|
544
|
+
(maxTarget, input) => maxOpId(maxTarget, input.target_op),
|
|
545
|
+
null
|
|
546
|
+
);
|
|
547
|
+
const result = await this.flushCompactionGroup(
|
|
548
|
+
bucket,
|
|
549
|
+
{
|
|
550
|
+
inputs,
|
|
551
|
+
ops: operations,
|
|
552
|
+
changed: true,
|
|
553
|
+
targetOp
|
|
554
|
+
},
|
|
555
|
+
bucketContext,
|
|
556
|
+
context
|
|
557
|
+
);
|
|
558
|
+
return result.stats;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private async finalizeCompactedBucket({
|
|
562
|
+
context,
|
|
563
|
+
compactedOpId,
|
|
564
|
+
compactionResult,
|
|
565
|
+
puts
|
|
566
|
+
}: {
|
|
567
|
+
context: CompactionContext;
|
|
568
|
+
compactedOpId: InternalOpId;
|
|
569
|
+
compactionResult: CompactionResult;
|
|
570
|
+
puts: number;
|
|
571
|
+
}) {
|
|
572
|
+
await context.lease.throwIfLost();
|
|
573
|
+
const startedStats = bucketStats(context.state);
|
|
574
|
+
const delta = {
|
|
575
|
+
count: compactionResult.bucketStats.count - startedStats.count,
|
|
576
|
+
bytes: compactionResult.bucketStats.bytes - startedStats.bytes,
|
|
577
|
+
chunks: compactionResult.bucketStats.chunks - startedStats.chunks
|
|
578
|
+
};
|
|
579
|
+
const coveredClaimedHead = compactedOpId >= context.lastOp;
|
|
580
|
+
const concurrentWriteCheck = { $gt: ['$last_op', context.lastOp] };
|
|
581
|
+
const remainingFullWorkCheck = coveredClaimedHead ? concurrentWriteCheck : true;
|
|
582
|
+
const nextAfterPartialFullCompact = this.rescheduleAtOrAfter(
|
|
583
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } },
|
|
584
|
+
context.rescheduleNotBefore
|
|
585
|
+
);
|
|
586
|
+
const nextCheckForUncompactedWork = this.rescheduleAtOrAfter(
|
|
587
|
+
{
|
|
588
|
+
$min: [
|
|
589
|
+
new Date(firstUncompactedWrite(context.state).getTime() + this.maxCompactFullIntervalMs),
|
|
590
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
context.rescheduleNotBefore
|
|
594
|
+
);
|
|
595
|
+
const update: mongo.Document = {
|
|
596
|
+
compacted_state: {
|
|
597
|
+
op_id: compactedOpId,
|
|
598
|
+
checksum: BigInt(compactionResult.compactedState.checksum),
|
|
599
|
+
count: compactionResult.compactedState.count,
|
|
600
|
+
bytes: compactionResult.compactedState.bytes,
|
|
601
|
+
chunks: compactionResult.compactedState.chunks,
|
|
602
|
+
at: '$$NOW'
|
|
603
|
+
},
|
|
604
|
+
bucket_stats: {
|
|
605
|
+
count: { $add: ['$bucket_stats.count', delta.count] },
|
|
606
|
+
bytes: { $add: ['$bucket_stats.bytes', delta.bytes] },
|
|
607
|
+
chunks: { $add: ['$bucket_stats.chunks', delta.chunks] }
|
|
608
|
+
},
|
|
609
|
+
first_uncompacted_write:
|
|
610
|
+
context.kind == CompactionKind.Full
|
|
611
|
+
? { $cond: [remainingFullWorkCheck, '$$NOW', '$$REMOVE'] }
|
|
612
|
+
: '$first_uncompacted_write',
|
|
613
|
+
next_compact_check:
|
|
614
|
+
context.kind == CompactionKind.Full
|
|
615
|
+
? { $cond: [remainingFullWorkCheck, nextAfterPartialFullCompact, '$$REMOVE'] }
|
|
616
|
+
: nextCheckForUncompactedWork
|
|
617
|
+
};
|
|
618
|
+
if (context.kind == CompactionKind.Full) {
|
|
619
|
+
update.last_full_compact = {
|
|
620
|
+
op_id: compactedOpId,
|
|
621
|
+
count: compactionResult.compactedState.count,
|
|
622
|
+
puts,
|
|
623
|
+
at: '$$NOW'
|
|
624
|
+
};
|
|
197
625
|
}
|
|
198
626
|
|
|
199
|
-
|
|
627
|
+
await context.lease.finalize(update);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private async finalizeSkippedBucket(context: CompactionContext) {
|
|
631
|
+
// A maxOpId cap can exclude the first remaining document entirely. Avoid
|
|
632
|
+
// immediately claiming the same no-progress bucket again in this run.
|
|
633
|
+
await this.rescheduleClaimedBucket(
|
|
634
|
+
context.lease,
|
|
635
|
+
{
|
|
636
|
+
...context.decision,
|
|
637
|
+
nextCompactCheck: {
|
|
638
|
+
$max: [
|
|
639
|
+
context.decision.nextCompactCheck,
|
|
640
|
+
{ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.minCompactChunkIntervalMs } }
|
|
641
|
+
]
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
context.rescheduleNotBefore
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Read bucket stats directly from bucket_data documents.
|
|
650
|
+
*/
|
|
651
|
+
private async readBucketStats(
|
|
652
|
+
bucket: string,
|
|
653
|
+
definitionId: BucketDefinitionId,
|
|
654
|
+
maxOp: InternalOpId,
|
|
655
|
+
lowerBound?: BucketDataKey
|
|
656
|
+
): Promise<BucketStatsWithChecksum> {
|
|
657
|
+
const context = new BucketDataContextV3(this.db, {
|
|
200
658
|
bucket,
|
|
201
|
-
definitionId
|
|
659
|
+
definitionId,
|
|
202
660
|
replicationStreamId: this.group_id
|
|
203
661
|
});
|
|
662
|
+
const [stats] = await this.db
|
|
663
|
+
.bucketData(this.group_id, definitionId)
|
|
664
|
+
.aggregate<{ count: number; bytes: number | bigint; chunks: number; checksum: bigint }>([
|
|
665
|
+
{
|
|
666
|
+
$match: {
|
|
667
|
+
_id:
|
|
668
|
+
lowerBound == null
|
|
669
|
+
? { $gte: context.minId, $lte: context.docId(maxOp) }
|
|
670
|
+
: { $gt: lowerBound, $lte: context.docId(maxOp) }
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
$group: {
|
|
675
|
+
_id: null,
|
|
676
|
+
count: { $sum: '$count' },
|
|
677
|
+
bytes: { $sum: '$size' },
|
|
678
|
+
chunks: { $sum: 1 },
|
|
679
|
+
checksum: { $sum: '$checksum' }
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
])
|
|
683
|
+
.toArray();
|
|
684
|
+
return {
|
|
685
|
+
count: Number(stats?.count ?? 0),
|
|
686
|
+
bytes: BigInt(stats?.bytes ?? 0),
|
|
687
|
+
chunks: Number(stats?.chunks ?? 0),
|
|
688
|
+
checksum:
|
|
689
|
+
typeof stats?.checksum == 'bigint'
|
|
690
|
+
? Number(BigInt.asIntN(32, stats.checksum))
|
|
691
|
+
: addChecksums(0, Number(stats?.checksum ?? 0))
|
|
692
|
+
};
|
|
204
693
|
}
|
|
205
694
|
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
695
|
+
private async compactSingleBucketFully(context: CompactionContext) {
|
|
696
|
+
const bucket = context.state._id.b;
|
|
697
|
+
const resolvedDefinitionId = context.state._id.d;
|
|
698
|
+
const bucketContext = new BucketDataContextV3(this.db, {
|
|
699
|
+
bucket,
|
|
700
|
+
definitionId: resolvedDefinitionId,
|
|
701
|
+
replicationStreamId: this.group_id
|
|
702
|
+
});
|
|
213
703
|
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
214
|
-
const
|
|
215
|
-
|
|
704
|
+
const dataContext = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
216
705
|
const lowerBound = bucketContext.minId;
|
|
217
|
-
let upperBound = bucketContext.docId(
|
|
706
|
+
let upperBound = bucketContext.docId(context.targetOp + 1n);
|
|
218
707
|
|
|
219
|
-
let totalChecksum = 0;
|
|
220
708
|
let totalOpCount = 0;
|
|
221
|
-
let totalOpBytes = 0;
|
|
222
709
|
|
|
223
710
|
let lastNotPut: bigint | null = null;
|
|
224
711
|
let opsSincePut = 0;
|
|
225
712
|
let compactedOpId: bigint | null = null;
|
|
226
713
|
let clearBoundary: { opId: bigint; documentId: BucketDataKey } | null = null;
|
|
714
|
+
let compactedStats = emptyBucketStats();
|
|
227
715
|
const seen = new Map<string, bigint>();
|
|
228
716
|
let trackingSize = 0;
|
|
717
|
+
let putCount = 0;
|
|
229
718
|
let pendingGroup: PendingCompactionGroup | null = null;
|
|
230
719
|
|
|
231
720
|
// --- Read batch from MongoDB ---
|
|
232
721
|
while (true) {
|
|
233
722
|
this.signal?.throwIfAborted();
|
|
723
|
+
await context.lease.throwIfLost();
|
|
234
724
|
|
|
235
725
|
const pipeline: mongo.Document[] = [
|
|
236
726
|
{
|
|
@@ -280,7 +770,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
280
770
|
// merging is useful, and writes each final object at most once.
|
|
281
771
|
for (const doc of batchDocs) {
|
|
282
772
|
compactedOpId ??= doc._id.o;
|
|
283
|
-
const originalOps = Array.from(loadBucketDataDocument(
|
|
773
|
+
const originalOps = Array.from(loadBucketDataDocument(dataContext, doc));
|
|
284
774
|
|
|
285
775
|
let changed = false;
|
|
286
776
|
const compactedOps: BucketDataDoc[] = [];
|
|
@@ -299,6 +789,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
299
789
|
row_id: undefined,
|
|
300
790
|
source_table: undefined,
|
|
301
791
|
source_key: undefined,
|
|
792
|
+
subkey: undefined,
|
|
302
793
|
data: null
|
|
303
794
|
});
|
|
304
795
|
changed = true;
|
|
@@ -313,6 +804,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
313
804
|
}
|
|
314
805
|
compactedOps.push(op);
|
|
315
806
|
if (op.op == 'PUT') {
|
|
807
|
+
putCount++;
|
|
316
808
|
lastNotPut = null;
|
|
317
809
|
opsSincePut = 0;
|
|
318
810
|
} else {
|
|
@@ -334,10 +826,6 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
334
826
|
}
|
|
335
827
|
compactedOps.reverse();
|
|
336
828
|
|
|
337
|
-
for (const op of compactedOps) {
|
|
338
|
-
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
339
|
-
totalOpBytes += op.data?.length ?? 0;
|
|
340
|
-
}
|
|
341
829
|
totalOpCount += compactedOps.length;
|
|
342
830
|
|
|
343
831
|
const candidate: PendingCompactionGroup = {
|
|
@@ -361,13 +849,14 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
361
849
|
};
|
|
362
850
|
} else {
|
|
363
851
|
const flushedGroup = pendingGroup;
|
|
364
|
-
const
|
|
852
|
+
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext);
|
|
853
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
365
854
|
if (
|
|
366
855
|
lastNotPut != null &&
|
|
367
856
|
flushedGroup.ops[0].o <= lastNotPut &&
|
|
368
857
|
flushedGroup.ops[flushedGroup.ops.length - 1].o >= lastNotPut
|
|
369
858
|
) {
|
|
370
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
859
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
371
860
|
}
|
|
372
861
|
pendingGroup = candidate;
|
|
373
862
|
}
|
|
@@ -385,16 +874,18 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
385
874
|
}
|
|
386
875
|
|
|
387
876
|
if (pendingGroup != null) {
|
|
388
|
-
const
|
|
877
|
+
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext);
|
|
878
|
+
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
389
879
|
if (
|
|
390
880
|
lastNotPut != null &&
|
|
391
881
|
pendingGroup.ops[0].o <= lastNotPut &&
|
|
392
882
|
pendingGroup.ops[pendingGroup.ops.length - 1].o >= lastNotPut
|
|
393
883
|
) {
|
|
394
|
-
clearBoundary = { opId: lastNotPut, documentId };
|
|
884
|
+
clearBoundary = { opId: lastNotPut, documentId: result.documentId };
|
|
395
885
|
}
|
|
396
886
|
}
|
|
397
887
|
if (compactedOpId == null) {
|
|
888
|
+
await this.finalizeSkippedBucket(context);
|
|
398
889
|
return;
|
|
399
890
|
}
|
|
400
891
|
|
|
@@ -404,36 +895,33 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
404
895
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
405
896
|
}
|
|
406
897
|
|
|
407
|
-
|
|
898
|
+
const clearResult = await this.clearBucketLeading(
|
|
408
899
|
lastNotPut,
|
|
409
900
|
clearBoundary.documentId,
|
|
410
901
|
bucketContext,
|
|
411
902
|
collection,
|
|
412
|
-
|
|
903
|
+
dataContext
|
|
413
904
|
);
|
|
905
|
+
totalOpCount += clearResult.opCountDiff;
|
|
906
|
+
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
414
907
|
}
|
|
415
908
|
|
|
909
|
+
const tailStats =
|
|
910
|
+
compactedOpId == context.lastOp
|
|
911
|
+
? undefined
|
|
912
|
+
: await this.readBucketStats(bucket, resolvedDefinitionId, context.lastOp, bucketContext.docId(compactedOpId));
|
|
913
|
+
const result: CompactionResult = {
|
|
914
|
+
compactedState: compactedStats,
|
|
915
|
+
bucketStats: tailStats == null ? compactedStats : combineAdjacentStats(compactedStats, tailStats)
|
|
916
|
+
};
|
|
917
|
+
|
|
416
918
|
// --- 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
|
-
}
|
|
919
|
+
await this.finalizeCompactedBucket({ context, compactedOpId, compactionResult: result, puts: putCount });
|
|
435
920
|
|
|
436
|
-
|
|
921
|
+
this.compactedBucketCount++;
|
|
922
|
+
this.logger.info(
|
|
923
|
+
`Compacted bucket ${bucket}: ${totalOpCount} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`
|
|
924
|
+
);
|
|
437
925
|
}
|
|
438
926
|
|
|
439
927
|
/**
|
|
@@ -447,9 +935,12 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
447
935
|
group: PendingCompactionGroup,
|
|
448
936
|
bucketContext: BucketDataContextV3,
|
|
449
937
|
context: { replicationStreamId: number; definitionId: string }
|
|
450
|
-
): Promise<
|
|
938
|
+
): Promise<CompactionGroupResult> {
|
|
451
939
|
if (group.inputs.length == 1 && !group.changed) {
|
|
452
|
-
return
|
|
940
|
+
return {
|
|
941
|
+
documentId: group.inputs[0]._id,
|
|
942
|
+
stats: statsForDocument(group.inputs[0])
|
|
943
|
+
};
|
|
453
944
|
}
|
|
454
945
|
|
|
455
946
|
const inputs = group.inputs;
|
|
@@ -458,6 +949,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
458
949
|
const expectedChecksum = inputs.reduce((sum, doc) => sum + doc.checksum, 0n);
|
|
459
950
|
const expectedOpCount = inputs.reduce((sum, doc) => sum + doc.count, 0);
|
|
460
951
|
const oldStoragePaths = inputs.flatMap((doc) => (doc.storage_ref ? [doc.storage_ref.path] : []));
|
|
952
|
+
const oldStorageBytes = inputs.reduce((sum, document) => sum + ObjectStorageUsage.bytes(document), 0n);
|
|
461
953
|
const {
|
|
462
954
|
documents,
|
|
463
955
|
storagePaths: newStoragePaths,
|
|
@@ -498,6 +990,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
498
990
|
await bucketContext.collection.deleteMany({ _id: { $in: idsToDelete } }, { session });
|
|
499
991
|
await bucketContext.collection.insertMany(documents, { session });
|
|
500
992
|
await this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, session);
|
|
993
|
+
await this.recordObjectStorageReplacement(oldStorageBytes, documents, context.definitionId, session);
|
|
501
994
|
},
|
|
502
995
|
{
|
|
503
996
|
writeConcern: { w: 'majority' },
|
|
@@ -507,7 +1000,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
507
1000
|
} finally {
|
|
508
1001
|
await session.endSession();
|
|
509
1002
|
}
|
|
510
|
-
return
|
|
1003
|
+
return {
|
|
1004
|
+
documentId: documents[0]._id,
|
|
1005
|
+
stats: statsForDocuments(documents)
|
|
1006
|
+
};
|
|
511
1007
|
}
|
|
512
1008
|
|
|
513
1009
|
/**
|
|
@@ -516,7 +1012,8 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
516
1012
|
* before the known boundary document, then splits that boundary document
|
|
517
1013
|
* if it contains ops on both sides of lastNotPut.
|
|
518
1014
|
*
|
|
519
|
-
* Returns the op count
|
|
1015
|
+
* Returns the op count and stored-stat changes after replacing cleared ops
|
|
1016
|
+
* with CLEAR ops.
|
|
520
1017
|
*/
|
|
521
1018
|
private async clearBucketLeading(
|
|
522
1019
|
lastNotPut: bigint,
|
|
@@ -524,8 +1021,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
524
1021
|
bucketContext: BucketDataContextV3,
|
|
525
1022
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
526
1023
|
context: { replicationStreamId: number; definitionId: string }
|
|
527
|
-
): Promise<
|
|
1024
|
+
): Promise<ClearCompactionResult> {
|
|
528
1025
|
let opCountDiff = 0;
|
|
1026
|
+
let before = emptyBucketStats();
|
|
1027
|
+
let after = emptyBucketStats();
|
|
529
1028
|
const session = this.db.client.startSession();
|
|
530
1029
|
try {
|
|
531
1030
|
let done = false;
|
|
@@ -542,11 +1041,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
542
1041
|
);
|
|
543
1042
|
done = batch.done;
|
|
544
1043
|
opCountDiff += batch.opCountDiff;
|
|
1044
|
+
before = combineAdjacentStats(before, batch.before);
|
|
1045
|
+
after = combineAdjacentStats(after, batch.after);
|
|
545
1046
|
}
|
|
546
1047
|
|
|
547
1048
|
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
548
1049
|
// potentially followed by PUT operations. This is only a single document, so no need for batching.
|
|
549
|
-
|
|
1050
|
+
const boundaryResult = await this.clearBoundaryDocument(
|
|
550
1051
|
session,
|
|
551
1052
|
lastNotPut,
|
|
552
1053
|
boundaryDocId,
|
|
@@ -554,11 +1055,14 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
554
1055
|
collection,
|
|
555
1056
|
context
|
|
556
1057
|
);
|
|
1058
|
+
opCountDiff += boundaryResult.opCountDiff;
|
|
1059
|
+
before = combineAdjacentStats(before, boundaryResult.before);
|
|
1060
|
+
after = combineAdjacentStats(after, boundaryResult.after);
|
|
557
1061
|
} finally {
|
|
558
1062
|
await session.endSession();
|
|
559
1063
|
}
|
|
560
1064
|
|
|
561
|
-
return opCountDiff;
|
|
1065
|
+
return { opCountDiff, before, after };
|
|
562
1066
|
}
|
|
563
1067
|
|
|
564
1068
|
private async clearLeadingFullDocuments(
|
|
@@ -568,18 +1072,23 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
568
1072
|
bucketContext: BucketDataContextV3,
|
|
569
1073
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
570
1074
|
context: { replicationStreamId: number; definitionId: string }
|
|
571
|
-
): Promise<{ done: boolean; opCountDiff: number }> {
|
|
1075
|
+
): Promise<{ done: boolean; opCountDiff: number } & CompactionStatsReplacement> {
|
|
572
1076
|
const bucket = bucketContext.key.bucket;
|
|
573
1077
|
this.signal?.throwIfAborted();
|
|
574
|
-
|
|
1078
|
+
let prepared: PreparedObjectStorageUpload[] | undefined;
|
|
575
1079
|
let done = false;
|
|
576
1080
|
let opCountDiff = 0;
|
|
1081
|
+
let before = emptyBucketStats();
|
|
1082
|
+
let after = emptyBucketStats();
|
|
577
1083
|
|
|
578
1084
|
await session.withTransaction(
|
|
579
1085
|
async () => {
|
|
580
1086
|
done = false;
|
|
581
1087
|
opCountDiff = 0;
|
|
1088
|
+
before = emptyBucketStats();
|
|
1089
|
+
after = emptyBucketStats();
|
|
582
1090
|
const oldStoragePaths: string[] = [];
|
|
1091
|
+
let oldStorageBytes = 0n;
|
|
583
1092
|
const query = collection.find(
|
|
584
1093
|
{
|
|
585
1094
|
_id: {
|
|
@@ -595,6 +1104,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
595
1104
|
min_op: 1,
|
|
596
1105
|
checksum: 1,
|
|
597
1106
|
count: 1,
|
|
1107
|
+
size: 1,
|
|
598
1108
|
target_op: 1,
|
|
599
1109
|
has_clear_op: 1,
|
|
600
1110
|
storage_ref: 1
|
|
@@ -609,6 +1119,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
609
1119
|
let lastDocId: BucketDataKey | null = null;
|
|
610
1120
|
let clearOpCount = 0;
|
|
611
1121
|
let gotNonClearOp = false;
|
|
1122
|
+
const inputStats = emptyBucketStats();
|
|
612
1123
|
|
|
613
1124
|
for await (const doc of query.stream()) {
|
|
614
1125
|
if (doc.min_op > lastNotPut) {
|
|
@@ -618,9 +1129,15 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
618
1129
|
}
|
|
619
1130
|
|
|
620
1131
|
lastDocId = doc._id;
|
|
1132
|
+
const documentStats = statsForDocument(doc);
|
|
1133
|
+
inputStats.count += documentStats.count;
|
|
1134
|
+
inputStats.bytes += documentStats.bytes;
|
|
1135
|
+
inputStats.chunks += documentStats.chunks;
|
|
1136
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
621
1137
|
if (doc.storage_ref) {
|
|
622
1138
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
623
1139
|
}
|
|
1140
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
624
1141
|
|
|
625
1142
|
// The compaction scan established that every operation before the
|
|
626
1143
|
// boundary is MOVE/REMOVE/CLEAR. Root metadata is sufficient to fold
|
|
@@ -646,6 +1163,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
646
1163
|
return;
|
|
647
1164
|
}
|
|
648
1165
|
|
|
1166
|
+
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
649
1167
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
650
1168
|
await collection.deleteMany(
|
|
651
1169
|
{
|
|
@@ -669,8 +1187,11 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
669
1187
|
});
|
|
670
1188
|
await collection.insertOne(persisted.documents[0], { session });
|
|
671
1189
|
await this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, session);
|
|
1190
|
+
await this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, session);
|
|
672
1191
|
|
|
673
1192
|
opCountDiff = -clearedOpCount + 1;
|
|
1193
|
+
before = inputStats;
|
|
1194
|
+
after = statsForDocuments(persisted.documents);
|
|
674
1195
|
},
|
|
675
1196
|
{
|
|
676
1197
|
writeConcern: { w: 'majority' },
|
|
@@ -678,7 +1199,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
678
1199
|
}
|
|
679
1200
|
);
|
|
680
1201
|
|
|
681
|
-
return { done, opCountDiff };
|
|
1202
|
+
return { done, opCountDiff, before, after };
|
|
682
1203
|
}
|
|
683
1204
|
|
|
684
1205
|
private async clearBoundaryDocument(
|
|
@@ -688,16 +1209,21 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
688
1209
|
bucketContext: BucketDataContextV3,
|
|
689
1210
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
690
1211
|
context: { replicationStreamId: number; definitionId: string }
|
|
691
|
-
): Promise<
|
|
1212
|
+
): Promise<ClearCompactionResult> {
|
|
692
1213
|
const bucket = bucketContext.key.bucket;
|
|
693
1214
|
this.signal?.throwIfAborted();
|
|
694
1215
|
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
695
1216
|
let opCountDiff = 0;
|
|
1217
|
+
let before = emptyBucketStats();
|
|
1218
|
+
let after = emptyBucketStats();
|
|
696
1219
|
|
|
697
1220
|
await session.withTransaction(
|
|
698
1221
|
async () => {
|
|
699
1222
|
opCountDiff = 0;
|
|
1223
|
+
before = emptyBucketStats();
|
|
1224
|
+
after = emptyBucketStats();
|
|
700
1225
|
const oldStoragePaths: string[] = [];
|
|
1226
|
+
let oldStorageBytes = 0n;
|
|
701
1227
|
const query = collection.find(
|
|
702
1228
|
{
|
|
703
1229
|
// This is a range query, but should only ever return two documents:
|
|
@@ -716,6 +1242,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
716
1242
|
min_op: 1,
|
|
717
1243
|
checksum: 1,
|
|
718
1244
|
count: 1,
|
|
1245
|
+
size: 1,
|
|
719
1246
|
target_op: 1,
|
|
720
1247
|
ops: 1,
|
|
721
1248
|
storage_ref: 1
|
|
@@ -729,6 +1256,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
729
1256
|
let clearedOpCount = 0;
|
|
730
1257
|
let maxTargetOp: bigint | null = null;
|
|
731
1258
|
const boundarySurvivors: BucketDataDoc[] = [];
|
|
1259
|
+
const inputStats = emptyBucketStats();
|
|
732
1260
|
|
|
733
1261
|
for await (const doc of query.stream()) {
|
|
734
1262
|
docsRead++;
|
|
@@ -736,10 +1264,17 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
736
1264
|
throw new ReplicationAssertionError(`Unexpected extra document before CLEAR boundary in bucket ${bucket}`);
|
|
737
1265
|
}
|
|
738
1266
|
|
|
1267
|
+
const documentStats = statsForDocument(doc);
|
|
1268
|
+
inputStats.count += documentStats.count;
|
|
1269
|
+
inputStats.bytes += documentStats.bytes;
|
|
1270
|
+
inputStats.chunks += documentStats.chunks;
|
|
1271
|
+
inputStats.checksum = addChecksums(inputStats.checksum, documentStats.checksum);
|
|
1272
|
+
|
|
739
1273
|
const isBoundaryDoc = doc._id.o == boundaryDocId.o;
|
|
740
1274
|
if (doc.storage_ref) {
|
|
741
1275
|
oldStoragePaths.push(doc.storage_ref.path);
|
|
742
1276
|
}
|
|
1277
|
+
oldStorageBytes += ObjectStorageUsage.bytes(doc);
|
|
743
1278
|
await hydrateBucketDataDocuments([doc], this.storage.objectStorage, { signal: this.signal });
|
|
744
1279
|
maxTargetOp = maxOpId(maxTargetOp, doc.target_op);
|
|
745
1280
|
for (const op of loadBucketDataDocument(context, doc)) {
|
|
@@ -800,8 +1335,11 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
800
1335
|
});
|
|
801
1336
|
await collection.insertMany(persisted.documents, { session });
|
|
802
1337
|
await this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, session);
|
|
1338
|
+
await this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, session);
|
|
803
1339
|
|
|
804
1340
|
opCountDiff = -clearedOpCount + 1;
|
|
1341
|
+
before = inputStats;
|
|
1342
|
+
after = statsForDocuments(persisted.documents);
|
|
805
1343
|
},
|
|
806
1344
|
{
|
|
807
1345
|
writeConcern: { w: 'majority' },
|
|
@@ -809,7 +1347,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
809
1347
|
}
|
|
810
1348
|
);
|
|
811
1349
|
|
|
812
|
-
return opCountDiff;
|
|
1350
|
+
return { opCountDiff, before, after };
|
|
813
1351
|
}
|
|
814
1352
|
|
|
815
1353
|
/**
|
|
@@ -852,6 +1390,22 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
852
1390
|
);
|
|
853
1391
|
}
|
|
854
1392
|
|
|
1393
|
+
private async recordObjectStorageReplacement(
|
|
1394
|
+
oldBytes: bigint,
|
|
1395
|
+
newDocuments: Iterable<Pick<BucketDataDocumentV3, 'storage_ref'>>,
|
|
1396
|
+
definitionId: BucketDefinitionId,
|
|
1397
|
+
session: mongo.ClientSession
|
|
1398
|
+
): Promise<void> {
|
|
1399
|
+
if (!this.storage.objectStorage) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
let newBytes = 0n;
|
|
1403
|
+
for (const document of newDocuments) {
|
|
1404
|
+
newBytes += ObjectStorageUsage.bytes(document);
|
|
1405
|
+
}
|
|
1406
|
+
await this.objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, session);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
855
1409
|
private async persistBucketData(
|
|
856
1410
|
bucket: string,
|
|
857
1411
|
chunks: BucketDataDoc[][],
|
|
@@ -908,7 +1462,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
908
1462
|
}
|
|
909
1463
|
|
|
910
1464
|
const { ops, ...metadata } = serialized;
|
|
911
|
-
const { fileSize } = await store.store(upload.path, ops
|
|
1465
|
+
const { fileSize } = await store.store(upload.path, ops!, { signal: this.signal });
|
|
912
1466
|
storagePaths.add(upload.path);
|
|
913
1467
|
return {
|
|
914
1468
|
...metadata,
|