@powersync/service-module-mongodb-storage 0.21.0 → 0.23.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 +43 -0
- package/dist/storage/MongoBucketStorage.d.ts +10 -1
- package/dist/storage/MongoBucketStorage.js +37 -7
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +24 -10
- package/dist/storage/implementation/MongoBucketBatch.js +134 -89
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
- package/dist/storage/implementation/MongoIdSequence.js +14 -1
- package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
- package/dist/storage/implementation/MongoStorageProvider.js +4 -2
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +5 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.js +17 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +38 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.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 +14 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +5 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +149 -139
- 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/MongoSyncBucketStorageV1.js +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +6 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +148 -162
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +10 -4
- package/dist/storage/implementation/v3/MongoCompactorV3.js +175 -76
- 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/compact-utils.d.ts +3 -2
- package/dist/storage/implementation/v3/compact-utils.js +12 -5
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +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.d.ts +2 -1
- package/dist/utils/test-utils.js +5 -3
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/MongoBucketStorage.ts +47 -8
- package/src/storage/implementation/MongoBucketBatch.ts +205 -111
- package/src/storage/implementation/MongoCompactor.ts +2 -0
- package/src/storage/implementation/MongoIdSequence.ts +21 -1
- package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
- package/src/storage/implementation/MongoStorageProvider.ts +12 -2
- package/src/storage/implementation/MongoSyncBucketStorage.ts +19 -0
- package/src/storage/implementation/MongoSyncRulesLock.ts +56 -3
- 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 +15 -0
- package/src/storage/implementation/models.ts +1 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +167 -160
- package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +1 -1
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +217 -242
- package/src/storage/implementation/v3/MongoCompactorV3.ts +284 -108
- package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
- package/src/storage/implementation/v3/compact-utils.ts +16 -7
- package/src/storage/implementation/v3/models.ts +1 -1
- 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 +6 -3
- package/test/src/__snapshots__/storage.test.ts.snap +102 -2
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +2 -2
- package/test/src/__snapshots__/storage_sync.test.ts.snap +12 -12
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +19 -13
- package/test/src/compact-utils.test.ts +33 -8
- package/test/src/concurrent_writers.test.ts +599 -0
- package/test/src/helpers/concurrentWriter.mjs +29 -0
- package/test/src/helpers/s3TestFactory.ts +12 -12
- package/test/src/object_storage_usage.test.ts +18 -5
- package/test/src/parameter_compacting_v1.test.ts +2 -2
- package/test/src/parameter_compacting_v3.test.ts +2 -2
- package/test/src/parameter_compaction_fence.test.ts +3 -3
- package/test/src/storage.test.ts +32 -33
- package/test/src/storage_compacting.test.ts +402 -24
- package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
- package/test/src/storage_s3_checksums.test.ts +4 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +8 -8
- package/test/src/storage_s3_reading.test.ts +51 -5
- package/test/src/storage_s3_writing.test.ts +3 -3
- package/test/src/storage_sync.test.ts +52 -46
- package/test/src/storeCurrentData.test.ts +5 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
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';
|
|
6
7
|
import { BucketDataContextV3 } from './BucketDataContextV3.js';
|
|
7
8
|
import { DEFAULT_MAX_DOC_SIZE_BYTES } from './chunking.js';
|
|
8
|
-
import { applyStatsReplacement, bucketStats, chooseCompactionKind, combineAdjacentStats, combineChunkStats, CompactionContext, CompactionKind, emptyBucketStats, firstUncompactedWrite,
|
|
9
|
+
import { applyStatsReplacement, bucketStats, chooseCompactionKind, chooseRequestedCompactionKind, combineAdjacentStats, combineChunkStats, CompactionContext, CompactionKind, emptyBucketStats, firstUncompactedWrite, readCompactionBatch, statsForDocument, statsForDocuments, unclaimedSnapshotFilter } from './compact-utils.js';
|
|
9
10
|
import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
|
|
10
11
|
import { AVAILABLE_LEASE_EXPR, CompactionLease } from './CompactionLease.js';
|
|
11
12
|
import { BucketDataObjectStorage, hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
@@ -21,6 +22,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
21
22
|
maxCompactFullIntervalMs;
|
|
22
23
|
compactLeaseDurationMs;
|
|
23
24
|
maxOpIdCap;
|
|
25
|
+
forceChunkCompaction;
|
|
24
26
|
objectStorageUsage;
|
|
25
27
|
constructor(bucketStorage, db, options) {
|
|
26
28
|
super(bucketStorage, db, options);
|
|
@@ -29,6 +31,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
29
31
|
this.maxCompactFullIntervalMs = options.maxCompactFullIntervalMs ?? DEFAULT_MAX_COMPACT_FULL_INTERVAL_MS;
|
|
30
32
|
this.compactLeaseDurationMs = options.compactLeaseDurationMs ?? DEFAULT_COMPACT_LEASE_DURATION_MS;
|
|
31
33
|
this.maxOpIdCap = options.maxOpId;
|
|
34
|
+
this.forceChunkCompaction = options.forceChunkCompaction ?? false;
|
|
32
35
|
this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId());
|
|
33
36
|
}
|
|
34
37
|
async compact() {
|
|
@@ -55,7 +58,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
55
58
|
// processes the work that existed when it started.
|
|
56
59
|
await this.compactScheduledBuckets({
|
|
57
60
|
dueAheadMs: DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS,
|
|
58
|
-
|
|
61
|
+
requestedKind: CompactionKind.Chunks
|
|
59
62
|
});
|
|
60
63
|
}
|
|
61
64
|
else {
|
|
@@ -109,7 +112,8 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
109
112
|
* Batching specifically help to cover cases of many buckets where no compaction is required:
|
|
110
113
|
* Instead of sequentially claiming and then rescheduling a bucket, this handles it in bulk.
|
|
111
114
|
*
|
|
112
|
-
*
|
|
115
|
+
* Chunk merges overlap a bounded number of buckets. Full compaction stays
|
|
116
|
+
* sequential because its working set includes operation deduplication state.
|
|
113
117
|
*
|
|
114
118
|
* Any concurrent workers may read the same batch. Rescheduling filters out buckets handled
|
|
115
119
|
* by a concurrent worker or replication write, while buckets that do need compaction are
|
|
@@ -123,45 +127,55 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
123
127
|
// Writers derive next_compact_check from MongoDB's $$NOW. Use the same
|
|
124
128
|
// clock for the fixed job boundary so clock skew cannot exclude work at
|
|
125
129
|
// the exact initial-replication interval.
|
|
126
|
-
const
|
|
127
|
-
.aggregate([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
128
|
-
.toArray();
|
|
130
|
+
const jobStartedAt = await this.readCompactionTime();
|
|
129
131
|
const dueBefore = new Date(jobStartedAt.getTime() + (options.dueAheadMs ?? 0));
|
|
130
|
-
const
|
|
132
|
+
const requestedKind = options.requestedKind;
|
|
131
133
|
const rescheduleNotBefore = new Date(dueBefore.getTime() + 1);
|
|
134
|
+
// Keep accounting documents bounded by workers, not buckets or scan batches.
|
|
135
|
+
const workerUsage = Array.from({ length: this.storage.factory.chunkCompactionConcurrency }, () => new ObjectStorageUsage(this.db, this.group_id, createObjectStorageUsageWriterId()));
|
|
132
136
|
while (true) {
|
|
133
137
|
this.signal?.throwIfAborted();
|
|
134
138
|
const states = await this.findScheduledBucketBatch(dueBefore);
|
|
135
139
|
if (states.length == 0) {
|
|
136
140
|
break;
|
|
137
141
|
}
|
|
142
|
+
// Keep eligibility bounded by dueBefore, but classify with the current
|
|
143
|
+
// server time so buckets that age into full compaction can advance.
|
|
144
|
+
const batchStartedAt = await this.readCompactionTime();
|
|
138
145
|
const scheduled = [];
|
|
139
146
|
for (const state of states) {
|
|
140
147
|
try {
|
|
141
148
|
scheduled.push({
|
|
142
149
|
state,
|
|
143
|
-
decision: chooseCompactionKind(state,
|
|
144
|
-
|
|
150
|
+
decision: chooseCompactionKind(state, batchStartedAt, this),
|
|
151
|
+
selectedKind: chooseRequestedCompactionKind(state, requestedKind, this)
|
|
145
152
|
});
|
|
146
153
|
}
|
|
147
154
|
catch (error) {
|
|
148
155
|
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
|
-
const noOpStates = scheduled.filter(({ state, decision,
|
|
158
|
+
const noOpStates = scheduled.filter(({ state, decision, selectedKind }) => state.compact_lease == null && (requestedKind == null ? decision.kind : selectedKind) == null);
|
|
152
159
|
await this.rescheduleUnclaimedBuckets(noOpStates, rescheduleNotBefore);
|
|
153
|
-
|
|
154
|
-
const kind =
|
|
160
|
+
const processBucket = async ({ state, decision, selectedKind }, objectStorageUsage, chunksOnly = false) => {
|
|
161
|
+
const kind = requestedKind == null ? decision.kind : selectedKind;
|
|
155
162
|
if (state.compact_lease == null && kind == null) {
|
|
156
|
-
|
|
163
|
+
return;
|
|
157
164
|
}
|
|
158
165
|
try {
|
|
159
166
|
await using lease = await this.claimBucket({ _id: state._id, next_compact_check: { $lte: dueBefore } });
|
|
160
167
|
if (lease == null) {
|
|
161
|
-
|
|
168
|
+
return;
|
|
162
169
|
}
|
|
163
170
|
const claimedDecision = chooseCompactionKind(lease.state, lease.startedAt, this);
|
|
164
|
-
const claimedKind =
|
|
171
|
+
const claimedKind = requestedKind == null
|
|
172
|
+
? claimedDecision.kind
|
|
173
|
+
: chooseRequestedCompactionKind(lease.state, requestedKind, this);
|
|
174
|
+
if (chunksOnly && claimedKind === CompactionKind.Full) {
|
|
175
|
+
// The decision changed after scanning. Release the lease without
|
|
176
|
+
// rescheduling; the next batch will classify it with a fresh timestamp.
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
165
179
|
if (claimedKind == null) {
|
|
166
180
|
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
167
181
|
}
|
|
@@ -171,7 +185,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
171
185
|
await this.rescheduleClaimedBucket(lease, claimedDecision, rescheduleNotBefore);
|
|
172
186
|
}
|
|
173
187
|
else {
|
|
174
|
-
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore);
|
|
188
|
+
await this.compactClaimedBucket(lease, claimedKind, claimedDecision, rescheduleNotBefore, objectStorageUsage);
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
191
|
catch (error) {
|
|
@@ -182,6 +196,73 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
182
196
|
}
|
|
183
197
|
await this.rescheduleFailedBucket(state, rescheduleNotBefore, error);
|
|
184
198
|
}
|
|
199
|
+
};
|
|
200
|
+
const chunkBuckets = scheduled.filter(({ decision, selectedKind }) => (requestedKind == null ? decision.kind : selectedKind) === CompactionKind.Chunks);
|
|
201
|
+
const sequentialBuckets = scheduled.filter(({ decision, selectedKind }) => (requestedKind == null ? decision.kind : selectedKind) !== CompactionKind.Chunks);
|
|
202
|
+
await this.runChunkCompactionWorkers(chunkBuckets, workerUsage, (entry, usage) => processBucket(entry, usage, true));
|
|
203
|
+
// Full compaction cannot overlap chunk workers from this job, and only
|
|
204
|
+
// one full bucket is processed at a time.
|
|
205
|
+
for (const entry of sequentialBuckets) {
|
|
206
|
+
await processBucket(entry, this.objectStorageUsage);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Use MongoDB's clock, matching scheduling and lease timestamps. */
|
|
211
|
+
async readCompactionTime() {
|
|
212
|
+
const [{ now }] = await this.db.db
|
|
213
|
+
.aggregate([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
214
|
+
.toArray();
|
|
215
|
+
return now;
|
|
216
|
+
}
|
|
217
|
+
/** Process one scheduled batch with a fixed pool of workers. */
|
|
218
|
+
async runChunkCompactionWorkers(buckets, workerUsage, processBucket) {
|
|
219
|
+
const signal = this.signal;
|
|
220
|
+
let nextBucket = 0;
|
|
221
|
+
let failed = false;
|
|
222
|
+
const runWorker = async (usage) => {
|
|
223
|
+
try {
|
|
224
|
+
while (!failed && nextBucket < buckets.length) {
|
|
225
|
+
// Taking an entry has no await, so each worker gets a different bucket.
|
|
226
|
+
// A worker takes another only after finishing its current bucket.
|
|
227
|
+
const bucket = buckets[nextBucket++];
|
|
228
|
+
// This pool bounds one job; the factory semaphore bounds all jobs together.
|
|
229
|
+
// Acquire before claiming the bucket lease, and hold until it is released.
|
|
230
|
+
const acquired = await acquireSemaphoreAbortable(this.storage.factory.chunkCompactionSlots, signal);
|
|
231
|
+
if (acquired === 'aborted') {
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const [, releaseSlot] = acquired;
|
|
236
|
+
try {
|
|
237
|
+
// A sibling may have failed while this worker waited for a slot.
|
|
238
|
+
if (failed)
|
|
239
|
+
return;
|
|
240
|
+
signal?.throwIfAborted();
|
|
241
|
+
await processBucket(bucket, usage);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
releaseSlot();
|
|
245
|
+
}
|
|
246
|
+
// Let replication and other event-loop work run between buckets.
|
|
247
|
+
await setImmediate();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
// Drain work already started, but do not let siblings start new buckets.
|
|
252
|
+
failed = true;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
// Concurrent transactions must not all increment the same usage document.
|
|
257
|
+
// Reuse one writer for each worker instead of creating one per bucket.
|
|
258
|
+
const workers = workerUsage.map(runWorker);
|
|
259
|
+
// Do not release the caller's replication lock or run cleanup while a
|
|
260
|
+
// sibling worker still owns a bucket lease or is finishing a replacement.
|
|
261
|
+
// Wait for every worker even on failure, then propagate the first error.
|
|
262
|
+
const results = await Promise.allSettled(workers);
|
|
263
|
+
for (const result of results) {
|
|
264
|
+
if (result.status === 'rejected') {
|
|
265
|
+
throw result.reason;
|
|
185
266
|
}
|
|
186
267
|
}
|
|
187
268
|
}
|
|
@@ -238,10 +319,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
238
319
|
async claimBucket(filter, sort) {
|
|
239
320
|
return CompactionLease.claim(this.db.bucketState(this.group_id), filter, sort, this.compactLeaseDurationMs);
|
|
240
321
|
}
|
|
241
|
-
async compactClaimedBucket(lease, kind, decision, rescheduleNotBefore) {
|
|
322
|
+
async compactClaimedBucket(lease, kind, decision, rescheduleNotBefore, objectStorageUsage = this.objectStorageUsage) {
|
|
242
323
|
const context = new CompactionContext(lease, kind, decision, rescheduleNotBefore, this.compactionTarget(lease.state));
|
|
243
324
|
lease.startRenewal();
|
|
244
|
-
await this.compactSingleBucket(context);
|
|
325
|
+
await this.compactSingleBucket(context, objectStorageUsage);
|
|
245
326
|
}
|
|
246
327
|
async rescheduleClaimedBucket(lease, decision, notBefore) {
|
|
247
328
|
await lease.reschedule(this.rescheduleAtOrAfter(decision.nextCompactCheck, notBefore));
|
|
@@ -271,11 +352,11 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
271
352
|
}
|
|
272
353
|
return new ObjectStorageLifecycle(this.db, this.group_id, this.storage.objectStorage);
|
|
273
354
|
}
|
|
274
|
-
async compactSingleBucket(context) {
|
|
355
|
+
async compactSingleBucket(context, objectStorageUsage) {
|
|
275
356
|
if (context.kind == CompactionKind.Chunks) {
|
|
276
|
-
return this.compactSingleBucketChunks(context);
|
|
357
|
+
return this.compactSingleBucketChunks(context, objectStorageUsage);
|
|
277
358
|
}
|
|
278
|
-
return this.compactSingleBucketFully(context);
|
|
359
|
+
return this.compactSingleBucketFully(context, objectStorageUsage);
|
|
279
360
|
}
|
|
280
361
|
/**
|
|
281
362
|
* Merge adjacent bucket-data chunks without inspecting their operations
|
|
@@ -283,7 +364,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
283
364
|
* update the persisted checksum state and to decide whether a group can fit
|
|
284
365
|
* in one chunk.
|
|
285
366
|
*/
|
|
286
|
-
async compactSingleBucketChunks(context) {
|
|
367
|
+
async compactSingleBucketChunks(context, objectStorageUsage) {
|
|
287
368
|
const bucket = context.state._id.b;
|
|
288
369
|
const resolvedDefinitionId = context.state._id.d;
|
|
289
370
|
const bucketContext = new BucketDataContextV3(this.db, {
|
|
@@ -362,7 +443,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
362
443
|
}
|
|
363
444
|
const nextSize = pendingSize + doc.size;
|
|
364
445
|
if (pendingChunks.length > 0 && nextSize > DEFAULT_MAX_DOC_SIZE_BYTES) {
|
|
365
|
-
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
446
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext, objectStorageUsage);
|
|
366
447
|
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
367
448
|
pendingChunks = [];
|
|
368
449
|
pendingSize = 0;
|
|
@@ -376,7 +457,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
376
457
|
}
|
|
377
458
|
}
|
|
378
459
|
if (pendingChunks.length > 0) {
|
|
379
|
-
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext);
|
|
460
|
+
const groupStats = await this.flushChunkMerge(bucket, pendingChunks, collection, dataContext, bucketContext, objectStorageUsage);
|
|
380
461
|
compactedTail = combineAdjacentStats(compactedTail, groupStats);
|
|
381
462
|
}
|
|
382
463
|
if (compactedOpId == null) {
|
|
@@ -397,33 +478,44 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
397
478
|
this.compactedBucketCount++;
|
|
398
479
|
this.logger.info(`Compacted bucket chunks ${bucket}: ${result.bucketStats.count} ops, ${result.bucketStats.chunks} chunks, ${formatBytes(result.bucketStats.bytes)}`);
|
|
399
480
|
}
|
|
400
|
-
async flushChunkMerge(bucket, inputs, collection, context, bucketContext) {
|
|
481
|
+
async flushChunkMerge(bucket, inputs, collection, context, bucketContext, objectStorageUsage) {
|
|
401
482
|
if (inputs.length == 1) {
|
|
402
483
|
return statsForDocument(inputs[0]);
|
|
403
484
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
485
|
+
try {
|
|
486
|
+
this.signal?.throwIfAborted();
|
|
487
|
+
// The metadata scan deliberately excluded ops. Read inline payloads only
|
|
488
|
+
// for this merge group; object-storage payloads are fetched below using
|
|
489
|
+
// the same rule.
|
|
490
|
+
const inlineInputs = inputs.filter((input) => input.storage_ref == null);
|
|
491
|
+
if (inlineInputs.length > 0) {
|
|
492
|
+
const inlineDocuments = await collection
|
|
493
|
+
.find({ _id: { $in: inlineInputs.map((input) => input._id) } }, { projection: { _id: 1, ops: 1 } })
|
|
494
|
+
.toArray();
|
|
495
|
+
const opsById = new Map(inlineDocuments.map((document) => [document._id.o.toString(), document.ops]));
|
|
496
|
+
for (const input of inlineInputs) {
|
|
497
|
+
input.ops = opsById.get(input._id.o.toString());
|
|
498
|
+
}
|
|
415
499
|
}
|
|
500
|
+
await hydrateBucketDataDocuments(inputs, this.storage.objectStorage, { signal: this.signal });
|
|
501
|
+
const operations = inputs.flatMap((input) => Array.from(loadBucketDataDocument(context, input)));
|
|
502
|
+
const targetOp = inputs.reduce((maxTarget, input) => maxOpId(maxTarget, input.target_op), null);
|
|
503
|
+
const result = await this.flushCompactionGroup(bucket, {
|
|
504
|
+
inputs,
|
|
505
|
+
ops: operations,
|
|
506
|
+
changed: true,
|
|
507
|
+
targetOp
|
|
508
|
+
}, bucketContext, context, objectStorageUsage);
|
|
509
|
+
return result.stats;
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
// The scan batch also references these documents. Do not retain hydrated
|
|
513
|
+
// operations after finishing this merge group.
|
|
514
|
+
for (const input of inputs) {
|
|
515
|
+
delete input.ops;
|
|
516
|
+
}
|
|
517
|
+
await setImmediate();
|
|
416
518
|
}
|
|
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
519
|
}
|
|
428
520
|
async finalizeCompactedBucket({ context, compactedOpId, compactionResult, puts }) {
|
|
429
521
|
await context.lease.throwIfLost();
|
|
@@ -526,7 +618,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
526
618
|
: addChecksums(0, Number(stats?.checksum ?? 0))
|
|
527
619
|
};
|
|
528
620
|
}
|
|
529
|
-
async compactSingleBucketFully(context) {
|
|
621
|
+
async compactSingleBucketFully(context, objectStorageUsage) {
|
|
530
622
|
const bucket = context.state._id.b;
|
|
531
623
|
const resolvedDefinitionId = context.state._id.d;
|
|
532
624
|
const bucketContext = new BucketDataContextV3(this.db, {
|
|
@@ -673,7 +765,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
673
765
|
}
|
|
674
766
|
else {
|
|
675
767
|
const flushedGroup = pendingGroup;
|
|
676
|
-
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext);
|
|
768
|
+
const result = await this.flushCompactionGroup(bucket, flushedGroup, bucketContext, dataContext, objectStorageUsage);
|
|
677
769
|
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
678
770
|
if (lastNotPut != null &&
|
|
679
771
|
flushedGroup.ops[0].o <= lastNotPut &&
|
|
@@ -692,7 +784,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
692
784
|
this.logger.info(`Compacted batch of ${batchDocs.length} documents for bucket ${bucket}`);
|
|
693
785
|
}
|
|
694
786
|
if (pendingGroup != null) {
|
|
695
|
-
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext);
|
|
787
|
+
const result = await this.flushCompactionGroup(bucket, pendingGroup, bucketContext, dataContext, objectStorageUsage);
|
|
696
788
|
compactedStats = combineAdjacentStats(compactedStats, result.stats);
|
|
697
789
|
if (lastNotPut != null &&
|
|
698
790
|
pendingGroup.ops[0].o <= lastNotPut &&
|
|
@@ -709,7 +801,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
709
801
|
if (clearBoundary == null || clearBoundary.opId != lastNotPut) {
|
|
710
802
|
throw new ReplicationAssertionError(`Missing CLEAR boundary document for bucket ${bucket}`);
|
|
711
803
|
}
|
|
712
|
-
const clearResult = await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, dataContext);
|
|
804
|
+
const clearResult = await this.clearBucketLeading(lastNotPut, clearBoundary.documentId, bucketContext, collection, dataContext, objectStorageUsage);
|
|
713
805
|
totalOpCount += clearResult.opCountDiff;
|
|
714
806
|
compactedStats = applyStatsReplacement(compactedStats, clearResult.before, clearResult.after);
|
|
715
807
|
}
|
|
@@ -731,7 +823,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
731
823
|
* If verification or the transaction fails, the prepared markers retain enough
|
|
732
824
|
* information for the uploaded objects to be cleaned up later.
|
|
733
825
|
*/
|
|
734
|
-
async flushCompactionGroup(bucket, group, bucketContext, context) {
|
|
826
|
+
async flushCompactionGroup(bucket, group, bucketContext, context, objectStorageUsage) {
|
|
735
827
|
if (group.inputs.length == 1 && !group.changed) {
|
|
736
828
|
return {
|
|
737
829
|
documentId: group.inputs[0]._id,
|
|
@@ -768,10 +860,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
768
860
|
verification.opCountSum !== expectedOpCount) {
|
|
769
861
|
throw new ConcurrentCompactionError(`Inputs changed while compacting bucket ${bucket}; restarting from the latest bucket state`);
|
|
770
862
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
863
|
+
// Replacement documents can reuse deleted IDs, so retain delete-before-insert ordering.
|
|
864
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
865
|
+
writes.deleteMany(bucketContext.collection, { _id: { $in: idsToDelete } });
|
|
866
|
+
writes.insertMany(bucketContext.collection, documents);
|
|
867
|
+
this.finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes);
|
|
868
|
+
this.recordObjectStorageReplacement(oldStorageBytes, documents, context.definitionId, writes, objectStorageUsage);
|
|
869
|
+
await writes.execute();
|
|
775
870
|
}, {
|
|
776
871
|
writeConcern: { w: 'majority' },
|
|
777
872
|
readConcern: { level: 'snapshot' }
|
|
@@ -794,7 +889,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
794
889
|
* Returns the op count and stored-stat changes after replacing cleared ops
|
|
795
890
|
* with CLEAR ops.
|
|
796
891
|
*/
|
|
797
|
-
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
892
|
+
async clearBucketLeading(lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
798
893
|
let opCountDiff = 0;
|
|
799
894
|
let before = emptyBucketStats();
|
|
800
895
|
let after = emptyBucketStats();
|
|
@@ -804,7 +899,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
804
899
|
// First step is to clear full chunks that contain only CLEAR/MOVE/REMOVE operations.
|
|
805
900
|
// There can be many of them, so we do one batch at a time.
|
|
806
901
|
while (!done) {
|
|
807
|
-
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context);
|
|
902
|
+
const batch = await this.clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage);
|
|
808
903
|
done = batch.done;
|
|
809
904
|
opCountDiff += batch.opCountDiff;
|
|
810
905
|
before = combineAdjacentStats(before, batch.before);
|
|
@@ -812,7 +907,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
812
907
|
}
|
|
813
908
|
// The final step is to process the "boundary" document: It may contain some CLEAR/MOVE/REMOVE operations,
|
|
814
909
|
// 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);
|
|
910
|
+
const boundaryResult = await this.clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage);
|
|
816
911
|
opCountDiff += boundaryResult.opCountDiff;
|
|
817
912
|
before = combineAdjacentStats(before, boundaryResult.before);
|
|
818
913
|
after = combineAdjacentStats(after, boundaryResult.after);
|
|
@@ -822,7 +917,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
822
917
|
}
|
|
823
918
|
return { opCountDiff, before, after };
|
|
824
919
|
}
|
|
825
|
-
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
920
|
+
async clearLeadingFullDocuments(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
826
921
|
const bucket = bucketContext.key.bucket;
|
|
827
922
|
this.signal?.throwIfAborted();
|
|
828
923
|
let prepared;
|
|
@@ -902,12 +997,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
902
997
|
}
|
|
903
998
|
prepared ??= await this.prepareCompactionUploads(bucket, context, [lastNotPut]);
|
|
904
999
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastDocId?.o}`);
|
|
905
|
-
|
|
1000
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1001
|
+
writes.deleteMany(collection, {
|
|
906
1002
|
_id: {
|
|
907
1003
|
$gte: bucketContext.minId,
|
|
908
1004
|
$lte: lastDocId
|
|
909
1005
|
}
|
|
910
|
-
}
|
|
1006
|
+
});
|
|
911
1007
|
const clearOp = {
|
|
912
1008
|
bucketKey: { ...context, bucket },
|
|
913
1009
|
o: lastDocId.o,
|
|
@@ -918,9 +1014,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
918
1014
|
const persisted = await this.persistBucketData(bucket, [[clearOp]], context, prepared, {
|
|
919
1015
|
targetOp: maxTargetOp
|
|
920
1016
|
});
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1017
|
+
writes.insertOne(collection, persisted.documents[0]);
|
|
1018
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1019
|
+
this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, writes, objectStorageUsage);
|
|
1020
|
+
await writes.execute();
|
|
924
1021
|
opCountDiff = -clearedOpCount + 1;
|
|
925
1022
|
before = inputStats;
|
|
926
1023
|
after = statsForDocuments(persisted.documents);
|
|
@@ -930,7 +1027,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
930
1027
|
});
|
|
931
1028
|
return { done, opCountDiff, before, after };
|
|
932
1029
|
}
|
|
933
|
-
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context) {
|
|
1030
|
+
async clearBoundaryDocument(session, lastNotPut, boundaryDocId, bucketContext, collection, context, objectStorageUsage) {
|
|
934
1031
|
const bucket = bucketContext.key.bucket;
|
|
935
1032
|
this.signal?.throwIfAborted();
|
|
936
1033
|
const prepared = await this.prepareCompactionUploads(bucket, context, [lastNotPut, boundaryDocId.o]);
|
|
@@ -1012,12 +1109,13 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1012
1109
|
throw new Error(`CLEAR boundary document not found for bucket ${bucket}`);
|
|
1013
1110
|
}
|
|
1014
1111
|
this.logger.info(`Flushing CLEAR for ${clearedOpCount} ops at ${lastNotPut}`);
|
|
1015
|
-
|
|
1112
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
1113
|
+
writes.deleteMany(collection, {
|
|
1016
1114
|
_id: {
|
|
1017
1115
|
$gte: bucketContext.minId,
|
|
1018
1116
|
$lte: boundaryDocId
|
|
1019
1117
|
}
|
|
1020
|
-
}
|
|
1118
|
+
});
|
|
1021
1119
|
const clearOp = {
|
|
1022
1120
|
bucketKey: { ...context, bucket },
|
|
1023
1121
|
o: lastNotPut,
|
|
@@ -1034,9 +1132,10 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1034
1132
|
const persisted = await this.persistBucketData(bucket, chunks, context, prepared, {
|
|
1035
1133
|
targetOp: maxTargetOp ?? undefined
|
|
1036
1134
|
});
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1135
|
+
writes.insertMany(collection, persisted.documents);
|
|
1136
|
+
this.finishObjectStorageReplacement(oldStoragePaths, persisted.storagePaths, persisted.uploads, writes);
|
|
1137
|
+
this.recordObjectStorageReplacement(oldStorageBytes, persisted.documents, context.definitionId, writes, objectStorageUsage);
|
|
1138
|
+
await writes.execute();
|
|
1040
1139
|
opCountDiff = -clearedOpCount + 1;
|
|
1041
1140
|
before = inputStats;
|
|
1042
1141
|
after = statsForDocuments(persisted.documents);
|
|
@@ -1064,14 +1163,14 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1064
1163
|
return lifecycle.prepareUploads(paths);
|
|
1065
1164
|
}
|
|
1066
1165
|
/** Publish replacement uploads and retire superseded objects in the same transaction. */
|
|
1067
|
-
|
|
1166
|
+
finishObjectStorageReplacement(oldStoragePaths, newStoragePaths, uploads, writes) {
|
|
1068
1167
|
if (!this.storage.objectStorage) {
|
|
1069
1168
|
return;
|
|
1070
1169
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1170
|
+
this.objectStorageLifecycle.publishUploads(uploads, writes);
|
|
1171
|
+
this.objectStorageLifecycle.retire(Array.from(oldStoragePaths).filter((path) => !newStoragePaths.has(path)), writes);
|
|
1073
1172
|
}
|
|
1074
|
-
|
|
1173
|
+
recordObjectStorageReplacement(oldBytes, newDocuments, definitionId, writes, objectStorageUsage) {
|
|
1075
1174
|
if (!this.storage.objectStorage) {
|
|
1076
1175
|
return;
|
|
1077
1176
|
}
|
|
@@ -1079,7 +1178,7 @@ export class MongoCompactorV3 extends MongoCompactor {
|
|
|
1079
1178
|
for (const document of newDocuments) {
|
|
1080
1179
|
newBytes += ObjectStorageUsage.bytes(document);
|
|
1081
1180
|
}
|
|
1082
|
-
|
|
1181
|
+
objectStorageUsage.applyDelta(definitionId, newBytes - oldBytes, writes);
|
|
1083
1182
|
}
|
|
1084
1183
|
async persistBucketData(bucket, chunks, context, preparedUploads, options) {
|
|
1085
1184
|
const serializedChunks = chunks.map((chunk) => serializeBucketData(bucket, chunk, options));
|