@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
|
@@ -7,6 +7,7 @@ import { CompactionLease } from '@module/storage/implementation/v3/CompactionLea
|
|
|
7
7
|
import { BucketDataDocumentV3 } from '@module/storage/implementation/v3/models.js';
|
|
8
8
|
import { ObjectStorageError } from '@module/storage/implementation/v3/object-storage/ObjectStorage.js';
|
|
9
9
|
import { VersionedPowerSyncMongoV3 } from '@module/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
10
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
10
11
|
import { replicaIdToSubkey } from '@module/utils/util.js';
|
|
11
12
|
import { logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
12
13
|
import {
|
|
@@ -16,9 +17,11 @@ import {
|
|
|
16
17
|
SyncRulesBucketStorage,
|
|
17
18
|
updateSyncRulesFromYaml
|
|
18
19
|
} from '@powersync/service-core';
|
|
19
|
-
import { bucketRequest, compactActive, register, test_utils } from '@powersync/service-core-tests';
|
|
20
|
+
import { bucketRequest, compactActive, getTestStorage, register, test_utils } from '@powersync/service-core-tests';
|
|
20
21
|
import * as bson from 'bson';
|
|
21
22
|
import { describe, expect, test, vi } from 'vitest';
|
|
23
|
+
import { env } from './env.js';
|
|
24
|
+
import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
|
|
22
25
|
import { INITIALIZED_MONGO_STORAGE_FACTORY, TEST_STORAGE_VERSIONS } from './util.js';
|
|
23
26
|
|
|
24
27
|
function makeOp(
|
|
@@ -103,7 +106,7 @@ bucket_definitions:
|
|
|
103
106
|
{ storageVersion }
|
|
104
107
|
)
|
|
105
108
|
);
|
|
106
|
-
const bucketStorage = factory
|
|
109
|
+
const bucketStorage = await getTestStorage(factory, syncRules);
|
|
107
110
|
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
108
111
|
const { checkpoint } = await populate(bucketStorage, 1);
|
|
109
112
|
|
|
@@ -161,15 +164,16 @@ bucket_definitions:
|
|
|
161
164
|
{ storageVersion }
|
|
162
165
|
)
|
|
163
166
|
);
|
|
164
|
-
const bucketStorage = factory
|
|
167
|
+
const bucketStorage = await getTestStorage(factory, syncRules);
|
|
165
168
|
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
166
169
|
|
|
167
170
|
await populate(bucketStorage, 2);
|
|
168
171
|
const { checkpoint } = await bucketStorage.getCheckpoint();
|
|
169
172
|
|
|
170
|
-
// V3's initial lite pass
|
|
173
|
+
// Force V3's initial lite pass to process these small buckets for cache verification.
|
|
171
174
|
// Earlier storage versions use the default minimum-change threshold.
|
|
172
175
|
const result0 = await bucketStorage.compactInitialReplication({
|
|
176
|
+
forceChunkCompaction: true,
|
|
173
177
|
maxOpId: checkpoint
|
|
174
178
|
});
|
|
175
179
|
expect(result0.buckets).toEqual(storageVersion >= storage.STORAGE_VERSION_3 ? 2 : 0);
|
|
@@ -177,6 +181,7 @@ bucket_definitions:
|
|
|
177
181
|
// For V1/V2, lower the threshold to populate the checksum cache. V3 has
|
|
178
182
|
// already updated its compacted state, so another initial pass is a no-op.
|
|
179
183
|
const result1 = await bucketStorage.compactInitialReplication({
|
|
184
|
+
forceChunkCompaction: true,
|
|
180
185
|
maxOpId: checkpoint,
|
|
181
186
|
minBucketChanges: 1
|
|
182
187
|
});
|
|
@@ -184,6 +189,7 @@ bucket_definitions:
|
|
|
184
189
|
|
|
185
190
|
// Repeating it stays a no-op.
|
|
186
191
|
const result2 = await bucketStorage.compactInitialReplication({
|
|
192
|
+
forceChunkCompaction: true,
|
|
187
193
|
maxOpId: checkpoint,
|
|
188
194
|
minBucketChanges: 1
|
|
189
195
|
});
|
|
@@ -211,6 +217,7 @@ bucket_definitions:
|
|
|
211
217
|
.createMongoCompactor({
|
|
212
218
|
maxOpId: checkpoint,
|
|
213
219
|
compactChunksOnly: true,
|
|
220
|
+
forceChunkCompaction: true,
|
|
214
221
|
minCompactChunkIntervalMs: 1
|
|
215
222
|
})
|
|
216
223
|
.compact();
|
|
@@ -220,7 +227,10 @@ bucket_definitions:
|
|
|
220
227
|
|
|
221
228
|
test('v3 repeated initial chunk compaction reschedules overdue full work beyond the current run', async () => {
|
|
222
229
|
const { bucketStorage, checkpoint } = await setup(storage.STORAGE_VERSION_3);
|
|
223
|
-
const firstResult = await bucketStorage.compactInitialReplication({
|
|
230
|
+
const firstResult = await bucketStorage.compactInitialReplication({
|
|
231
|
+
forceChunkCompaction: true,
|
|
232
|
+
maxOpId: checkpoint
|
|
233
|
+
});
|
|
224
234
|
expect(firstResult.buckets).toBe(2);
|
|
225
235
|
|
|
226
236
|
const bucketStateCollection = (bucketStorage.db as VersionedPowerSyncMongoV3).bucketState(
|
|
@@ -240,6 +250,7 @@ bucket_definitions:
|
|
|
240
250
|
.aggregate<{ now: Date }>([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
|
|
241
251
|
.toArray();
|
|
242
252
|
const result = await bucketStorage.compactInitialReplication({
|
|
253
|
+
forceChunkCompaction: true,
|
|
243
254
|
maxOpId: checkpoint,
|
|
244
255
|
signal: AbortSignal.timeout(2_000)
|
|
245
256
|
});
|
|
@@ -286,7 +297,11 @@ bucket_definitions:
|
|
|
286
297
|
});
|
|
287
298
|
|
|
288
299
|
describe('Mongo Sync Parameter Storage Compact', () => {
|
|
289
|
-
|
|
300
|
+
for (const storageVersion of TEST_STORAGE_VERSIONS) {
|
|
301
|
+
describe(`storage v${storageVersion}`, () => {
|
|
302
|
+
register.registerParameterCompactTests({ ...INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion });
|
|
303
|
+
});
|
|
304
|
+
}
|
|
290
305
|
});
|
|
291
306
|
|
|
292
307
|
/**
|
|
@@ -345,7 +360,7 @@ bucket_definitions:
|
|
|
345
360
|
{ storageVersion: 3 }
|
|
346
361
|
)
|
|
347
362
|
);
|
|
348
|
-
const bucketStorage = factory
|
|
363
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
349
364
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
350
365
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
351
366
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -385,6 +400,91 @@ bucket_definitions:
|
|
|
385
400
|
});
|
|
386
401
|
}
|
|
387
402
|
|
|
403
|
+
test.each([1, 7])(
|
|
404
|
+
'initial compaction bulk-reschedules buckets with %s chunks without reading data',
|
|
405
|
+
async (chunks) => {
|
|
406
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId, db } = await setupV3Storage();
|
|
407
|
+
const documents = Array.from({ length: chunks }, (_, index) =>
|
|
408
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, String(index), 'data', ctx, sourceTableId)])
|
|
409
|
+
);
|
|
410
|
+
await insertDocs(collection, documents);
|
|
411
|
+
await bucketStateCollection.insertOne({
|
|
412
|
+
_id: { d: ctx.definitionId, b: BUCKET },
|
|
413
|
+
last_op: BigInt(chunks),
|
|
414
|
+
next_compact_check: new Date(0),
|
|
415
|
+
// Even overdue full compaction must not force a small initial chunk merge.
|
|
416
|
+
first_uncompacted_write: new Date(0),
|
|
417
|
+
bucket_stats: { count: chunks, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks }
|
|
418
|
+
});
|
|
419
|
+
const claim = vi.spyOn(CompactionLease, 'claim');
|
|
420
|
+
const data = vi.spyOn(db, 'bucketData');
|
|
421
|
+
try {
|
|
422
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: BigInt(chunks) })).toEqual({ buckets: 0 });
|
|
423
|
+
expect(claim).not.toHaveBeenCalled();
|
|
424
|
+
expect(data).not.toHaveBeenCalled();
|
|
425
|
+
const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
|
|
426
|
+
expect(state?.compacted_state).toBeUndefined();
|
|
427
|
+
expect(state?.compact_lease).toBeUndefined();
|
|
428
|
+
expect(state?.last_full_compact).toBeUndefined();
|
|
429
|
+
expect(state!.next_compact_check!.getTime()).toBeGreaterThan(Date.now());
|
|
430
|
+
expect(await collection.find().toArray()).toEqual(documents);
|
|
431
|
+
} finally {
|
|
432
|
+
claim.mockRestore();
|
|
433
|
+
data.mockRestore();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
test('initial compaction rechecks the chunk threshold after claiming', async () => {
|
|
439
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId, db } = await setupV3Storage();
|
|
440
|
+
const documents = Array.from({ length: 8 }, (_, index) =>
|
|
441
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, String(index), 'data', ctx, sourceTableId)])
|
|
442
|
+
);
|
|
443
|
+
await insertDocs(collection, documents);
|
|
444
|
+
const bytes = BigInt(documents.reduce((sum, doc) => sum + doc.size, 0));
|
|
445
|
+
await bucketStateCollection.insertOne({
|
|
446
|
+
_id: { d: ctx.definitionId, b: BUCKET },
|
|
447
|
+
last_op: 8n,
|
|
448
|
+
next_compact_check: new Date(0),
|
|
449
|
+
first_uncompacted_write: new Date(),
|
|
450
|
+
bucket_stats: { count: 8, bytes, chunks: 8 }
|
|
451
|
+
});
|
|
452
|
+
const originalClaim = CompactionLease.claim.bind(CompactionLease);
|
|
453
|
+
const claim = vi.spyOn(CompactionLease, 'claim').mockImplementationOnce(async (...args) => {
|
|
454
|
+
// Another compactor finishes before we acquire the lease, leaving one
|
|
455
|
+
// new chunk beyond its cached prefix instead of the eight we selected.
|
|
456
|
+
await bucketStateCollection.updateOne(
|
|
457
|
+
{ _id: { d: ctx.definitionId, b: BUCKET } },
|
|
458
|
+
{
|
|
459
|
+
$set: {
|
|
460
|
+
compacted_state: {
|
|
461
|
+
op_id: 7n,
|
|
462
|
+
checksum: 196n,
|
|
463
|
+
count: 7,
|
|
464
|
+
bytes: bytes - BigInt(documents[7].size),
|
|
465
|
+
chunks: 7,
|
|
466
|
+
at: new Date()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
return originalClaim(...args);
|
|
472
|
+
});
|
|
473
|
+
const data = vi.spyOn(db, 'bucketData');
|
|
474
|
+
try {
|
|
475
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: 8n })).toEqual({ buckets: 0 });
|
|
476
|
+
expect(claim).toHaveBeenCalledTimes(1);
|
|
477
|
+
expect(data).not.toHaveBeenCalled();
|
|
478
|
+
const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
|
|
479
|
+
expect(state?.compacted_state?.op_id).toBe(7n);
|
|
480
|
+
expect(state?.compact_lease).toBeUndefined();
|
|
481
|
+
expect(state!.next_compact_check!.getTime()).toBeGreaterThan(Date.now());
|
|
482
|
+
} finally {
|
|
483
|
+
claim.mockRestore();
|
|
484
|
+
data.mockRestore();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
388
488
|
test('a successful lease renewal clears a transient renewal error', async () => {
|
|
389
489
|
const { bucketStateCollection, ctx } = await setupV3Storage();
|
|
390
490
|
await insertBucketState(bucketStateCollection, ctx.definitionId, 1n);
|
|
@@ -446,7 +546,7 @@ bucket_definitions:
|
|
|
446
546
|
);
|
|
447
547
|
|
|
448
548
|
await (bucketStorage as MongoSyncBucketStorage)
|
|
449
|
-
.createMongoCompactor({ maxOpId: 2n, compactChunksOnly: true })
|
|
549
|
+
.createMongoCompactor({ maxOpId: 2n, compactChunksOnly: true, forceChunkCompaction: true })
|
|
450
550
|
.compact();
|
|
451
551
|
|
|
452
552
|
const [badState, goodState] = await Promise.all([
|
|
@@ -460,7 +560,7 @@ bucket_definitions:
|
|
|
460
560
|
|
|
461
561
|
test('aborting scheduled compaction does not reschedule the remaining batch', async () => {
|
|
462
562
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3Storage();
|
|
463
|
-
const buckets =
|
|
563
|
+
const buckets = Array.from({ length: 8 }, (_, index) => `bucket${index}[]`);
|
|
464
564
|
const documents = buckets.map((bucket) =>
|
|
465
565
|
serializeBucketData(bucket, [makeOp(2, bucket, bucket, { ...ctx, bucket }, sourceTableId)])
|
|
466
566
|
);
|
|
@@ -490,6 +590,7 @@ bucket_definitions:
|
|
|
490
590
|
.createMongoCompactor({
|
|
491
591
|
maxOpId: 2n,
|
|
492
592
|
compactChunksOnly: true,
|
|
593
|
+
forceChunkCompaction: true,
|
|
493
594
|
signal: abortController.signal,
|
|
494
595
|
logger: testLogger
|
|
495
596
|
})
|
|
@@ -499,8 +600,11 @@ bucket_definitions:
|
|
|
499
600
|
const states = await bucketStateCollection.find({ '_id.b': { $in: buckets } }).toArray();
|
|
500
601
|
const completed = states.filter((state) => state.compacted_state != null);
|
|
501
602
|
const remaining = states.filter((state) => state.compacted_state == null);
|
|
502
|
-
|
|
503
|
-
|
|
603
|
+
// Already-started buckets may finish after abort, but the queued remainder
|
|
604
|
+
// must not be claimed or rescheduled.
|
|
605
|
+
expect(completed.length).toBeGreaterThanOrEqual(1);
|
|
606
|
+
expect(completed.length).toBeLessThanOrEqual(2);
|
|
607
|
+
expect(remaining.length).toBeGreaterThanOrEqual(6);
|
|
504
608
|
for (const state of remaining) {
|
|
505
609
|
expect(state.next_compact_check).toEqual(new Date(0));
|
|
506
610
|
expect(state.compact_lease).toBeUndefined();
|
|
@@ -890,7 +994,7 @@ bucket_definitions:
|
|
|
890
994
|
{ storageVersion: 3 }
|
|
891
995
|
)
|
|
892
996
|
);
|
|
893
|
-
const bucketStorage = factory
|
|
997
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
894
998
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
895
999
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
896
1000
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -1067,7 +1171,7 @@ bucket_definitions:
|
|
|
1067
1171
|
{ storageVersion: 3 }
|
|
1068
1172
|
)
|
|
1069
1173
|
);
|
|
1070
|
-
const bucketStorage = factory
|
|
1174
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
1071
1175
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1072
1176
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
1073
1177
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -1460,7 +1564,7 @@ bucket_definitions:
|
|
|
1460
1564
|
{ storageVersion: 3 }
|
|
1461
1565
|
)
|
|
1462
1566
|
);
|
|
1463
|
-
const bucketStorage = factory
|
|
1567
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
1464
1568
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1465
1569
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
1466
1570
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -1637,8 +1741,8 @@ bucket_definitions:
|
|
|
1637
1741
|
describe('Streaming compactor', () => {
|
|
1638
1742
|
const BUCKET = 'global[]';
|
|
1639
1743
|
|
|
1640
|
-
async function setupV3() {
|
|
1641
|
-
await using factory = await
|
|
1744
|
+
async function setupV3(factoryGenerator = INITIALIZED_MONGO_STORAGE_FACTORY) {
|
|
1745
|
+
await using factory = await factoryGenerator.factory();
|
|
1642
1746
|
const syncRules = await factory.updateSyncRules(
|
|
1643
1747
|
updateSyncRulesFromYaml(
|
|
1644
1748
|
`
|
|
@@ -1649,7 +1753,7 @@ bucket_definitions:
|
|
|
1649
1753
|
{ storageVersion: 3 }
|
|
1650
1754
|
)
|
|
1651
1755
|
);
|
|
1652
|
-
const bucketStorage = factory
|
|
1756
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
1653
1757
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1654
1758
|
const definitionId = bucketStorage.storageIds.bucketDefinitionIds[0];
|
|
1655
1759
|
const collection = db.bucketData(bucketStorage.replicationStreamId, definitionId);
|
|
@@ -1746,6 +1850,29 @@ bucket_definitions:
|
|
|
1746
1850
|
expect(state?.bucket_stats).toEqual({ count: 4, bytes, chunks: documents.length });
|
|
1747
1851
|
}
|
|
1748
1852
|
|
|
1853
|
+
test('initial compaction merges eight chunks immediately without full compaction', async () => {
|
|
1854
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1855
|
+
const inputs = Array.from({ length: 8 }, (_, index) =>
|
|
1856
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, 'A', 'data', ctx, sourceTableId)])
|
|
1857
|
+
);
|
|
1858
|
+
await insertDocs(collection, inputs);
|
|
1859
|
+
await bucketStateCollection.insertOne({
|
|
1860
|
+
_id: { d: ctx.definitionId, b: BUCKET },
|
|
1861
|
+
last_op: 8n,
|
|
1862
|
+
next_compact_check: new Date(),
|
|
1863
|
+
first_uncompacted_write: new Date(0),
|
|
1864
|
+
bucket_stats: { count: 8, bytes: BigInt(inputs.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: 8n })).toEqual({ buckets: 1 });
|
|
1868
|
+
const documents = await collection.find({ '_id.b': BUCKET }).toArray();
|
|
1869
|
+
expect(documents).toHaveLength(1);
|
|
1870
|
+
expect(documents[0].ops!.map((op) => op.o)).toEqual([1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n]);
|
|
1871
|
+
const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
|
|
1872
|
+
expect(state?.compacted_state).toMatchObject({ op_id: 8n, count: 8, checksum: 252n });
|
|
1873
|
+
expect(state?.last_full_compact).toBeUndefined();
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1749
1876
|
test('initial compaction merges small chunks and refreshes bucket metadata', async () => {
|
|
1750
1877
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1751
1878
|
await insertDocs(collection, [
|
|
@@ -1754,7 +1881,7 @@ bucket_definitions:
|
|
|
1754
1881
|
]);
|
|
1755
1882
|
await insertBucketState(bucketStateCollection, ctx.definitionId, 4n);
|
|
1756
1883
|
|
|
1757
|
-
const result = await bucketStorage.compactInitialReplication({ maxOpId: 4n });
|
|
1884
|
+
const result = await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 4n });
|
|
1758
1885
|
|
|
1759
1886
|
expect(result).toEqual({ buckets: 1 });
|
|
1760
1887
|
const documents = await collection.find({ '_id.b': BUCKET }).sort({ '_id.o': 1 }).toArray();
|
|
@@ -1769,6 +1896,254 @@ bucket_definitions:
|
|
|
1769
1896
|
});
|
|
1770
1897
|
});
|
|
1771
1898
|
|
|
1899
|
+
test.each([
|
|
1900
|
+
{ payloadSize: 100, workers: undefined, abort: false },
|
|
1901
|
+
{ payloadSize: 450_000, workers: undefined, abort: false },
|
|
1902
|
+
{ payloadSize: 450_000, workers: 4, abort: false },
|
|
1903
|
+
{ payloadSize: 100, workers: 1, abort: false },
|
|
1904
|
+
{ payloadSize: 450_000, workers: 4, abort: true }
|
|
1905
|
+
])('chunk compaction shares configured workers across runs: %o', async ({ payloadSize, workers, abort }) => {
|
|
1906
|
+
const concurrency = workers ?? 4;
|
|
1907
|
+
const objectStorage = new MemoryObjectStorage();
|
|
1908
|
+
const generator = mongoTestStorageFactoryGenerator({
|
|
1909
|
+
url: env.MONGO_TEST_URL,
|
|
1910
|
+
isCI: env.CI,
|
|
1911
|
+
objectStorage,
|
|
1912
|
+
chunkCompactionConcurrency: workers,
|
|
1913
|
+
inlineThresholdBytes: 0
|
|
1914
|
+
});
|
|
1915
|
+
const { bucketStorage, syncRules, collection, bucketStateCollection, ctx, sourceTableId, db } =
|
|
1916
|
+
await setupV3(generator);
|
|
1917
|
+
const bucketCount = 12;
|
|
1918
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
1919
|
+
const bucket = `global[${i}]`;
|
|
1920
|
+
const documents = [1, 2].map((id) =>
|
|
1921
|
+
serializeBucketData(bucket, [makeOp(id, String(id), 'x'.repeat(payloadSize), ctx, sourceTableId)])
|
|
1922
|
+
);
|
|
1923
|
+
await collection.insertMany(documents);
|
|
1924
|
+
await bucketStateCollection.insertOne({
|
|
1925
|
+
_id: { d: ctx.definitionId, b: bucket },
|
|
1926
|
+
last_op: 2n,
|
|
1927
|
+
next_compact_check: new Date(0),
|
|
1928
|
+
first_uncompacted_write: new Date(0),
|
|
1929
|
+
bucket_stats: {
|
|
1930
|
+
count: 2,
|
|
1931
|
+
bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)),
|
|
1932
|
+
chunks: 2
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
const gate = Promise.withResolvers<void>();
|
|
1938
|
+
const originalPut = objectStorage.put.bind(objectStorage);
|
|
1939
|
+
let uploads = 0;
|
|
1940
|
+
const put = vi.spyOn(objectStorage, 'put').mockImplementation(async (...args) => {
|
|
1941
|
+
uploads++;
|
|
1942
|
+
await gate.promise;
|
|
1943
|
+
await originalPut(...args);
|
|
1944
|
+
});
|
|
1945
|
+
const controller = new AbortController();
|
|
1946
|
+
let settled = false;
|
|
1947
|
+
const runs = Promise.allSettled(
|
|
1948
|
+
Array.from({ length: 2 }, () =>
|
|
1949
|
+
bucketStorage.factory
|
|
1950
|
+
.getInstance(syncRules)
|
|
1951
|
+
.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 2n, signal: controller.signal })
|
|
1952
|
+
)
|
|
1953
|
+
).finally(() => {
|
|
1954
|
+
settled = true;
|
|
1955
|
+
});
|
|
1956
|
+
try {
|
|
1957
|
+
await vi.waitFor(() => expect(uploads).toBe(concurrency));
|
|
1958
|
+
// Let other ready workers reach admission; the held uploads must prevent
|
|
1959
|
+
// any additional payloads from reaching S3, across both compactor instances.
|
|
1960
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1961
|
+
expect(uploads).toBe(concurrency);
|
|
1962
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBeLessThanOrEqual(
|
|
1963
|
+
concurrency
|
|
1964
|
+
);
|
|
1965
|
+
expect(await collection.countDocuments()).toBe(bucketCount * 2);
|
|
1966
|
+
if (abort) {
|
|
1967
|
+
controller.abort();
|
|
1968
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1969
|
+
expect(settled).toBe(false);
|
|
1970
|
+
}
|
|
1971
|
+
} finally {
|
|
1972
|
+
gate.resolve();
|
|
1973
|
+
await runs;
|
|
1974
|
+
put.mockRestore();
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
const results = await runs;
|
|
1978
|
+
if (abort) {
|
|
1979
|
+
expect(results.every((result) => result.status === 'rejected')).toBe(true);
|
|
1980
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
|
|
1981
|
+
// Cancellation must return all worker slots.
|
|
1982
|
+
expect(await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 2n })).toEqual({
|
|
1983
|
+
buckets: bucketCount
|
|
1984
|
+
});
|
|
1985
|
+
} else {
|
|
1986
|
+
expect(results.every((result) => result.status === 'fulfilled')).toBe(true);
|
|
1987
|
+
expect(results.reduce((sum, result) => sum + (result.status === 'fulfilled' ? result.value.buckets : 0), 0)).toBe(
|
|
1988
|
+
bucketCount
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const documents = await collection.find().toArray();
|
|
1993
|
+
expect(documents).toHaveLength(bucketCount);
|
|
1994
|
+
expect(documents.every((doc) => doc.count === 2 && doc.checksum === 21n && doc.storage_ref != null)).toBe(true);
|
|
1995
|
+
expect(
|
|
1996
|
+
await bucketStateCollection.countDocuments({ 'compacted_state.op_id': 2n, compact_lease: { $exists: false } })
|
|
1997
|
+
).toBe(bucketCount);
|
|
1998
|
+
const usage = await db.objectStorageUsage.find({ '_id.g': ctx.replicationStreamId }).toArray();
|
|
1999
|
+
const activeBytes = usage.reduce((sum, doc) => sum + BigInt(doc.definitions[ctx.definitionId] ?? 0), 0n);
|
|
2000
|
+
expect(activeBytes).toBe(documents.reduce((sum, doc) => sum + BigInt(doc.storage_ref!.file_size), 0n));
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
test.each([2, 3])('failed chunk worker stops queued work and drains siblings (%s workers)', async (workerCount) => {
|
|
2004
|
+
const { bucketStorage } = await setupV3();
|
|
2005
|
+
const compactor = bucketStorage.createMongoCompactor({}) as any;
|
|
2006
|
+
const failure = new Error('worker failed');
|
|
2007
|
+
const failing = Promise.withResolvers<void>();
|
|
2008
|
+
const draining = Promise.withResolvers<void>();
|
|
2009
|
+
const started: number[] = [];
|
|
2010
|
+
let settled = false;
|
|
2011
|
+
// The factory has two slots. With three workers, one also waits for a slot
|
|
2012
|
+
// when its sibling fails; it must release that slot without starting work.
|
|
2013
|
+
const run = compactor
|
|
2014
|
+
.runChunkCompactionWorkers(
|
|
2015
|
+
[0, 1, 2, 3, 4],
|
|
2016
|
+
Array.from({ length: workerCount }, () => compactor.objectStorageUsage),
|
|
2017
|
+
async (bucket: number) => {
|
|
2018
|
+
started.push(bucket);
|
|
2019
|
+
if (bucket === 0) await failing.promise;
|
|
2020
|
+
if (bucket === 1) await draining.promise;
|
|
2021
|
+
}
|
|
2022
|
+
)
|
|
2023
|
+
.then(
|
|
2024
|
+
() => undefined,
|
|
2025
|
+
(error: unknown) => error
|
|
2026
|
+
)
|
|
2027
|
+
.finally(() => {
|
|
2028
|
+
settled = true;
|
|
2029
|
+
});
|
|
2030
|
+
try {
|
|
2031
|
+
await vi.waitFor(() => expect(started).toEqual([0, 1]));
|
|
2032
|
+
failing.reject(failure);
|
|
2033
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2034
|
+
expect(started).toEqual([0, 1]);
|
|
2035
|
+
expect(settled).toBe(false);
|
|
2036
|
+
} finally {
|
|
2037
|
+
failing.resolve();
|
|
2038
|
+
draining.resolve();
|
|
2039
|
+
await run;
|
|
2040
|
+
}
|
|
2041
|
+
expect(await run).toBe(failure);
|
|
2042
|
+
expect(started).toEqual([0, 1]);
|
|
2043
|
+
expect(bucketStorage.factory.chunkCompactionSlots.getValue()).toBe(2);
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
test('unforced chunk workers drain before full compaction and reclassify aged buckets on the next scan', async () => {
|
|
2047
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
2048
|
+
const buckets = ['chunk1[]', 'chunk2[]', 'promoted[]', 'full1[]', 'full2[]'];
|
|
2049
|
+
for (const bucket of buckets) {
|
|
2050
|
+
const documents = Array.from({ length: 8 }, (_, index) =>
|
|
2051
|
+
serializeBucketData(bucket, [makeOp(index + 1, String(index), 'data', { ...ctx, bucket }, sourceTableId)])
|
|
2052
|
+
);
|
|
2053
|
+
await collection.insertMany(documents);
|
|
2054
|
+
await bucketStateCollection.insertOne({
|
|
2055
|
+
_id: { d: ctx.definitionId, b: bucket },
|
|
2056
|
+
last_op: 8n,
|
|
2057
|
+
next_compact_check: new Date(0),
|
|
2058
|
+
first_uncompacted_write: bucket.startsWith('full') ? new Date(0) : new Date(),
|
|
2059
|
+
bucket_stats: { count: 8, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const originalClaim = CompactionLease.claim.bind(CompactionLease);
|
|
2064
|
+
const jobStartedAt = new Date();
|
|
2065
|
+
let serverTime = jobStartedAt;
|
|
2066
|
+
let promoted = false;
|
|
2067
|
+
const claim = vi.spyOn(CompactionLease, 'claim').mockImplementation(async (...args) => {
|
|
2068
|
+
if (!promoted && (args[1]._id as { b: string })?.b === 'promoted[]') {
|
|
2069
|
+
promoted = true;
|
|
2070
|
+
// Advance the server clock past the full-compaction interval without
|
|
2071
|
+
// changing bucket state. A fixed scan timestamp would retry forever.
|
|
2072
|
+
serverTime = new Date(jobStartedAt.getTime() + 3 * 60 * 60 * 1000);
|
|
2073
|
+
}
|
|
2074
|
+
const lease = await originalClaim(...args);
|
|
2075
|
+
if (lease != null) {
|
|
2076
|
+
Object.defineProperty(lease, 'startedAt', { value: serverTime });
|
|
2077
|
+
}
|
|
2078
|
+
return lease;
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
const compactor = bucketStorage.createMongoCompactor({ maxOpId: 8n });
|
|
2082
|
+
const internal = compactor as any;
|
|
2083
|
+
let timeReads = 0;
|
|
2084
|
+
const clock = vi.spyOn(internal, 'readCompactionTime').mockImplementation(async () => {
|
|
2085
|
+
if (++timeReads > 5) {
|
|
2086
|
+
throw new Error('Compaction did not advance after the bucket aged');
|
|
2087
|
+
}
|
|
2088
|
+
return serverTime;
|
|
2089
|
+
});
|
|
2090
|
+
const scan = vi.spyOn(internal, 'findScheduledBucketBatch');
|
|
2091
|
+
const originalChunks = internal.compactSingleBucketChunks.bind(compactor);
|
|
2092
|
+
const originalFull = internal.compactSingleBucketFully.bind(compactor);
|
|
2093
|
+
const gate = Promise.withResolvers<void>();
|
|
2094
|
+
let activeChunks = 0;
|
|
2095
|
+
let activeFull = 0;
|
|
2096
|
+
let maxActiveFull = 0;
|
|
2097
|
+
const fullBuckets: string[] = [];
|
|
2098
|
+
const chunks = vi.spyOn(internal, 'compactSingleBucketChunks').mockImplementation(async (...args) => {
|
|
2099
|
+
activeChunks++;
|
|
2100
|
+
try {
|
|
2101
|
+
await gate.promise;
|
|
2102
|
+
return await originalChunks(...args);
|
|
2103
|
+
} finally {
|
|
2104
|
+
activeChunks--;
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
const full = vi.spyOn(internal, 'compactSingleBucketFully').mockImplementation(async (...args) => {
|
|
2108
|
+
expect(activeChunks).toBe(0);
|
|
2109
|
+
activeFull++;
|
|
2110
|
+
maxActiveFull = Math.max(maxActiveFull, activeFull);
|
|
2111
|
+
fullBuckets.push((args[0] as any).state._id.b);
|
|
2112
|
+
try {
|
|
2113
|
+
return await originalFull(...args);
|
|
2114
|
+
} finally {
|
|
2115
|
+
activeFull--;
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
const run = compactor.compact();
|
|
2119
|
+
try {
|
|
2120
|
+
await vi.waitFor(() => expect(activeChunks).toBe(2));
|
|
2121
|
+
expect(full).not.toHaveBeenCalled();
|
|
2122
|
+
} finally {
|
|
2123
|
+
gate.resolve();
|
|
2124
|
+
try {
|
|
2125
|
+
await run;
|
|
2126
|
+
expect(timeReads).toBe(3); // Job start, first batch, and promoted bucket's batch.
|
|
2127
|
+
expect(scan).toHaveBeenCalledTimes(3); // Includes the final empty scan.
|
|
2128
|
+
for (const [cutoff] of scan.mock.calls) {
|
|
2129
|
+
expect(cutoff).toEqual(jobStartedAt);
|
|
2130
|
+
}
|
|
2131
|
+
} finally {
|
|
2132
|
+
clock.mockRestore();
|
|
2133
|
+
scan.mockRestore();
|
|
2134
|
+
claim.mockRestore();
|
|
2135
|
+
chunks.mockRestore();
|
|
2136
|
+
full.mockRestore();
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
expect(await run).toBe(5);
|
|
2140
|
+
expect(promoted).toBe(true);
|
|
2141
|
+
expect(maxActiveFull).toBe(1);
|
|
2142
|
+
expect(fullBuckets.sort()).toEqual(['full1[]', 'full2[]', 'promoted[]']);
|
|
2143
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
|
|
2144
|
+
expect(await collection.countDocuments()).toBe(5);
|
|
2145
|
+
});
|
|
2146
|
+
|
|
1772
2147
|
test('capped chunk compaction repairs stale bucket stats after a committed first merge', async () => {
|
|
1773
2148
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1774
2149
|
const operations = [
|
|
@@ -1796,7 +2171,7 @@ bucket_definitions:
|
|
|
1796
2171
|
}
|
|
1797
2172
|
});
|
|
1798
2173
|
|
|
1799
|
-
const result = await bucketStorage.compactInitialReplication({ maxOpId: 4n });
|
|
2174
|
+
const result = await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 4n });
|
|
1800
2175
|
|
|
1801
2176
|
expect(result).toEqual({ buckets: 1 });
|
|
1802
2177
|
const expectedStats = {
|
|
@@ -1915,9 +2290,9 @@ bucket_definitions:
|
|
|
1915
2290
|
bucket_stats: { count: 4, bytes: 200n, chunks: 4 }
|
|
1916
2291
|
});
|
|
1917
2292
|
|
|
1918
|
-
await expect(
|
|
1919
|
-
|
|
1920
|
-
);
|
|
2293
|
+
await expect(
|
|
2294
|
+
bucketStorage.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true, forceChunkCompaction: true }).compact()
|
|
2295
|
+
).resolves.toBe(1);
|
|
1921
2296
|
|
|
1922
2297
|
const currentDocuments = await collection.find({ '_id.b': BUCKET }).sort({ '_id.o': 1 }).toArray();
|
|
1923
2298
|
expect(currentDocuments.flatMap((document) => document.ops!.map((op) => op.o))).toEqual([1n, 3n, 4n]);
|
|
@@ -1937,7 +2312,8 @@ bucket_definitions:
|
|
|
1937
2312
|
const { bucketStorage, collection, bucketStateCollection, ctx } = await setupCompactedTail();
|
|
1938
2313
|
const failedCompactor = bucketStorage.createMongoCompactor({
|
|
1939
2314
|
maxOpId: 4n,
|
|
1940
|
-
compactChunksOnly: true
|
|
2315
|
+
compactChunksOnly: true,
|
|
2316
|
+
forceChunkCompaction: true
|
|
1941
2317
|
});
|
|
1942
2318
|
const originalFlush = (failedCompactor as any).flushCompactionGroup.bind(failedCompactor);
|
|
1943
2319
|
const flushSpy = vi
|
|
@@ -1958,7 +2334,9 @@ bucket_definitions:
|
|
|
1958
2334
|
{ $set: { next_compact_check: new Date(0) } }
|
|
1959
2335
|
);
|
|
1960
2336
|
|
|
1961
|
-
const result = await bucketStorage
|
|
2337
|
+
const result = await bucketStorage
|
|
2338
|
+
.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true, forceChunkCompaction: true })
|
|
2339
|
+
.compact();
|
|
1962
2340
|
|
|
1963
2341
|
expect(result).toBe(1);
|
|
1964
2342
|
await expectRecoveredCompactedTail(collection, bucketStateCollection, ctx.definitionId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
2
2
|
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
3
|
-
import { bucketRequest, test_utils } from '@powersync/service-core-tests';
|
|
3
|
+
import { bucketRequest, getTestStorage, test_utils } from '@powersync/service-core-tests';
|
|
4
4
|
import { describe, expect, test } from 'vitest';
|
|
5
5
|
import { DEFAULT_INLINE_THRESHOLD_BYTES } from '../../src/storage/implementation/common/PersistedBatch.js';
|
|
6
6
|
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
@@ -31,7 +31,7 @@ describe('Object storage inline threshold', () => {
|
|
|
31
31
|
const { memoryStorage, factoryGen } = s3Factory();
|
|
32
32
|
await using factory = await factoryGen.factory();
|
|
33
33
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
34
|
-
const bucketStorage = factory
|
|
34
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
35
35
|
expect(bucketStorage.inlineThresholdBytes).toBe(DEFAULT_INLINE_THRESHOLD_BYTES);
|
|
36
36
|
|
|
37
37
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
@@ -77,7 +77,7 @@ describe('Object storage inline threshold', () => {
|
|
|
77
77
|
const { memoryStorage, factoryGen } = s3Factory(256);
|
|
78
78
|
await using factory = await factoryGen.factory();
|
|
79
79
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
80
|
-
const bucketStorage = factory
|
|
80
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
81
81
|
expect(bucketStorage.inlineThresholdBytes).toBe(256);
|
|
82
82
|
|
|
83
83
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
storage,
|
|
5
5
|
updateSyncRulesFromYaml
|
|
6
6
|
} from '@powersync/service-core';
|
|
7
|
-
import { bucketRequest, compactActive, test_utils } from '@powersync/service-core-tests';
|
|
7
|
+
import { bucketRequest, compactActive, getTestStorage, test_utils } from '@powersync/service-core-tests';
|
|
8
8
|
import { describe, expect, test } from 'vitest';
|
|
9
9
|
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
10
10
|
import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
@@ -28,7 +28,7 @@ describe('V3 checksums with S3 object storage', () => {
|
|
|
28
28
|
const { factoryGen } = s3Factory();
|
|
29
29
|
await using factory = await factoryGen.factory();
|
|
30
30
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
31
|
-
const bucketStorage = factory
|
|
31
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
32
32
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
33
33
|
|
|
34
34
|
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
@@ -75,7 +75,7 @@ describe('V3 checksums with S3 object storage', () => {
|
|
|
75
75
|
const { factoryGen } = s3Factory();
|
|
76
76
|
await using factory = await factoryGen.factory();
|
|
77
77
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
78
|
-
const bucketStorage = factory
|
|
78
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
79
79
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
80
80
|
|
|
81
81
|
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
@@ -114,7 +114,7 @@ describe('V3 checksums with S3 object storage', () => {
|
|
|
114
114
|
const { factoryGen } = s3Factory();
|
|
115
115
|
await using factory = await factoryGen.factory();
|
|
116
116
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
117
|
-
const bucketStorage = factory
|
|
117
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
118
118
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
119
119
|
|
|
120
120
|
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|