@powersync/service-module-mongodb-storage 0.24.1 → 0.25.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 +19 -0
- package/dist/migrations/db/migrations/1684951997326-init.js +3 -29
- package/dist/migrations/db/migrations/1684951997326-init.js.map +1 -1
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js +3 -24
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js.map +1 -1
- package/dist/migrations/db/migrations/1760433882550-bucket-state-index2.js +3 -24
- package/dist/migrations/db/migrations/1760433882550-bucket-state-index2.js.map +1 -1
- package/dist/migrations/db/migrations/1794268800000-v1-storage-collections.d.ts +3 -0
- package/dist/migrations/db/migrations/1794268800000-v1-storage-collections.js +21 -0
- package/dist/migrations/db/migrations/1794268800000-v1-storage-collections.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -0
- package/dist/storage/MongoBucketStorage.js +61 -4
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +3 -0
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.js +11 -5
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +2 -0
- package/dist/storage/implementation/db.js +14 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +2 -5
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
- package/dist/types/types.d.ts +6 -0
- package/dist/types/types.js +6 -0
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +0 -4
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +2 -2
- package/src/migrations/db/migrations/1684951997326-init.ts +3 -36
- package/src/migrations/db/migrations/1741697235857-bucket-state-index.ts +3 -31
- package/src/migrations/db/migrations/1760433882550-bucket-state-index2.ts +3 -31
- package/src/migrations/db/migrations/1794268800000-v1-storage-collections.ts +22 -0
- package/src/storage/MongoBucketStorage.ts +73 -4
- package/src/storage/implementation/MongoStorageProvider.ts +4 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +12 -5
- package/src/storage/implementation/db.ts +15 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +2 -5
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +3 -1
- package/src/types/types.ts +6 -0
- package/src/utils/test-utils.ts +0 -5
- package/test/src/storage_sync.test.ts +92 -1
- package/test/src/v1_collections.test.ts +233 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -26,7 +26,9 @@ export class VersionedPowerSyncMongoV1 extends BaseVersionedPowerSyncMongo {
|
|
|
26
26
|
return this.upstream.source_tables as any as mongo.Collection<SourceTableDocumentV1>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async initializeStreamStorage(_replicationStreamId: number): Promise<void> {
|
|
29
|
+
async initializeStreamStorage(_replicationStreamId: number): Promise<void> {
|
|
30
|
+
await this.upstream.initializeV1StorageCollections();
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
get bucketDataV1(): mongo.Collection<BucketDataDocumentV1> {
|
|
32
34
|
return this.upstream.bucket_data;
|
package/src/types/types.ts
CHANGED
|
@@ -54,6 +54,12 @@ export const MongoStorageConfig = service_types.configFile.BaseStorageConfig.and
|
|
|
54
54
|
* This is an experimental option, and may be removed in a future release.
|
|
55
55
|
*/
|
|
56
56
|
bulk_read_preference: MongoStorageReadPreference.optional(),
|
|
57
|
+
/**
|
|
58
|
+
* Read preference for collection discovery and collection stats used by storage metrics.
|
|
59
|
+
* For example, use secondaryPreferred to offload collection stats from the primary.
|
|
60
|
+
* If unset, MongoDB driver defaults are used.
|
|
61
|
+
*/
|
|
62
|
+
stats_read_preference: MongoStorageReadPreference.optional(),
|
|
57
63
|
/**
|
|
58
64
|
* Throttle clear operations after sync config deploys, by pausing between batches.
|
|
59
65
|
* The rate produces a pause proportional to the previous batch duration.
|
package/src/utils/test-utils.ts
CHANGED
|
@@ -15,11 +15,6 @@ export function mongoTestStorageFactoryGenerator(factoryOptions: MongoTestStorag
|
|
|
15
15
|
factory: async (options?: TestStorageOptions) => {
|
|
16
16
|
const db = connectMongoForTests(factoryOptions.url, factoryOptions.isCI, factoryOptions.monitorCommands);
|
|
17
17
|
|
|
18
|
-
// None of the tests insert data into this collection, so it was never created
|
|
19
|
-
if (!(await db.db.listCollections({ name: db.bucket_parameters.collectionName }).hasNext())) {
|
|
20
|
-
await db.db.createCollection('bucket_parameters');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
// Full migrations are not currently run for tests, so we manually create this
|
|
24
19
|
await db.createCheckpointEventsCollection();
|
|
25
20
|
|
|
@@ -7,7 +7,13 @@ import {
|
|
|
7
7
|
storage,
|
|
8
8
|
updateSyncRulesFromYaml
|
|
9
9
|
} from '@powersync/service-core';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
bucketRequest,
|
|
12
|
+
getTestStorage,
|
|
13
|
+
register,
|
|
14
|
+
releaseTestStorageLease,
|
|
15
|
+
test_utils
|
|
16
|
+
} from '@powersync/service-core-tests';
|
|
11
17
|
import { parseSyncConfig, RequestParameters, ScopedParameterLookup } from '@powersync/service-sync-rules';
|
|
12
18
|
import * as bson from 'bson';
|
|
13
19
|
import { describe, expect, test } from 'vitest';
|
|
@@ -2178,6 +2184,91 @@ streams:
|
|
|
2178
2184
|
}
|
|
2179
2185
|
|
|
2180
2186
|
describe('sync - mongodb', () => {
|
|
2187
|
+
test.each([
|
|
2188
|
+
[1, false],
|
|
2189
|
+
[2, false],
|
|
2190
|
+
[1, true],
|
|
2191
|
+
[2, true]
|
|
2192
|
+
] as const)('v%i activation cleans up v4 streams (previously stranded: %s)', async (storageVersion, stranded) => {
|
|
2193
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
2194
|
+
const yaml = /* yaml */ `
|
|
2195
|
+
# sync config fixture.
|
|
2196
|
+
config:
|
|
2197
|
+
edition: 3
|
|
2198
|
+
streams:
|
|
2199
|
+
global:
|
|
2200
|
+
query: SELECT id FROM test
|
|
2201
|
+
`;
|
|
2202
|
+
|
|
2203
|
+
async function activate(version: number, lsn: string) {
|
|
2204
|
+
const stream = await factory.updateSyncRules(updateSyncRulesFromYaml(yaml, { storageVersion: version }));
|
|
2205
|
+
const bucketStorage = await getTestStorage(factory, stream);
|
|
2206
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
2207
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
2208
|
+
await writer.save({
|
|
2209
|
+
sourceTable,
|
|
2210
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
2211
|
+
after: { id: 'item-1' },
|
|
2212
|
+
afterReplicaId: test_utils.rid('item-1')
|
|
2213
|
+
});
|
|
2214
|
+
await writer.markAllSnapshotDone(lsn);
|
|
2215
|
+
await writer.commit(lsn);
|
|
2216
|
+
return stream;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const previous = await activate(storage.STORAGE_VERSION_4, '1/1');
|
|
2220
|
+
await releaseTestStorageLease(factory, previous.replicationStreamId);
|
|
2221
|
+
const oldCollections = await factory.db.db
|
|
2222
|
+
.listCollections(
|
|
2223
|
+
{
|
|
2224
|
+
name: new RegExp(
|
|
2225
|
+
`^(bucket_data|source_records|source_table|bucket_state|parameter_index)_${previous.replicationStreamId}(_|$)`
|
|
2226
|
+
)
|
|
2227
|
+
},
|
|
2228
|
+
{ nameOnly: true }
|
|
2229
|
+
)
|
|
2230
|
+
.toArray();
|
|
2231
|
+
expect(oldCollections.some((collection) => collection.name.startsWith('bucket_data_'))).toBe(true);
|
|
2232
|
+
expect(oldCollections.some((collection) => collection.name.startsWith('source_records_'))).toBe(true);
|
|
2233
|
+
|
|
2234
|
+
const replacement = await activate(storageVersion, '2/1');
|
|
2235
|
+
const previousDoc = (await factory.db.sync_rules.findOne({
|
|
2236
|
+
_id: previous.replicationStreamId
|
|
2237
|
+
})) as ReplicationStreamDocumentV3;
|
|
2238
|
+
expect(previousDoc.state).toBe(storage.SyncRuleState.STOP);
|
|
2239
|
+
if (stranded) {
|
|
2240
|
+
// Reproduce databases left behind by older services: only the stream was stopped.
|
|
2241
|
+
await factory.db.sync_rules.updateOne(
|
|
2242
|
+
{ _id: previous.replicationStreamId },
|
|
2243
|
+
{ $set: { 'sync_configs.$[].state': storage.SyncRuleState.ACTIVE } }
|
|
2244
|
+
);
|
|
2245
|
+
} else {
|
|
2246
|
+
expect(previousDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.STOP]);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
const stopped = await factory.getStoppedReplicationStreams();
|
|
2250
|
+
expect(stopped.map((stream) => stream.replicationStreamId)).toEqual([previous.replicationStreamId]);
|
|
2251
|
+
const lock = await stopped[0].lock();
|
|
2252
|
+
try {
|
|
2253
|
+
await factory.getInstance(stopped[0], { skipLifecycleHooks: true }).terminate({ clearStorage: true });
|
|
2254
|
+
} finally {
|
|
2255
|
+
await lock.release();
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
expect((await factory.db.sync_rules.findOne({ _id: previous.replicationStreamId }))?.state).toBe(
|
|
2259
|
+
storage.SyncRuleState.TERMINATED
|
|
2260
|
+
);
|
|
2261
|
+
const remaining = new Set(
|
|
2262
|
+
(await factory.db.db.listCollections({}, { nameOnly: true }).toArray()).map((c) => c.name)
|
|
2263
|
+
);
|
|
2264
|
+
for (const collection of oldCollections) expect(remaining.has(collection.name)).toBe(false);
|
|
2265
|
+
expect(await factory.getStoppedReplicationStreams()).toEqual([]);
|
|
2266
|
+
expect((await factory.getActiveSyncConfig())?.replicationStream.replicationStreamId).toBe(
|
|
2267
|
+
replacement.replicationStreamId
|
|
2268
|
+
);
|
|
2269
|
+
expect(await factory.db.current_data.countDocuments({ '_id.g': replacement.replicationStreamId })).toBe(1);
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2181
2272
|
test('v3 activation stops legacy active sync rules', async () => {
|
|
2182
2273
|
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
2183
2274
|
const mongoFactory = factory as MongoBucketStorage;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { MongoBucketStorage } from '@module/storage/MongoBucketStorage.js';
|
|
2
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
3
|
+
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
4
|
+
import { getTestStorage, releaseTestStorageLease, test_utils } from '@powersync/service-core-tests';
|
|
5
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
6
|
+
import { up as parameterIndexes } from '../../src/migrations/db/migrations/1684951997326-init.js';
|
|
7
|
+
import { up as bucketIndexes } from '../../src/migrations/db/migrations/1741697235857-bucket-state-index.js';
|
|
8
|
+
import { up as dirtyBucketIndexes } from '../../src/migrations/db/migrations/1760433882550-bucket-state-index2.js';
|
|
9
|
+
import {
|
|
10
|
+
up as removeUnusedV1Collections,
|
|
11
|
+
down as restoreV1Collections
|
|
12
|
+
} from '../../src/migrations/db/migrations/1794268800000-v1-storage-collections.js';
|
|
13
|
+
import { env } from './env.js';
|
|
14
|
+
|
|
15
|
+
const collectionNames = ['current_data', 'bucket_data', 'bucket_parameters', 'bucket_state', 'source_tables'];
|
|
16
|
+
const migrationContext = {
|
|
17
|
+
service_context: {
|
|
18
|
+
configuration: { storage: { type: 'mongodb', uri: env.MONGO_TEST_URL }, slot_name_prefix: 'test_' }
|
|
19
|
+
}
|
|
20
|
+
} as unknown as Parameters<typeof parameterIndexes>[0];
|
|
21
|
+
const testStorage = mongoTestStorageFactoryGenerator({
|
|
22
|
+
url: env.MONGO_TEST_URL,
|
|
23
|
+
isCI: env.CI,
|
|
24
|
+
monitorCommands: true,
|
|
25
|
+
supportsMultipleSyncConfigs: true
|
|
26
|
+
});
|
|
27
|
+
const yaml = /* yaml */ `
|
|
28
|
+
# sync config fixture.
|
|
29
|
+
config:
|
|
30
|
+
edition: 3
|
|
31
|
+
streams:
|
|
32
|
+
global:
|
|
33
|
+
query: SELECT id FROM test
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
async function activate(factory: MongoBucketStorage, version: number, lsn: string) {
|
|
37
|
+
const stream = await factory.updateSyncRules(updateSyncRulesFromYaml(yaml, { storageVersion: version }));
|
|
38
|
+
const bucketStorage = await getTestStorage(factory, stream);
|
|
39
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
40
|
+
const sourceTable = await test_utils.resolveTestTable(
|
|
41
|
+
writer,
|
|
42
|
+
'test',
|
|
43
|
+
['id'],
|
|
44
|
+
testStorage,
|
|
45
|
+
stream.replicationStreamId
|
|
46
|
+
);
|
|
47
|
+
await writer.save({
|
|
48
|
+
sourceTable,
|
|
49
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
50
|
+
after: { id: 'item-1' },
|
|
51
|
+
afterReplicaId: test_utils.rid('item-1')
|
|
52
|
+
});
|
|
53
|
+
await writer.markAllSnapshotDone(lsn);
|
|
54
|
+
await writer.commit(lsn);
|
|
55
|
+
return stream;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function names(factory: MongoBucketStorage) {
|
|
59
|
+
return (await factory.db.db.listCollections({}, { nameOnly: true }).toArray()).map((c) => c.name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function terminateStopped(factory: MongoBucketStorage) {
|
|
63
|
+
for (const stream of await factory.getStoppedReplicationStreams()) {
|
|
64
|
+
const lock = await stream.lock();
|
|
65
|
+
try {
|
|
66
|
+
await factory.getInstance(stream, { skipLifecycleHooks: true }).terminate({ clearStorage: true });
|
|
67
|
+
} finally {
|
|
68
|
+
await lock.release();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('v1/v2 collection lifecycle', () => {
|
|
74
|
+
test('migrations and v4 initialization do not create v1/v2 collections', async () => {
|
|
75
|
+
await using factory = await testStorage.factory();
|
|
76
|
+
await factory.db.drop();
|
|
77
|
+
await factory.db.createCheckpointEventsCollection();
|
|
78
|
+
for (const migration of [parameterIndexes, bucketIndexes, dirtyBucketIndexes, removeUnusedV1Collections]) {
|
|
79
|
+
await migration(migrationContext);
|
|
80
|
+
}
|
|
81
|
+
for (const name of collectionNames) expect(await names(factory)).not.toContain(name);
|
|
82
|
+
await activate(factory, storage.STORAGE_VERSION_4, '1/1');
|
|
83
|
+
for (const name of collectionNames) expect(await names(factory)).not.toContain(name);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test.each([1, 2])('drops unused v%i collections instead of deleting individual records', async (version) => {
|
|
87
|
+
await using factory = await testStorage.factory();
|
|
88
|
+
const previous = await activate(factory, version, '1/1');
|
|
89
|
+
await releaseTestStorageLease(factory, previous.replicationStreamId);
|
|
90
|
+
expect(await names(factory)).toEqual(expect.arrayContaining(collectionNames));
|
|
91
|
+
const current = await activate(factory, storage.STORAGE_VERSION_4, '2/1');
|
|
92
|
+
const dropped: string[] = [];
|
|
93
|
+
const deleted: string[] = [];
|
|
94
|
+
factory.db.client.on('commandStarted', (event) => {
|
|
95
|
+
if (event.commandName === 'drop') dropped.push(event.command.drop);
|
|
96
|
+
if (event.commandName === 'delete') deleted.push(event.command.delete);
|
|
97
|
+
});
|
|
98
|
+
await terminateStopped(factory);
|
|
99
|
+
expect(dropped).toEqual(expect.arrayContaining(collectionNames));
|
|
100
|
+
expect(deleted.filter((name) => collectionNames.includes(name))).toEqual([]);
|
|
101
|
+
for (const name of collectionNames) expect(await names(factory)).not.toContain(name);
|
|
102
|
+
expect((await factory.db.sync_rules.findOne({ _id: previous.replicationStreamId }))?.state).toBe(
|
|
103
|
+
storage.SyncRuleState.TERMINATED
|
|
104
|
+
);
|
|
105
|
+
expect((await factory.getActiveSyncConfig())?.replicationStream.replicationStreamId).toBe(
|
|
106
|
+
current.replicationStreamId
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const returning = await activate(factory, version, '3/1');
|
|
110
|
+
await releaseTestStorageLease(factory, current.replicationStreamId);
|
|
111
|
+
await terminateStopped(factory);
|
|
112
|
+
expect(await names(factory)).toEqual(expect.arrayContaining(collectionNames));
|
|
113
|
+
expect((await factory.db.bucket_parameters.listIndexes().toArray()).map((index) => index.name)).toContain(
|
|
114
|
+
'lookup1'
|
|
115
|
+
);
|
|
116
|
+
expect((await factory.db.bucket_state.listIndexes().toArray()).map((index) => index.name)).toEqual(
|
|
117
|
+
expect.arrayContaining(['bucket_updates', 'dirty_count'])
|
|
118
|
+
);
|
|
119
|
+
expect(await factory.db.current_data.countDocuments({ '_id.g': returning.replicationStreamId })).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test.each([storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED])(
|
|
123
|
+
'retains shared collections for a stream in state %s and deletes only stopped stream records',
|
|
124
|
+
async (state) => {
|
|
125
|
+
await using factory = await testStorage.factory();
|
|
126
|
+
const previous = await activate(factory, storage.STORAGE_VERSION_1, '1/1');
|
|
127
|
+
await releaseTestStorageLease(factory, previous.replicationStreamId);
|
|
128
|
+
const current = await activate(factory, storage.STORAGE_VERSION_2, '2/1');
|
|
129
|
+
await factory.db.sync_rules.updateOne({ _id: current.replicationStreamId }, { $set: { state } });
|
|
130
|
+
await terminateStopped(factory);
|
|
131
|
+
expect(await names(factory)).toEqual(expect.arrayContaining(collectionNames));
|
|
132
|
+
expect(await factory.db.current_data.countDocuments({ '_id.g': previous.replicationStreamId })).toBe(0);
|
|
133
|
+
expect(await factory.db.current_data.countDocuments({ '_id.g': current.replicationStreamId })).toBe(1);
|
|
134
|
+
// A missing storage_version also identifies a v1 stream.
|
|
135
|
+
await factory.db.sync_rules.updateOne({ _id: current.replicationStreamId }, { $unset: { storage_version: 1 } });
|
|
136
|
+
expect(await factory.dropUnusedV1StorageCollections()).toBe(false);
|
|
137
|
+
await removeUnusedV1Collections(migrationContext);
|
|
138
|
+
expect(await names(factory)).toEqual(expect.arrayContaining(collectionNames));
|
|
139
|
+
expect(await factory.db.current_data.countDocuments({ '_id.g': current.replicationStreamId })).toBe(1);
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
test('restarting and clearing an active v4 stream do not drop v1 collections', async () => {
|
|
144
|
+
await using factory = await testStorage.factory();
|
|
145
|
+
const previous = await activate(factory, storage.STORAGE_VERSION_2, '1/1');
|
|
146
|
+
await releaseTestStorageLease(factory, previous.replicationStreamId);
|
|
147
|
+
const current = await activate(factory, storage.STORAGE_VERSION_4, '2/1');
|
|
148
|
+
await factory.db.sync_rules.updateOne(
|
|
149
|
+
{ _id: previous.replicationStreamId },
|
|
150
|
+
{ $set: { state: storage.SyncRuleState.TERMINATED } }
|
|
151
|
+
);
|
|
152
|
+
await using restarted = await testStorage.factory({ doNotClear: true });
|
|
153
|
+
const drop = vi.spyOn(restarted, 'dropUnusedV1StorageCollections');
|
|
154
|
+
expect(await restarted.getStoppedReplicationStreams()).toEqual([]);
|
|
155
|
+
expect(drop).not.toHaveBeenCalled();
|
|
156
|
+
expect(await names(restarted)).toEqual(expect.arrayContaining(collectionNames));
|
|
157
|
+
await factory.getInstance(current).clear();
|
|
158
|
+
expect(await names(factory)).toEqual(expect.arrayContaining(collectionNames));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('migration removes unused v1 collections and rollback recreates collections and indexes', async () => {
|
|
162
|
+
await using factory = await testStorage.factory();
|
|
163
|
+
const previous = await activate(factory, storage.STORAGE_VERSION_2, '1/1');
|
|
164
|
+
await releaseTestStorageLease(factory, previous.replicationStreamId);
|
|
165
|
+
const current = await activate(factory, storage.STORAGE_VERSION_4, '2/1');
|
|
166
|
+
await factory.db.sync_rules.updateOne(
|
|
167
|
+
{ _id: previous.replicationStreamId },
|
|
168
|
+
{ $set: { state: storage.SyncRuleState.TERMINATED } }
|
|
169
|
+
);
|
|
170
|
+
await removeUnusedV1Collections(migrationContext);
|
|
171
|
+
for (const name of collectionNames) expect(await names(factory)).not.toContain(name);
|
|
172
|
+
// Both directions are idempotent, and rollback must retain any existing data.
|
|
173
|
+
await removeUnusedV1Collections(migrationContext);
|
|
174
|
+
await restoreV1Collections(migrationContext);
|
|
175
|
+
await factory.db.current_data.insertOne({ _id: { g: 99, t: 'test', k: 'item' } } as any);
|
|
176
|
+
await restoreV1Collections(migrationContext);
|
|
177
|
+
expect(await names(factory)).toEqual(expect.arrayContaining(collectionNames));
|
|
178
|
+
expect(await factory.db.current_data.countDocuments({ '_id.g': 99 })).toBe(1);
|
|
179
|
+
expect((await factory.db.bucket_parameters.listIndexes().toArray()).map((index) => index.name)).toEqual(
|
|
180
|
+
expect.arrayContaining(['_id_', 'lookup1'])
|
|
181
|
+
);
|
|
182
|
+
expect((await factory.db.bucket_state.listIndexes().toArray()).map((index) => index.name)).toEqual(
|
|
183
|
+
expect.arrayContaining(['_id_', 'bucket_updates', 'dirty_count'])
|
|
184
|
+
);
|
|
185
|
+
expect((await factory.getActiveSyncConfig())?.replicationStream.replicationStreamId).toBe(
|
|
186
|
+
current.replicationStreamId
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('retries a partially completed collection drop', async () => {
|
|
191
|
+
await using factory = await testStorage.factory();
|
|
192
|
+
const previous = await activate(factory, storage.STORAGE_VERSION_2, '1/1');
|
|
193
|
+
await releaseTestStorageLease(factory, previous.replicationStreamId);
|
|
194
|
+
await activate(factory, storage.STORAGE_VERSION_4, '2/1');
|
|
195
|
+
const drop = vi.spyOn(factory.db.bucket_parameters, 'drop').mockRejectedValueOnce(new Error('drop interrupted'));
|
|
196
|
+
await expect(terminateStopped(factory)).rejects.toThrow('drop interrupted');
|
|
197
|
+
expect((await factory.db.sync_rules.findOne({ _id: previous.replicationStreamId }))?.state).toBe(
|
|
198
|
+
storage.SyncRuleState.STOP
|
|
199
|
+
);
|
|
200
|
+
await terminateStopped(factory);
|
|
201
|
+
for (const name of collectionNames) expect(await names(factory)).not.toContain(name);
|
|
202
|
+
drop.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('serializes collection drops with deployment by another worker', async () => {
|
|
206
|
+
await using factory = await testStorage.factory();
|
|
207
|
+
await using other = await testStorage.factory({ doNotClear: true });
|
|
208
|
+
const entered = Promise.withResolvers<void>();
|
|
209
|
+
const resume = Promise.withResolvers<void>();
|
|
210
|
+
const originalDrop = factory.db.current_data.drop.bind(factory.db.current_data);
|
|
211
|
+
const drop = vi.spyOn(factory.db.current_data, 'drop').mockImplementation(async (options) => {
|
|
212
|
+
entered.resolve();
|
|
213
|
+
await resume.promise;
|
|
214
|
+
return originalDrop(options);
|
|
215
|
+
});
|
|
216
|
+
const cleanup = factory.dropUnusedV1StorageCollections();
|
|
217
|
+
await entered.promise;
|
|
218
|
+
let deployed = false;
|
|
219
|
+
const deployment = other.updateSyncRules(updateSyncRulesFromYaml(yaml, { storageVersion: 2 })).then(() => {
|
|
220
|
+
deployed = true;
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
224
|
+
expect(deployed).toBe(false);
|
|
225
|
+
} finally {
|
|
226
|
+
resume.resolve();
|
|
227
|
+
await cleanup;
|
|
228
|
+
drop.mockRestore();
|
|
229
|
+
}
|
|
230
|
+
await deployment;
|
|
231
|
+
expect(deployed).toBe(true);
|
|
232
|
+
}, 15_000);
|
|
233
|
+
});
|