@powersync/service-module-mongodb-storage 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -0
- package/dist/storage/MongoBucketStorage.js +7 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -1
- package/dist/storage/implementation/MongoBucketBatch.js +9 -8
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +4 -2
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
- package/dist/storage/implementation/common/PersistedBatch.js +6 -4
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +5 -0
- package/dist/storage/implementation/db.js +13 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +13 -6
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +2 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +17 -14
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +6 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.js +164 -69
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/types/types.d.ts +5 -0
- package/dist/types/types.js +12 -0
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +1 -0
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +6 -6
- package/src/storage/MongoBucketStorage.ts +12 -1
- package/src/storage/implementation/MongoBucketBatch.ts +13 -10
- package/src/storage/implementation/MongoStorageProvider.ts +12 -2
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
- package/src/storage/implementation/common/PersistedBatch.ts +12 -8
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +14 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +17 -8
- package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +51 -49
- package/src/storage/implementation/v3/MongoCompactorV3.ts +276 -97
- package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
- package/src/types/types.ts +18 -0
- package/src/utils/test-utils.ts +1 -0
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/object_storage_usage.test.ts +16 -4
- package/test/src/storage_compacting.test.ts +257 -5
- package/test/src/storage_s3_reading.test.ts +47 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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 {
|
|
@@ -19,6 +20,8 @@ import {
|
|
|
19
20
|
import { bucketRequest, compactActive, 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(
|
|
@@ -460,7 +463,7 @@ bucket_definitions:
|
|
|
460
463
|
|
|
461
464
|
test('aborting scheduled compaction does not reschedule the remaining batch', async () => {
|
|
462
465
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3Storage();
|
|
463
|
-
const buckets =
|
|
466
|
+
const buckets = Array.from({ length: 8 }, (_, index) => `bucket${index}[]`);
|
|
464
467
|
const documents = buckets.map((bucket) =>
|
|
465
468
|
serializeBucketData(bucket, [makeOp(2, bucket, bucket, { ...ctx, bucket }, sourceTableId)])
|
|
466
469
|
);
|
|
@@ -499,8 +502,11 @@ bucket_definitions:
|
|
|
499
502
|
const states = await bucketStateCollection.find({ '_id.b': { $in: buckets } }).toArray();
|
|
500
503
|
const completed = states.filter((state) => state.compacted_state != null);
|
|
501
504
|
const remaining = states.filter((state) => state.compacted_state == null);
|
|
502
|
-
|
|
503
|
-
|
|
505
|
+
// Already-started buckets may finish after abort, but the queued remainder
|
|
506
|
+
// must not be claimed or rescheduled.
|
|
507
|
+
expect(completed.length).toBeGreaterThanOrEqual(1);
|
|
508
|
+
expect(completed.length).toBeLessThanOrEqual(2);
|
|
509
|
+
expect(remaining.length).toBeGreaterThanOrEqual(6);
|
|
504
510
|
for (const state of remaining) {
|
|
505
511
|
expect(state.next_compact_check).toEqual(new Date(0));
|
|
506
512
|
expect(state.compact_lease).toBeUndefined();
|
|
@@ -1637,8 +1643,8 @@ bucket_definitions:
|
|
|
1637
1643
|
describe('Streaming compactor', () => {
|
|
1638
1644
|
const BUCKET = 'global[]';
|
|
1639
1645
|
|
|
1640
|
-
async function setupV3() {
|
|
1641
|
-
await using factory = await
|
|
1646
|
+
async function setupV3(factoryGenerator = INITIALIZED_MONGO_STORAGE_FACTORY) {
|
|
1647
|
+
await using factory = await factoryGenerator.factory();
|
|
1642
1648
|
const syncRules = await factory.updateSyncRules(
|
|
1643
1649
|
updateSyncRulesFromYaml(
|
|
1644
1650
|
`
|
|
@@ -1769,6 +1775,252 @@ bucket_definitions:
|
|
|
1769
1775
|
});
|
|
1770
1776
|
});
|
|
1771
1777
|
|
|
1778
|
+
test.each([
|
|
1779
|
+
{ payloadSize: 100, workers: undefined, abort: false },
|
|
1780
|
+
{ payloadSize: 450_000, workers: undefined, abort: false },
|
|
1781
|
+
{ payloadSize: 450_000, workers: 4, abort: false },
|
|
1782
|
+
{ payloadSize: 100, workers: 1, abort: false },
|
|
1783
|
+
{ payloadSize: 450_000, workers: 4, abort: true }
|
|
1784
|
+
])('chunk compaction shares configured workers across runs: %o', async ({ payloadSize, workers, abort }) => {
|
|
1785
|
+
const concurrency = workers ?? 4;
|
|
1786
|
+
const objectStorage = new MemoryObjectStorage();
|
|
1787
|
+
const generator = mongoTestStorageFactoryGenerator({
|
|
1788
|
+
url: env.MONGO_TEST_URL,
|
|
1789
|
+
isCI: env.CI,
|
|
1790
|
+
objectStorage,
|
|
1791
|
+
chunkCompactionConcurrency: workers,
|
|
1792
|
+
inlineThresholdBytes: 0
|
|
1793
|
+
});
|
|
1794
|
+
const { bucketStorage, syncRules, collection, bucketStateCollection, ctx, sourceTableId, db } =
|
|
1795
|
+
await setupV3(generator);
|
|
1796
|
+
const bucketCount = 12;
|
|
1797
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
1798
|
+
const bucket = `global[${i}]`;
|
|
1799
|
+
const documents = [1, 2].map((id) =>
|
|
1800
|
+
serializeBucketData(bucket, [makeOp(id, String(id), 'x'.repeat(payloadSize), ctx, sourceTableId)])
|
|
1801
|
+
);
|
|
1802
|
+
await collection.insertMany(documents);
|
|
1803
|
+
await bucketStateCollection.insertOne({
|
|
1804
|
+
_id: { d: ctx.definitionId, b: bucket },
|
|
1805
|
+
last_op: 2n,
|
|
1806
|
+
next_compact_check: new Date(0),
|
|
1807
|
+
first_uncompacted_write: new Date(0),
|
|
1808
|
+
bucket_stats: {
|
|
1809
|
+
count: 2,
|
|
1810
|
+
bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)),
|
|
1811
|
+
chunks: 2
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
const gate = Promise.withResolvers<void>();
|
|
1817
|
+
const originalPut = objectStorage.put.bind(objectStorage);
|
|
1818
|
+
let uploads = 0;
|
|
1819
|
+
const put = vi.spyOn(objectStorage, 'put').mockImplementation(async (...args) => {
|
|
1820
|
+
uploads++;
|
|
1821
|
+
await gate.promise;
|
|
1822
|
+
await originalPut(...args);
|
|
1823
|
+
});
|
|
1824
|
+
const controller = new AbortController();
|
|
1825
|
+
let settled = false;
|
|
1826
|
+
const runs = Promise.allSettled(
|
|
1827
|
+
Array.from({ length: 2 }, () =>
|
|
1828
|
+
bucketStorage.factory
|
|
1829
|
+
.getInstance(syncRules)
|
|
1830
|
+
.compactInitialReplication({ maxOpId: 2n, signal: controller.signal })
|
|
1831
|
+
)
|
|
1832
|
+
).finally(() => {
|
|
1833
|
+
settled = true;
|
|
1834
|
+
});
|
|
1835
|
+
try {
|
|
1836
|
+
await vi.waitFor(() => expect(uploads).toBe(concurrency));
|
|
1837
|
+
// Let other ready workers reach admission; the held uploads must prevent
|
|
1838
|
+
// any additional payloads from reaching S3, across both compactor instances.
|
|
1839
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1840
|
+
expect(uploads).toBe(concurrency);
|
|
1841
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBeLessThanOrEqual(
|
|
1842
|
+
concurrency
|
|
1843
|
+
);
|
|
1844
|
+
expect(await collection.countDocuments()).toBe(bucketCount * 2);
|
|
1845
|
+
if (abort) {
|
|
1846
|
+
controller.abort();
|
|
1847
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1848
|
+
expect(settled).toBe(false);
|
|
1849
|
+
}
|
|
1850
|
+
} finally {
|
|
1851
|
+
gate.resolve();
|
|
1852
|
+
await runs;
|
|
1853
|
+
put.mockRestore();
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const results = await runs;
|
|
1857
|
+
if (abort) {
|
|
1858
|
+
expect(results.every((result) => result.status === 'rejected')).toBe(true);
|
|
1859
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
|
|
1860
|
+
// Cancellation must return all worker slots.
|
|
1861
|
+
expect(await bucketStorage.compactInitialReplication({ maxOpId: 2n })).toEqual({ buckets: bucketCount });
|
|
1862
|
+
} else {
|
|
1863
|
+
expect(results.every((result) => result.status === 'fulfilled')).toBe(true);
|
|
1864
|
+
expect(results.reduce((sum, result) => sum + (result.status === 'fulfilled' ? result.value.buckets : 0), 0)).toBe(
|
|
1865
|
+
bucketCount
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const documents = await collection.find().toArray();
|
|
1870
|
+
expect(documents).toHaveLength(bucketCount);
|
|
1871
|
+
expect(documents.every((doc) => doc.count === 2 && doc.checksum === 21n && doc.storage_ref != null)).toBe(true);
|
|
1872
|
+
expect(
|
|
1873
|
+
await bucketStateCollection.countDocuments({ 'compacted_state.op_id': 2n, compact_lease: { $exists: false } })
|
|
1874
|
+
).toBe(bucketCount);
|
|
1875
|
+
const usage = await db.objectStorageUsage.find({ '_id.g': ctx.replicationStreamId }).toArray();
|
|
1876
|
+
const activeBytes = usage.reduce((sum, doc) => sum + BigInt(doc.definitions[ctx.definitionId] ?? 0), 0n);
|
|
1877
|
+
expect(activeBytes).toBe(documents.reduce((sum, doc) => sum + BigInt(doc.storage_ref!.file_size), 0n));
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
test.each([2, 3])('failed chunk worker stops queued work and drains siblings (%s workers)', async (workerCount) => {
|
|
1881
|
+
const { bucketStorage } = await setupV3();
|
|
1882
|
+
const compactor = bucketStorage.createMongoCompactor({}) as any;
|
|
1883
|
+
const failure = new Error('worker failed');
|
|
1884
|
+
const failing = Promise.withResolvers<void>();
|
|
1885
|
+
const draining = Promise.withResolvers<void>();
|
|
1886
|
+
const started: number[] = [];
|
|
1887
|
+
let settled = false;
|
|
1888
|
+
// The factory has two slots. With three workers, one also waits for a slot
|
|
1889
|
+
// when its sibling fails; it must release that slot without starting work.
|
|
1890
|
+
const run = compactor
|
|
1891
|
+
.runChunkCompactionWorkers(
|
|
1892
|
+
[0, 1, 2, 3, 4],
|
|
1893
|
+
Array.from({ length: workerCount }, () => compactor.objectStorageUsage),
|
|
1894
|
+
async (bucket: number) => {
|
|
1895
|
+
started.push(bucket);
|
|
1896
|
+
if (bucket === 0) await failing.promise;
|
|
1897
|
+
if (bucket === 1) await draining.promise;
|
|
1898
|
+
}
|
|
1899
|
+
)
|
|
1900
|
+
.then(
|
|
1901
|
+
() => undefined,
|
|
1902
|
+
(error: unknown) => error
|
|
1903
|
+
)
|
|
1904
|
+
.finally(() => {
|
|
1905
|
+
settled = true;
|
|
1906
|
+
});
|
|
1907
|
+
try {
|
|
1908
|
+
await vi.waitFor(() => expect(started).toEqual([0, 1]));
|
|
1909
|
+
failing.reject(failure);
|
|
1910
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1911
|
+
expect(started).toEqual([0, 1]);
|
|
1912
|
+
expect(settled).toBe(false);
|
|
1913
|
+
} finally {
|
|
1914
|
+
failing.resolve();
|
|
1915
|
+
draining.resolve();
|
|
1916
|
+
await run;
|
|
1917
|
+
}
|
|
1918
|
+
expect(await run).toBe(failure);
|
|
1919
|
+
expect(started).toEqual([0, 1]);
|
|
1920
|
+
expect(bucketStorage.factory.chunkCompactionSlots.getValue()).toBe(2);
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
test('unforced chunk workers drain before full compaction and reclassify aged buckets on the next scan', async () => {
|
|
1924
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1925
|
+
const buckets = ['chunk1[]', 'chunk2[]', 'promoted[]', 'full1[]', 'full2[]'];
|
|
1926
|
+
for (const bucket of buckets) {
|
|
1927
|
+
const documents = Array.from({ length: 8 }, (_, index) =>
|
|
1928
|
+
serializeBucketData(bucket, [makeOp(index + 1, String(index), 'data', { ...ctx, bucket }, sourceTableId)])
|
|
1929
|
+
);
|
|
1930
|
+
await collection.insertMany(documents);
|
|
1931
|
+
await bucketStateCollection.insertOne({
|
|
1932
|
+
_id: { d: ctx.definitionId, b: bucket },
|
|
1933
|
+
last_op: 8n,
|
|
1934
|
+
next_compact_check: new Date(0),
|
|
1935
|
+
first_uncompacted_write: bucket.startsWith('full') ? new Date(0) : new Date(),
|
|
1936
|
+
bucket_stats: { count: 8, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const originalClaim = CompactionLease.claim.bind(CompactionLease);
|
|
1941
|
+
const jobStartedAt = new Date();
|
|
1942
|
+
let serverTime = jobStartedAt;
|
|
1943
|
+
let promoted = false;
|
|
1944
|
+
const claim = vi.spyOn(CompactionLease, 'claim').mockImplementation(async (...args) => {
|
|
1945
|
+
if (!promoted && (args[1]._id as { b: string })?.b === 'promoted[]') {
|
|
1946
|
+
promoted = true;
|
|
1947
|
+
// Advance the server clock past the full-compaction interval without
|
|
1948
|
+
// changing bucket state. A fixed scan timestamp would retry forever.
|
|
1949
|
+
serverTime = new Date(jobStartedAt.getTime() + 3 * 60 * 60 * 1000);
|
|
1950
|
+
}
|
|
1951
|
+
const lease = await originalClaim(...args);
|
|
1952
|
+
if (lease != null) {
|
|
1953
|
+
Object.defineProperty(lease, 'startedAt', { value: serverTime });
|
|
1954
|
+
}
|
|
1955
|
+
return lease;
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
const compactor = bucketStorage.createMongoCompactor({ maxOpId: 8n });
|
|
1959
|
+
const internal = compactor as any;
|
|
1960
|
+
let timeReads = 0;
|
|
1961
|
+
const clock = vi.spyOn(internal, 'readCompactionTime').mockImplementation(async () => {
|
|
1962
|
+
if (++timeReads > 5) {
|
|
1963
|
+
throw new Error('Compaction did not advance after the bucket aged');
|
|
1964
|
+
}
|
|
1965
|
+
return serverTime;
|
|
1966
|
+
});
|
|
1967
|
+
const scan = vi.spyOn(internal, 'findScheduledBucketBatch');
|
|
1968
|
+
const originalChunks = internal.compactSingleBucketChunks.bind(compactor);
|
|
1969
|
+
const originalFull = internal.compactSingleBucketFully.bind(compactor);
|
|
1970
|
+
const gate = Promise.withResolvers<void>();
|
|
1971
|
+
let activeChunks = 0;
|
|
1972
|
+
let activeFull = 0;
|
|
1973
|
+
let maxActiveFull = 0;
|
|
1974
|
+
const fullBuckets: string[] = [];
|
|
1975
|
+
const chunks = vi.spyOn(internal, 'compactSingleBucketChunks').mockImplementation(async (...args) => {
|
|
1976
|
+
activeChunks++;
|
|
1977
|
+
try {
|
|
1978
|
+
await gate.promise;
|
|
1979
|
+
return await originalChunks(...args);
|
|
1980
|
+
} finally {
|
|
1981
|
+
activeChunks--;
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
const full = vi.spyOn(internal, 'compactSingleBucketFully').mockImplementation(async (...args) => {
|
|
1985
|
+
expect(activeChunks).toBe(0);
|
|
1986
|
+
activeFull++;
|
|
1987
|
+
maxActiveFull = Math.max(maxActiveFull, activeFull);
|
|
1988
|
+
fullBuckets.push((args[0] as any).state._id.b);
|
|
1989
|
+
try {
|
|
1990
|
+
return await originalFull(...args);
|
|
1991
|
+
} finally {
|
|
1992
|
+
activeFull--;
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
const run = compactor.compact();
|
|
1996
|
+
try {
|
|
1997
|
+
await vi.waitFor(() => expect(activeChunks).toBe(2));
|
|
1998
|
+
expect(full).not.toHaveBeenCalled();
|
|
1999
|
+
} finally {
|
|
2000
|
+
gate.resolve();
|
|
2001
|
+
try {
|
|
2002
|
+
await run;
|
|
2003
|
+
expect(timeReads).toBe(3); // Job start, first batch, and promoted bucket's batch.
|
|
2004
|
+
expect(scan).toHaveBeenCalledTimes(3); // Includes the final empty scan.
|
|
2005
|
+
for (const [cutoff] of scan.mock.calls) {
|
|
2006
|
+
expect(cutoff).toEqual(jobStartedAt);
|
|
2007
|
+
}
|
|
2008
|
+
} finally {
|
|
2009
|
+
clock.mockRestore();
|
|
2010
|
+
scan.mockRestore();
|
|
2011
|
+
claim.mockRestore();
|
|
2012
|
+
chunks.mockRestore();
|
|
2013
|
+
full.mockRestore();
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
expect(await run).toBe(5);
|
|
2017
|
+
expect(promoted).toBe(true);
|
|
2018
|
+
expect(maxActiveFull).toBe(1);
|
|
2019
|
+
expect(fullBuckets.sort()).toEqual(['full1[]', 'full2[]', 'promoted[]']);
|
|
2020
|
+
expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
|
|
2021
|
+
expect(await collection.countDocuments()).toBe(5);
|
|
2022
|
+
});
|
|
2023
|
+
|
|
1772
2024
|
test('capped chunk compaction repairs stale bucket stats after a committed first merge', async () => {
|
|
1773
2025
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
1774
2026
|
const operations = [
|
|
@@ -2,7 +2,7 @@ import { DeleteObjectsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
|
2
2
|
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
3
3
|
import { bucketRequest, test_utils } from '@powersync/service-core-tests';
|
|
4
4
|
import * as bson from 'bson';
|
|
5
|
-
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
6
6
|
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
7
7
|
import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
8
8
|
import { hydrateBucketDataDocuments } from '../../src/storage/implementation/v3/object-storage/BucketDataObjectStorage.js';
|
|
@@ -576,6 +576,52 @@ describe('S3 object storage reads', () => {
|
|
|
576
576
|
expect(maxActiveOperations).toBe(4);
|
|
577
577
|
});
|
|
578
578
|
|
|
579
|
+
test('drains sibling downloads before reporting a hydration failure', async () => {
|
|
580
|
+
const objectStorage = new MemoryObjectStorage();
|
|
581
|
+
const gate = Promise.withResolvers<void>();
|
|
582
|
+
const failure = new Error('download failed');
|
|
583
|
+
const documents = ['failed', 'pending'].map((path, index) => ({
|
|
584
|
+
_id: { b: 'bucket', o: BigInt(index + 1) },
|
|
585
|
+
min_op: BigInt(index + 1),
|
|
586
|
+
checksum: 0n,
|
|
587
|
+
count: 0,
|
|
588
|
+
size: 1,
|
|
589
|
+
storage_ref: { path, file_size: 1 }
|
|
590
|
+
}));
|
|
591
|
+
const get = vi.spyOn(objectStorage, 'get').mockImplementation(async (path) => {
|
|
592
|
+
if (path === 'failed') {
|
|
593
|
+
throw failure;
|
|
594
|
+
}
|
|
595
|
+
await gate.promise;
|
|
596
|
+
return {
|
|
597
|
+
data: bson.serialize({ ops: [] }),
|
|
598
|
+
metadata: { contentType: 'application/bson', contentEncoding: null }
|
|
599
|
+
};
|
|
600
|
+
});
|
|
601
|
+
let settled = false;
|
|
602
|
+
const result = hydrateBucketDataDocuments(documents, objectStorage, {}).then(
|
|
603
|
+
() => {
|
|
604
|
+
settled = true;
|
|
605
|
+
return undefined;
|
|
606
|
+
},
|
|
607
|
+
(error) => {
|
|
608
|
+
settled = true;
|
|
609
|
+
return error;
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
try {
|
|
613
|
+
await vi.waitFor(() => expect(get).toHaveBeenCalledTimes(2));
|
|
614
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
615
|
+
expect(settled).toBe(false);
|
|
616
|
+
} finally {
|
|
617
|
+
gate.resolve();
|
|
618
|
+
await result;
|
|
619
|
+
get.mockRestore();
|
|
620
|
+
}
|
|
621
|
+
expect(await result).toBe(failure);
|
|
622
|
+
expect(documents[1]).toHaveProperty('ops', []);
|
|
623
|
+
});
|
|
624
|
+
|
|
579
625
|
test('aborts active object downloads', async () => {
|
|
580
626
|
const objectStorage = new MemoryObjectStorage();
|
|
581
627
|
const controller = new AbortController();
|