@powersync/service-module-mongodb-storage 0.18.3 → 0.20.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 +53 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +13 -12
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +4 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +58 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +19 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +35 -23
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +108 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +43 -2
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +9 -1
- package/dist/storage/implementation/v3/source-table-utils.js +20 -21
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +23 -22
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +20 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +76 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +53 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +40 -23
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +122 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +45 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/storage/implementation/v3/source-table-utils.ts +43 -35
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +124 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ObjectStorage } from '@module/storage/implementation/v3/object-storage/ObjectStorage.js';
|
|
2
|
+
import { S3ObjectStorage } from '@module/storage/implementation/v3/object-storage/S3ObjectStorage.js';
|
|
3
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { MemoryObjectStorage } from './MemoryObjectStorage.js';
|
|
6
|
+
|
|
7
|
+
const minioStorages: S3ObjectStorage[] = [];
|
|
8
|
+
|
|
9
|
+
export interface S3TestFactoryOptions {
|
|
10
|
+
url: string;
|
|
11
|
+
isCI: boolean;
|
|
12
|
+
inlineThresholdBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createTestStorageSuite(options: S3TestFactoryOptions, objectStorage: ObjectStorage) {
|
|
16
|
+
return {
|
|
17
|
+
objectStorage,
|
|
18
|
+
factoryGen: mongoTestStorageFactoryGenerator({
|
|
19
|
+
url: options.url,
|
|
20
|
+
isCI: options.isCI,
|
|
21
|
+
objectStorage,
|
|
22
|
+
inlineThresholdBytes: options.inlineThresholdBytes ?? 0
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates an ObjectStorage instance for S3 tests.
|
|
29
|
+
* Set MINIO_ENDPOINT to switch all S3 tests from MemoryObjectStorage
|
|
30
|
+
* to a real MinIO/S3 endpoint.
|
|
31
|
+
* MINIO_ENDPOINT=http://localhost:9000
|
|
32
|
+
*/
|
|
33
|
+
export function createS3TestStorageSuite(options: S3TestFactoryOptions) {
|
|
34
|
+
const minioEndpoint = process.env.MINIO_ENDPOINT;
|
|
35
|
+
let objectStorage: ObjectStorage;
|
|
36
|
+
if (minioEndpoint) {
|
|
37
|
+
const s3 = new S3ObjectStorage({
|
|
38
|
+
bucket: 'powersync-s3-test',
|
|
39
|
+
region: 'us-east-1',
|
|
40
|
+
prefix: `test-${process.pid}-${randomUUID()}`,
|
|
41
|
+
endpoint: minioEndpoint,
|
|
42
|
+
forcePathStyle: true,
|
|
43
|
+
accessKeyId: process.env.MINIO_ACCESS_KEY ?? 'minioadmin',
|
|
44
|
+
secretAccessKey: process.env.MINIO_SECRET_KEY ?? 'minioadmin'
|
|
45
|
+
});
|
|
46
|
+
minioStorages.push(s3);
|
|
47
|
+
objectStorage = s3;
|
|
48
|
+
} else {
|
|
49
|
+
objectStorage = new MemoryObjectStorage();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return createTestStorageSuite(options, objectStorage);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Remove all objects created by MinIO-backed suites in the current test. */
|
|
56
|
+
export async function cleanupS3TestStorage(): Promise<void> {
|
|
57
|
+
for (const storage of minioStorages.splice(0)) {
|
|
58
|
+
await storage.deletePrefix('bucket-data/');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Creates an explicitly memory-backed suite for tests that inspect stored objects. */
|
|
63
|
+
export function createMemoryS3TestStorageSuite(options: S3TestFactoryOptions) {
|
|
64
|
+
const objectStorage = new MemoryObjectStorage();
|
|
65
|
+
return {
|
|
66
|
+
...createTestStorageSuite(options, objectStorage),
|
|
67
|
+
objectStorage
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { ObjectStorageLifecycle } from '../../src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js';
|
|
4
|
+
import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
|
|
5
|
+
|
|
6
|
+
describe('ObjectStorageLifecycle', () => {
|
|
7
|
+
test('escapes bucket names as path segments', () => {
|
|
8
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 12, null as any);
|
|
9
|
+
|
|
10
|
+
const path = lifecycle.allocatePath('definition', 'bucket/with spaces?and%escapes', 1n, 2n);
|
|
11
|
+
|
|
12
|
+
expect(path).toMatch(/^bucket-data\/12\/definition\/bucket%2Fwith%20spaces%3Fand%25escapes\/1-2-[0-9a-f-]+\.bson$/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('hashes bucket path segments longer than 128 bytes', () => {
|
|
16
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 12, null as any);
|
|
17
|
+
const bucket = '💾/long bucket parameter/'.repeat(20);
|
|
18
|
+
const hash = createHash('sha256').update(bucket).digest('base64url');
|
|
19
|
+
|
|
20
|
+
const path = lifecycle.allocatePath('definition', bucket, 1n, 2n);
|
|
21
|
+
|
|
22
|
+
expect(path).toMatch(new RegExp(`^bucket-data/12/definition/sha256-${hash}/1-2-[0-9a-f-]+\\.bson$`));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('deletePrefix removes only objects under the exact prefix', async () => {
|
|
26
|
+
const objectStorage = new MemoryObjectStorage();
|
|
27
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 1, objectStorage);
|
|
28
|
+
for (const path of [
|
|
29
|
+
'bucket-data/1/definition/object-1.bson',
|
|
30
|
+
'bucket-data/12/definition/object-2.bson',
|
|
31
|
+
'bucket-data/1-other/definition/object-3.bson'
|
|
32
|
+
]) {
|
|
33
|
+
await objectStorage.put(path, new Uint8Array(), {
|
|
34
|
+
contentType: 'application/bson',
|
|
35
|
+
contentEncoding: null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await expect(lifecycle.deletePrefix(lifecycle.streamPrefix())).resolves.toEqual({ objectCount: 1 });
|
|
40
|
+
expect([...objectStorage.store.keys()]).toEqual([
|
|
41
|
+
'bucket-data/12/definition/object-2.bson',
|
|
42
|
+
'bucket-data/1-other/definition/object-3.bson'
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('definition prefixes do not match ids with the same string prefix', async () => {
|
|
47
|
+
const objectStorage = new MemoryObjectStorage();
|
|
48
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 7, objectStorage);
|
|
49
|
+
for (const path of ['bucket-data/7/1/object-1.bson', 'bucket-data/7/12/object-2.bson']) {
|
|
50
|
+
await objectStorage.put(path, new Uint8Array(), {
|
|
51
|
+
contentType: 'application/bson',
|
|
52
|
+
contentEncoding: null
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await lifecycle.deletePrefix(lifecycle.definitionPrefix('1'));
|
|
57
|
+
expect([...objectStorage.store.keys()]).toEqual(['bucket-data/7/12/object-2.bson']);
|
|
58
|
+
});
|
|
59
|
+
});
|
package/test/src/setup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { container } from '@powersync/lib-services-framework';
|
|
2
2
|
import { METRICS_HELPER } from '@powersync/service-core-tests';
|
|
3
|
-
import { beforeAll, beforeEach } from 'vitest';
|
|
3
|
+
import { afterEach, beforeAll, beforeEach } from 'vitest';
|
|
4
|
+
import { cleanupS3TestStorage } from './helpers/s3TestFactory.js';
|
|
4
5
|
|
|
5
6
|
beforeAll(async () => {
|
|
6
7
|
// Executes for every test file
|
|
@@ -10,3 +11,7 @@ beforeAll(async () => {
|
|
|
10
11
|
beforeEach(async () => {
|
|
11
12
|
METRICS_HELPER.resetMetrics();
|
|
12
13
|
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await cleanupS3TestStorage();
|
|
17
|
+
});
|
package/test/src/storage.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
3
|
+
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
4
|
+
import { register, test_utils } from '@powersync/service-core-tests';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
4
6
|
import { env } from './env.js';
|
|
5
7
|
import { INITIALIZED_MONGO_STORAGE_FACTORY, TEST_STORAGE_VERSIONS } from './util.js';
|
|
6
8
|
|
|
@@ -17,6 +19,218 @@ for (let storageVersion of TEST_STORAGE_VERSIONS) {
|
|
|
17
19
|
|
|
18
20
|
describe(`Mongo Sync Bucket Storage - Checkpoints - v${storageVersion}`, () =>
|
|
19
21
|
register.registerDataStorageCheckpointTests({ ...INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion }));
|
|
22
|
+
|
|
23
|
+
describe(`Mongo Sync Bucket Storage - write checkpoint metadata - v${storageVersion}`, () => {
|
|
24
|
+
test('uses checkpoint_requested_at as the client-requested checkpoint marker', async () => {
|
|
25
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
26
|
+
const syncRules = await factory.updateSyncRules(
|
|
27
|
+
updateSyncRulesFromYaml(
|
|
28
|
+
`
|
|
29
|
+
bucket_definitions:
|
|
30
|
+
global:
|
|
31
|
+
data: []
|
|
32
|
+
`,
|
|
33
|
+
{ storageVersion }
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
37
|
+
|
|
38
|
+
// user1 has no existing row, so this covers updateMany with upsert.
|
|
39
|
+
// The initial request stores checkpoint id 42 at source head 5/0.
|
|
40
|
+
const requestedResult = await bucketStorage.createManagedWriteCheckpoints([
|
|
41
|
+
{ user_id: 'user1', heads: { '1': '5/0' }, checkpoint_request_id: 42n }
|
|
42
|
+
]);
|
|
43
|
+
expect(requestedResult.writeCheckpoints.get('user1')).toEqual(42n);
|
|
44
|
+
expect(requestedResult.shouldAdvance).toBe(true);
|
|
45
|
+
|
|
46
|
+
const requested = await factory.db.write_checkpoints.findOne({ user_id: 'user1' });
|
|
47
|
+
expect(requested?.client_id).toEqual(42n);
|
|
48
|
+
expect(requested?.lsns).toEqual({ '1': '5/0' });
|
|
49
|
+
expect(requested?.processed_at_lsn).toBeNull();
|
|
50
|
+
expect(requested?.checkpoint_requested_at).toBeInstanceOf(Date);
|
|
51
|
+
|
|
52
|
+
// Request id 41 is lower than the stored id 42, so the entire stale
|
|
53
|
+
// request is ignored, including its newer 6/0 source head.
|
|
54
|
+
await bucketStorage.createManagedWriteCheckpoints([
|
|
55
|
+
{ user_id: 'user1', heads: { '1': '6/0' }, checkpoint_request_id: 41n }
|
|
56
|
+
]);
|
|
57
|
+
const stale = await factory.db.write_checkpoints.findOne({ user_id: 'user1' });
|
|
58
|
+
expect(stale?.checkpoint_requested_at).toEqual(requested?.checkpoint_requested_at);
|
|
59
|
+
|
|
60
|
+
const expiredRequestedAt = new Date('2024-01-01T00:00:00.000Z');
|
|
61
|
+
await factory.db.write_checkpoints.updateOne(
|
|
62
|
+
{ user_id: 'user1' },
|
|
63
|
+
{ $set: { checkpoint_requested_at: expiredRequestedAt } }
|
|
64
|
+
);
|
|
65
|
+
// Although the previous incoming request was 41, the stored id is still
|
|
66
|
+
// 42. This is therefore an equal-id retry of the original request.
|
|
67
|
+
await bucketStorage.createManagedWriteCheckpoints([
|
|
68
|
+
{ user_id: 'user1', heads: { '1': '6/0' }, checkpoint_request_id: 42n }
|
|
69
|
+
]);
|
|
70
|
+
const retried = await factory.db.write_checkpoints.findOne({ user_id: 'user1' });
|
|
71
|
+
// Retrying the current id refreshes its retention timestamp without
|
|
72
|
+
// replacing the original source head or resetting its processed state.
|
|
73
|
+
expect(retried?.checkpoint_requested_at).toBeInstanceOf(Date);
|
|
74
|
+
expect(retried!.checkpoint_requested_at!.getTime()).toBeGreaterThan(expiredRequestedAt.getTime());
|
|
75
|
+
expect(retried?.lsns).toEqual({ '1': '5/0' });
|
|
76
|
+
expect(retried?.processed_at_lsn).toBeNull();
|
|
77
|
+
|
|
78
|
+
await factory.db.write_checkpoints.updateOne(
|
|
79
|
+
{ user_id: 'user1' },
|
|
80
|
+
{ $set: { checkpoint_requested_at: expiredRequestedAt } }
|
|
81
|
+
);
|
|
82
|
+
await bucketStorage.createManagedWriteCheckpoints([
|
|
83
|
+
{ user_id: 'user1', heads: { '1': '6/0' }, checkpoint_request_id: 43n }
|
|
84
|
+
]);
|
|
85
|
+
const advanced = await factory.db.write_checkpoints.findOne({ user_id: 'user1' });
|
|
86
|
+
// A greater id refreshes retention and advances the stored checkpoint.
|
|
87
|
+
expect(advanced?.checkpoint_requested_at).toBeInstanceOf(Date);
|
|
88
|
+
expect(advanced!.checkpoint_requested_at!.getTime()).toBeGreaterThan(expiredRequestedAt.getTime());
|
|
89
|
+
expect(advanced?.client_id).toEqual(43n);
|
|
90
|
+
expect(advanced?.lsns).toEqual({ '1': '6/0' });
|
|
91
|
+
|
|
92
|
+
await bucketStorage.createManagedWriteCheckpoints([{ user_id: 'user1', heads: { '1': '7/0' } }]);
|
|
93
|
+
const generated = await factory.db.write_checkpoints.findOne({ user_id: 'user1' });
|
|
94
|
+
// Generated checkpoints unset the field, keeping the document out of the
|
|
95
|
+
// partial checkpoint_requested_at index.
|
|
96
|
+
expect(generated).not.toBeNull();
|
|
97
|
+
expect(generated?.checkpoint_requested_at).toBeUndefined();
|
|
98
|
+
|
|
99
|
+
await bucketStorage.createManagedWriteCheckpoints([
|
|
100
|
+
{ user_id: 'user2', heads: { '1': '8/0' }, checkpoint_request_id: 50n }
|
|
101
|
+
]);
|
|
102
|
+
await factory.db.write_checkpoints.updateOne(
|
|
103
|
+
{ user_id: 'user2' },
|
|
104
|
+
{ $set: { checkpoint_requested_at: new Date('2024-01-01T00:00:00.000Z') } }
|
|
105
|
+
);
|
|
106
|
+
await bucketStorage.compact({
|
|
107
|
+
compactBuckets: [],
|
|
108
|
+
deleteCheckpointRequestsBefore: new Date('2024-02-01T00:00:00.000Z')
|
|
109
|
+
});
|
|
110
|
+
// The request is expired but still pending, so compaction must retain it.
|
|
111
|
+
await expect(factory.db.write_checkpoints.findOne({ user_id: 'user2' })).resolves.not.toBeNull();
|
|
112
|
+
|
|
113
|
+
// Advancing replication to the request head sets processed_at_lsn.
|
|
114
|
+
await using managedWriter = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
115
|
+
await managedWriter.markAllSnapshotDone('1/1');
|
|
116
|
+
await managedWriter.keepalive('8/0');
|
|
117
|
+
|
|
118
|
+
await bucketStorage.compact({
|
|
119
|
+
compactBuckets: [],
|
|
120
|
+
deleteCheckpointRequestsBefore: new Date('2024-02-01T00:00:00.000Z')
|
|
121
|
+
});
|
|
122
|
+
// The request is now both expired and processed, so it can be removed.
|
|
123
|
+
await expect(factory.db.write_checkpoints.findOne({ user_id: 'user2' })).resolves.toBeNull();
|
|
124
|
+
|
|
125
|
+
bucketStorage.setWriteCheckpointMode(storage.WriteCheckpointMode.CUSTOM);
|
|
126
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
127
|
+
await writer.markAllSnapshotDone('1/1');
|
|
128
|
+
const customCheckpointRequestedAt = new Date('2024-01-01T00:00:00.000Z');
|
|
129
|
+
writer.addCustomWriteCheckpoint({
|
|
130
|
+
user_id: 'custom1',
|
|
131
|
+
checkpoint: 51n,
|
|
132
|
+
checkpoint_requested_at: customCheckpointRequestedAt
|
|
133
|
+
});
|
|
134
|
+
await writer.flush();
|
|
135
|
+
const customRequested = await factory.db.custom_write_checkpoints.findOne({ user_id: 'custom1' });
|
|
136
|
+
expect(customRequested?.checkpoint_requested_at).toEqual(customCheckpointRequestedAt);
|
|
137
|
+
|
|
138
|
+
writer.addCustomWriteCheckpoint({
|
|
139
|
+
user_id: 'custom1',
|
|
140
|
+
checkpoint: 52n
|
|
141
|
+
});
|
|
142
|
+
await writer.flush();
|
|
143
|
+
const customGenerated = await factory.db.custom_write_checkpoints.findOne({ user_id: 'custom1' });
|
|
144
|
+
expect(customGenerated).not.toBeNull();
|
|
145
|
+
expect(customGenerated?.checkpoint_requested_at).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* It's extremely rare (but technically possible) to have duplicate write checkpoint records
|
|
150
|
+
* for a full user_id. This is normally not an issue for checkpoints requested from `write-checkpoint2.json`,
|
|
151
|
+
* but it can be for requests in the `sync/checkpoint-request` flow.
|
|
152
|
+
* That flow seeds the client with the current latest state (if available). We need to ensure that the client
|
|
153
|
+
* does NOT start at a lower bound if there are duplicate records.
|
|
154
|
+
*/
|
|
155
|
+
test('updates duplicate managed rows on supplied checkpoint requests', async () => {
|
|
156
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
157
|
+
const syncRules = await factory.updateSyncRules(
|
|
158
|
+
updateSyncRulesFromYaml(
|
|
159
|
+
`
|
|
160
|
+
bucket_definitions:
|
|
161
|
+
global:
|
|
162
|
+
data: []
|
|
163
|
+
`,
|
|
164
|
+
{ storageVersion }
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
168
|
+
|
|
169
|
+
// Replicate an extremely rare possibility where there are multiple records
|
|
170
|
+
await factory.db.write_checkpoints.insertMany([
|
|
171
|
+
{
|
|
172
|
+
_id: new mongo.ObjectId('000000000000000000000001'),
|
|
173
|
+
user_id: 'user1',
|
|
174
|
+
client_id: 50n,
|
|
175
|
+
lsns: { '1': '50/0' },
|
|
176
|
+
processed_at_lsn: '50/0'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
_id: new mongo.ObjectId('000000000000000000000002'),
|
|
180
|
+
user_id: 'user1',
|
|
181
|
+
client_id: 42n,
|
|
182
|
+
lsns: { '1': '42/0' },
|
|
183
|
+
processed_at_lsn: null,
|
|
184
|
+
checkpoint_requested_at: new Date('2024-01-01T00:00:00.000Z')
|
|
185
|
+
}
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
// Simulate a client without a sequence state trying to seed its state.
|
|
189
|
+
const seeded = await bucketStorage.createManagedWriteCheckpoints([
|
|
190
|
+
{ user_id: 'user1', heads: { '1': '1/0' }, checkpoint_request_id: 1n }
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
// The result should be the current max state.
|
|
194
|
+
const seededCheckpoint = seeded.writeCheckpoints.get('user1')!;
|
|
195
|
+
expect(seededCheckpoint).toEqual(50n);
|
|
196
|
+
expect(seeded.shouldAdvance).toBe(false);
|
|
197
|
+
|
|
198
|
+
// Simulate a client using the above state for its next checkpoint request.
|
|
199
|
+
const nextCheckpoint = seededCheckpoint + 1n;
|
|
200
|
+
const incremented = await bucketStorage.createManagedWriteCheckpoints([
|
|
201
|
+
{ user_id: 'user1', heads: { '1': `${nextCheckpoint}/0` }, checkpoint_request_id: nextCheckpoint }
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
// This should be accepted by the service.
|
|
205
|
+
expect(incremented.writeCheckpoints.get('user1')).toEqual(nextCheckpoint);
|
|
206
|
+
expect(incremented.shouldAdvance).toBe(true);
|
|
207
|
+
|
|
208
|
+
// The service should now reconcile all duplicates to the latest state.
|
|
209
|
+
// Duplicates can now also automatically be deleted when they expire.
|
|
210
|
+
const docs = await factory.db.write_checkpoints.find({ user_id: 'user1' }, { sort: { _id: 1 } }).toArray();
|
|
211
|
+
expect(
|
|
212
|
+
docs.map((doc) => ({
|
|
213
|
+
client_id: doc.client_id,
|
|
214
|
+
lsn: doc.lsns['1'],
|
|
215
|
+
processed_at_lsn: doc.processed_at_lsn,
|
|
216
|
+
is_checkpoint_request: doc.checkpoint_requested_at != null
|
|
217
|
+
}))
|
|
218
|
+
).toEqual([
|
|
219
|
+
{
|
|
220
|
+
client_id: nextCheckpoint,
|
|
221
|
+
lsn: '51/0',
|
|
222
|
+
processed_at_lsn: null,
|
|
223
|
+
is_checkpoint_request: true
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
client_id: nextCheckpoint,
|
|
227
|
+
lsn: '51/0',
|
|
228
|
+
processed_at_lsn: null,
|
|
229
|
+
is_checkpoint_request: true
|
|
230
|
+
}
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
20
234
|
}
|
|
21
235
|
|
|
22
236
|
describe('Sync Bucket Validation', register.registerBucketValidationTests);
|