@powersync/service-module-mongodb-storage 0.18.2 → 0.19.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 +48 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +2 -0
- package/dist/storage/implementation/MongoChecksums.js +3 -2
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -5
- package/dist/storage/implementation/MongoSyncBucketStorage.js +25 -14
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +16 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +20 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +3 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +109 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +38 -1
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoChecksums.ts +5 -2
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +34 -24
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +16 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +54 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +3 -1
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +123 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +40 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +95 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -1,12 +1,71 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { addChecksums, utils } from '@powersync/service-core';
|
|
4
|
-
import { MongoCompactor } from '../MongoCompactor.js';
|
|
4
|
+
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
5
5
|
import { cacheKey } from '../OperationBatch.js';
|
|
6
|
-
import { loadBucketDataDocument, serializeBucketData } from './bucket-format.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
7
|
+
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
8
|
+
import { DEFAULT_MAX_DOC_SIZE_BYTES } from './chunking.js';
|
|
9
|
+
import { BucketDataObjectStorage, hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
10
|
+
import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
|
|
11
|
+
/**
|
|
12
|
+
* Read one bounded prefix from a descending compaction cursor.
|
|
13
|
+
*
|
|
14
|
+
* The document that would cross the byte limit is deliberately not returned:
|
|
15
|
+
* pagination resumes below the last returned `_id`, so that document remains
|
|
16
|
+
* eligible for the next query. The first document is always accepted to ensure
|
|
17
|
+
* progress when a single document exceeds the configured byte limit.
|
|
18
|
+
*
|
|
19
|
+
* `hasMore` is conservative when the document limit is reached. An extra empty
|
|
20
|
+
* query is preferable to exhausting the cursor just to determine whether the
|
|
21
|
+
* limited MongoDB query contained another document.
|
|
22
|
+
*/
|
|
23
|
+
async function readCompactionBatch(cursor, options) {
|
|
24
|
+
const documents = [];
|
|
25
|
+
let cumulativeBytes = 0;
|
|
26
|
+
try {
|
|
27
|
+
for await (const document of cursor) {
|
|
28
|
+
if (documents.length > 0 && cumulativeBytes + document.size > options.byteLimit) {
|
|
29
|
+
return { documents, hasMore: true };
|
|
30
|
+
}
|
|
31
|
+
documents.push(document);
|
|
32
|
+
cumulativeBytes += document.size;
|
|
33
|
+
if (documents.length >= options.documentLimit) {
|
|
34
|
+
return { documents, hasMore: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { documents, hasMore: false };
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await cursor.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
9
43
|
export class MongoCompactorV3 extends MongoCompactor {
|
|
44
|
+
async compact() {
|
|
45
|
+
if (this.storage.objectStorage) {
|
|
46
|
+
// Clean these before compacting - should be quick in most cases.
|
|
47
|
+
try {
|
|
48
|
+
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
// In this case, still continue normal compact process
|
|
52
|
+
this.logger.error(`Failed to clean up object storage deletion markers before compaction`, e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await super.compact();
|
|
56
|
+
if (this.storage.objectStorage) {
|
|
57
|
+
// Cleanup for any produced during compacting.
|
|
58
|
+
// Note that markers only expire after a delay, so this may skip many produced during this compact
|
|
59
|
+
// run. However, during long compact runs, this may also have many ones it can clean up.
|
|
60
|
+
await this.objectStorageLifecycle.cleanup(this.logger);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
get objectStorageLifecycle() {
|
|
64
|
+
if (!this.storage.objectStorage) {
|
|
65
|
+
throw new Error('Object storage is not configured');
|
|
66
|
+
}
|
|
67
|
+
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
68
|
+
}
|
|
10
69
|
async *dirtyBucketBatches(options) {
|
|
11
70
|
if (options.minBucketChanges <= 0) {
|
|
12
71
|
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
@@ -76,7 +135,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
76
135
|
if (resolvedDefinitionId == null) {
|
|
77
136
|
return null;
|
|
78
137
|
}
|
|
79
|
-
return new
|
|
138
|
+
return new BucketDataContextV3(this.db, {
|
|
80
139
|
bucket,
|
|
81
140
|
definitionId: resolvedDefinitionId,
|
|
82
141
|
replicationStreamId: this.group_id
|
|
@@ -91,22 +150,23 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
91
150
|
const collection = this.db.bucketData(this.group_id, resolvedDefinitionId);
|
|
92
151
|
const context = { replicationStreamId: this.group_id, definitionId: resolvedDefinitionId };
|
|
93
152
|
const lowerBound = bucketContext.minId;
|
|
94
|
-
let upperBound = bucketContext.
|
|
153
|
+
let upperBound = bucketContext.docId(this.maxOpId + 1n);
|
|
95
154
|
let totalChecksum = 0;
|
|
96
155
|
let totalOpCount = 0;
|
|
97
156
|
let totalOpBytes = 0;
|
|
98
157
|
let lastNotPut = null;
|
|
99
158
|
let opsSincePut = 0;
|
|
100
|
-
let
|
|
159
|
+
let compactedOpId = null;
|
|
160
|
+
let clearBoundary = null;
|
|
101
161
|
const seen = new Map();
|
|
102
162
|
let trackingSize = 0;
|
|
163
|
+
let pendingGroup = null;
|
|
103
164
|
// --- Read batch from MongoDB ---
|
|
104
165
|
while (true) {
|
|
105
166
|
this.signal?.throwIfAborted();
|
|
106
167
|
const pipeline = [
|
|
107
168
|
{
|
|
108
169
|
$match: {
|
|
109
|
-
'_id.b': bucket,
|
|
110
170
|
_id: {
|
|
111
171
|
$gte: lowerBound,
|
|
112
172
|
$lt: upperBound
|
|
@@ -124,110 +184,74 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
124
184
|
size: 1,
|
|
125
185
|
target_op: 1,
|
|
126
186
|
ops: 1,
|
|
127
|
-
|
|
187
|
+
storage_ref: 1
|
|
128
188
|
}
|
|
129
189
|
}
|
|
130
190
|
];
|
|
131
|
-
const
|
|
132
|
-
.aggregate(pipeline, {
|
|
191
|
+
const batch = await readCompactionBatch(collection.aggregate(pipeline, {
|
|
133
192
|
batchSize: this.moveBatchQueryLimit + 1
|
|
134
|
-
})
|
|
135
|
-
.
|
|
136
|
-
|
|
193
|
+
}), {
|
|
194
|
+
byteLimit: this.moveBatchByteLimit,
|
|
195
|
+
documentLimit: this.moveBatchQueryLimit
|
|
196
|
+
});
|
|
197
|
+
const batchDocs = batch.documents;
|
|
198
|
+
if (batchDocs.length == 0) {
|
|
137
199
|
// No more documents in this bucket — compaction complete.
|
|
138
200
|
break;
|
|
139
201
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
cumulativeBytes += Number(rawBatch[i].bsonSize);
|
|
145
|
-
if (cumulativeBytes > this.moveBatchByteLimit && i > 0) {
|
|
146
|
-
// Byte limit exceeded; cut batch at current index. Always include
|
|
147
|
-
// at least one document (i > 0 guard) to guarantee forward progress.
|
|
148
|
-
batchCutIndex = i;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const batchDocs = rawBatch.slice(0, batchCutIndex);
|
|
153
|
-
// --- Decode documents into individual ops ---
|
|
154
|
-
// Processable: document has at least one op <= maxOpId.
|
|
155
|
-
// Only processable docs are deleted and recreated; the rest survive untouched.
|
|
156
|
-
const batchOps = [];
|
|
157
|
-
const processableDocs = [];
|
|
202
|
+
await hydrateBucketDataDocuments(batchDocs, this.storage.objectStorage, { signal: this.signal });
|
|
203
|
+
// Compact each document independently, then greedily merge adjacent
|
|
204
|
+
// post-compaction results. This preserves existing boundaries unless
|
|
205
|
+
// merging is useful, and writes each final object at most once.
|
|
158
206
|
for (const doc of batchDocs) {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const key = `${op.table}/${op.row_id}/${cacheKey(op.source_table, op.source_key)}`;
|
|
203
|
-
const targetOp = seen.get(key);
|
|
204
|
-
if (targetOp != null) {
|
|
205
|
-
surviving.push({
|
|
206
|
-
...op,
|
|
207
|
-
op: 'MOVE',
|
|
208
|
-
target_op: targetOp,
|
|
209
|
-
table: undefined,
|
|
210
|
-
row_id: undefined,
|
|
211
|
-
source_table: undefined,
|
|
212
|
-
source_key: undefined,
|
|
213
|
-
data: null
|
|
214
|
-
});
|
|
215
|
-
if (lastNotPut == null) {
|
|
216
|
-
lastNotPut = op.o;
|
|
207
|
+
compactedOpId ??= doc._id.o;
|
|
208
|
+
const originalOps = Array.from(loadBucketDataDocument(context, doc));
|
|
209
|
+
let changed = false;
|
|
210
|
+
const compactedOps = [];
|
|
211
|
+
let maxTargetOp = doc.target_op ?? null;
|
|
212
|
+
for (let index = originalOps.length - 1; index >= 0; index--) {
|
|
213
|
+
const op = originalOps[index];
|
|
214
|
+
if (op.op == 'PUT' || op.op == 'REMOVE') {
|
|
215
|
+
const key = `${op.table}/${op.row_id}/${cacheKey(op.source_table, op.source_key)}`;
|
|
216
|
+
const targetOp = seen.get(key);
|
|
217
|
+
if (targetOp != null) {
|
|
218
|
+
maxTargetOp = maxOpId(maxTargetOp, targetOp);
|
|
219
|
+
compactedOps.push({
|
|
220
|
+
...op,
|
|
221
|
+
op: 'MOVE',
|
|
222
|
+
table: undefined,
|
|
223
|
+
row_id: undefined,
|
|
224
|
+
source_table: undefined,
|
|
225
|
+
source_key: undefined,
|
|
226
|
+
data: null
|
|
227
|
+
});
|
|
228
|
+
changed = true;
|
|
229
|
+
if (lastNotPut == null) {
|
|
230
|
+
lastNotPut = op.o;
|
|
231
|
+
}
|
|
232
|
+
opsSincePut += 1;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
if (trackingSize < this.idLimitBytes) {
|
|
236
|
+
seen.set(utils.flatstr(key), op.o);
|
|
237
|
+
trackingSize += key.length + 140;
|
|
238
|
+
}
|
|
239
|
+
compactedOps.push(op);
|
|
240
|
+
if (op.op == 'PUT') {
|
|
241
|
+
lastNotPut = null;
|
|
242
|
+
opsSincePut = 0;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (lastNotPut == null) {
|
|
246
|
+
lastNotPut = op.o;
|
|
247
|
+
}
|
|
248
|
+
opsSincePut += 1;
|
|
249
|
+
}
|
|
217
250
|
}
|
|
218
|
-
opsSincePut += 1;
|
|
219
251
|
}
|
|
220
252
|
else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
trackingSize += key.length + 140;
|
|
224
|
-
}
|
|
225
|
-
surviving.push(op);
|
|
226
|
-
if (op.op == 'PUT') {
|
|
227
|
-
lastNotPut = null;
|
|
228
|
-
opsSincePut = 0;
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
253
|
+
compactedOps.push(op);
|
|
254
|
+
if (op.op != 'CLEAR') {
|
|
231
255
|
if (lastNotPut == null) {
|
|
232
256
|
lastNotPut = op.o;
|
|
233
257
|
}
|
|
@@ -235,103 +259,68 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
235
259
|
}
|
|
236
260
|
}
|
|
237
261
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
lastNotPut = op.o;
|
|
243
|
-
}
|
|
244
|
-
opsSincePut += 1;
|
|
245
|
-
}
|
|
262
|
+
compactedOps.reverse();
|
|
263
|
+
for (const op of compactedOps) {
|
|
264
|
+
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
265
|
+
totalOpBytes += op.data?.length ?? 0;
|
|
246
266
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
else {
|
|
257
|
-
const boundaryOp = lastNotPut;
|
|
258
|
-
const boundaryDoc = newDocs.find((doc) => doc.min_op <= boundaryOp && doc._id.o >= boundaryOp);
|
|
259
|
-
if (boundaryDoc != null) {
|
|
260
|
-
clearBoundaryDocId = boundaryDoc._id;
|
|
267
|
+
totalOpCount += compactedOps.length;
|
|
268
|
+
const candidate = {
|
|
269
|
+
inputs: [doc],
|
|
270
|
+
ops: compactedOps,
|
|
271
|
+
changed,
|
|
272
|
+
targetOp: maxTargetOp
|
|
273
|
+
};
|
|
274
|
+
if (pendingGroup == null) {
|
|
275
|
+
pendingGroup = candidate;
|
|
261
276
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{ $match: { _id: { $in: idsToDelete } } },
|
|
273
|
-
{
|
|
274
|
-
$group: {
|
|
275
|
-
_id: null,
|
|
276
|
-
docCount: { $sum: 1 },
|
|
277
|
-
checksumSum: { $sum: '$checksum' },
|
|
278
|
-
opCountSum: { $sum: '$count' }
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
], { session })
|
|
282
|
-
.next();
|
|
283
|
-
if (verification == null || // all docs deleted
|
|
284
|
-
verification.docCount !== expectedDocCount || // some docs deleted
|
|
285
|
-
verification.checksumSum !== expectedChecksum || // docs modified in-place
|
|
286
|
-
verification.opCountSum !== expectedOpCount // ops added/removed within docs
|
|
287
|
-
) {
|
|
288
|
-
throw new Error(`Concurrent modification detected in bucket ${bucket}. Aborting compaction for this batch.`);
|
|
289
|
-
}
|
|
290
|
-
await bucketContext.collection.deleteMany({
|
|
291
|
-
_id: { $in: idsToDelete }
|
|
292
|
-
}, { session });
|
|
293
|
-
if (newDocs.length > 0) {
|
|
294
|
-
await bucketContext.collection.insertMany(newDocs, { session });
|
|
277
|
+
else {
|
|
278
|
+
const mergedOps = [...candidate.ops, ...pendingGroup.ops];
|
|
279
|
+
const mergedSize = serializeBucketData(bucket, mergedOps, { targetOp: maxTargetOp }).size;
|
|
280
|
+
if (mergedSize <= DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
281
|
+
pendingGroup = {
|
|
282
|
+
inputs: [...candidate.inputs, ...pendingGroup.inputs],
|
|
283
|
+
ops: mergedOps,
|
|
284
|
+
changed: candidate.changed || pendingGroup.changed,
|
|
285
|
+
targetOp: maxOpId(maxTargetOp, pendingGroup.targetOp)
|
|
286
|
+
};
|
|
295
287
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
for (const chunk of chunks) {
|
|
306
|
-
for (const op of chunk) {
|
|
307
|
-
if (op.o <= this.maxOpId) {
|
|
308
|
-
totalChecksum = addChecksums(totalChecksum, Number(op.checksum));
|
|
309
|
-
totalOpBytes += op.data?.length ?? 0;
|
|
288
|
+
else {
|
|
289
|
+
const flushedGroup = pendingGroup;
|
|
290
|
+
const documentId = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, context);
|
|
291
|
+
if (lastNotPut != null &&
|
|
292
|
+
flushedGroup.ops[0].o <= lastNotPut &&
|
|
293
|
+
flushedGroup.ops[flushedGroup.ops.length - 1].o >= lastNotPut) {
|
|
294
|
+
clearBoundary = { opId: lastNotPut, documentId };
|
|
295
|
+
}
|
|
296
|
+
pendingGroup = candidate;
|
|
310
297
|
}
|
|
311
298
|
}
|
|
312
299
|
}
|
|
313
|
-
totalOpCount += surviving.filter((op) => op.o <= this.maxOpId).length;
|
|
314
300
|
// --- Advance to next batch ---
|
|
315
|
-
upperBound =
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
// The upperBound is already set to the last doc we processed
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
// Processed all docs in the raw batch. If we got fewer than the query
|
|
322
|
-
// limit, there are no more documents in this bucket — compaction complete.
|
|
323
|
-
if (rawBatch.length < this.moveBatchQueryLimit) {
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
301
|
+
upperBound = batchDocs[batchDocs.length - 1]._id;
|
|
302
|
+
if (!batch.hasMore) {
|
|
303
|
+
break;
|
|
326
304
|
}
|
|
327
305
|
this.logger.info(`Compacted batch of ${batchDocs.length} documents for bucket ${bucket}`);
|
|
328
306
|
}
|
|
307
|
+
if (pendingGroup != null) {
|
|
308
|
+
const documentId = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, context);
|
|
309
|
+
if (lastNotPut != null &&
|
|
310
|
+
pendingGroup.ops[0].o <= lastNotPut &&
|
|
311
|
+
pendingGroup.ops[pendingGroup.ops.length - 1].o >= lastNotPut) {
|
|
312
|
+
clearBoundary = { opId: lastNotPut, documentId };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (compactedOpId == null) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
329
318
|
// --- Clear: collapse leading MOVE/REMOVE/CLEAR sequence ---
|
|
330
319
|
if (lastNotPut != null && opsSincePut >= 2) {
|
|
331
|
-
if (
|
|
320
|
+
if (clearBoundary == null || clearBoundary.opId != lastNotPut) {
|
|
332
321
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
333
322
|
}
|
|
334
|
-
totalOpCount += await this.clearBucketLeading(lastNotPut,
|
|
323
|
+
totalOpCount += await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, context);
|
|
335
324
|
}
|
|
336
325
|
// --- Finalize: update bucket checksums and state ---
|
|
337
326
|
this.updateBucketChecksums({
|
|
@@ -344,13 +333,65 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
344
333
|
checksum: totalChecksum,
|
|
345
334
|
opCount: totalOpCount,
|
|
346
335
|
opBytes: totalOpBytes
|
|
347
|
-
});
|
|
336
|
+
}, compactedOpId);
|
|
348
337
|
if (this.bucketStateUpdates.length > 0) {
|
|
349
338
|
await this.writeBucketStateUpdates();
|
|
350
339
|
this.bucketStateUpdates = [];
|
|
351
340
|
}
|
|
352
341
|
logger.info(`Compacted bucket ${bucket}: ${totalOpCount} surviving ops`);
|
|
353
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Persist replacement objects before starting the transaction, then atomically
|
|
345
|
+
* publish their lifecycle markers alongside the MongoDB document replacement.
|
|
346
|
+
* If verification or the transaction fails, the prepared markers retain enough
|
|
347
|
+
* information for the uploaded objects to be cleaned up later.
|
|
348
|
+
*/
|
|
349
|
+
async flushCompactionGroup(bucket, group, bucketContext, context) {
|
|
350
|
+
if (group.inputs.length == 1 && !group.changed) {
|
|
351
|
+
return group.inputs[0]._id;
|
|
352
|
+
}
|
|
353
|
+
const inputs = group.inputs;
|
|
354
|
+
const idsToDelete = inputs.map((doc) => doc._id);
|
|
355
|
+
const expectedDocCount = inputs.length;
|
|
356
|
+
const expectedChecksum = inputs.reduce((sum, doc) => sum + doc.checksum, 0n);
|
|
357
|
+
const expectedOpCount = inputs.reduce((sum, doc) => sum + doc.count, 0);
|
|
358
|
+
const oldStoragePaths = inputs.flatMap((doc) => (doc.storage_ref ? [doc.storage_ref.path] : []));
|
|
359
|
+
const { documents, storagePaths: newStoragePaths, uploads } = await this.persistBucketData(bucket, [group.ops], context, undefined, { targetOp: group.targetOp });
|
|
360
|
+
const session = this.db.client.startSession();
|
|
361
|
+
try {
|
|
362
|
+
await session.withTransaction(async () => {
|
|
363
|
+
const verification = await bucketContext.collection
|
|
364
|
+
.aggregate([
|
|
365
|
+
{ $match: { _id: { $in: idsToDelete } } },
|
|
366
|
+
{
|
|
367
|
+
$group: {
|
|
368
|
+
_id: null,
|
|
369
|
+
docCount: { $sum: 1 },
|
|
370
|
+
checksumSum: { $sum: '$checksum' },
|
|
371
|
+
opCountSum: { $sum: '$count' }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
], { session })
|
|
375
|
+
.next();
|
|
376
|
+
if (verification == null ||
|
|
377
|
+
verification.docCount !== expectedDocCount ||
|
|
378
|
+
verification.checksumSum !== expectedChecksum ||
|
|
379
|
+
verification.opCountSum !== expectedOpCount) {
|
|
380
|
+
throw new ConcurrentCompactionError(`Inputs changed while compacting bucket ${bucket}; restarting from the latest bucket state`);
|
|
381
|
+
}
|
|
382
|
+
await bucketContext.collection.deleteMany({ _id: { $in: idsToDelete } }, { session });
|
|
383
|
+
await bucketContext.collection.insertMany(documents, { session });
|
|
384
|
+
await this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, session);
|
|
385
|
+
}, {
|
|
386
|
+
writeConcern: { w: 'majority' },
|
|
387
|
+
readConcern: { level: 'snapshot' }
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
await session.endSession();
|
|
392
|
+
}
|
|
393
|
+
return documents[0]._id;
|
|
394
|
+
}
|
|
354
395
|
/**
|
|
355
396
|
* Collapse the leading sequence of MOVE/REMOVE/CLEAR ops at the start
|
|
356
397
|
* of the bucket into a single CLEAR op. Reads whole clearable documents
|
|
@@ -382,10 +423,14 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
382
423
|
}
|
|
383
424
|
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
384
425
|
const bucket = bucketContext.key.bucket;
|
|
426
|
+
this.signal?.throwIfAborted();
|
|
427
|
+
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
385
428
|
let done = false;
|
|
386
429
|
let opCountDiff = 0;
|
|
387
|
-
this.signal?.throwIfAborted();
|
|
388
430
|
await session.withTransaction(async () => {
|
|
431
|
+
done = false;
|
|
432
|
+
opCountDiff = 0;
|
|
433
|
+
const oldStoragePaths = [];
|
|
389
434
|
const query = collection.find({
|
|
390
435
|
_id: {
|
|
391
436
|
$gte: bucketContext.minId,
|
|
@@ -400,7 +445,8 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
400
445
|
checksum: 1,
|
|
401
446
|
count: 1,
|
|
402
447
|
target_op: 1,
|
|
403
|
-
|
|
448
|
+
has_clear_op: 1,
|
|
449
|
+
storage_ref: 1
|
|
404
450
|
},
|
|
405
451
|
limit: this.clearBatchLimit
|
|
406
452
|
});
|
|
@@ -415,28 +461,26 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
415
461
|
throw new ReplicationAssertionError(`Unexpected document before CLEAR boundary with min_op ${doc.min_op} > ${lastNotPut} in bucket ${bucket}`);
|
|
416
462
|
}
|
|
417
463
|
lastDocId = doc._id;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
throw new ReplicationAssertionError(`Unexpected multiple CLEAR operations in bucket ${bucket}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
gotNonClearOp = true;
|
|
433
|
-
}
|
|
434
|
-
combinedChecksum = addChecksums(combinedChecksum, Number(op.checksum));
|
|
435
|
-
clearedOpCount++;
|
|
436
|
-
if (op.target_op != null && (maxTargetOp == null || op.target_op > maxTargetOp)) {
|
|
437
|
-
maxTargetOp = op.target_op;
|
|
464
|
+
if (doc.storage_ref) {
|
|
465
|
+
oldStoragePaths.push(doc.storage_ref.path);
|
|
466
|
+
}
|
|
467
|
+
// The compaction scan established that every operation before the
|
|
468
|
+
// boundary is MOVE/REMOVE/CLEAR. Root metadata is sufficient to fold
|
|
469
|
+
// whole documents into one CLEAR, so avoid downloading their payloads.
|
|
470
|
+
if (doc.has_clear_op) {
|
|
471
|
+
clearOpCount++;
|
|
472
|
+
if (clearOpCount > 1) {
|
|
473
|
+
throw new ReplicationAssertionError(`Unexpected multiple CLEAR operations in bucket ${bucket}`);
|
|
438
474
|
}
|
|
439
475
|
}
|
|
476
|
+
if (!doc.has_clear_op || doc.count > 1) {
|
|
477
|
+
gotNonClearOp = true;
|
|
478
|
+
}
|
|
479
|
+
combinedChecksum = addChecksums(combinedChecksum, Number(doc.checksum));
|
|
480
|
+
clearedOpCount += doc.count;
|
|
481
|
+
if (doc.target_op != null && (maxTargetOp == null || doc.target_op > maxTargetOp)) {
|
|
482
|
+
maxTargetOp = doc.target_op;
|
|
483
|
+
}
|
|
440
484
|
}
|
|
441
485
|
if (!gotNonClearOp) {
|
|
442
486
|
done = true;
|
|
@@ -454,10 +498,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
454
498
|
o: lastDocId.o,
|
|
455
499
|
op: 'CLEAR',
|
|
456
500
|
checksum: BigInt(combinedChecksum),
|
|
457
|
-
data: null
|
|
458
|
-
target_op: maxTargetOp
|
|
501
|
+
data: null
|
|
459
502
|
};
|
|
460
|
-
await
|
|
503
|
+
const persisted = await this.persistBucketData(bucket, [[clearOp]], context, prepared, {
|
|
504
|
+
targetOp: maxTargetOp
|
|
505
|
+
});
|
|
506
|
+
await collection.insertOne(persisted.documents[0], { session });
|
|
507
|
+
await this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, session);
|
|
461
508
|
opCountDiff = -clearedOpCount + 1;
|
|
462
509
|
}, {
|
|
463
510
|
writeConcern: { w: 'majority' },
|
|
@@ -467,8 +514,12 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
467
514
|
}
|
|
468
515
|
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
469
516
|
const bucket = bucketContext.key.bucket;
|
|
517
|
+
this.signal?.throwIfAborted();
|
|
518
|
+
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
470
519
|
let opCountDiff = 0;
|
|
471
520
|
await session.withTransaction(async () => {
|
|
521
|
+
opCountDiff = 0;
|
|
522
|
+
const oldStoragePaths = [];
|
|
472
523
|
const query = collection.find({
|
|
473
524
|
// This is a range query, but should only ever return two documents:
|
|
474
525
|
// 1. The CLEAR op from the previous clearLeadingFullDocuments.
|
|
@@ -486,7 +537,8 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
486
537
|
checksum: 1,
|
|
487
538
|
count: 1,
|
|
488
539
|
target_op: 1,
|
|
489
|
-
ops: 1
|
|
540
|
+
ops: 1,
|
|
541
|
+
storage_ref: 1
|
|
490
542
|
},
|
|
491
543
|
limit: 3
|
|
492
544
|
});
|
|
@@ -501,6 +553,11 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
501
553
|
throw new ReplicationAssertionError(`Unexpected extra document before CLEAR boundary in bucket ${bucket}`);
|
|
502
554
|
}
|
|
503
555
|
const isBoundaryDoc = doc._id.o == boundaryDocId.o;
|
|
556
|
+
if (doc.storage_ref) {
|
|
557
|
+
oldStoragePaths.push(doc.storage_ref.path);
|
|
558
|
+
}
|
|
559
|
+
await hydrateBucketDataDocuments([doc], this.storage.objectStorage, { signal: this.signal });
|
|
560
|
+
maxTargetOp = maxOpId(maxTargetOp, doc.target_op);
|
|
504
561
|
for (const op of loadBucketDataDocument(context, doc)) {
|
|
505
562
|
if (!isBoundaryDoc && op.op != 'CLEAR') {
|
|
506
563
|
throw new ReplicationAssertionError(`Unexpected ${op.op} operation before CLEAR boundary in bucket ${bucket}`);
|
|
@@ -511,9 +568,6 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
511
568
|
}
|
|
512
569
|
combinedChecksum = addChecksums(combinedChecksum, Number(op.checksum));
|
|
513
570
|
clearedOpCount++;
|
|
514
|
-
if (op.target_op != null && (maxTargetOp == null || op.target_op > maxTargetOp)) {
|
|
515
|
-
maxTargetOp = op.target_op;
|
|
516
|
-
}
|
|
517
571
|
}
|
|
518
572
|
else if (isBoundaryDoc) {
|
|
519
573
|
boundarySurvivors.push(op);
|
|
@@ -538,14 +592,19 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
538
592
|
o: lastNotPut,
|
|
539
593
|
op: 'CLEAR',
|
|
540
594
|
checksum: BigInt(combinedChecksum),
|
|
541
|
-
data: null
|
|
542
|
-
target_op: maxTargetOp
|
|
595
|
+
data: null
|
|
543
596
|
};
|
|
544
|
-
|
|
597
|
+
const chunks = [[clearOp]];
|
|
545
598
|
if (boundarySurvivors.length > 0) {
|
|
546
|
-
|
|
547
|
-
|
|
599
|
+
// These operations are a subset of one existing document, so keeping
|
|
600
|
+
// them together cannot increase its stored ops payload.
|
|
601
|
+
chunks.push(boundarySurvivors);
|
|
548
602
|
}
|
|
603
|
+
const persisted = await this.persistBucketData(bucket, chunks, context, prepared, {
|
|
604
|
+
targetOp: maxTargetOp ?? undefined
|
|
605
|
+
});
|
|
606
|
+
await collection.insertMany(persisted.documents, { session });
|
|
607
|
+
await this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, session);
|
|
549
608
|
opCountDiff = -clearedOpCount + 1;
|
|
550
609
|
}, {
|
|
551
610
|
writeConcern: { w: 'majority' },
|
|
@@ -553,5 +612,81 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
553
612
|
});
|
|
554
613
|
return opCountDiff;
|
|
555
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Reserve stable object paths before starting a retryable MongoDB transaction.
|
|
617
|
+
* Each retry can safely overwrite the same paths, while the pre-existing
|
|
618
|
+
* deletion markers remain visible to the transaction that publishes them.
|
|
619
|
+
*
|
|
620
|
+
* CLEAR compaction reserves its maximum output count. Unused markers remain
|
|
621
|
+
* pending so they can clean up a path that an earlier transaction attempt may
|
|
622
|
+
* have uploaded before retrying with fewer output documents.
|
|
623
|
+
*/
|
|
624
|
+
async prepareCompactionUploads(bucket, context, opIdHints) {
|
|
625
|
+
if (!this.storage.objectStorage) {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
const lifecycle = this.objectStorageLifecycle;
|
|
629
|
+
const paths = opIdHints.map((opIdHint) => lifecycle.allocatePath(context.definitionId, bucket, opIdHint, opIdHint));
|
|
630
|
+
return lifecycle.prepareUploads(paths);
|
|
631
|
+
}
|
|
632
|
+
/** Publish replacement uploads and retire superseded objects in the same transaction. */
|
|
633
|
+
async finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, session) {
|
|
634
|
+
if (!this.storage.objectStorage) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
await this.objectStorageLifecycle.publishUploads(uploads, session);
|
|
638
|
+
await this.objectStorageLifecycle.retire(Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)), session);
|
|
639
|
+
}
|
|
640
|
+
async persistBucketData(bucket, chunks, context, preparedUploads, options) {
|
|
641
|
+
const serializedChunks = chunks.map((chunk) => serializeBucketData(bucket, chunk, options));
|
|
642
|
+
if (!this.storage.objectStorage) {
|
|
643
|
+
return {
|
|
644
|
+
documents: serializedChunks,
|
|
645
|
+
storagePaths: new Set(),
|
|
646
|
+
uploads: []
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const store = new BucketDataObjectStorage(this.storage.objectStorage);
|
|
650
|
+
const storagePaths = new Set();
|
|
651
|
+
const lifecycle = this.objectStorageLifecycle;
|
|
652
|
+
// Base placement on the final compacted size. Unchanged documents are not
|
|
653
|
+
// rewritten, while small MOVE/merge results and CLEAR ops stay inline.
|
|
654
|
+
const storedIndexes = serializedChunks.flatMap((document, index) => document.size > this.storage.inlineThresholdBytes ? [index] : []);
|
|
655
|
+
const uploadsByIndex = new Map();
|
|
656
|
+
if (preparedUploads) {
|
|
657
|
+
for (const index of storedIndexes) {
|
|
658
|
+
const upload = preparedUploads[index];
|
|
659
|
+
if (!upload) {
|
|
660
|
+
throw new ServiceAssertionError(`Missing prepared object storage path for compacted document at index ${index}`);
|
|
661
|
+
}
|
|
662
|
+
uploadsByIndex.set(index, upload);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
const paths = storedIndexes.map((index) => {
|
|
667
|
+
const chunk = chunks[index];
|
|
668
|
+
return lifecycle.allocatePath(context.definitionId, bucket, chunk[0].o, chunk[chunk.length - 1].o);
|
|
669
|
+
});
|
|
670
|
+
const prepared = await lifecycle.prepareUploads(paths);
|
|
671
|
+
storedIndexes.forEach((index, preparedIndex) => uploadsByIndex.set(index, prepared[preparedIndex]));
|
|
672
|
+
}
|
|
673
|
+
// S3ObjectStorage applies one shared concurrency limit across all callers,
|
|
674
|
+
// so compaction can schedule its uploads together without creating a
|
|
675
|
+
// separate limiter here.
|
|
676
|
+
const documents = await Promise.all(serializedChunks.map(async (serialized, index) => {
|
|
677
|
+
const upload = uploadsByIndex.get(index);
|
|
678
|
+
if (!upload) {
|
|
679
|
+
return serialized;
|
|
680
|
+
}
|
|
681
|
+
const { ops, ...metadata } = serialized;
|
|
682
|
+
const { fileSize } = await store.store(upload.path, ops);
|
|
683
|
+
storagePaths.add(upload.path);
|
|
684
|
+
return {
|
|
685
|
+
...metadata,
|
|
686
|
+
storage_ref: { path: upload.path, file_size: fileSize }
|
|
687
|
+
};
|
|
688
|
+
}));
|
|
689
|
+
return { documents, storagePaths, uploads: Array.from(uploadsByIndex.values()) };
|
|
690
|
+
}
|
|
556
691
|
}
|
|
557
692
|
//# sourceMappingURL=MongoCompactorV3.js.map
|