@powersync/service-module-mongodb-storage 0.15.4 → 0.17.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 +69 -0
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +2 -2
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +8 -6
- package/dist/storage/MongoBucketStorage.js +153 -66
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +15 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/CheckpointState.d.ts +20 -0
- package/dist/storage/implementation/CheckpointState.js +31 -0
- package/dist/storage/implementation/CheckpointState.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +48 -35
- package/dist/storage/implementation/MongoBucketBatch.js +118 -379
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
- package/dist/storage/implementation/MongoChecksums.d.ts +29 -17
- package/dist/storage/implementation/MongoChecksums.js +13 -72
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
- package/dist/storage/implementation/MongoCompactor.js +229 -296
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js +67 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +22 -5
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +56 -13
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +61 -32
- package/dist/storage/implementation/MongoSyncBucketStorage.js +85 -523
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncRulesLock.js +19 -13
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
- package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
- package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
- package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
- package/dist/storage/implementation/common/PersistedBatch.js +237 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
- package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
- package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +35 -0
- package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
- package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
- package/dist/storage/implementation/db.d.ts +41 -36
- package/dist/storage/implementation/db.js +77 -99
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +79 -66
- package/dist/storage/implementation/models.js +20 -1
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +27 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +407 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +50 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +354 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +25 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
- package/dist/storage/implementation/v1/models.d.ts +45 -0
- package/dist/storage/implementation/v1/models.js +37 -0
- package/dist/storage/implementation/v1/models.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +30 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +463 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +4 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +63 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +508 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +28 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +22 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +74 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +101 -0
- package/dist/storage/implementation/v3/models.js +34 -0
- package/dist/storage/implementation/v3/models.js.map +1 -0
- package/dist/storage/storage-index.d.ts +6 -3
- package/dist/storage/storage-index.js +6 -3
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/util.d.ts +10 -3
- package/dist/utils/util.js +24 -3
- package/dist/utils/util.js.map +1 -1
- package/package.json +9 -9
- package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
- package/src/storage/MongoBucketStorage.ts +254 -99
- package/src/storage/implementation/BucketDefinitionMapping.ts +75 -0
- package/src/storage/implementation/CheckpointState.ts +59 -0
- package/src/storage/implementation/MongoBucketBatch.ts +182 -490
- package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
- package/src/storage/implementation/MongoChecksums.ts +53 -75
- package/src/storage/implementation/MongoCompactor.ts +374 -404
- package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
- package/src/storage/implementation/MongoPersistedSyncRules.ts +82 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +78 -16
- package/src/storage/implementation/MongoSyncBucketStorage.ts +179 -628
- package/src/storage/implementation/MongoSyncRulesLock.ts +20 -16
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
- package/src/storage/implementation/OperationBatch.ts +1 -1
- package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
- package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
- package/src/storage/implementation/common/PersistedBatch.ts +364 -0
- package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +48 -0
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
- package/src/storage/implementation/db.ts +110 -131
- package/src/storage/implementation/models.ts +102 -79
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +509 -0
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +543 -0
- package/src/storage/implementation/v1/PersistedBatchV1.ts +229 -0
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
- package/src/storage/implementation/v1/models.ts +99 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +607 -0
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
- package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +11 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +678 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +317 -0
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +117 -0
- package/src/storage/implementation/v3/models.ts +164 -0
- package/src/storage/storage-index.ts +6 -3
- package/src/utils/util.ts +34 -5
- package/test/src/storage_compacting.test.ts +57 -29
- package/test/src/storage_sync.test.ts +767 -5
- package/test/src/storeCurrentData.test.ts +211 -0
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
- package/dist/storage/implementation/PersistedBatch.js +0 -354
- package/dist/storage/implementation/PersistedBatch.js.map +0 -1
- package/src/storage/implementation/PersistedBatch.ts +0 -432
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { BaseObserver, DO_NOT_LOG,
|
|
3
|
-
import { BroadcastIterable, CHECKPOINT_INVALIDATE_ALL,
|
|
4
|
-
import { JSONBig } from '@powersync/service-jsonbig';
|
|
5
|
-
import * as bson from 'bson';
|
|
2
|
+
import { BaseObserver, DO_NOT_LOG, ReplicationAbortedError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { BroadcastIterable, CHECKPOINT_INVALIDATE_ALL, mergeAsyncIterables, storage } from '@powersync/service-core';
|
|
6
4
|
import { LRUCache } from 'lru-cache';
|
|
7
5
|
import * as timers from 'timers/promises';
|
|
8
|
-
import {
|
|
9
|
-
import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
10
|
-
import { MongoChecksums } from './MongoChecksums.js';
|
|
11
|
-
import { MongoCompactor } from './MongoCompactor.js';
|
|
12
|
-
import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
6
|
+
import { retryOnMongoMaxTimeMSExpired } from '../../utils/util.js';
|
|
13
7
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
14
8
|
/**
|
|
15
9
|
* Only keep checkpoints around for a minute, before fetching a fresh one.
|
|
@@ -26,31 +20,43 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
26
20
|
group_id;
|
|
27
21
|
sync_rules;
|
|
28
22
|
slot_name;
|
|
29
|
-
[DO_NOT_LOG] = true;
|
|
30
23
|
db;
|
|
24
|
+
[DO_NOT_LOG] = true;
|
|
31
25
|
checksums;
|
|
32
26
|
parsedSyncRulesCache;
|
|
33
27
|
writeCheckpointAPI;
|
|
28
|
+
logger;
|
|
29
|
+
storageConfig;
|
|
30
|
+
#storageInitialized = false;
|
|
34
31
|
constructor(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options) {
|
|
35
32
|
super();
|
|
36
33
|
this.factory = factory;
|
|
37
34
|
this.group_id = group_id;
|
|
38
35
|
this.sync_rules = sync_rules;
|
|
39
36
|
this.slot_name = slot_name;
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
storageConfig: options?.storageConfig
|
|
44
|
-
});
|
|
37
|
+
this.storageConfig = options.storageConfig;
|
|
38
|
+
this.db = factory.db.versioned(this.storageConfig);
|
|
39
|
+
this.checksums = this.createMongoChecksums(options);
|
|
45
40
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
46
41
|
db: this.db,
|
|
47
42
|
mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
|
|
48
43
|
sync_rules_id: group_id
|
|
49
44
|
});
|
|
45
|
+
this.logger = sync_rules.logger;
|
|
50
46
|
}
|
|
51
47
|
get writeCheckpointMode() {
|
|
52
48
|
return this.writeCheckpointAPI.writeCheckpointMode;
|
|
53
49
|
}
|
|
50
|
+
get mapping() {
|
|
51
|
+
return this.sync_rules.mapping;
|
|
52
|
+
}
|
|
53
|
+
get versionContext() {
|
|
54
|
+
return {
|
|
55
|
+
db: this.db,
|
|
56
|
+
group_id: this.group_id,
|
|
57
|
+
mapping: this.mapping
|
|
58
|
+
};
|
|
59
|
+
}
|
|
54
60
|
setWriteCheckpointMode(mode) {
|
|
55
61
|
this.writeCheckpointAPI.setWriteCheckpointMode(mode);
|
|
56
62
|
}
|
|
@@ -65,12 +71,8 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
65
71
|
}
|
|
66
72
|
getParsedSyncRules(options) {
|
|
67
73
|
const { parsed, options: cachedOptions } = this.parsedSyncRulesCache ?? {};
|
|
68
|
-
/**
|
|
69
|
-
* Check if the cached sync rules, if present, had the same options.
|
|
70
|
-
* Parse sync rules if the options are different or if there is no cached value.
|
|
71
|
-
*/
|
|
72
74
|
if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
|
|
73
|
-
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).
|
|
75
|
+
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncConfig(), options };
|
|
74
76
|
}
|
|
75
77
|
return this.parsedSyncRulesCache.parsed;
|
|
76
78
|
}
|
|
@@ -79,334 +81,60 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
79
81
|
}
|
|
80
82
|
async getCheckpointInternal() {
|
|
81
83
|
return await this.db.client.withSession({ snapshot: true }, async (session) => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
|
|
85
|
-
});
|
|
86
|
-
if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
|
|
87
|
-
// Sync rules not active - return null
|
|
84
|
+
const state = await this.fetchCheckpointState(session);
|
|
85
|
+
if (state == null) {
|
|
88
86
|
return null;
|
|
89
87
|
}
|
|
90
|
-
// Specifically using operationTime instead of clusterTime
|
|
91
|
-
// There are 3 fields in the response:
|
|
92
|
-
// 1. operationTime, not exposed for snapshot sessions (used for causal consistency)
|
|
93
|
-
// 2. clusterTime (used for connection management)
|
|
94
|
-
// 3. atClusterTime, which is session.snapshotTime
|
|
95
|
-
// We use atClusterTime, to match the driver's internal snapshot handling.
|
|
96
|
-
// There are cases where clusterTime > operationTime and atClusterTime,
|
|
97
|
-
// which could cause snapshot queries using this as the snapshotTime to timeout.
|
|
98
|
-
// This was specifically observed on MongoDB 6.0 and 7.0.
|
|
99
88
|
const snapshotTime = session.snapshotTime;
|
|
100
89
|
if (snapshotTime == null) {
|
|
101
90
|
throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
|
|
102
91
|
}
|
|
103
|
-
return new MongoReplicationCheckpoint(this,
|
|
104
|
-
// null/0n is a valid checkpoint in some cases, for example if the initial snapshot was empty
|
|
105
|
-
doc.last_checkpoint ?? 0n, doc.last_checkpoint_lsn ?? null, snapshotTime);
|
|
92
|
+
return new MongoReplicationCheckpoint(this, state.checkpoint, state.lsn, snapshotTime);
|
|
106
93
|
});
|
|
107
94
|
}
|
|
95
|
+
async initializeStorage() {
|
|
96
|
+
if (this.#storageInitialized) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await this.db.initializeStreamStorage(this.group_id);
|
|
100
|
+
await this.initializeVersionStorage();
|
|
101
|
+
this.#storageInitialized = true;
|
|
102
|
+
}
|
|
108
103
|
async createWriter(options) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const writer = new MongoBucketBatch({
|
|
114
|
-
logger: options.logger,
|
|
104
|
+
await this.initializeStorage();
|
|
105
|
+
const state = await this.getWriterSyncState();
|
|
106
|
+
const batchOptions = {
|
|
107
|
+
logger: options.logger ?? this.logger,
|
|
115
108
|
db: this.db,
|
|
116
|
-
syncRules: this.sync_rules.parsed(options).
|
|
109
|
+
syncRules: this.sync_rules.parsed(options).hydratedSyncConfig(),
|
|
110
|
+
mapping: this.sync_rules.mapping,
|
|
117
111
|
groupId: this.group_id,
|
|
118
112
|
slotName: this.slot_name,
|
|
119
|
-
lastCheckpointLsn:
|
|
120
|
-
resumeFromLsn:
|
|
121
|
-
keepaliveOp:
|
|
113
|
+
lastCheckpointLsn: state.lastCheckpointLsn,
|
|
114
|
+
resumeFromLsn: state.resumeFromLsn,
|
|
115
|
+
keepaliveOp: state.keepaliveOp,
|
|
122
116
|
storeCurrentData: options.storeCurrentData,
|
|
123
117
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
124
|
-
markRecordUnavailable: options.markRecordUnavailable
|
|
125
|
-
|
|
118
|
+
markRecordUnavailable: options.markRecordUnavailable,
|
|
119
|
+
hooks: options.hooks,
|
|
120
|
+
syncConfigId: state.syncConfigId,
|
|
121
|
+
tracer: options.tracer
|
|
122
|
+
};
|
|
123
|
+
const writer = this.createWriterImpl(batchOptions);
|
|
126
124
|
this.iterateListeners((cb) => cb.batchStarted?.(writer));
|
|
127
125
|
return writer;
|
|
128
126
|
}
|
|
129
|
-
/**
|
|
130
|
-
* @deprecated Use `createWriter()` with `await using` instead.
|
|
131
|
-
*/
|
|
132
127
|
async startBatch(options, callback) {
|
|
133
128
|
await using writer = await this.createWriter(options);
|
|
134
129
|
await callback(writer);
|
|
135
130
|
await writer.flush();
|
|
136
131
|
return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
|
|
137
132
|
}
|
|
138
|
-
async
|
|
139
|
-
|
|
140
|
-
const { schema, name, objectId, replicaIdColumns } = entity_descriptor;
|
|
141
|
-
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
142
|
-
name: column.name,
|
|
143
|
-
type: column.type,
|
|
144
|
-
type_oid: column.typeId
|
|
145
|
-
}));
|
|
146
|
-
let result = null;
|
|
147
|
-
await this.db.client.withSession(async (session) => {
|
|
148
|
-
const col = this.db.source_tables;
|
|
149
|
-
let filter = {
|
|
150
|
-
group_id: group_id,
|
|
151
|
-
connection_id: connection_id,
|
|
152
|
-
schema_name: schema,
|
|
153
|
-
table_name: name,
|
|
154
|
-
replica_id_columns2: normalizedReplicaIdColumns
|
|
155
|
-
};
|
|
156
|
-
if (objectId != null) {
|
|
157
|
-
filter.relation_id = objectId;
|
|
158
|
-
}
|
|
159
|
-
let doc = await col.findOne(filter, { session });
|
|
160
|
-
if (doc == null) {
|
|
161
|
-
doc = {
|
|
162
|
-
_id: new bson.ObjectId(),
|
|
163
|
-
group_id: group_id,
|
|
164
|
-
connection_id: connection_id,
|
|
165
|
-
relation_id: objectId,
|
|
166
|
-
schema_name: schema,
|
|
167
|
-
table_name: name,
|
|
168
|
-
replica_id_columns: null,
|
|
169
|
-
replica_id_columns2: normalizedReplicaIdColumns,
|
|
170
|
-
snapshot_done: false,
|
|
171
|
-
snapshot_status: undefined
|
|
172
|
-
};
|
|
173
|
-
await col.insertOne(doc, { session });
|
|
174
|
-
}
|
|
175
|
-
const sourceTable = new storage.SourceTable({
|
|
176
|
-
id: doc._id,
|
|
177
|
-
connectionTag: connection_tag,
|
|
178
|
-
objectId: objectId,
|
|
179
|
-
schema: schema,
|
|
180
|
-
name: name,
|
|
181
|
-
replicaIdColumns: replicaIdColumns,
|
|
182
|
-
snapshotComplete: doc.snapshot_done ?? true
|
|
183
|
-
});
|
|
184
|
-
sourceTable.syncEvent = options.sync_rules.tableTriggersEvent(sourceTable);
|
|
185
|
-
sourceTable.syncData = options.sync_rules.tableSyncsData(sourceTable);
|
|
186
|
-
sourceTable.syncParameters = options.sync_rules.tableSyncsParameters(sourceTable);
|
|
187
|
-
sourceTable.snapshotStatus =
|
|
188
|
-
doc.snapshot_status == null
|
|
189
|
-
? undefined
|
|
190
|
-
: {
|
|
191
|
-
lastKey: doc.snapshot_status.last_key?.buffer ?? null,
|
|
192
|
-
totalEstimatedCount: doc.snapshot_status.total_estimated_count,
|
|
193
|
-
replicatedCount: doc.snapshot_status.replicated_count
|
|
194
|
-
};
|
|
195
|
-
let dropTables = [];
|
|
196
|
-
// Detect tables that are either renamed, or have different replica_id_columns
|
|
197
|
-
let truncateFilter = [{ schema_name: schema, table_name: name }];
|
|
198
|
-
if (objectId != null) {
|
|
199
|
-
// Only detect renames if the source uses relation ids.
|
|
200
|
-
truncateFilter.push({ relation_id: objectId });
|
|
201
|
-
}
|
|
202
|
-
const truncate = await col
|
|
203
|
-
.find({
|
|
204
|
-
group_id: group_id,
|
|
205
|
-
connection_id: connection_id,
|
|
206
|
-
_id: { $ne: doc._id },
|
|
207
|
-
$or: truncateFilter
|
|
208
|
-
}, { session })
|
|
209
|
-
.toArray();
|
|
210
|
-
dropTables = truncate.map((doc) => new storage.SourceTable({
|
|
211
|
-
id: doc._id,
|
|
212
|
-
connectionTag: connection_tag,
|
|
213
|
-
objectId: doc.relation_id,
|
|
214
|
-
schema: doc.schema_name,
|
|
215
|
-
name: doc.table_name,
|
|
216
|
-
replicaIdColumns: doc.replica_id_columns2?.map((c) => ({ name: c.name, typeOid: c.type_oid, type: c.type })) ?? [],
|
|
217
|
-
snapshotComplete: doc.snapshot_done ?? true
|
|
218
|
-
}));
|
|
219
|
-
result = {
|
|
220
|
-
table: sourceTable,
|
|
221
|
-
dropTables: dropTables
|
|
222
|
-
};
|
|
223
|
-
});
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
async getParameterSets(checkpoint, lookups) {
|
|
227
|
-
return this.db.client.withSession({ snapshot: true }, async (session) => {
|
|
228
|
-
// Set the session's snapshot time to the checkpoint's snapshot time.
|
|
229
|
-
// An alternative would be to create the session when the checkpoint is created, but managing
|
|
230
|
-
// the session lifetime would become more complex.
|
|
231
|
-
// Starting and ending sessions are cheap (synchronous when no transactions are used),
|
|
232
|
-
// so this should be fine.
|
|
233
|
-
// This is a roundabout way of setting {readConcern: {atClusterTime: clusterTime}}, since
|
|
234
|
-
// that is not exposed directly by the driver.
|
|
235
|
-
// Future versions of the driver may change the snapshotTime behavior, so we need tests to
|
|
236
|
-
// validate that this works as expected. We test this in the compacting tests.
|
|
237
|
-
setSessionSnapshotTime(session, checkpoint.snapshotTime);
|
|
238
|
-
const lookupFilter = lookups.map((lookup) => {
|
|
239
|
-
return storage.serializeLookup(lookup);
|
|
240
|
-
});
|
|
241
|
-
// This query does not use indexes super efficiently, apart from the lookup filter.
|
|
242
|
-
// From some experimentation I could do individual lookups more efficient using an index
|
|
243
|
-
// on {'key.g': 1, lookup: 1, 'key.t': 1, 'key.k': 1, _id: -1},
|
|
244
|
-
// but could not do the same using $group.
|
|
245
|
-
// For now, just rely on compacting to remove extraneous data.
|
|
246
|
-
// For a description of the data format, see the `/docs/parameters-lookups.md` file.
|
|
247
|
-
const rows = await this.db.bucket_parameters
|
|
248
|
-
.aggregate([
|
|
249
|
-
{
|
|
250
|
-
$match: {
|
|
251
|
-
'key.g': this.group_id,
|
|
252
|
-
lookup: { $in: lookupFilter },
|
|
253
|
-
_id: { $lte: checkpoint.checkpoint }
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
$sort: {
|
|
258
|
-
_id: -1
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
$group: {
|
|
263
|
-
_id: { key: '$key', lookup: '$lookup' },
|
|
264
|
-
bucket_parameters: {
|
|
265
|
-
$first: '$bucket_parameters'
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
], {
|
|
270
|
-
session,
|
|
271
|
-
readConcern: 'snapshot',
|
|
272
|
-
// Limit the time for the operation to complete, to avoid getting connection timeouts
|
|
273
|
-
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
|
|
274
|
-
})
|
|
275
|
-
.toArray()
|
|
276
|
-
.catch((e) => {
|
|
277
|
-
throw lib_mongo.mapQueryError(e, 'while evaluating parameter queries');
|
|
278
|
-
});
|
|
279
|
-
const groupedParameters = rows.map((row) => {
|
|
280
|
-
return row.bucket_parameters;
|
|
281
|
-
});
|
|
282
|
-
return groupedParameters.flat();
|
|
283
|
-
});
|
|
133
|
+
async getParameterSets(checkpoint, lookups, limit) {
|
|
134
|
+
return this.getParameterSetsImpl(checkpoint, lookups, limit);
|
|
284
135
|
}
|
|
285
136
|
async *getBucketDataBatch(checkpoint, dataBuckets, options) {
|
|
286
|
-
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
let filters = [];
|
|
290
|
-
const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
291
|
-
if (checkpoint == null) {
|
|
292
|
-
throw new ServiceAssertionError('checkpoint is null');
|
|
293
|
-
}
|
|
294
|
-
const end = checkpoint;
|
|
295
|
-
for (let { bucket: name, start } of dataBuckets) {
|
|
296
|
-
filters.push({
|
|
297
|
-
_id: {
|
|
298
|
-
$gt: {
|
|
299
|
-
g: this.group_id,
|
|
300
|
-
b: name,
|
|
301
|
-
o: start
|
|
302
|
-
},
|
|
303
|
-
$lte: {
|
|
304
|
-
g: this.group_id,
|
|
305
|
-
b: name,
|
|
306
|
-
o: end
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
// Internal naming:
|
|
312
|
-
// We do a query for one "batch", which may consist of multiple "chunks".
|
|
313
|
-
// Each chunk is limited to single bucket, and is limited in length and size.
|
|
314
|
-
// There are also overall batch length and size limits.
|
|
315
|
-
const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
|
|
316
|
-
const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
|
|
317
|
-
const cursor = this.db.bucket_data.find({
|
|
318
|
-
$or: filters
|
|
319
|
-
}, {
|
|
320
|
-
session: undefined,
|
|
321
|
-
sort: { _id: 1 },
|
|
322
|
-
limit: batchLimit,
|
|
323
|
-
// Increase batch size above the default 101, so that we can fill an entire batch in
|
|
324
|
-
// one go.
|
|
325
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
326
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
327
|
-
batchSize: batchLimit + 1,
|
|
328
|
-
// Raw mode is returns an array of Buffer instead of parsed documents.
|
|
329
|
-
// We use it so that:
|
|
330
|
-
// 1. We can calculate the document size accurately without serializing again.
|
|
331
|
-
// 2. We can delay parsing the results until it's needed.
|
|
332
|
-
// We manually use bson.deserialize below
|
|
333
|
-
raw: true,
|
|
334
|
-
// Limit the time for the operation to complete, to avoid getting connection timeouts
|
|
335
|
-
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
|
|
336
|
-
});
|
|
337
|
-
// We want to limit results to a single batch to avoid high memory usage.
|
|
338
|
-
// This approach uses MongoDB's batch limits to limit the data here, which limits
|
|
339
|
-
// to the lower of the batch count and size limits.
|
|
340
|
-
// This is similar to using `singleBatch: true` in the find options, but allows
|
|
341
|
-
// detecting "hasMore".
|
|
342
|
-
let { data, hasMore: batchHasMore } = await readSingleBatch(cursor).catch((e) => {
|
|
343
|
-
throw lib_mongo.mapQueryError(e, 'while reading bucket data');
|
|
344
|
-
});
|
|
345
|
-
if (data.length == batchLimit) {
|
|
346
|
-
// Limit reached - could have more data, despite the cursor being drained.
|
|
347
|
-
batchHasMore = true;
|
|
348
|
-
}
|
|
349
|
-
let chunkSizeBytes = 0;
|
|
350
|
-
let currentChunk = null;
|
|
351
|
-
let targetOp = null;
|
|
352
|
-
// Ordered by _id, meaning buckets are grouped together
|
|
353
|
-
for (let rawData of data) {
|
|
354
|
-
const row = bson.deserialize(rawData, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS);
|
|
355
|
-
const bucket = row._id.b;
|
|
356
|
-
if (currentChunk == null || currentChunk.bucket != bucket || chunkSizeBytes >= chunkSizeLimitBytes) {
|
|
357
|
-
// We need to start a new chunk
|
|
358
|
-
let start = undefined;
|
|
359
|
-
if (currentChunk != null) {
|
|
360
|
-
// There is an existing chunk we need to yield
|
|
361
|
-
if (currentChunk.bucket == bucket) {
|
|
362
|
-
// Current and new chunk have the same bucket, so need has_more on the current one.
|
|
363
|
-
// If currentChunk.bucket != bucket, then we reached the end of the previous bucket,
|
|
364
|
-
// and has_more = false in that case.
|
|
365
|
-
currentChunk.has_more = true;
|
|
366
|
-
start = currentChunk.next_after;
|
|
367
|
-
}
|
|
368
|
-
const yieldChunk = currentChunk;
|
|
369
|
-
currentChunk = null;
|
|
370
|
-
chunkSizeBytes = 0;
|
|
371
|
-
yield { chunkData: yieldChunk, targetOp: targetOp };
|
|
372
|
-
targetOp = null;
|
|
373
|
-
}
|
|
374
|
-
if (start == null) {
|
|
375
|
-
const startOpId = bucketMap.get(bucket);
|
|
376
|
-
if (startOpId == null) {
|
|
377
|
-
throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
|
|
378
|
-
}
|
|
379
|
-
start = internalToExternalOpId(startOpId);
|
|
380
|
-
}
|
|
381
|
-
currentChunk = {
|
|
382
|
-
bucket,
|
|
383
|
-
after: start,
|
|
384
|
-
has_more: false,
|
|
385
|
-
data: [],
|
|
386
|
-
next_after: start
|
|
387
|
-
};
|
|
388
|
-
targetOp = null;
|
|
389
|
-
}
|
|
390
|
-
const entry = mapOpEntry(row);
|
|
391
|
-
if (row.target_op != null) {
|
|
392
|
-
// MOVE, CLEAR
|
|
393
|
-
if (targetOp == null || row.target_op > targetOp) {
|
|
394
|
-
targetOp = row.target_op;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
currentChunk.data.push(entry);
|
|
398
|
-
currentChunk.next_after = entry.op_id;
|
|
399
|
-
chunkSizeBytes += rawData.byteLength;
|
|
400
|
-
}
|
|
401
|
-
if (currentChunk != null) {
|
|
402
|
-
const yieldChunk = currentChunk;
|
|
403
|
-
currentChunk = null;
|
|
404
|
-
// This is the final chunk in the batch.
|
|
405
|
-
// There may be more data if and only if the batch we retrieved isn't complete.
|
|
406
|
-
yieldChunk.has_more = batchHasMore;
|
|
407
|
-
yield { chunkData: yieldChunk, targetOp: targetOp };
|
|
408
|
-
targetOp = null;
|
|
409
|
-
}
|
|
137
|
+
yield* this.getBucketDataBatchImpl(checkpoint, dataBuckets, options);
|
|
410
138
|
}
|
|
411
139
|
async getChecksums(checkpoint, buckets) {
|
|
412
140
|
return this.checksums.getChecksums(checkpoint, buckets);
|
|
@@ -415,95 +143,37 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
415
143
|
this.checksums.clearCache();
|
|
416
144
|
}
|
|
417
145
|
async terminate(options) {
|
|
418
|
-
// Default is to clear the storage except when explicitly requested not to.
|
|
419
146
|
if (!options || options?.clearStorage) {
|
|
420
147
|
await this.clear(options);
|
|
421
148
|
}
|
|
422
|
-
await this.
|
|
423
|
-
_id: this.group_id
|
|
424
|
-
}, {
|
|
425
|
-
$set: {
|
|
426
|
-
state: storage.SyncRuleState.TERMINATED,
|
|
427
|
-
persisted_lsn: null,
|
|
428
|
-
snapshot_done: false
|
|
429
|
-
}
|
|
430
|
-
});
|
|
149
|
+
await this.terminateSyncRuleState();
|
|
431
150
|
await this.db.notifyCheckpoint();
|
|
432
151
|
}
|
|
433
152
|
async getStatus() {
|
|
434
|
-
|
|
435
|
-
_id: this.group_id
|
|
436
|
-
}, {
|
|
437
|
-
projection: {
|
|
438
|
-
snapshot_done: 1,
|
|
439
|
-
last_checkpoint_lsn: 1,
|
|
440
|
-
state: 1,
|
|
441
|
-
snapshot_lsn: 1
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
if (doc == null) {
|
|
445
|
-
throw new ServiceAssertionError('Cannot find sync rules status');
|
|
446
|
-
}
|
|
447
|
-
return {
|
|
448
|
-
snapshot_done: doc.snapshot_done,
|
|
449
|
-
snapshot_lsn: doc.snapshot_lsn ?? null,
|
|
450
|
-
active: doc.state == 'ACTIVE',
|
|
451
|
-
checkpoint_lsn: doc.last_checkpoint_lsn
|
|
452
|
-
};
|
|
153
|
+
return this.getStatusImpl();
|
|
453
154
|
}
|
|
454
155
|
async clear(options) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
try {
|
|
460
|
-
await this.clearIteration();
|
|
461
|
-
logger.info(`${this.slot_name} Done clearing data`);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
catch (e) {
|
|
465
|
-
if (lib_mongo.isMongoServerError(e) && e.codeName == 'MaxTimeMSExpired') {
|
|
466
|
-
logger.info(`${this.slot_name} Cleared batch of data in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`);
|
|
467
|
-
await timers.setTimeout(lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5);
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
throw e;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
156
|
+
const signal = options?.signal;
|
|
157
|
+
if (signal?.aborted) {
|
|
158
|
+
throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
|
|
473
159
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await this.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
snapshot_lsn: 1
|
|
160
|
+
await this.clearSyncRuleState();
|
|
161
|
+
await this.clearBucketData(signal);
|
|
162
|
+
await this.clearParameterIndexes(signal);
|
|
163
|
+
await this.clearSourceRecords(signal);
|
|
164
|
+
await this.clearBucketState(signal);
|
|
165
|
+
await this.clearSourceTables(signal);
|
|
166
|
+
this.#storageInitialized = false;
|
|
167
|
+
}
|
|
168
|
+
async clearDeleteMany(label, operation, signal) {
|
|
169
|
+
await retryOnMongoMaxTimeMSExpired(operation, {
|
|
170
|
+
signal,
|
|
171
|
+
abortMessage: 'Aborted clearing data',
|
|
172
|
+
retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5,
|
|
173
|
+
onRetry: () => {
|
|
174
|
+
this.logger.info(`Cleared batch of ${label} in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`);
|
|
490
175
|
}
|
|
491
|
-
}
|
|
492
|
-
await this.db.bucket_data.deleteMany({
|
|
493
|
-
_id: idPrefixFilter({ g: this.group_id }, ['b', 'o'])
|
|
494
|
-
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
495
|
-
await this.db.bucket_parameters.deleteMany({
|
|
496
|
-
'key.g': this.group_id
|
|
497
|
-
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
498
|
-
await this.db.common_current_data.deleteMany({
|
|
499
|
-
_id: idPrefixFilter({ g: this.group_id }, ['t', 'k'])
|
|
500
|
-
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
501
|
-
await this.db.bucket_state.deleteMany({
|
|
502
|
-
_id: idPrefixFilter({ g: this.group_id }, ['b'])
|
|
503
|
-
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
504
|
-
await this.db.source_tables.deleteMany({
|
|
505
|
-
group_id: this.group_id
|
|
506
|
-
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
176
|
+
});
|
|
507
177
|
}
|
|
508
178
|
async reportError(e) {
|
|
509
179
|
const message = String(e.message ?? 'Replication failure');
|
|
@@ -522,83 +192,52 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
522
192
|
const checkpoint = await this.getCheckpointInternal();
|
|
523
193
|
maxOpId = checkpoint?.checkpoint ?? undefined;
|
|
524
194
|
}
|
|
525
|
-
await
|
|
195
|
+
await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
|
|
526
196
|
if (maxOpId != null && options?.compactParameterData) {
|
|
527
|
-
await
|
|
197
|
+
await this.createMongoParameterCompactor(maxOpId, options).compact();
|
|
528
198
|
}
|
|
529
199
|
}
|
|
530
200
|
async populatePersistentChecksumCache(options) {
|
|
531
|
-
logger.info(`Populating persistent checksum cache...`);
|
|
201
|
+
this.logger.info(`Populating persistent checksum cache...`);
|
|
532
202
|
const start = Date.now();
|
|
533
|
-
|
|
534
|
-
// We can optimize this in the future.
|
|
535
|
-
const compactor = new MongoCompactor(this, this.db, {
|
|
203
|
+
const compactor = this.createMongoCompactor({
|
|
536
204
|
...options,
|
|
537
|
-
|
|
538
|
-
|
|
205
|
+
memoryLimitMB: 0,
|
|
206
|
+
logger: this.logger
|
|
539
207
|
});
|
|
540
208
|
const result = await compactor.populateChecksums({
|
|
541
|
-
// There are cases with millions of small buckets, in which case it can take very long to
|
|
542
|
-
// populate the checksums, with minimal benefit. We skip the small buckets here.
|
|
543
209
|
minBucketChanges: options.minBucketChanges ?? 10
|
|
544
210
|
});
|
|
545
211
|
const duration = Date.now() - start;
|
|
546
|
-
logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
|
|
212
|
+
this.logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
|
|
547
213
|
return result;
|
|
548
214
|
}
|
|
549
|
-
/**
|
|
550
|
-
* Instance-wide watch on the latest available checkpoint (op_id + lsn).
|
|
551
|
-
*/
|
|
552
215
|
async *watchActiveCheckpoint(signal) {
|
|
553
216
|
if (signal.aborted) {
|
|
554
217
|
return;
|
|
555
218
|
}
|
|
556
|
-
// If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS) before we get another checkpoint,
|
|
557
|
-
// to avoid stale checkpoint snapshots. This is what checkpointTimeoutStream() is for.
|
|
558
|
-
// Essentially, even if there are no actual checkpoint changes, we want a new snapshotTime every minute or so,
|
|
559
|
-
// to ensure that any new clients connecting will get a valid snapshotTime.
|
|
560
219
|
const stream = mergeAsyncIterables([this.checkpointChangesStream(signal), this.checkpointTimeoutStream(signal)], signal);
|
|
561
|
-
// We only watch changes to the active sync rules.
|
|
562
|
-
// If it changes to inactive, we abort and restart with the new sync rules.
|
|
563
220
|
for await (const _ of stream) {
|
|
564
221
|
if (signal.aborted) {
|
|
565
|
-
// Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
|
|
566
222
|
break;
|
|
567
223
|
}
|
|
568
224
|
const op = await this.getCheckpointInternal();
|
|
569
225
|
if (op == null) {
|
|
570
|
-
// Sync rules have changed - abort and restart.
|
|
571
|
-
// We do a soft close of the stream here - no error
|
|
572
226
|
break;
|
|
573
227
|
}
|
|
574
|
-
// Previously, we only yielded when the checkpoint or lsn changed.
|
|
575
|
-
// However, we always want to use the latest snapshotTime, so we skip that filtering here.
|
|
576
|
-
// That filtering could be added in the per-user streams if needed, but in general the capped collection
|
|
577
|
-
// should already only contain useful changes in most cases.
|
|
578
228
|
yield op;
|
|
579
229
|
}
|
|
580
230
|
}
|
|
581
|
-
// Nothing is done here until a subscriber starts to iterate
|
|
582
231
|
sharedIter = new BroadcastIterable((signal) => {
|
|
583
232
|
return this.watchActiveCheckpoint(signal);
|
|
584
233
|
});
|
|
585
|
-
/**
|
|
586
|
-
* User-specific watch on the latest checkpoint and/or write checkpoint.
|
|
587
|
-
*/
|
|
588
234
|
async *watchCheckpointChanges(options) {
|
|
589
235
|
let lastCheckpoint = null;
|
|
590
236
|
const iter = this.sharedIter[Symbol.asyncIterator](options.signal);
|
|
591
237
|
let writeCheckpoint = null;
|
|
592
|
-
// true if we queried the initial write checkpoint, even if it doesn't exist
|
|
593
238
|
let queriedInitialWriteCheckpoint = false;
|
|
594
239
|
for await (const nextCheckpoint of iter) {
|
|
595
|
-
// lsn changes are not important by itself.
|
|
596
|
-
// What is important is:
|
|
597
|
-
// 1. checkpoint (op_id) changes.
|
|
598
|
-
// 2. write checkpoint changes for the specific user
|
|
599
240
|
if (nextCheckpoint.lsn != null && !queriedInitialWriteCheckpoint) {
|
|
600
|
-
// Lookup the first write checkpoint for the user when we can.
|
|
601
|
-
// There will not actually be one in all cases.
|
|
602
241
|
writeCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
603
242
|
sync_rules_id: this.group_id,
|
|
604
243
|
user_id: options.user_id,
|
|
@@ -611,14 +250,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
611
250
|
if (lastCheckpoint != null &&
|
|
612
251
|
lastCheckpoint.checkpoint == nextCheckpoint.checkpoint &&
|
|
613
252
|
lastCheckpoint.lsn == nextCheckpoint.lsn) {
|
|
614
|
-
// No change - wait for next one
|
|
615
|
-
// In some cases, many LSNs may be produced in a short time.
|
|
616
|
-
// Add a delay to throttle the loop a bit.
|
|
617
253
|
await timers.setTimeout(20 + 10 * Math.random());
|
|
618
254
|
continue;
|
|
619
255
|
}
|
|
620
256
|
if (lastCheckpoint == null) {
|
|
621
|
-
// First message for this stream - "INVALIDATE_ALL" means it will lookup all data
|
|
622
257
|
yield {
|
|
623
258
|
base: nextCheckpoint,
|
|
624
259
|
writeCheckpoint,
|
|
@@ -632,8 +267,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
632
267
|
});
|
|
633
268
|
let updatedWriteCheckpoint = updates.updatedWriteCheckpoints.get(options.user_id) ?? null;
|
|
634
269
|
if (updates.invalidateWriteCheckpoints) {
|
|
635
|
-
// Invalidated means there were too many updates to track the individual ones,
|
|
636
|
-
// so we switch to "polling" (querying directly in each stream).
|
|
637
270
|
updatedWriteCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
638
271
|
sync_rules_id: this.group_id,
|
|
639
272
|
user_id: options.user_id,
|
|
@@ -644,8 +277,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
644
277
|
}
|
|
645
278
|
if (updatedWriteCheckpoint != null && (writeCheckpoint == null || updatedWriteCheckpoint > writeCheckpoint)) {
|
|
646
279
|
writeCheckpoint = updatedWriteCheckpoint;
|
|
647
|
-
// If it happened that we haven't queried a write checkpoint at this point,
|
|
648
|
-
// then we don't need to anymore, since we got an updated one.
|
|
649
280
|
queriedInitialWriteCheckpoint = true;
|
|
650
281
|
}
|
|
651
282
|
yield {
|
|
@@ -662,12 +293,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
662
293
|
lastCheckpoint = nextCheckpoint;
|
|
663
294
|
}
|
|
664
295
|
}
|
|
665
|
-
/**
|
|
666
|
-
* This watches the checkpoint_events capped collection for new documents inserted,
|
|
667
|
-
* and yields whenever one or more documents are inserted.
|
|
668
|
-
*
|
|
669
|
-
* The actual checkpoint must be queried on the sync_rules collection after this.
|
|
670
|
-
*/
|
|
671
296
|
async *checkpointChangesStream(signal) {
|
|
672
297
|
if (signal.aborted) {
|
|
673
298
|
return;
|
|
@@ -679,16 +304,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
679
304
|
signal.addEventListener('abort', () => {
|
|
680
305
|
cursor.close().catch(() => { });
|
|
681
306
|
});
|
|
682
|
-
// Yield once on start, regardless of whether there are documents in the cursor.
|
|
683
|
-
// This is to ensure that the first iteration of the generator yields immediately.
|
|
684
307
|
yield;
|
|
685
308
|
try {
|
|
686
309
|
while (!signal.aborted) {
|
|
687
310
|
const doc = await cursor.tryNext().catch((e) => {
|
|
688
311
|
if (lib_mongo.isMongoServerError(e) && e.codeName === 'CappedPositionLost') {
|
|
689
|
-
// Cursor position lost, potentially due to a high rate of notifications
|
|
690
312
|
cursor = query();
|
|
691
|
-
// Treat as an event found, before querying the new cursor again
|
|
692
313
|
return {};
|
|
693
314
|
}
|
|
694
315
|
else {
|
|
@@ -698,8 +319,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
698
319
|
if (cursor.closed) {
|
|
699
320
|
return;
|
|
700
321
|
}
|
|
701
|
-
// Skip buffered documents, if any. We don't care about the contents,
|
|
702
|
-
// we only want to know when new documents are inserted.
|
|
703
322
|
cursor.readBufferedDocuments();
|
|
704
323
|
if (doc != null) {
|
|
705
324
|
yield;
|
|
@@ -723,7 +342,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
723
342
|
}
|
|
724
343
|
catch (e) {
|
|
725
344
|
if (e.name == 'AbortError') {
|
|
726
|
-
// This is how we typically abort this stream, when all listeners are done
|
|
727
345
|
return;
|
|
728
346
|
}
|
|
729
347
|
throw e;
|
|
@@ -734,74 +352,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
734
352
|
}
|
|
735
353
|
}
|
|
736
354
|
async getDataBucketChanges(options) {
|
|
737
|
-
|
|
738
|
-
const bucketStateUpdates = await this.db.bucket_state
|
|
739
|
-
.find({
|
|
740
|
-
// We have an index on (_id.g, last_op).
|
|
741
|
-
'_id.g': this.group_id,
|
|
742
|
-
last_op: { $gt: options.lastCheckpoint.checkpoint }
|
|
743
|
-
}, {
|
|
744
|
-
projection: {
|
|
745
|
-
'_id.b': 1
|
|
746
|
-
},
|
|
747
|
-
limit: limit + 1,
|
|
748
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
749
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
750
|
-
batchSize: limit + 2,
|
|
751
|
-
singleBatch: true
|
|
752
|
-
})
|
|
753
|
-
.toArray();
|
|
754
|
-
const buckets = bucketStateUpdates.map((doc) => doc._id.b);
|
|
755
|
-
const invalidateDataBuckets = buckets.length > limit;
|
|
756
|
-
return {
|
|
757
|
-
invalidateDataBuckets: invalidateDataBuckets,
|
|
758
|
-
updatedDataBuckets: invalidateDataBuckets ? new Set() : new Set(buckets)
|
|
759
|
-
};
|
|
355
|
+
return this.getDataBucketChangesImpl(options);
|
|
760
356
|
}
|
|
761
357
|
async getParameterBucketChanges(options) {
|
|
762
|
-
|
|
763
|
-
const parameterUpdates = await this.db.bucket_parameters
|
|
764
|
-
.find({
|
|
765
|
-
_id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint },
|
|
766
|
-
'key.g': this.group_id
|
|
767
|
-
}, {
|
|
768
|
-
projection: {
|
|
769
|
-
lookup: 1
|
|
770
|
-
},
|
|
771
|
-
limit: limit + 1,
|
|
772
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
773
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
774
|
-
batchSize: limit + 2,
|
|
775
|
-
singleBatch: true
|
|
776
|
-
})
|
|
777
|
-
.toArray();
|
|
778
|
-
const invalidateParameterUpdates = parameterUpdates.length > limit;
|
|
779
|
-
return {
|
|
780
|
-
invalidateParameterBuckets: invalidateParameterUpdates,
|
|
781
|
-
updatedParameterLookups: invalidateParameterUpdates
|
|
782
|
-
? new Set()
|
|
783
|
-
: new Set(parameterUpdates.map((p) => JSONBig.stringify(deserializeParameterLookup(p.lookup))))
|
|
784
|
-
};
|
|
358
|
+
return this.getParameterBucketChangesImpl(options);
|
|
785
359
|
}
|
|
786
|
-
// If we processed all connections together for each checkpoint, we could do a single lookup for all connections.
|
|
787
|
-
// In practice, specific connections may fall behind. So instead, we just cache the results of each specific lookup.
|
|
788
|
-
// TODO (later):
|
|
789
|
-
// We can optimize this by implementing it like ChecksumCache: We can use partial cache results to do
|
|
790
|
-
// more efficient lookups in some cases.
|
|
791
360
|
checkpointChangesCache = new LRUCache({
|
|
792
|
-
// Limit to 50 cache entries, or 10MB, whichever comes first.
|
|
793
|
-
// Some rough calculations:
|
|
794
|
-
// If we process 10 checkpoints per second, and a connection may be 2 seconds behind, we could have
|
|
795
|
-
// up to 20 relevant checkpoints. That gives us 20*20 = 400 potentially-relevant cache entries.
|
|
796
|
-
// That is a worst-case scenario, so we don't actually store that many. In real life, the cache keys
|
|
797
|
-
// would likely be clustered around a few values, rather than spread over all 400 potential values.
|
|
798
361
|
max: 50,
|
|
799
362
|
maxSize: 12 * 1024 * 1024,
|
|
800
363
|
sizeCalculation: (value) => {
|
|
801
|
-
// Estimate of memory usage
|
|
802
364
|
const paramSize = [...value.updatedParameterLookups].reduce((a, b) => a + b.length, 0);
|
|
803
365
|
const bucketSize = [...value.updatedDataBuckets].reduce((a, b) => a + b.length, 0);
|
|
804
|
-
const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
|
|
366
|
+
const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
|
|
805
367
|
return 100 + paramSize + bucketSize + writeCheckpointSize;
|
|
806
368
|
},
|
|
807
369
|
fetchMethod: async (_key, _staleValue, options) => {
|
|
@@ -835,14 +397,14 @@ class MongoReplicationCheckpoint {
|
|
|
835
397
|
this.snapshotTime = snapshotTime;
|
|
836
398
|
this.#storage = storage;
|
|
837
399
|
}
|
|
838
|
-
async getParameterSets(lookups) {
|
|
839
|
-
return this.#storage.getParameterSets(this, lookups);
|
|
400
|
+
async getParameterSets(lookups, limit) {
|
|
401
|
+
return this.#storage.getParameterSets(this, lookups, limit);
|
|
840
402
|
}
|
|
841
403
|
}
|
|
842
404
|
class EmptyReplicationCheckpoint {
|
|
843
405
|
checkpoint = 0n;
|
|
844
406
|
lsn = null;
|
|
845
|
-
async getParameterSets(
|
|
407
|
+
async getParameterSets(_lookups) {
|
|
846
408
|
return [];
|
|
847
409
|
}
|
|
848
410
|
}
|