@powersync/service-module-mongodb-storage 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -0
- package/dist/storage/MongoBucketStorage.js +7 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -1
- package/dist/storage/implementation/MongoBucketBatch.js +9 -8
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +4 -2
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
- package/dist/storage/implementation/common/PersistedBatch.js +6 -4
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +5 -0
- package/dist/storage/implementation/db.js +13 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +13 -6
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +2 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +17 -14
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +6 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.js +164 -69
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/types/types.d.ts +5 -0
- package/dist/types/types.js +12 -0
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +1 -0
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +6 -6
- package/src/storage/MongoBucketStorage.ts +12 -1
- package/src/storage/implementation/MongoBucketBatch.ts +13 -10
- package/src/storage/implementation/MongoStorageProvider.ts +12 -2
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
- package/src/storage/implementation/common/PersistedBatch.ts +12 -8
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +14 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +17 -8
- package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +51 -49
- package/src/storage/implementation/v3/MongoCompactorV3.ts +276 -97
- package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
- package/src/types/types.ts +18 -0
- package/src/utils/test-utils.ts +1 -0
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/object_storage_usage.test.ts +16 -4
- package/test/src/storage_compacting.test.ts +257 -5
- package/test/src/storage_s3_reading.test.ts +47 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
-
import { addChecksums, formatBytes, utils } from '@powersync/service-core';
|
|
2
|
+
import { acquireSemaphoreAbortable, addChecksums, formatBytes, utils } from '@powersync/service-core';
|
|
3
|
+
import { setImmediate } from 'node:timers/promises';
|
|
3
4
|
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
4
5
|
import { cacheKey } from '../OperationBatch.js';
|
|
5
6
|
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
@@ -109,7 +110,8 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
109
110
|
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
110
111
|
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
111
112
|
*
|
|
112
|
-
*
|
|
113
|
+
* Chunk merges overlap a bounded number of buckets. Full compaction stays
|
|
114
|
+
* sequential because its working set includes operation deduplication state.
|
|
113
115
|
*
|
|
114
116
|
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
115
117
|
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
@@ -123,24 +125,27 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
123
125
|
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
124
126
|
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
125
127
|
// the exact initial-replication interval.
|
|
126
|
-
const
|
|
127
|
-
.aggregate([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
128
|
-
.toArray();
|
|
128
|
+
const jobStartedAt = await this.readCompactionTime();
|
|
129
129
|
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
130
130
|
const forceKind = options.forceKind;
|
|
131
131
|
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
132
|
+
// Keep accounting documents bounded by workers, not buckets or scan batches.
|
|
133
|
+
const workerUsage = Array.from({ length: this.storage.factory.chunkCompactionConcurrency }, () => new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId()));
|
|
132
134
|
while (true) {
|
|
133
135
|
this.signal?.throwIfAborted();
|
|
134
136
|
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
135
137
|
if (states.length == 0) {
|
|
136
138
|
break;
|
|
137
139
|
}
|
|
140
|
+
// Keep eligibility bounded by dueBefore, but classify with the current
|
|
141
|
+
// server time so buckets that age into full compaction can advance.
|
|
142
|
+
const batchStartedAt = await this.readCompactionTime();
|
|
138
143
|
const scheduled = [];
|
|
139
144
|
for (const state of states) {
|
|
140
145
|
try {
|
|
141
146
|
scheduled.push({
|
|
142
147
|
state,
|
|
143
|
-
decision: chooseCompactionKind(state,
|
|
148
|
+
decision: chooseCompactionKind(state, batchStartedAt, this),
|
|
144
149
|
forcedKind: forcedCompactionKind(state, forceKind, this)
|
|
145
150
|
});
|
|
146
151
|
}
|
|
@@ -150,18 +155,23 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
150
155
|
}
|
|
151
156
|
const noOpStates = scheduled.filter(({ state, decision, forcedKind }) => state.compact_lease == null && (forceKind == null ? decision.kind : forcedKind) == null);
|
|
152
157
|
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
153
|
-
|
|
158
|
+
const processBucket = async ({ state, decision, forcedKind }, objectStorageUsage, chunksOnly = false) => {
|
|
154
159
|
const kind = forceKind == null ? decision.kind : forcedKind;
|
|
155
160
|
if (state.compact_lease == null && kind == null) {
|
|
156
|
-
|
|
161
|
+
return;
|
|
157
162
|
}
|
|
158
163
|
try {
|
|
159
164
|
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
160
165
|
if (lease == null) {
|
|
161
|
-
|
|
166
|
+
return;
|
|
162
167
|
}
|
|
163
168
|
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
164
169
|
const claimedKind = forceKind == null ? claimedDecision.kind : forcedCompactionKind(lease.state, forceKind, this);
|
|
170
|
+
if (chunksOnly && claimedKind === CompactionKind.Full) {
|
|
171
|
+
// The decision changed after scanning. Release the lease without
|
|
172
|
+
// rescheduling; the next batch will classify it with a fresh timestamp.
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
165
175
|
if (claimedKind == null) {
|
|
166
176
|
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
167
177
|
}
|
|
@@ -171,7 +181,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
171
181
|
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
172
182
|
}
|
|
173
183
|
else {
|
|
174
|
-
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore);
|
|
184
|
+
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore, objectStorageUsage);
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
catch (error) {
|
|
@@ -182,6 +192,73 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
182
192
|
}
|
|
183
193
|
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
184
194
|
}
|
|
195
|
+
};
|
|
196
|
+
const chunkBuckets = scheduled.filter(({ decision, forcedKind }) => (forceKind == null ? decision.kind : forcedKind) === CompactionKind.Chunks);
|
|
197
|
+
const sequentialBuckets = scheduled.filter(({ decision, forcedKind }) => (forceKind == null ? decision.kind : forcedKind) !== CompactionKind.Chunks);
|
|
198
|
+
await this.runChunkCompactionWorkers(chunkBuckets, workerUsage, (entry, usage) => processBucket(entry, usage, true));
|
|
199
|
+
// Full compaction cannot overlap chunk workers from this job, and only
|
|
200
|
+
// one full bucket is processed at a time.
|
|
201
|
+
for (const entry of sequentialBuckets) {
|
|
202
|
+
await processBucket(entry, this.objectStorageUsage);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Use MongoDB's clock, matching scheduling and lease timestamps. */
|
|
207
|
+
async readCompactionTime() {
|
|
208
|
+
const [{ now }] = await this.db.db
|
|
209
|
+
.aggregate([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
210
|
+
.toArray();
|
|
211
|
+
return now;
|
|
212
|
+
}
|
|
213
|
+
/** Process one scheduled batch with a fixed pool of workers. */
|
|
214
|
+
async runChunkCompactionWorkers(buckets, workerUsage, processBucket) {
|
|
215
|
+
const signal = this.signal;
|
|
216
|
+
let nextBucket = 0;
|
|
217
|
+
let failed = false;
|
|
218
|
+
const runWorker = async (usage) => {
|
|
219
|
+
try {
|
|
220
|
+
while (!failed && nextBucket < buckets.length) {
|
|
221
|
+
// Taking an entry has no await, so each worker gets a different bucket.
|
|
222
|
+
// A worker takes another only after finishing its current bucket.
|
|
223
|
+
const bucket = buckets[nextBucket++];
|
|
224
|
+
// This pool bounds one job; the factory semaphore bounds all jobs together.
|
|
225
|
+
// Acquire before claiming the bucket lease, and hold until it is released.
|
|
226
|
+
const acquired = await acquireSemaphoreAbortable(this.storage.factory.chunkCompactionSlots, signal);
|
|
227
|
+
if (acquired === 'aborted') {
|
|
228
|
+
signal?.throwIfAborted();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const [, releaseSlot] = acquired;
|
|
232
|
+
try {
|
|
233
|
+
// A sibling may have failed while this worker waited for a slot.
|
|
234
|
+
if (failed)
|
|
235
|
+
return;
|
|
236
|
+
signal?.throwIfAborted();
|
|
237
|
+
await processBucket(bucket, usage);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
releaseSlot();
|
|
241
|
+
}
|
|
242
|
+
// Let replication and other event-loop work run between buckets.
|
|
243
|
+
await setImmediate();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// Drain work already started, but do not let siblings start new buckets.
|
|
248
|
+
failed = true;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
// Concurrent transactions must not all increment the same usage document.
|
|
253
|
+
// Reuse one writer for each worker instead of creating one per bucket.
|
|
254
|
+
const workers = workerUsage.map(runWorker);
|
|
255
|
+
// Do not release the caller's replication lock or run cleanup while a
|
|
256
|
+
// sibling worker still owns a bucket lease or is finishing a replacement.
|
|
257
|
+
// Wait for every worker even on failure, then propagate the first error.
|
|
258
|
+
const results = await Promise.allSettled(workers);
|
|
259
|
+
for (const result of results) {
|
|
260
|
+
if (result.status === 'rejected') {
|
|
261
|
+
throw result.reason;
|
|
185
262
|
}
|
|
186
263
|
}
|
|
187
264
|
}
|
|
@@ -238,10 +315,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
238
315
|
async claimBucket(filter, sort) {
|
|
239
316
|
return CompactionLease.claim(this.db.bucketState(this.group_id), filter, sort, this.compactLeaseDurationMs);
|
|
240
317
|
}
|
|
241
|
-
async compactClaimedBucket(lease, kind, decision, rescheduleNotBefore) {
|
|
318
|
+
async compactClaimedBucket(lease, kind, decision, rescheduleNotBefore, objectStorageUsage = this.objectStorageUsage) {
|
|
242
319
|
const context = new CompactionContext(lease, kind, decision, rescheduleNotBefore, this.compactionTarget(lease.state));
|
|
243
320
|
lease.startRenewal();
|
|
244
|
-
await this.compactSingleBucket(context);
|
|
321
|
+
await this.compactSingleBucket(context, objectStorageUsage);
|
|
245
322
|
}
|
|
246
323
|
async rescheduleClaimedBucket(lease, decision, notBefore) {
|
|
247
324
|
await lease.reschedule(this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore));
|
|
@@ -271,11 +348,11 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
271
348
|
}
|
|
272
349
|
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
273
350
|
}
|
|
274
|
-
async compactSingleBucket(context) {
|
|
351
|
+
async compactSingleBucket(context, objectStorageUsage) {
|
|
275
352
|
if (context.kind == CompactionKind.Chunks) {
|
|
276
|
-
return this.compactSingleBucketChunks(context);
|
|
353
|
+
return this.compactSingleBucketChunks(context, objectStorageUsage);
|
|
277
354
|
}
|
|
278
|
-
return this.compactSingleBucketFully(context);
|
|
355
|
+
return this.compactSingleBucketFully(context, objectStorageUsage);
|
|
279
356
|
}
|
|
280
357
|
/**
|
|
281
358
|
* Merge adjacent bucket-data chunks without inspecting their operations
|
|
@@ -283,7 +360,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
283
360
|
* update the persisted checksum state and to decide whether a group can fit
|
|
284
361
|
* in one chunk.
|
|
285
362
|
*/
|
|
286
|
-
async compactSingleBucketChunks(context) {
|
|
363
|
+
async compactSingleBucketChunks(context, objectStorageUsage) {
|
|
287
364
|
const bucket = context.state._id.b;
|
|
288
365
|
const resolvedDefinitionId = context.state._id.d;
|
|
289
366
|
const bucketContext = new BucketDataContextV3(this.db, {
|
|
@@ -362,7 +439,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
362
439
|
}
|
|
363
440
|
const nextSize = pendingSize + doc.size;
|
|
364
441
|
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
365
|
-
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
442
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext, objectStorageUsage);
|
|
366
443
|
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
367
444
|
pendingChunks = [];
|
|
368
445
|
pendingSize = 0;
|
|
@@ -376,7 +453,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
376
453
|
}
|
|
377
454
|
}
|
|
378
455
|
if (pendingChunks.length > 0) {
|
|
379
|
-
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
456
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext, objectStorageUsage);
|
|
380
457
|
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
381
458
|
}
|
|
382
459
|
if (compactedOpId == null) {
|
|
@@ -397,33 +474,44 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
397
474
|
this.compactedBucketCount++;
|
|
398
475
|
this.logger.info(`Compacted bucket chunks ${bucket}: ${result.bucketStats.count} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`);
|
|
399
476
|
}
|
|
400
|
-
async flushChunkMerge(bucket, inputs, collection, context, bucketContext) {
|
|
477
|
+
async flushChunkMerge(bucket, inputs, collection, context, bucketContext, objectStorageUsage) {
|
|
401
478
|
if (inputs.length == 1) {
|
|
402
479
|
return statsForDocument(inputs[0]);
|
|
403
480
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
481
|
+
try {
|
|
482
|
+
this.signal?.throwIfAborted();
|
|
483
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
484
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
485
|
+
// the same rule.
|
|
486
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
487
|
+
if (inlineInputs.length > 0) {
|
|
488
|
+
const inlineDocuments = await collection
|
|
489
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
490
|
+
.toArray();
|
|
491
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
492
|
+
for (const input of inlineInputs) {
|
|
493
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
494
|
+
}
|
|
415
495
|
}
|
|
496
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
497
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
498
|
+
const targetOp = inputs.reduce((maxTarget, input) => maxOpId(maxTarget, input.target_op), null);
|
|
499
|
+
const result = await this.flushCompactionGroup(bucket, {
|
|
500
|
+
inputs,
|
|
501
|
+
ops: operations,
|
|
502
|
+
changed: true,
|
|
503
|
+
targetOp
|
|
504
|
+
}, bucketContext, context, objectStorageUsage);
|
|
505
|
+
return result.stats;
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
// The scan batch also references these documents. Do not retain hydrated
|
|
509
|
+
// operations after finishing this merge group.
|
|
510
|
+
for (const input of inputs) {
|
|
511
|
+
delete input.ops;
|
|
512
|
+
}
|
|
513
|
+
await setImmediate();
|
|
416
514
|
}
|
|
417
|
-
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
418
|
-
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
419
|
-
const targetOp = inputs.reduce((maxTarget, input) => maxOpId(maxTarget, input.target_op), null);
|
|
420
|
-
const result = await this.flushCompactionGroup(bucket, {
|
|
421
|
-
inputs,
|
|
422
|
-
ops: operations,
|
|
423
|
-
changed: true,
|
|
424
|
-
targetOp
|
|
425
|
-
}, bucketContext, context);
|
|
426
|
-
return result.stats;
|
|
427
515
|
}
|
|
428
516
|
async finalizeCompactedBucket({ context, compactedOpId, compactionResult, puts }) {
|
|
429
517
|
await context.lease.throwIfLost();
|
|
@@ -526,7 +614,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
526
614
|
: addChecksums(0, Number(stats?.checksum ?? 0))
|
|
527
615
|
};
|
|
528
616
|
}
|
|
529
|
-
async compactSingleBucketFully(context) {
|
|
617
|
+
async compactSingleBucketFully(context, objectStorageUsage) {
|
|
530
618
|
const bucket = context.state._id.b;
|
|
531
619
|
const resolvedDefinitionId = context.state._id.d;
|
|
532
620
|
const bucketContext = new BucketDataContextV3(this.db, {
|
|
@@ -673,7 +761,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
673
761
|
}
|
|
674
762
|
else {
|
|
675
763
|
const flushedGroup = pendingGroup;
|
|
676
|
-
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext);
|
|
764
|
+
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext, objectStorageUsage);
|
|
677
765
|
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
678
766
|
if (lastNotPut != null &&
|
|
679
767
|
flushedGroup.ops[0].o <= lastNotPut &&
|
|
@@ -692,7 +780,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
692
780
|
this.logger.info(`Compacted batch of ${batchDocs.length} documents for bucket ${bucket}`);
|
|
693
781
|
}
|
|
694
782
|
if (pendingGroup != null) {
|
|
695
|
-
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext);
|
|
783
|
+
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext, objectStorageUsage);
|
|
696
784
|
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
697
785
|
if (lastNotPut != null &&
|
|
698
786
|
pendingGroup.ops[0].o <= lastNotPut &&
|
|
@@ -709,7 +797,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
709
797
|
if (clearBoundary == null || clearBoundary.opId != lastNotPut) {
|
|
710
798
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
711
799
|
}
|
|
712
|
-
const clearResult = await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, dataContext);
|
|
800
|
+
const clearResult = await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, dataContext, objectStorageUsage);
|
|
713
801
|
totalOpCount += clearResult.opCountDiff;
|
|
714
802
|
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
715
803
|
}
|
|
@@ -731,7 +819,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
731
819
|
* If verification or the transaction fails, the prepared markers retain enough
|
|
732
820
|
* information for the uploaded objects to be cleaned up later.
|
|
733
821
|
*/
|
|
734
|
-
async flushCompactionGroup(bucket, group, bucketContext, context) {
|
|
822
|
+
async flushCompactionGroup(bucket, group, bucketContext, context, objectStorageUsage) {
|
|
735
823
|
if (group.inputs.length == 1 && !group.changed) {
|
|
736
824
|
return {
|
|
737
825
|
documentId: group.inputs[0]._id,
|
|
@@ -768,10 +856,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
768
856
|
verification.opCountSum !== expectedOpCount) {
|
|
769
857
|
throw new ConcurrentCompactionError(`Inputs changed while compacting bucket ${bucket}; restarting from the latest bucket state`);
|
|
770
858
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
859
|
+
// Replacement documents can reuse deleted IDs, so retain delete-before-insert ordering.
|
|
860
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
861
|
+
writes.deleteMany(bucketContext.collection, { _id: { $in: idsToDelete } });
|
|
862
|
+
writes.insertMany(bucketContext.collection, documents);
|
|
863
|
+
this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes);
|
|
864
|
+
this.recordObjectStorageReplacement(oldStorageBytes, documents, context.definitionId, writes, objectStorageUsage);
|
|
865
|
+
await writes.execute();
|
|
775
866
|
}, {
|
|
776
867
|
writeConcern: { w: 'majority' },
|
|
777
868
|
readConcern: { level: 'snapshot' }
|
|
@@ -794,7 +885,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
794
885
|
* Returns the op count and stored-stat changes after replacing cleared ops
|
|
795
886
|
* with CLEAR ops.
|
|
796
887
|
*/
|
|
797
|
-
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
888
|
+
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
798
889
|
let opCountDiff = 0;
|
|
799
890
|
let before = emptyBucketStats();
|
|
800
891
|
let after = emptyBucketStats();
|
|
@@ -804,7 +895,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
804
895
|
// First step is to clear full chunks that contain only CLEAR/MOVE/REMOVE operations.
|
|
805
896
|
// There can be many of them, so we do one batch at a time.
|
|
806
897
|
while (!done) {
|
|
807
|
-
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context);
|
|
898
|
+
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage);
|
|
808
899
|
done = batch.done;
|
|
809
900
|
opCountDiff += batch.opCountDiff;
|
|
810
901
|
before = combineAdjacentStats(before, batch.before);
|
|
@@ -812,7 +903,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
812
903
|
}
|
|
813
904
|
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
814
905
|
// potentially followed by PUT operations. This is only a single document, so no need for batching.
|
|
815
|
-
const boundaryResult = await this.clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context);
|
|
906
|
+
const boundaryResult = await this.clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage);
|
|
816
907
|
opCountDiff += boundaryResult.opCountDiff;
|
|
817
908
|
before = combineAdjacentStats(before, boundaryResult.before);
|
|
818
909
|
after = combineAdjacentStats(after, boundaryResult.after);
|
|
@@ -822,7 +913,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
822
913
|
}
|
|
823
914
|
return { opCountDiff, before, after };
|
|
824
915
|
}
|
|
825
|
-
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
916
|
+
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
826
917
|
const bucket = bucketContext.key.bucket;
|
|
827
918
|
this.signal?.throwIfAborted();
|
|
828
919
|
let prepared;
|
|
@@ -902,12 +993,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
902
993
|
}
|
|
903
994
|
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
904
995
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
905
|
-
|
|
996
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
997
|
+
writes.deleteMany(collection, {
|
|
906
998
|
_id: {
|
|
907
999
|
$gte: bucketContext.minId,
|
|
908
1000
|
$lte: lastDocId
|
|
909
1001
|
}
|
|
910
|
-
}
|
|
1002
|
+
});
|
|
911
1003
|
const clearOp = {
|
|
912
1004
|
bucketKey: { ...context, bucket },
|
|
913
1005
|
o: lastDocId.o,
|
|
@@ -918,9 +1010,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
918
1010
|
const persisted = await this.persistBucketData(bucket, [[clearOp]], context, prepared, {
|
|
919
1011
|
targetOp: maxTargetOp
|
|
920
1012
|
});
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1013
|
+
writes.insertOne(collection, persisted.documents[0]);
|
|
1014
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1015
|
+
this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, writes, objectStorageUsage);
|
|
1016
|
+
await writes.execute();
|
|
924
1017
|
opCountDiff = -clearedOpCount + 1;
|
|
925
1018
|
before = inputStats;
|
|
926
1019
|
after = statsForDocuments(persisted.documents);
|
|
@@ -930,7 +1023,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
930
1023
|
});
|
|
931
1024
|
return { done, opCountDiff, before, after };
|
|
932
1025
|
}
|
|
933
|
-
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
1026
|
+
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
934
1027
|
const bucket = bucketContext.key.bucket;
|
|
935
1028
|
this.signal?.throwIfAborted();
|
|
936
1029
|
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
@@ -1012,12 +1105,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1012
1105
|
throw new Error(`CLEAR boundary document not found for bucket ${bucket}`);
|
|
1013
1106
|
}
|
|
1014
1107
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastNotPut}`);
|
|
1015
|
-
|
|
1108
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1109
|
+
writes.deleteMany(collection, {
|
|
1016
1110
|
_id: {
|
|
1017
1111
|
$gte: bucketContext.minId,
|
|
1018
1112
|
$lte: boundaryDocId
|
|
1019
1113
|
}
|
|
1020
|
-
}
|
|
1114
|
+
});
|
|
1021
1115
|
const clearOp = {
|
|
1022
1116
|
bucketKey: { ...context, bucket },
|
|
1023
1117
|
o: lastNotPut,
|
|
@@ -1034,9 +1128,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1034
1128
|
const persisted = await this.persistBucketData(bucket, chunks, context, prepared, {
|
|
1035
1129
|
targetOp: maxTargetOp ?? undefined
|
|
1036
1130
|
});
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1131
|
+
writes.insertMany(collection, persisted.documents);
|
|
1132
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1133
|
+
this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, writes, objectStorageUsage);
|
|
1134
|
+
await writes.execute();
|
|
1040
1135
|
opCountDiff = -clearedOpCount + 1;
|
|
1041
1136
|
before = inputStats;
|
|
1042
1137
|
after = statsForDocuments(persisted.documents);
|
|
@@ -1064,14 +1159,14 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1064
1159
|
return lifecycle.prepareUploads(paths);
|
|
1065
1160
|
}
|
|
1066
1161
|
/** Publish replacement uploads and retire superseded objects in the same transaction. */
|
|
1067
|
-
|
|
1162
|
+
finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes) {
|
|
1068
1163
|
if (!this.storage.objectStorage) {
|
|
1069
1164
|
return;
|
|
1070
1165
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1166
|
+
this.objectStorageLifecycle.publishUploads(uploads, writes);
|
|
1167
|
+
this.objectStorageLifecycle.retire(Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)), writes);
|
|
1073
1168
|
}
|
|
1074
|
-
|
|
1169
|
+
recordObjectStorageReplacement(oldBytes, newDocuments, definitionId, writes, objectStorageUsage) {
|
|
1075
1170
|
if (!this.storage.objectStorage) {
|
|
1076
1171
|
return;
|
|
1077
1172
|
}
|
|
@@ -1079,7 +1174,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1079
1174
|
for (const document of newDocuments) {
|
|
1080
1175
|
newBytes += ObjectStorageUsage.bytes(document);
|
|
1081
1176
|
}
|
|
1082
|
-
|
|
1177
|
+
objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, writes);
|
|
1083
1178
|
}
|
|
1084
1179
|
async persistBucketData(bucket, chunks, context, preparedUploads, options) {
|
|
1085
1180
|
const serializedChunks = chunks.map((chunk) => serializeBucketData(bucket, chunk, options));
|