@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,10 +1,19 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
acquireSemaphoreAbortable,
|
|
5
|
+
addChecksums,
|
|
6
|
+
formatBytes,
|
|
7
|
+
InternalOpId,
|
|
8
|
+
storage,
|
|
9
|
+
utils
|
|
10
|
+
} from '@powersync/service-core';
|
|
4
11
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
12
|
+
import { setImmediate } from 'node:timers/promises';
|
|
5
13
|
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
14
|
import { BucketDataKey } from '../models.js';
|
|
7
15
|
import { ConcurrentCompactionError, MongoCompactor } from '../MongoCompactor.js';
|
|
16
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
8
17
|
import { cacheKey } from '../OperationBatch.js';
|
|
9
18
|
import { loadBucketDataDocument, maxOpId, serializeBucketData } from './bucket-format.js';
|
|
10
19
|
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
@@ -161,7 +170,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
161
170
|
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
162
171
|
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
163
172
|
*
|
|
164
|
-
*
|
|
173
|
+
* Chunk merges overlap a bounded number of buckets. Full compaction stays
|
|
174
|
+
* sequential because its working set includes operation deduplication state.
|
|
165
175
|
*
|
|
166
176
|
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
167
177
|
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
@@ -175,18 +185,24 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
175
185
|
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
176
186
|
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
177
187
|
// the exact initial-replication interval.
|
|
178
|
-
const
|
|
179
|
-
.aggregate<{ now: Date }>([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
180
|
-
.toArray();
|
|
188
|
+
const jobStartedAt = await this.readCompactionTime();
|
|
181
189
|
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
182
190
|
const forceKind = options.forceKind;
|
|
183
191
|
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
192
|
+
// Keep accounting documents bounded by workers, not buckets or scan batches.
|
|
193
|
+
const workerUsage = Array.from(
|
|
194
|
+
{ length: this.storage.factory.chunkCompactionConcurrency },
|
|
195
|
+
() => new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId())
|
|
196
|
+
);
|
|
184
197
|
while (true) {
|
|
185
198
|
this.signal?.throwIfAborted();
|
|
186
199
|
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
187
200
|
if (states.length == 0) {
|
|
188
201
|
break;
|
|
189
202
|
}
|
|
203
|
+
// Keep eligibility bounded by dueBefore, but classify with the current
|
|
204
|
+
// server time so buckets that age into full compaction can advance.
|
|
205
|
+
const batchStartedAt = await this.readCompactionTime();
|
|
190
206
|
|
|
191
207
|
const scheduled: {
|
|
192
208
|
state: BucketStateDocumentV3;
|
|
@@ -197,7 +213,7 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
197
213
|
try {
|
|
198
214
|
scheduled.push({
|
|
199
215
|
state,
|
|
200
|
-
decision: chooseCompactionKind(state,
|
|
216
|
+
decision: chooseCompactionKind(state, batchStartedAt, this),
|
|
201
217
|
forcedKind: forcedCompactionKind(state, forceKind, this)
|
|
202
218
|
});
|
|
203
219
|
} catch (error) {
|
|
@@ -210,20 +226,29 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
210
226
|
);
|
|
211
227
|
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
212
228
|
|
|
213
|
-
|
|
229
|
+
const processBucket = async (
|
|
230
|
+
{ state, decision, forcedKind }: (typeof scheduled)[number],
|
|
231
|
+
objectStorageUsage: ObjectStorageUsage,
|
|
232
|
+
chunksOnly = false
|
|
233
|
+
) => {
|
|
214
234
|
const kind = forceKind == null ? decision.kind : forcedKind;
|
|
215
235
|
if (state.compact_lease == null && kind == null) {
|
|
216
|
-
|
|
236
|
+
return;
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
try {
|
|
220
240
|
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
221
241
|
if (lease == null) {
|
|
222
|
-
|
|
242
|
+
return;
|
|
223
243
|
}
|
|
224
244
|
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
225
245
|
const claimedKind =
|
|
226
246
|
forceKind == null ? claimedDecision.kind : forcedCompactionKind(lease.state, forceKind, this);
|
|
247
|
+
if (chunksOnly && claimedKind === CompactionKind.Full) {
|
|
248
|
+
// The decision changed after scanning. Release the lease without
|
|
249
|
+
// rescheduling; the next batch will classify it with a fresh timestamp.
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
227
252
|
if (claimedKind == null) {
|
|
228
253
|
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
229
254
|
} else if (this.isCompactionTargetCovered(lease.state, claimedKind)) {
|
|
@@ -231,7 +256,13 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
231
256
|
// already-published progress. Keep any newer work scheduled.
|
|
232
257
|
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
233
258
|
} else {
|
|
234
|
-
await this.compactClaimedBucket(
|
|
259
|
+
await this.compactClaimedBucket(
|
|
260
|
+
lease,
|
|
261
|
+
claimedKind,
|
|
262
|
+
claimedDecision,
|
|
263
|
+
rescheduleNotBefore,
|
|
264
|
+
objectStorageUsage
|
|
265
|
+
);
|
|
235
266
|
}
|
|
236
267
|
} catch (error) {
|
|
237
268
|
if (this.signal?.aborted) {
|
|
@@ -241,6 +272,87 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
241
272
|
}
|
|
242
273
|
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
243
274
|
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const chunkBuckets = scheduled.filter(
|
|
278
|
+
({ decision, forcedKind }) => (forceKind == null ? decision.kind : forcedKind) === CompactionKind.Chunks
|
|
279
|
+
);
|
|
280
|
+
const sequentialBuckets = scheduled.filter(
|
|
281
|
+
({ decision, forcedKind }) => (forceKind == null ? decision.kind : forcedKind) !== CompactionKind.Chunks
|
|
282
|
+
);
|
|
283
|
+
await this.runChunkCompactionWorkers(chunkBuckets, workerUsage, (entry, usage) =>
|
|
284
|
+
processBucket(entry, usage, true)
|
|
285
|
+
);
|
|
286
|
+
// Full compaction cannot overlap chunk workers from this job, and only
|
|
287
|
+
// one full bucket is processed at a time.
|
|
288
|
+
for (const entry of sequentialBuckets) {
|
|
289
|
+
await processBucket(entry, this.objectStorageUsage);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Use MongoDB's clock, matching scheduling and lease timestamps. */
|
|
295
|
+
private async readCompactionTime(): Promise<Date> {
|
|
296
|
+
const [{ now }] = await this.db.db
|
|
297
|
+
.aggregate<{ now: Date }>([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
298
|
+
.toArray();
|
|
299
|
+
return now;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Process one scheduled batch with a fixed pool of workers. */
|
|
303
|
+
private async runChunkCompactionWorkers<T>(
|
|
304
|
+
buckets: readonly T[],
|
|
305
|
+
workerUsage: readonly ObjectStorageUsage[],
|
|
306
|
+
processBucket: (bucket: T, usage: ObjectStorageUsage) => Promise<void>
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
const signal = this.signal;
|
|
309
|
+
let nextBucket = 0;
|
|
310
|
+
let failed = false;
|
|
311
|
+
|
|
312
|
+
const runWorker = async (usage: ObjectStorageUsage) => {
|
|
313
|
+
try {
|
|
314
|
+
while (!failed && nextBucket < buckets.length) {
|
|
315
|
+
// Taking an entry has no await, so each worker gets a different bucket.
|
|
316
|
+
// A worker takes another only after finishing its current bucket.
|
|
317
|
+
const bucket = buckets[nextBucket++];
|
|
318
|
+
|
|
319
|
+
// This pool bounds one job; the factory semaphore bounds all jobs together.
|
|
320
|
+
// Acquire before claiming the bucket lease, and hold until it is released.
|
|
321
|
+
const acquired = await acquireSemaphoreAbortable(this.storage.factory.chunkCompactionSlots, signal);
|
|
322
|
+
if (acquired === 'aborted') {
|
|
323
|
+
signal?.throwIfAborted();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const [, releaseSlot] = acquired;
|
|
327
|
+
try {
|
|
328
|
+
// A sibling may have failed while this worker waited for a slot.
|
|
329
|
+
if (failed) return;
|
|
330
|
+
signal?.throwIfAborted();
|
|
331
|
+
await processBucket(bucket, usage);
|
|
332
|
+
} finally {
|
|
333
|
+
releaseSlot();
|
|
334
|
+
}
|
|
335
|
+
// Let replication and other event-loop work run between buckets.
|
|
336
|
+
await setImmediate();
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// Drain work already started, but do not let siblings start new buckets.
|
|
340
|
+
failed = true;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Concurrent transactions must not all increment the same usage document.
|
|
346
|
+
// Reuse one writer for each worker instead of creating one per bucket.
|
|
347
|
+
const workers = workerUsage.map(runWorker);
|
|
348
|
+
|
|
349
|
+
// Do not release the caller's replication lock or run cleanup while a
|
|
350
|
+
// sibling worker still owns a bucket lease or is finishing a replacement.
|
|
351
|
+
// Wait for every worker even on failure, then propagate the first error.
|
|
352
|
+
const results = await Promise.allSettled(workers);
|
|
353
|
+
for (const result of results) {
|
|
354
|
+
if (result.status === 'rejected') {
|
|
355
|
+
throw result.reason;
|
|
244
356
|
}
|
|
245
357
|
}
|
|
246
358
|
}
|
|
@@ -314,7 +426,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
314
426
|
lease: CompactionLease,
|
|
315
427
|
kind: CompactionKind,
|
|
316
428
|
decision: CompactionDecision,
|
|
317
|
-
rescheduleNotBefore?: Date
|
|
429
|
+
rescheduleNotBefore?: Date,
|
|
430
|
+
objectStorageUsage = this.objectStorageUsage
|
|
318
431
|
) {
|
|
319
432
|
const context = new CompactionContext(
|
|
320
433
|
lease,
|
|
@@ -324,7 +437,7 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
324
437
|
this.compactionTarget(lease.state)
|
|
325
438
|
);
|
|
326
439
|
lease.startRenewal();
|
|
327
|
-
await this.compactSingleBucket(context);
|
|
440
|
+
await this.compactSingleBucket(context, objectStorageUsage);
|
|
328
441
|
}
|
|
329
442
|
|
|
330
443
|
private async rescheduleClaimedBucket(lease: CompactionLease, decision: CompactionDecision, notBefore?: Date) {
|
|
@@ -360,12 +473,12 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
360
473
|
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
361
474
|
}
|
|
362
475
|
|
|
363
|
-
private async compactSingleBucket(context: CompactionContext) {
|
|
476
|
+
private async compactSingleBucket(context: CompactionContext, objectStorageUsage: ObjectStorageUsage) {
|
|
364
477
|
if (context.kind == CompactionKind.Chunks) {
|
|
365
|
-
return this.compactSingleBucketChunks(context);
|
|
478
|
+
return this.compactSingleBucketChunks(context, objectStorageUsage);
|
|
366
479
|
}
|
|
367
480
|
|
|
368
|
-
return this.compactSingleBucketFully(context);
|
|
481
|
+
return this.compactSingleBucketFully(context, objectStorageUsage);
|
|
369
482
|
}
|
|
370
483
|
|
|
371
484
|
/**
|
|
@@ -374,7 +487,7 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
374
487
|
* update the persisted checksum state and to decide whether a group can fit
|
|
375
488
|
* in one chunk.
|
|
376
489
|
*/
|
|
377
|
-
private async compactSingleBucketChunks(context: CompactionContext) {
|
|
490
|
+
private async compactSingleBucketChunks(context: CompactionContext, objectStorageUsage: ObjectStorageUsage) {
|
|
378
491
|
const bucket = context.state._id.b;
|
|
379
492
|
const resolvedDefinitionId = context.state._id.d;
|
|
380
493
|
const bucketContext = new BucketDataContextV3(this.db, {
|
|
@@ -467,7 +580,14 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
467
580
|
|
|
468
581
|
const nextSize = pendingSize + doc.size;
|
|
469
582
|
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
470
|
-
const groupStats = await this.flushChunkMerge(
|
|
583
|
+
const groupStats = await this.flushChunkMerge(
|
|
584
|
+
bucket,
|
|
585
|
+
pendingChunks,
|
|
586
|
+
collection,
|
|
587
|
+
dataContext,
|
|
588
|
+
bucketContext,
|
|
589
|
+
objectStorageUsage
|
|
590
|
+
);
|
|
471
591
|
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
472
592
|
pendingChunks = [];
|
|
473
593
|
pendingSize = 0;
|
|
@@ -484,7 +604,14 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
484
604
|
}
|
|
485
605
|
|
|
486
606
|
if (pendingChunks.length > 0) {
|
|
487
|
-
const groupStats = await this.flushChunkMerge(
|
|
607
|
+
const groupStats = await this.flushChunkMerge(
|
|
608
|
+
bucket,
|
|
609
|
+
pendingChunks,
|
|
610
|
+
collection,
|
|
611
|
+
dataContext,
|
|
612
|
+
bucketContext,
|
|
613
|
+
objectStorageUsage
|
|
614
|
+
);
|
|
488
615
|
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
489
616
|
}
|
|
490
617
|
|
|
@@ -518,44 +645,57 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
518
645
|
inputs: BucketDataDocumentV3[],
|
|
519
646
|
collection: mongo.Collection<BucketDataDocumentV3>,
|
|
520
647
|
context: { replicationStreamId: number; definitionId: string },
|
|
521
|
-
bucketContext: BucketDataContextV3
|
|
648
|
+
bucketContext: BucketDataContextV3,
|
|
649
|
+
objectStorageUsage: ObjectStorageUsage
|
|
522
650
|
): Promise<BucketStatsWithChecksum> {
|
|
523
651
|
if (inputs.length == 1) {
|
|
524
652
|
return statsForDocument(inputs[0]);
|
|
525
653
|
}
|
|
526
654
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
655
|
+
try {
|
|
656
|
+
this.signal?.throwIfAborted();
|
|
657
|
+
|
|
658
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
659
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
660
|
+
// the same rule.
|
|
661
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
662
|
+
if (inlineInputs.length > 0) {
|
|
663
|
+
const inlineDocuments = await collection
|
|
664
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
665
|
+
.toArray();
|
|
666
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
667
|
+
for (const input of inlineInputs) {
|
|
668
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
669
|
+
}
|
|
538
670
|
}
|
|
539
|
-
|
|
540
|
-
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
671
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
541
672
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
673
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
674
|
+
const targetOp = inputs.reduce<InternalOpId | null>(
|
|
675
|
+
(maxTarget, input) => maxOpId(maxTarget, input.target_op),
|
|
676
|
+
null
|
|
677
|
+
);
|
|
678
|
+
const result = await this.flushCompactionGroup(
|
|
679
|
+
bucket,
|
|
680
|
+
{
|
|
681
|
+
inputs,
|
|
682
|
+
ops: operations,
|
|
683
|
+
changed: true,
|
|
684
|
+
targetOp
|
|
685
|
+
},
|
|
686
|
+
bucketContext,
|
|
687
|
+
context,
|
|
688
|
+
objectStorageUsage
|
|
689
|
+
);
|
|
690
|
+
return result.stats;
|
|
691
|
+
} finally {
|
|
692
|
+
// The scan batch also references these documents. Do not retain hydrated
|
|
693
|
+
// operations after finishing this merge group.
|
|
694
|
+
for (const input of inputs) {
|
|
695
|
+
delete input.ops;
|
|
696
|
+
}
|
|
697
|
+
await setImmediate();
|
|
698
|
+
}
|
|
559
699
|
}
|
|
560
700
|
|
|
561
701
|
private async finalizeCompactedBucket({
|
|
@@ -692,7 +832,7 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
692
832
|
};
|
|
693
833
|
}
|
|
694
834
|
|
|
695
|
-
private async compactSingleBucketFully(context: CompactionContext) {
|
|
835
|
+
private async compactSingleBucketFully(context: CompactionContext, objectStorageUsage: ObjectStorageUsage) {
|
|
696
836
|
const bucket = context.state._id.b;
|
|
697
837
|
const resolvedDefinitionId = context.state._id.d;
|
|
698
838
|
const bucketContext = new BucketDataContextV3(this.db, {
|
|
@@ -849,7 +989,13 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
849
989
|
};
|
|
850
990
|
} else {
|
|
851
991
|
const flushedGroup = pendingGroup;
|
|
852
|
-
const result = await this.flushCompactionGroup(
|
|
992
|
+
const result = await this.flushCompactionGroup(
|
|
993
|
+
bucket,
|
|
994
|
+
flushedGroup,
|
|
995
|
+
bucketContext,
|
|
996
|
+
dataContext,
|
|
997
|
+
objectStorageUsage
|
|
998
|
+
);
|
|
853
999
|
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
854
1000
|
if (
|
|
855
1001
|
lastNotPut != null &&
|
|
@@ -874,7 +1020,13 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
874
1020
|
}
|
|
875
1021
|
|
|
876
1022
|
if (pendingGroup != null) {
|
|
877
|
-
const result = await this.flushCompactionGroup(
|
|
1023
|
+
const result = await this.flushCompactionGroup(
|
|
1024
|
+
bucket,
|
|
1025
|
+
pendingGroup,
|
|
1026
|
+
bucketContext,
|
|
1027
|
+
dataContext,
|
|
1028
|
+
objectStorageUsage
|
|
1029
|
+
);
|
|
878
1030
|
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
879
1031
|
if (
|
|
880
1032
|
lastNotPut != null &&
|
|
@@ -900,7 +1052,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
900
1052
|
clearBoundary.documentId,
|
|
901
1053
|
bucketContext,
|
|
902
1054
|
collection,
|
|
903
|
-
dataContext
|
|
1055
|
+
dataContext,
|
|
1056
|
+
objectStorageUsage
|
|
904
1057
|
);
|
|
905
1058
|
totalOpCount += clearResult.opCountDiff;
|
|
906
1059
|
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
@@ -934,7 +1087,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
934
1087
|
bucket: string,
|
|
935
1088
|
group: PendingCompactionGroup,
|
|
936
1089
|
bucketContext: BucketDataContextV3,
|
|
937
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
1090
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1091
|
+
objectStorageUsage: ObjectStorageUsage
|
|
938
1092
|
): Promise<CompactionGroupResult> {
|
|
939
1093
|
if (group.inputs.length == 1 && !group.changed) {
|
|
940
1094
|
return {
|
|
@@ -987,10 +1141,19 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
987
1141
|
);
|
|
988
1142
|
}
|
|
989
1143
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1144
|
+
// Replacement documents can reuse deleted IDs, so retain delete-before-insert ordering.
|
|
1145
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1146
|
+
writes.deleteMany(bucketContext.collection, { _id: { $in: idsToDelete } });
|
|
1147
|
+
writes.insertMany(bucketContext.collection, documents);
|
|
1148
|
+
this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes);
|
|
1149
|
+
this.recordObjectStorageReplacement(
|
|
1150
|
+
oldStorageBytes,
|
|
1151
|
+
documents,
|
|
1152
|
+
context.definitionId,
|
|
1153
|
+
writes,
|
|
1154
|
+
objectStorageUsage
|
|
1155
|
+
);
|
|
1156
|
+
await writes.execute();
|
|
994
1157
|
},
|
|
995
1158
|
{
|
|
996
1159
|
writeConcern: { w: 'majority' },
|
|
@@ -1020,7 +1183,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1020
1183
|
boundaryDocId: BucketDataKey,
|
|
1021
1184
|
bucketContext: BucketDataContextV3,
|
|
1022
1185
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
1023
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
1186
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1187
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1024
1188
|
): Promise<ClearCompactionResult> {
|
|
1025
1189
|
let opCountDiff = 0;
|
|
1026
1190
|
let before = emptyBucketStats();
|
|
@@ -1037,7 +1201,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1037
1201
|
boundaryDocId,
|
|
1038
1202
|
bucketContext,
|
|
1039
1203
|
collection,
|
|
1040
|
-
context
|
|
1204
|
+
context,
|
|
1205
|
+
objectStorageUsage
|
|
1041
1206
|
);
|
|
1042
1207
|
done = batch.done;
|
|
1043
1208
|
opCountDiff += batch.opCountDiff;
|
|
@@ -1053,7 +1218,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1053
1218
|
boundaryDocId,
|
|
1054
1219
|
bucketContext,
|
|
1055
1220
|
collection,
|
|
1056
|
-
context
|
|
1221
|
+
context,
|
|
1222
|
+
objectStorageUsage
|
|
1057
1223
|
);
|
|
1058
1224
|
opCountDiff += boundaryResult.opCountDiff;
|
|
1059
1225
|
before = combineAdjacentStats(before, boundaryResult.before);
|
|
@@ -1071,7 +1237,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1071
1237
|
boundaryDocId: BucketDataKey,
|
|
1072
1238
|
bucketContext: BucketDataContextV3,
|
|
1073
1239
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
1074
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
1240
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1241
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1075
1242
|
): Promise<{ done: boolean; opCountDiff: number } & CompactionStatsReplacement> {
|
|
1076
1243
|
const bucket = bucketContext.key.bucket;
|
|
1077
1244
|
this.signal?.throwIfAborted();
|
|
@@ -1165,15 +1332,13 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1165
1332
|
|
|
1166
1333
|
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
1167
1334
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
{ session }
|
|
1176
|
-
);
|
|
1335
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1336
|
+
writes.deleteMany(collection, {
|
|
1337
|
+
_id: {
|
|
1338
|
+
$gte: bucketContext.minId,
|
|
1339
|
+
$lte: lastDocId!
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1177
1342
|
|
|
1178
1343
|
const clearOp = {
|
|
1179
1344
|
bucketKey: { ...context, bucket },
|
|
@@ -1185,9 +1350,16 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1185
1350
|
const persisted = await this.persistBucketData(bucket, [[clearOp]], context, prepared, {
|
|
1186
1351
|
targetOp: maxTargetOp
|
|
1187
1352
|
});
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1353
|
+
writes.insertOne(collection, persisted.documents[0]);
|
|
1354
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1355
|
+
this.recordObjectStorageReplacement(
|
|
1356
|
+
oldStorageBytes,
|
|
1357
|
+
persisted.documents,
|
|
1358
|
+
context.definitionId,
|
|
1359
|
+
writes,
|
|
1360
|
+
objectStorageUsage
|
|
1361
|
+
);
|
|
1362
|
+
await writes.execute();
|
|
1191
1363
|
|
|
1192
1364
|
opCountDiff = -clearedOpCount + 1;
|
|
1193
1365
|
before = inputStats;
|
|
@@ -1208,7 +1380,8 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1208
1380
|
boundaryDocId: BucketDataKey,
|
|
1209
1381
|
bucketContext: BucketDataContextV3,
|
|
1210
1382
|
collection: mongo.Collection<BucketDataDocumentV3 & { bsonSize?: number | bigint }>,
|
|
1211
|
-
context: { replicationStreamId: number; definitionId: string }
|
|
1383
|
+
context: { replicationStreamId: number; definitionId: string },
|
|
1384
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1212
1385
|
): Promise<ClearCompactionResult> {
|
|
1213
1386
|
const bucket = bucketContext.key.bucket;
|
|
1214
1387
|
this.signal?.throwIfAborted();
|
|
@@ -1307,15 +1480,13 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1307
1480
|
}
|
|
1308
1481
|
|
|
1309
1482
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastNotPut}`);
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
{ session }
|
|
1318
|
-
);
|
|
1483
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1484
|
+
writes.deleteMany(collection, {
|
|
1485
|
+
_id: {
|
|
1486
|
+
$gte: bucketContext.minId,
|
|
1487
|
+
$lte: boundaryDocId
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1319
1490
|
|
|
1320
1491
|
const clearOp = {
|
|
1321
1492
|
bucketKey: { ...context, bucket },
|
|
@@ -1333,9 +1504,16 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1333
1504
|
const persisted = await this.persistBucketData(bucket, chunks, context, prepared, {
|
|
1334
1505
|
targetOp: maxTargetOp ?? undefined
|
|
1335
1506
|
});
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1507
|
+
writes.insertMany(collection, persisted.documents);
|
|
1508
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1509
|
+
this.recordObjectStorageReplacement(
|
|
1510
|
+
oldStorageBytes,
|
|
1511
|
+
persisted.documents,
|
|
1512
|
+
context.definitionId,
|
|
1513
|
+
writes,
|
|
1514
|
+
objectStorageUsage
|
|
1515
|
+
);
|
|
1516
|
+
await writes.execute();
|
|
1339
1517
|
|
|
1340
1518
|
opCountDiff = -clearedOpCount + 1;
|
|
1341
1519
|
before = inputStats;
|
|
@@ -1374,28 +1552,29 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1374
1552
|
}
|
|
1375
1553
|
|
|
1376
1554
|
/** Publish replacement uploads and retire superseded objects in the same transaction. */
|
|
1377
|
-
private
|
|
1555
|
+
private finishObjectStorageReplacement(
|
|
1378
1556
|
oldStoragePaths: Iterable<string>,
|
|
1379
1557
|
newStoragePaths: Set<string>,
|
|
1380
1558
|
uploads: PreparedObjectStorageUpload[],
|
|
1381
|
-
|
|
1382
|
-
):
|
|
1559
|
+
writes: MongoWriteBatch
|
|
1560
|
+
): void {
|
|
1383
1561
|
if (!this.storage.objectStorage) {
|
|
1384
1562
|
return;
|
|
1385
1563
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1564
|
+
this.objectStorageLifecycle.publishUploads(uploads, writes);
|
|
1565
|
+
this.objectStorageLifecycle.retire(
|
|
1388
1566
|
Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)),
|
|
1389
|
-
|
|
1567
|
+
writes
|
|
1390
1568
|
);
|
|
1391
1569
|
}
|
|
1392
1570
|
|
|
1393
|
-
private
|
|
1571
|
+
private recordObjectStorageReplacement(
|
|
1394
1572
|
oldBytes: bigint,
|
|
1395
1573
|
newDocuments: Iterable<Pick<BucketDataDocumentV3, 'storage_ref'>>,
|
|
1396
1574
|
definitionId: BucketDefinitionId,
|
|
1397
|
-
|
|
1398
|
-
|
|
1575
|
+
writes: MongoWriteBatch,
|
|
1576
|
+
objectStorageUsage: ObjectStorageUsage
|
|
1577
|
+
): void {
|
|
1399
1578
|
if (!this.storage.objectStorage) {
|
|
1400
1579
|
return;
|
|
1401
1580
|
}
|
|
@@ -1403,7 +1582,7 @@ export class MongoCompactorV3 extends MongoCompactor implements CompactIntervalC
|
|
|
1403
1582
|
for (const document of newDocuments) {
|
|
1404
1583
|
newBytes += ObjectStorageUsage.bytes(document);
|
|
1405
1584
|
}
|
|
1406
|
-
|
|
1585
|
+
objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, writes);
|
|
1407
1586
|
}
|
|
1408
1587
|
|
|
1409
1588
|
private async persistBucketData(
|