@powersync/service-module-mongodb-storage 0.24.0 → 0.24.2
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 +2 -0
- package/dist/storage/MongoBucketStorage.js +49 -84
- 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/v1/MongoBucketBatchV1.js +2 -5
- package/dist/storage/implementation/v1/MongoBucketBatchV1.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/package.json +3 -3
- package/src/storage/MongoBucketStorage.ts +61 -123
- package/src/storage/implementation/MongoStorageProvider.ts +4 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +2 -5
- package/src/types/types.ts +6 -0
- package/test/src/storage_sync.test.ts +110 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -52,6 +52,8 @@ export interface MongoBucketStorageOptions {
|
|
|
52
52
|
*/
|
|
53
53
|
replicationStreamNamePrefix: string;
|
|
54
54
|
readPreference?: mongo.ReadPreference;
|
|
55
|
+
/** Read preference for collection discovery and collection stats used by storage metrics. */
|
|
56
|
+
statsReadPreference?: mongo.ReadPreference;
|
|
55
57
|
checksumCacheTtlMs?: number;
|
|
56
58
|
clearBatchThrottleRate?: number;
|
|
57
59
|
defaultStorageVersion?: number;
|
|
@@ -714,7 +716,12 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
714
716
|
|
|
715
717
|
if (storageConfig.incrementalReprocessing) {
|
|
716
718
|
const v3 = doc as ReplicationStreamDocumentV3;
|
|
717
|
-
|
|
719
|
+
// Cleanup of a stopped stream requires all embedded configs, regardless of
|
|
720
|
+
// their individual states, so every config's storage can be terminated.
|
|
721
|
+
const matching =
|
|
722
|
+
v3.state == storage.SyncRuleState.STOP && stateFilter.includes(storage.SyncRuleState.STOP)
|
|
723
|
+
? v3.sync_configs
|
|
724
|
+
: v3.sync_configs.filter((c) => stateFilter.includes(c.state));
|
|
718
725
|
if (matching.length == 0) {
|
|
719
726
|
return null;
|
|
720
727
|
}
|
|
@@ -828,89 +835,47 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
828
835
|
}
|
|
829
836
|
|
|
830
837
|
async getStorageMetrics(): Promise<storage.StorageMetrics> {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
const aggregateStaticCollection = async <T extends mongo.Document>(collection: mongo.Collection<T>) => {
|
|
841
|
-
// We check whether the collection exists before getting the statistics. This avoids repeated
|
|
842
|
-
// errors in the MongoDB logs if the collection hasn't been created yet.
|
|
843
|
-
const exists =
|
|
844
|
-
(await this.db.db.listCollections({ name: collection.collectionName }, { nameOnly: true }).toArray()).length >
|
|
845
|
-
0;
|
|
846
|
-
if (!exists) {
|
|
847
|
-
return [{ storageStats: { size: 0 } }];
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
return collection
|
|
851
|
-
.aggregate([
|
|
852
|
-
{
|
|
853
|
-
$collStats: {
|
|
854
|
-
storageStats: {}
|
|
855
|
-
}
|
|
838
|
+
// We check which collections exist before getting statistics to avoid repeated errors in
|
|
839
|
+
// MongoDB logs for collections that have not been created yet.
|
|
840
|
+
const collections = await this.db.db
|
|
841
|
+
.listCollections(
|
|
842
|
+
{
|
|
843
|
+
name: {
|
|
844
|
+
$regex: '^(bucket_data|bucket_parameters|current_data)$|^(bucket_data_|parameter_index_|source_records_)'
|
|
856
845
|
}
|
|
857
|
-
|
|
858
|
-
.
|
|
859
|
-
.catch(ignoreNotExisting);
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const operations_aggregate = await aggregateStaticCollection(this.db.bucket_data);
|
|
863
|
-
const v3OperationCollections = await this.db.listBucketDataCollectionsV3();
|
|
864
|
-
const v3_operation_aggregates = await Promise.all(
|
|
865
|
-
v3OperationCollections.map((collection) =>
|
|
866
|
-
collection
|
|
867
|
-
.aggregate([
|
|
868
|
-
{
|
|
869
|
-
$collStats: {
|
|
870
|
-
storageStats: {}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
])
|
|
874
|
-
.toArray()
|
|
875
|
-
.catch(ignoreNotExisting)
|
|
876
|
-
)
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
const parameters_aggregate = await aggregateStaticCollection(this.db.bucket_parameters);
|
|
880
|
-
|
|
881
|
-
const v3ParameterCollections = await this.db.listAllParameterIndexCollectionsV3();
|
|
882
|
-
const v3_parameter_aggregates = await Promise.all(
|
|
883
|
-
v3ParameterCollections.map((collection) =>
|
|
884
|
-
collection
|
|
885
|
-
.aggregate([
|
|
886
|
-
{
|
|
887
|
-
$collStats: {
|
|
888
|
-
storageStats: {}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
])
|
|
892
|
-
.toArray()
|
|
893
|
-
.catch(ignoreNotExisting)
|
|
894
|
-
)
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
const v1_source_record_aggregate = await aggregateStaticCollection(this.db.current_data);
|
|
898
|
-
|
|
899
|
-
const v3SourceRecordCollections = await this.db.listAllSourceRecordCollectionsV3();
|
|
900
|
-
const source_record_aggregates = await Promise.all(
|
|
901
|
-
v3SourceRecordCollections.map((collection) =>
|
|
902
|
-
collection
|
|
903
|
-
.aggregate([
|
|
904
|
-
{
|
|
905
|
-
$collStats: {
|
|
906
|
-
storageStats: {}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
])
|
|
910
|
-
.toArray()
|
|
911
|
-
.catch(ignoreNotExisting)
|
|
846
|
+
},
|
|
847
|
+
{ nameOnly: true, readPreference: this.options.statsReadPreference }
|
|
912
848
|
)
|
|
913
|
-
|
|
849
|
+
.toArray();
|
|
850
|
+
const sizesByCollection = new Map<string, number>();
|
|
851
|
+
const statsPipeline: mongo.Document[] = [
|
|
852
|
+
{ $collStats: { storageStats: {} } },
|
|
853
|
+
{ $project: { ns: 1, size: '$storageStats.size' } }
|
|
854
|
+
];
|
|
855
|
+
// MongoDB has a limit of 1000 stages per aggregation pipeline. Additionally, we want
|
|
856
|
+
// to avoid too much work per aggregation, so we limit the number of collections per batch.
|
|
857
|
+
const COLLECTIONS_PER_BATCH = 100;
|
|
858
|
+
// Query batches sequentially so metrics collection does not fill the connection pool on standby pods.
|
|
859
|
+
// If a collection disappears during aggregation, let the next metrics collection retry.
|
|
860
|
+
for (let offset = 0; offset < collections.length; offset += COLLECTIONS_PER_BATCH) {
|
|
861
|
+
const batch = collections.slice(offset, offset + COLLECTIONS_PER_BATCH);
|
|
862
|
+
const pipeline = [
|
|
863
|
+
...statsPipeline,
|
|
864
|
+
...batch.slice(1).map((collection) => ({
|
|
865
|
+
$unionWith: { coll: collection.name, pipeline: statsPipeline }
|
|
866
|
+
}))
|
|
867
|
+
];
|
|
868
|
+
const stats = await this.db.db
|
|
869
|
+
.collection(batch[0].name)
|
|
870
|
+
.aggregate<{ ns: string; size: number | bigint }>(pipeline, {
|
|
871
|
+
readPreference: this.options.statsReadPreference
|
|
872
|
+
})
|
|
873
|
+
.toArray();
|
|
874
|
+
for (const stat of stats) {
|
|
875
|
+
const name = stat.ns.slice(this.db.db.databaseName.length + 1);
|
|
876
|
+
sizesByCollection.set(name, (sizesByCollection.get(name) ?? 0) + Number(stat.size));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
914
879
|
|
|
915
880
|
const v3StorageConfig = getMongoStorageConfig(storage.STORAGE_VERSION_3) as StorageConfig & {
|
|
916
881
|
incrementalReprocessing: true;
|
|
@@ -922,29 +887,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
922
887
|
.find({ storage_version: { $gte: storage.STORAGE_VERSION_3 } }, { projection: { _id: 1, sync_configs: 1 } })
|
|
923
888
|
.toArray()) as unknown as Pick<ReplicationStreamDocumentV3, '_id' | 'sync_configs'>[];
|
|
924
889
|
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
collection: { collectionName: string },
|
|
928
|
-
aggregate: { storageStats?: { size?: number | bigint } }[],
|
|
929
|
-
prefix: string,
|
|
930
|
-
sizes: Map<string, number>
|
|
931
|
-
) => {
|
|
932
|
-
const match = collection.collectionName.match(new RegExp(`^${prefix}(\\d+)_(.+)$`));
|
|
933
|
-
if (match == null) {
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
sizes.set(`${prefix}${match[1]}:${match[2]}`, Number(aggregate[0]?.storageStats?.size ?? 0));
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
v3OperationCollections.forEach((collection, index) =>
|
|
940
|
-
addCollectionSize(collection, v3_operation_aggregates[index], 'bucket_data_', collectionSizes)
|
|
941
|
-
);
|
|
942
|
-
v3ParameterCollections.forEach((collection, index) =>
|
|
943
|
-
addCollectionSize(collection, v3_parameter_aggregates[index], 'parameter_index_', collectionSizes)
|
|
944
|
-
);
|
|
945
|
-
v3SourceRecordCollections.forEach((collection, index) =>
|
|
946
|
-
addCollectionSize(collection, source_record_aggregates[index], 'source_records_', collectionSizes)
|
|
947
|
-
);
|
|
890
|
+
const sumCollectionSizesByPrefix = (prefix: string) =>
|
|
891
|
+
[...sizesByCollection].reduce((total, [name, size]) => total + (name.startsWith(prefix) ? size : 0), 0);
|
|
948
892
|
|
|
949
893
|
const syncConfigIds = v3StreamDocs.flatMap((stream) => (stream.sync_configs ?? []).map((config) => config._id));
|
|
950
894
|
const syncConfigDefinitions =
|
|
@@ -961,15 +905,13 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
961
905
|
number,
|
|
962
906
|
{ _id: ObjectId; bucket_data_source_ids: string[]; parameter_lookup_source_ids: string[] }[]
|
|
963
907
|
>();
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
})
|
|
972
|
-
);
|
|
908
|
+
for (const stream of v3StreamDocs) {
|
|
909
|
+
const sourceTables = await v3Db
|
|
910
|
+
.sourceTables(stream._id)
|
|
911
|
+
.find({}, { projection: { _id: 1, bucket_data_source_ids: 1, parameter_lookup_source_ids: 1 } })
|
|
912
|
+
.toArray();
|
|
913
|
+
sourceTablesByStream.set(stream._id, sourceTables);
|
|
914
|
+
}
|
|
973
915
|
|
|
974
916
|
const objectStorageSizeByDefinition = new Map<string, number>();
|
|
975
917
|
for (const usage of objectStorageDefinitionUsage) {
|
|
@@ -980,7 +922,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
980
922
|
}
|
|
981
923
|
|
|
982
924
|
const sumCollectionSizes = (prefix: string, streamId: number, ids: ReadonlySet<string>) =>
|
|
983
|
-
[...ids].reduce((total, id) => total + (
|
|
925
|
+
[...ids].reduce((total, id) => total + (sizesByCollection.get(`${prefix}${streamId}_${id}`) ?? 0), 0);
|
|
984
926
|
|
|
985
927
|
const syncConfigMetrics: storage.StorageSyncConfigMetrics[] = [];
|
|
986
928
|
for (const stream of v3StreamDocs) {
|
|
@@ -1003,7 +945,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
1003
945
|
) {
|
|
1004
946
|
return total;
|
|
1005
947
|
}
|
|
1006
|
-
return total + (
|
|
948
|
+
return total + (sizesByCollection.get(`source_records_${stream._id}_${sourceTable._id.toHexString()}`) ?? 0);
|
|
1007
949
|
}, 0);
|
|
1008
950
|
|
|
1009
951
|
syncConfigMetrics.push({
|
|
@@ -1026,15 +968,11 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
|
|
|
1026
968
|
0n
|
|
1027
969
|
);
|
|
1028
970
|
return {
|
|
1029
|
-
operations_size_bytes:
|
|
1030
|
-
Number(operations_aggregate[0].storageStats.size) +
|
|
1031
|
-
v3_operation_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
|
|
971
|
+
operations_size_bytes: (sizesByCollection.get('bucket_data') ?? 0) + sumCollectionSizesByPrefix('bucket_data_'),
|
|
1032
972
|
parameters_size_bytes:
|
|
1033
|
-
|
|
1034
|
-
v3_parameter_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
|
|
973
|
+
(sizesByCollection.get('bucket_parameters') ?? 0) + sumCollectionSizesByPrefix('parameter_index_'),
|
|
1035
974
|
replication_size_bytes:
|
|
1036
|
-
|
|
1037
|
-
source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0),
|
|
975
|
+
(sizesByCollection.get('current_data') ?? 0) + sumCollectionSizesByPrefix('source_records_'),
|
|
1038
976
|
object_storage_size_bytes: Number(totalObjectStorageSize),
|
|
1039
977
|
sync_config_metrics: syncConfigMetrics
|
|
1040
978
|
};
|
|
@@ -78,6 +78,10 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
78
78
|
chunkCompactionConcurrency,
|
|
79
79
|
replicationStreamNamePrefix: resolvedConfig.slot_name_prefix,
|
|
80
80
|
readPreference,
|
|
81
|
+
statsReadPreference:
|
|
82
|
+
decodedConfig.stats_read_preference == null
|
|
83
|
+
? undefined
|
|
84
|
+
: new lib_mongo.mongo.ReadPreference(decodedConfig.stats_read_preference),
|
|
81
85
|
clearBatchThrottleRate: normalizeClearBatchThrottleRate(decodedConfig.clear_batch_throttle_rate),
|
|
82
86
|
checksumCacheTtlMs: resolvedConfig.api_parameters.bucket_count_cache_ttl_minutes * 60_000,
|
|
83
87
|
defaultStorageVersion: decodedConfig.default_storage_version,
|
|
@@ -10,6 +10,7 @@ import { MongoIdSequence, OpIdRangeExhausted } from '../MongoIdSequence.js';
|
|
|
10
10
|
import { MongoSyncRulesLock } from '../MongoSyncRulesLock.js';
|
|
11
11
|
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
12
12
|
import { OperationBatch } from '../OperationBatch.js';
|
|
13
|
+
import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
|
|
13
14
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
14
15
|
import { SyncRuleDocumentBase } from '../models.js';
|
|
15
16
|
import { PersistedBatchV1 } from './PersistedBatchV1.js';
|
|
@@ -853,11 +854,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
853
854
|
_id: { $ne: this.replicationStreamId },
|
|
854
855
|
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
855
856
|
},
|
|
856
|
-
|
|
857
|
-
$set: {
|
|
858
|
-
state: storage.SyncRuleState.STOP
|
|
859
|
-
}
|
|
860
|
-
},
|
|
857
|
+
stopReplicationStreamPipeline(),
|
|
861
858
|
{ session }
|
|
862
859
|
);
|
|
863
860
|
activated = true;
|
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.
|
|
@@ -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';
|
|
@@ -1951,6 +1957,24 @@ streams:
|
|
|
1951
1957
|
}
|
|
1952
1958
|
);
|
|
1953
1959
|
|
|
1960
|
+
test.runIf(storageVersion >= 3)('storage metrics collect sizes across union batches', async () => {
|
|
1961
|
+
await using factory = await storageConfig.factory();
|
|
1962
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
1963
|
+
const before = await factory.getStorageMetrics();
|
|
1964
|
+
let expectedSize = 0;
|
|
1965
|
+
for (let index = 0; index < 101; index++) {
|
|
1966
|
+
const collection = mongoFactory.db.db.collection(`bucket_data_999_metrics_${index}`);
|
|
1967
|
+
await collection.insertOne({ value: 'x'.repeat(index + 1) });
|
|
1968
|
+
const [stats] = await collection.aggregate([{ $collStats: { storageStats: {} } }]).toArray();
|
|
1969
|
+
expectedSize += Number(stats.storageStats.size);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
const after = await factory.getStorageMetrics();
|
|
1973
|
+
expect(after.operations_size_bytes - before.operations_size_bytes).toBe(expectedSize);
|
|
1974
|
+
expect(after.parameters_size_bytes).toBe(before.parameters_size_bytes);
|
|
1975
|
+
expect(after.replication_size_bytes).toBe(before.replication_size_bytes);
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1954
1978
|
test.runIf(storageVersion < 3)('storage metrics include v1 current_data', async () => {
|
|
1955
1979
|
await using factory = await storageConfig.factory();
|
|
1956
1980
|
const syncRules = await factory.updateSyncRules(
|
|
@@ -2160,6 +2184,91 @@ streams:
|
|
|
2160
2184
|
}
|
|
2161
2185
|
|
|
2162
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
|
+
|
|
2163
2272
|
test('v3 activation stops legacy active sync rules', async () => {
|
|
2164
2273
|
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
2165
2274
|
const mongoFactory = factory as MongoBucketStorage;
|