@powersync/service-module-mongodb-storage 0.0.0-dev-20260909133214 → 0.0.0-dev-20260929095716
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 +52 -6
- package/dist/storage/MongoBucketStorage.d.ts +11 -2
- package/dist/storage/MongoBucketStorage.js +43 -13
- 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/MongoParsedSyncConfigSet.js +6 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +6 -6
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +5 -3
- 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 +4 -2
- package/dist/utils/test-utils.js +7 -4
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +6 -6
- package/src/storage/MongoBucketStorage.ts +54 -13
- 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/MongoParsedSyncConfigSet.ts +12 -1
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -31
- package/src/storage/implementation/MongoStorageProvider.ts +13 -3
- 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 +9 -5
- 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/connection_config.test.ts +159 -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 +15 -10
- package/test/src/storage_compacting.test.ts +397 -23
- 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 +55 -47
- 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
|
});
|
|
@@ -345,7 +356,7 @@ bucket_definitions:
|
|
|
345
356
|
{ storageVersion: 3 }
|
|
346
357
|
)
|
|
347
358
|
);
|
|
348
|
-
const bucketStorage = factory
|
|
359
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
349
360
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
350
361
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
351
362
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -385,6 +396,91 @@ bucket_definitions:
|
|
|
385
396
|
});
|
|
386
397
|
}
|
|
387
398
|
|
|
399
|
+
test.each([1, 7])(
|
|
400
|
+
'initial compaction bulk-reschedules buckets with %s chunks without reading data',
|
|
401
|
+
async (chunks) => {
|
|
402
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId, db } = await setupV3Storage();
|
|
403
|
+
const documents = Array.from({ length: chunks }, (_, index) =>
|
|
404
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, String(index), 'data', ctx, sourceTableId)])
|
|
405
|
+
);
|
|
406
|
+
await insertDocs(collection, documents);
|
|
407
|
+
await bucketStateCollection.insertOne({
|
|
408
|
+
_id: { d: ctx.definitionId, b: BUCKET },
|
|
409
|
+
last_op: BigInt(chunks),
|
|
410
|
+
next_compact_check: new Date(0),
|
|
411
|
+
// Even overdue full compaction must not force a small initial chunk merge.
|
|
412
|
+
first_uncompacted_write: new Date(0),
|
|
413
|
+
bucket_stats: { count: chunks, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks }
|
|
414
|
+
});
|
|
415
|
+
const claim = vi.spyOn(CompactionLease, 'claim');
|
|
416
|
+
const data = vi.spyOn(db, 'bucketData');
|
|
417
|
+
try {
|
|
418
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: BigInt(chunks) })).toEqual({ buckets: 0 });
|
|
419
|
+
expect(claim).not.toHaveBeenCalled();
|
|
420
|
+
expect(data).not.toHaveBeenCalled();
|
|
421
|
+
const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
|
|
422
|
+
expect(state?.compacted_state).toBeUndefined();
|
|
423
|
+
expect(state?.compact_lease).toBeUndefined();
|
|
424
|
+
expect(state?.last_full_compact).toBeUndefined();
|
|
425
|
+
expect(state!.next_compact_check!.getTime()).toBeGreaterThan(Date.now());
|
|
426
|
+
expect(await collection.find().toArray()).toEqual(documents);
|
|
427
|
+
} finally {
|
|
428
|
+
claim.mockRestore();
|
|
429
|
+
data.mockRestore();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
test('initial compaction rechecks the chunk threshold after claiming', async () => {
|
|
435
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId, db } = await setupV3Storage();
|
|
436
|
+
const documents = Array.from({ length: 8 }, (_, index) =>
|
|
437
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, String(index), 'data', ctx, sourceTableId)])
|
|
438
|
+
);
|
|
439
|
+
await insertDocs(collection, documents);
|
|
440
|
+
const bytes = BigInt(documents.reduce((sum, doc) => sum + doc.size, 0));
|
|
441
|
+
await bucketStateCollection.insertOne({
|
|
442
|
+
_id: { d: ctx.definitionId, b: BUCKET },
|
|
443
|
+
last_op: 8n,
|
|
444
|
+
next_compact_check: new Date(0),
|
|
445
|
+
first_uncompacted_write: new Date(),
|
|
446
|
+
bucket_stats: { count: 8, bytes, chunks: 8 }
|
|
447
|
+
});
|
|
448
|
+
const originalClaim = CompactionLease.claim.bind(CompactionLease);
|
|
449
|
+
const claim = vi.spyOn(CompactionLease, 'claim').mockImplementationOnce(async (...args) => {
|
|
450
|
+
// Another compactor finishes before we acquire the lease, leaving one
|
|
451
|
+
// new chunk beyond its cached prefix instead of the eight we selected.
|
|
452
|
+
await bucketStateCollection.updateOne(
|
|
453
|
+
{ _id: { d: ctx.definitionId, b: BUCKET } },
|
|
454
|
+
{
|
|
455
|
+
$set: {
|
|
456
|
+
compacted_state: {
|
|
457
|
+
op_id: 7n,
|
|
458
|
+
checksum: 196n,
|
|
459
|
+
count: 7,
|
|
460
|
+
bytes: bytes - BigInt(documents[7].size),
|
|
461
|
+
chunks: 7,
|
|
462
|
+
at: new Date()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
return originalClaim(...args);
|
|
468
|
+
});
|
|
469
|
+
const data = vi.spyOn(db, 'bucketData');
|
|
470
|
+
try {
|
|
471
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: 8n })).toEqual({ buckets: 0 });
|
|
472
|
+
expect(claim).toHaveBeenCalledTimes(1);
|
|
473
|
+
expect(data).not.toHaveBeenCalled();
|
|
474
|
+
const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
|
|
475
|
+
expect(state?.compacted_state?.op_id).toBe(7n);
|
|
476
|
+
expect(state?.compact_lease).toBeUndefined();
|
|
477
|
+
expect(state!.next_compact_check!.getTime()).toBeGreaterThan(Date.now());
|
|
478
|
+
} finally {
|
|
479
|
+
claim.mockRestore();
|
|
480
|
+
data.mockRestore();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
388
484
|
test('a successful lease renewal clears a transient renewal error', async () => {
|
|
389
485
|
const { bucketStateCollection, ctx } = await setupV3Storage();
|
|
390
486
|
await insertBucketState(bucketStateCollection, ctx.definitionId, 1n);
|
|
@@ -446,7 +542,7 @@ bucket_definitions:
|
|
|
446
542
|
);
|
|
447
543
|
|
|
448
544
|
await (bucketStorage as MongoSyncBucketStorage)
|
|
449
|
-
.createMongoCompactor({ maxOpId: 2n, compactChunksOnly: true })
|
|
545
|
+
.createMongoCompactor({ maxOpId: 2n, compactChunksOnly: true, forceChunkCompaction: true })
|
|
450
546
|
.compact();
|
|
451
547
|
|
|
452
548
|
const [badState, goodState] = await Promise.all([
|
|
@@ -460,7 +556,7 @@ bucket_definitions:
|
|
|
460
556
|
|
|
461
557
|
test('aborting scheduled compaction does not reschedule the remaining batch', async () => {
|
|
462
558
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3Storage();
|
|
463
|
-
const buckets =
|
|
559
|
+
const buckets = Array.from({ length: 8 }, (_, index) => `bucket${index}[]`);
|
|
464
560
|
const documents = buckets.map((bucket) =>
|
|
465
561
|
serializeBucketData(bucket, [makeOp(2, bucket, bucket, { ...ctx, bucket }, sourceTableId)])
|
|
466
562
|
);
|
|
@@ -490,6 +586,7 @@ bucket_definitions:
|
|
|
490
586
|
.createMongoCompactor({
|
|
491
587
|
maxOpId: 2n,
|
|
492
588
|
compactChunksOnly: true,
|
|
589
|
+
forceChunkCompaction: true,
|
|
493
590
|
signal: abortController.signal,
|
|
494
591
|
logger: testLogger
|
|
495
592
|
})
|
|
@@ -499,8 +596,11 @@ bucket_definitions:
|
|
|
499
596
|
const states = await bucketStateCollection.find({ '_id.b': { $in: buckets } }).toArray();
|
|
500
597
|
const completed = states.filter((state) => state.compacted_state != null);
|
|
501
598
|
const remaining = states.filter((state) => state.compacted_state == null);
|
|
502
|
-
|
|
503
|
-
|
|
599
|
+
// Already-started buckets may finish after abort, but the queued remainder
|
|
600
|
+
// must not be claimed or rescheduled.
|
|
601
|
+
expect(completed.length).toBeGreaterThanOrEqual(1);
|
|
602
|
+
expect(completed.length).toBeLessThanOrEqual(2);
|
|
603
|
+
expect(remaining.length).toBeGreaterThanOrEqual(6);
|
|
504
604
|
for (const state of remaining) {
|
|
505
605
|
expect(state.next_compact_check).toEqual(new Date(0));
|
|
506
606
|
expect(state.compact_lease).toBeUndefined();
|
|
@@ -890,7 +990,7 @@ bucket_definitions:
|
|
|
890
990
|
{ storageVersion: 3 }
|
|
891
991
|
)
|
|
892
992
|
);
|
|
893
|
-
const bucketStorage = factory
|
|
993
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
894
994
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
895
995
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
896
996
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -1067,7 +1167,7 @@ bucket_definitions:
|
|
|
1067
1167
|
{ storageVersion: 3 }
|
|
1068
1168
|
)
|
|
1069
1169
|
);
|
|
1070
|
-
const bucketStorage = factory
|
|
1170
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
1071
1171
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1072
1172
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
1073
1173
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -1460,7 +1560,7 @@ bucket_definitions:
|
|
|
1460
1560
|
{ storageVersion: 3 }
|
|
1461
1561
|
)
|
|
1462
1562
|
);
|
|
1463
|
-
const bucketStorage = factory
|
|
1563
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
1464
1564
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1465
1565
|
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
1466
1566
|
const definitionId = mapping.allBucketDefinitionIds()[0];
|
|
@@ -1637,8 +1737,8 @@ bucket_definitions:
|
|
|
1637
1737
|
describe('Streaming compactor', () => {
|
|
1638
1738
|
const BUCKET = 'global[]';
|
|
1639
1739
|
|
|
1640
|
-
async function setupV3() {
|
|
1641
|
-
await using factory = await
|
|
1740
|
+
async function setupV3(factoryGenerator = INITIALIZED_MONGO_STORAGE_FACTORY) {
|
|
1741
|
+
await using factory = await factoryGenerator.factory();
|
|
1642
1742
|
const syncRules = await factory.updateSyncRules(
|
|
1643
1743
|
updateSyncRulesFromYaml(
|
|
1644
1744
|
`
|
|
@@ -1649,7 +1749,7 @@ bucket_definitions:
|
|
|
1649
1749
|
{ storageVersion: 3 }
|
|
1650
1750
|
)
|
|
1651
1751
|
);
|
|
1652
|
-
const bucketStorage = factory
|
|
1752
|
+
const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
|
|
1653
1753
|
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1654
1754
|
const definitionId = bucketStorage.storageIds.bucketDefinitionIds[0];
|
|
1655
1755
|
const collection = db.bucketData(bucketStorage.replicationStreamId, definitionId);
|
|
@@ -1746,6 +1846,29 @@ bucket_definitions:
|
|
|
1746
1846
|
expect(state?.bucket_stats).toEqual({ count: 4, bytes, chunks: documents.length });
|
|
1747
1847
|
}
|
|
1748
1848
|
|
|
1849
|
+
test('initial compaction merges eight chunks immediately without full compaction', async () => {
|
|
1850
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1851
|
+
const inputs = Array.from({ length: 8 }, (_, index) =>
|
|
1852
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, 'A', 'data', ctx, sourceTableId)])
|
|
1853
|
+
);
|
|
1854
|
+
await insertDocs(collection, inputs);
|
|
1855
|
+
await bucketStateCollection.insertOne({
|
|
1856
|
+
_id: { d: ctx.definitionId, b: BUCKET },
|
|
1857
|
+
last_op: 8n,
|
|
1858
|
+
next_compact_check: new Date(),
|
|
1859
|
+
first_uncompacted_write: new Date(0),
|
|
1860
|
+
bucket_stats: { count: 8, bytes: BigInt(inputs.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: 8n })).toEqual({ buckets: 1 });
|
|
1864
|
+
const documents = await collection.find({ '_id.b': BUCKET }).toArray();
|
|
1865
|
+
expect(documents).toHaveLength(1);
|
|
1866
|
+
expect(documents[0].ops!.map((op) => op.o)).toEqual([1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n]);
|
|
1867
|
+
const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
|
|
1868
|
+
expect(state?.compacted_state).toMatchObject({ op_id: 8n, count: 8, checksum: 252n });
|
|
1869
|
+
expect(state?.last_full_compact).toBeUndefined();
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1749
1872
|
test('initial compaction merges small chunks and refreshes bucket metadata', async () => {
|
|
1750
1873
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1751
1874
|
await insertDocs(collection, [
|
|
@@ -1754,7 +1877,7 @@ bucket_definitions:
|
|
|
1754
1877
|
]);
|
|
1755
1878
|
await insertBucketState(bucketStateCollection, ctx.definitionId, 4n);
|
|
1756
1879
|
|
|
1757
|
-
const result = await bucketStorage.compactInitialReplication({ maxOpId: 4n });
|
|
1880
|
+
const result = await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 4n });
|
|
1758
1881
|
|
|
1759
1882
|
expect(result).toEqual({ buckets: 1 });
|
|
1760
1883
|
const documents = await collection.find({ '_id.b': BUCKET }).sort({ '_id.o': 1 }).toArray();
|
|
@@ -1769,6 +1892,254 @@ bucket_definitions:
|
|
|
1769
1892
|
});
|
|
1770
1893
|
});
|
|
1771
1894
|
|
|
1895
|
+
test.each([
|
|
1896
|
+
{ payloadSize: 100, workers: undefined, abort: false },
|
|
1897
|
+
{ payloadSize: 450_000, workers: undefined, abort: false },
|
|
1898
|
+
{ payloadSize: 450_000, workers: 4, abort: false },
|
|
1899
|
+
{ payloadSize: 100, workers: 1, abort: false },
|
|
1900
|
+
{ payloadSize: 450_000, workers: 4, abort: true }
|
|
1901
|
+
])('chunk compaction shares configured workers across runs: %o', async ({ payloadSize, workers, abort }) => {
|
|
1902
|
+
const concurrency = workers ?? 4;
|
|
1903
|
+
const objectStorage = new MemoryObjectStorage();
|
|
1904
|
+
const generator = mongoTestStorageFactoryGenerator({
|
|
1905
|
+
url: env.MONGO_TEST_URL,
|
|
1906
|
+
isCI: env.CI,
|
|
1907
|
+
objectStorage,
|
|
1908
|
+
chunkCompactionConcurrency: workers,
|
|
1909
|
+
inlineThresholdBytes: 0
|
|
1910
|
+
});
|
|
1911
|
+
const { bucketStorage, syncRules, collection, bucketStateCollection, ctx, sourceTableId, db } =
|
|
1912
|
+
await setupV3(generator);
|
|
1913
|
+
const bucketCount = 12;
|
|
1914
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
1915
|
+
const bucket = `global[${i}]`;
|
|
1916
|
+
const documents = [1, 2].map((id) =>
|
|
1917
|
+
serializeBucketData(bucket, [makeOp(id, String(id), 'x'.repeat(payloadSize), ctx, sourceTableId)])
|
|
1918
|
+
);
|
|
1919
|
+
await collection.insertMany(documents);
|
|
1920
|
+
await bucketStateCollection.insertOne({
|
|
1921
|
+
_id: { d: ctx.definitionId, b: bucket },
|
|
1922
|
+
last_op: 2n,
|
|
1923
|
+
next_compact_check: new Date(0),
|
|
1924
|
+
first_uncompacted_write: new Date(0),
|
|
1925
|
+
bucket_stats: {
|
|
1926
|
+
count: 2,
|
|
1927
|
+
bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)),
|
|
1928
|
+
chunks: 2
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const gate = Promise.withResolvers<void>();
|
|
1934
|
+
const originalPut = objectStorage.put.bind(objectStorage);
|
|
1935
|
+
let uploads = 0;
|
|
1936
|
+
const put = vi.spyOn(objectStorage, 'put').mockImplementation(async (...args) => {
|
|
1937
|
+
uploads++;
|
|
1938
|
+
await gate.promise;
|
|
1939
|
+
await originalPut(...args);
|
|
1940
|
+
});
|
|
1941
|
+
const controller = new AbortController();
|
|
1942
|
+
let settled = false;
|
|
1943
|
+
const runs = Promise.allSettled(
|
|
1944
|
+
Array.from({ length: 2 }, () =>
|
|
1945
|
+
bucketStorage.factory
|
|
1946
|
+
.getInstance(syncRules)
|
|
1947
|
+
.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 2n, signal: controller.signal })
|
|
1948
|
+
)
|
|
1949
|
+
).finally(() => {
|
|
1950
|
+
settled = true;
|
|
1951
|
+
});
|
|
1952
|
+
try {
|
|
1953
|
+
await vi.waitFor(() => expect(uploads).toBe(concurrency));
|
|
1954
|
+
// Let other ready workers reach admission; the held uploads must prevent
|
|
1955
|
+
// any additional payloads from reaching S3, across both compactor instances.
|
|
1956
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1957
|
+
expect(uploads).toBe(concurrency);
|
|
1958
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBeLessThanOrEqual(
|
|
1959
|
+
concurrency
|
|
1960
|
+
);
|
|
1961
|
+
expect(await collection.countDocuments()).toBe(bucketCount * 2);
|
|
1962
|
+
if (abort) {
|
|
1963
|
+
controller.abort();
|
|
1964
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1965
|
+
expect(settled).toBe(false);
|
|
1966
|
+
}
|
|
1967
|
+
} finally {
|
|
1968
|
+
gate.resolve();
|
|
1969
|
+
await runs;
|
|
1970
|
+
put.mockRestore();
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const results = await runs;
|
|
1974
|
+
if (abort) {
|
|
1975
|
+
expect(results.every((result) => result.status === 'rejected')).toBe(true);
|
|
1976
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
|
|
1977
|
+
// Cancellation must return all worker slots.
|
|
1978
|
+
expect(await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 2n })).toEqual({
|
|
1979
|
+
buckets: bucketCount
|
|
1980
|
+
});
|
|
1981
|
+
} else {
|
|
1982
|
+
expect(results.every((result) => result.status === 'fulfilled')).toBe(true);
|
|
1983
|
+
expect(results.reduce((sum, result) => sum + (result.status === 'fulfilled' ? result.value.buckets : 0), 0)).toBe(
|
|
1984
|
+
bucketCount
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const documents = await collection.find().toArray();
|
|
1989
|
+
expect(documents).toHaveLength(bucketCount);
|
|
1990
|
+
expect(documents.every((doc) => doc.count === 2 && doc.checksum === 21n && doc.storage_ref != null)).toBe(true);
|
|
1991
|
+
expect(
|
|
1992
|
+
await bucketStateCollection.countDocuments({ 'compacted_state.op_id': 2n, compact_lease: { $exists: false } })
|
|
1993
|
+
).toBe(bucketCount);
|
|
1994
|
+
const usage = await db.objectStorageUsage.find({ '_id.g': ctx.replicationStreamId }).toArray();
|
|
1995
|
+
const activeBytes = usage.reduce((sum, doc) => sum + BigInt(doc.definitions[ctx.definitionId] ?? 0), 0n);
|
|
1996
|
+
expect(activeBytes).toBe(documents.reduce((sum, doc) => sum + BigInt(doc.storage_ref!.file_size), 0n));
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
test.each([2, 3])('failed chunk worker stops queued work and drains siblings (%s workers)', async (workerCount) => {
|
|
2000
|
+
const { bucketStorage } = await setupV3();
|
|
2001
|
+
const compactor = bucketStorage.createMongoCompactor({}) as any;
|
|
2002
|
+
const failure = new Error('worker failed');
|
|
2003
|
+
const failing = Promise.withResolvers<void>();
|
|
2004
|
+
const draining = Promise.withResolvers<void>();
|
|
2005
|
+
const started: number[] = [];
|
|
2006
|
+
let settled = false;
|
|
2007
|
+
// The factory has two slots. With three workers, one also waits for a slot
|
|
2008
|
+
// when its sibling fails; it must release that slot without starting work.
|
|
2009
|
+
const run = compactor
|
|
2010
|
+
.runChunkCompactionWorkers(
|
|
2011
|
+
[0, 1, 2, 3, 4],
|
|
2012
|
+
Array.from({ length: workerCount }, () => compactor.objectStorageUsage),
|
|
2013
|
+
async (bucket: number) => {
|
|
2014
|
+
started.push(bucket);
|
|
2015
|
+
if (bucket === 0) await failing.promise;
|
|
2016
|
+
if (bucket === 1) await draining.promise;
|
|
2017
|
+
}
|
|
2018
|
+
)
|
|
2019
|
+
.then(
|
|
2020
|
+
() => undefined,
|
|
2021
|
+
(error: unknown) => error
|
|
2022
|
+
)
|
|
2023
|
+
.finally(() => {
|
|
2024
|
+
settled = true;
|
|
2025
|
+
});
|
|
2026
|
+
try {
|
|
2027
|
+
await vi.waitFor(() => expect(started).toEqual([0, 1]));
|
|
2028
|
+
failing.reject(failure);
|
|
2029
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2030
|
+
expect(started).toEqual([0, 1]);
|
|
2031
|
+
expect(settled).toBe(false);
|
|
2032
|
+
} finally {
|
|
2033
|
+
failing.resolve();
|
|
2034
|
+
draining.resolve();
|
|
2035
|
+
await run;
|
|
2036
|
+
}
|
|
2037
|
+
expect(await run).toBe(failure);
|
|
2038
|
+
expect(started).toEqual([0, 1]);
|
|
2039
|
+
expect(bucketStorage.factory.chunkCompactionSlots.getValue()).toBe(2);
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
test('unforced chunk workers drain before full compaction and reclassify aged buckets on the next scan', async () => {
|
|
2043
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
2044
|
+
const buckets = ['chunk1[]', 'chunk2[]', 'promoted[]', 'full1[]', 'full2[]'];
|
|
2045
|
+
for (const bucket of buckets) {
|
|
2046
|
+
const documents = Array.from({ length: 8 }, (_, index) =>
|
|
2047
|
+
serializeBucketData(bucket, [makeOp(index + 1, String(index), 'data', { ...ctx, bucket }, sourceTableId)])
|
|
2048
|
+
);
|
|
2049
|
+
await collection.insertMany(documents);
|
|
2050
|
+
await bucketStateCollection.insertOne({
|
|
2051
|
+
_id: { d: ctx.definitionId, b: bucket },
|
|
2052
|
+
last_op: 8n,
|
|
2053
|
+
next_compact_check: new Date(0),
|
|
2054
|
+
first_uncompacted_write: bucket.startsWith('full') ? new Date(0) : new Date(),
|
|
2055
|
+
bucket_stats: { count: 8, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
const originalClaim = CompactionLease.claim.bind(CompactionLease);
|
|
2060
|
+
const jobStartedAt = new Date();
|
|
2061
|
+
let serverTime = jobStartedAt;
|
|
2062
|
+
let promoted = false;
|
|
2063
|
+
const claim = vi.spyOn(CompactionLease, 'claim').mockImplementation(async (...args) => {
|
|
2064
|
+
if (!promoted && (args[1]._id as { b: string })?.b === 'promoted[]') {
|
|
2065
|
+
promoted = true;
|
|
2066
|
+
// Advance the server clock past the full-compaction interval without
|
|
2067
|
+
// changing bucket state. A fixed scan timestamp would retry forever.
|
|
2068
|
+
serverTime = new Date(jobStartedAt.getTime() + 3 * 60 * 60 * 1000);
|
|
2069
|
+
}
|
|
2070
|
+
const lease = await originalClaim(...args);
|
|
2071
|
+
if (lease != null) {
|
|
2072
|
+
Object.defineProperty(lease, 'startedAt', { value: serverTime });
|
|
2073
|
+
}
|
|
2074
|
+
return lease;
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
const compactor = bucketStorage.createMongoCompactor({ maxOpId: 8n });
|
|
2078
|
+
const internal = compactor as any;
|
|
2079
|
+
let timeReads = 0;
|
|
2080
|
+
const clock = vi.spyOn(internal, 'readCompactionTime').mockImplementation(async () => {
|
|
2081
|
+
if (++timeReads > 5) {
|
|
2082
|
+
throw new Error('Compaction did not advance after the bucket aged');
|
|
2083
|
+
}
|
|
2084
|
+
return serverTime;
|
|
2085
|
+
});
|
|
2086
|
+
const scan = vi.spyOn(internal, 'findScheduledBucketBatch');
|
|
2087
|
+
const originalChunks = internal.compactSingleBucketChunks.bind(compactor);
|
|
2088
|
+
const originalFull = internal.compactSingleBucketFully.bind(compactor);
|
|
2089
|
+
const gate = Promise.withResolvers<void>();
|
|
2090
|
+
let activeChunks = 0;
|
|
2091
|
+
let activeFull = 0;
|
|
2092
|
+
let maxActiveFull = 0;
|
|
2093
|
+
const fullBuckets: string[] = [];
|
|
2094
|
+
const chunks = vi.spyOn(internal, 'compactSingleBucketChunks').mockImplementation(async (...args) => {
|
|
2095
|
+
activeChunks++;
|
|
2096
|
+
try {
|
|
2097
|
+
await gate.promise;
|
|
2098
|
+
return await originalChunks(...args);
|
|
2099
|
+
} finally {
|
|
2100
|
+
activeChunks--;
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
const full = vi.spyOn(internal, 'compactSingleBucketFully').mockImplementation(async (...args) => {
|
|
2104
|
+
expect(activeChunks).toBe(0);
|
|
2105
|
+
activeFull++;
|
|
2106
|
+
maxActiveFull = Math.max(maxActiveFull, activeFull);
|
|
2107
|
+
fullBuckets.push((args[0] as any).state._id.b);
|
|
2108
|
+
try {
|
|
2109
|
+
return await originalFull(...args);
|
|
2110
|
+
} finally {
|
|
2111
|
+
activeFull--;
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
const run = compactor.compact();
|
|
2115
|
+
try {
|
|
2116
|
+
await vi.waitFor(() => expect(activeChunks).toBe(2));
|
|
2117
|
+
expect(full).not.toHaveBeenCalled();
|
|
2118
|
+
} finally {
|
|
2119
|
+
gate.resolve();
|
|
2120
|
+
try {
|
|
2121
|
+
await run;
|
|
2122
|
+
expect(timeReads).toBe(3); // Job start, first batch, and promoted bucket's batch.
|
|
2123
|
+
expect(scan).toHaveBeenCalledTimes(3); // Includes the final empty scan.
|
|
2124
|
+
for (const [cutoff] of scan.mock.calls) {
|
|
2125
|
+
expect(cutoff).toEqual(jobStartedAt);
|
|
2126
|
+
}
|
|
2127
|
+
} finally {
|
|
2128
|
+
clock.mockRestore();
|
|
2129
|
+
scan.mockRestore();
|
|
2130
|
+
claim.mockRestore();
|
|
2131
|
+
chunks.mockRestore();
|
|
2132
|
+
full.mockRestore();
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
expect(await run).toBe(5);
|
|
2136
|
+
expect(promoted).toBe(true);
|
|
2137
|
+
expect(maxActiveFull).toBe(1);
|
|
2138
|
+
expect(fullBuckets.sort()).toEqual(['full1[]', 'full2[]', 'promoted[]']);
|
|
2139
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
|
|
2140
|
+
expect(await collection.countDocuments()).toBe(5);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
1772
2143
|
test('capped chunk compaction repairs stale bucket stats after a committed first merge', async () => {
|
|
1773
2144
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1774
2145
|
const operations = [
|
|
@@ -1796,7 +2167,7 @@ bucket_definitions:
|
|
|
1796
2167
|
}
|
|
1797
2168
|
});
|
|
1798
2169
|
|
|
1799
|
-
const result = await bucketStorage.compactInitialReplication({ maxOpId: 4n });
|
|
2170
|
+
const result = await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 4n });
|
|
1800
2171
|
|
|
1801
2172
|
expect(result).toEqual({ buckets: 1 });
|
|
1802
2173
|
const expectedStats = {
|
|
@@ -1915,9 +2286,9 @@ bucket_definitions:
|
|
|
1915
2286
|
bucket_stats: { count: 4, bytes: 200n, chunks: 4 }
|
|
1916
2287
|
});
|
|
1917
2288
|
|
|
1918
|
-
await expect(
|
|
1919
|
-
|
|
1920
|
-
);
|
|
2289
|
+
await expect(
|
|
2290
|
+
bucketStorage.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true, forceChunkCompaction: true }).compact()
|
|
2291
|
+
).resolves.toBe(1);
|
|
1921
2292
|
|
|
1922
2293
|
const currentDocuments = await collection.find({ '_id.b': BUCKET }).sort({ '_id.o': 1 }).toArray();
|
|
1923
2294
|
expect(currentDocuments.flatMap((document) => document.ops!.map((op) => op.o))).toEqual([1n, 3n, 4n]);
|
|
@@ -1937,7 +2308,8 @@ bucket_definitions:
|
|
|
1937
2308
|
const { bucketStorage, collection, bucketStateCollection, ctx } = await setupCompactedTail();
|
|
1938
2309
|
const failedCompactor = bucketStorage.createMongoCompactor({
|
|
1939
2310
|
maxOpId: 4n,
|
|
1940
|
-
compactChunksOnly: true
|
|
2311
|
+
compactChunksOnly: true,
|
|
2312
|
+
forceChunkCompaction: true
|
|
1941
2313
|
});
|
|
1942
2314
|
const originalFlush = (failedCompactor as any).flushCompactionGroup.bind(failedCompactor);
|
|
1943
2315
|
const flushSpy = vi
|
|
@@ -1958,7 +2330,9 @@ bucket_definitions:
|
|
|
1958
2330
|
{ $set: { next_compact_check: new Date(0) } }
|
|
1959
2331
|
);
|
|
1960
2332
|
|
|
1961
|
-
const result = await bucketStorage
|
|
2333
|
+
const result = await bucketStorage
|
|
2334
|
+
.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true, forceChunkCompaction: true })
|
|
2335
|
+
.compact();
|
|
1962
2336
|
|
|
1963
2337
|
expect(result).toBe(1);
|
|
1964
2338
|
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);
|