@powersync/service-module-mongodb-storage 0.15.4 → 0.16.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 +35 -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 +2 -2
- package/dist/storage/MongoBucketStorage.js +47 -34
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +16 -14
- package/dist/storage/implementation/MongoBucketBatch.js +80 -115
- 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 +28 -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 +64 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +47 -29
- package/dist/storage/implementation/MongoSyncBucketStorage.js +94 -387
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
- 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/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 +36 -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 +32 -35
- package/dist/storage/implementation/db.js +77 -99
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +62 -33
- 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 +13 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -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 +41 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -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 +34 -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 +13 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -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 +5 -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 +41 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -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 +21 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +43 -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 +6 -6
- package/src/storage/MongoBucketStorage.ts +92 -59
- package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
- package/src/storage/implementation/MongoBucketBatch.ts +110 -144
- package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
- package/src/storage/implementation/MongoChecksums.ts +52 -75
- package/src/storage/implementation/MongoCompactor.ts +374 -404
- package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
- package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +17 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +181 -455
- package/src/storage/implementation/MongoSyncRulesLock.ts +11 -13
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
- package/src/storage/implementation/OperationBatch.ts +1 -1
- 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 +49 -0
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
- package/src/storage/implementation/db.ts +105 -129
- package/src/storage/implementation/models.ts +82 -36
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -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 +448 -0
- package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -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 +84 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -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 +12 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -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 +112 -0
- package/src/storage/implementation/v3/models.ts +96 -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 +351 -5
- 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,10 @@
|
|
|
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';
|
|
2
|
+
import { BaseObserver, DO_NOT_LOG, ReplicationAbortedError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { BroadcastIterable, CHECKPOINT_INVALIDATE_ALL, maxLsn, mergeAsyncIterables, storage } from '@powersync/service-core';
|
|
5
4
|
import * as bson from 'bson';
|
|
6
5
|
import { LRUCache } from 'lru-cache';
|
|
7
6
|
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';
|
|
7
|
+
import { retryOnMongoMaxTimeMSExpired } from '../../utils/util.js';
|
|
13
8
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
14
9
|
/**
|
|
15
10
|
* Only keep checkpoints around for a minute, before fetching a fresh one.
|
|
@@ -26,11 +21,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
26
21
|
group_id;
|
|
27
22
|
sync_rules;
|
|
28
23
|
slot_name;
|
|
29
|
-
[DO_NOT_LOG] = true;
|
|
30
24
|
db;
|
|
25
|
+
[DO_NOT_LOG] = true;
|
|
31
26
|
checksums;
|
|
32
27
|
parsedSyncRulesCache;
|
|
33
28
|
writeCheckpointAPI;
|
|
29
|
+
logger;
|
|
30
|
+
#storageInitialized = false;
|
|
34
31
|
constructor(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options) {
|
|
35
32
|
super();
|
|
36
33
|
this.factory = factory;
|
|
@@ -38,19 +35,27 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
38
35
|
this.sync_rules = sync_rules;
|
|
39
36
|
this.slot_name = slot_name;
|
|
40
37
|
this.db = factory.db.versioned(sync_rules.getStorageConfig());
|
|
41
|
-
this.checksums =
|
|
42
|
-
...options.checksumOptions,
|
|
43
|
-
storageConfig: options?.storageConfig
|
|
44
|
-
});
|
|
38
|
+
this.checksums = this.createMongoChecksums(options);
|
|
45
39
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
46
40
|
db: this.db,
|
|
47
41
|
mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
|
|
48
42
|
sync_rules_id: group_id
|
|
49
43
|
});
|
|
44
|
+
this.logger = sync_rules.logger;
|
|
50
45
|
}
|
|
51
46
|
get writeCheckpointMode() {
|
|
52
47
|
return this.writeCheckpointAPI.writeCheckpointMode;
|
|
53
48
|
}
|
|
49
|
+
get mapping() {
|
|
50
|
+
return this.sync_rules.mapping;
|
|
51
|
+
}
|
|
52
|
+
get versionContext() {
|
|
53
|
+
return {
|
|
54
|
+
db: this.db,
|
|
55
|
+
group_id: this.group_id,
|
|
56
|
+
mapping: this.mapping
|
|
57
|
+
};
|
|
58
|
+
}
|
|
54
59
|
setWriteCheckpointMode(mode) {
|
|
55
60
|
this.writeCheckpointAPI.setWriteCheckpointMode(mode);
|
|
56
61
|
}
|
|
@@ -65,10 +70,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
65
70
|
}
|
|
66
71
|
getParsedSyncRules(options) {
|
|
67
72
|
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
73
|
if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
|
|
73
74
|
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncRules(), options };
|
|
74
75
|
}
|
|
@@ -84,36 +85,34 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
84
85
|
projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
|
|
85
86
|
});
|
|
86
87
|
if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
|
|
87
|
-
// Sync rules not active - return null
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
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
90
|
const snapshotTime = session.snapshotTime;
|
|
100
91
|
if (snapshotTime == null) {
|
|
101
92
|
throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
|
|
102
93
|
}
|
|
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);
|
|
94
|
+
return new MongoReplicationCheckpoint(this, doc.last_checkpoint ?? 0n, doc.last_checkpoint_lsn ?? null, snapshotTime);
|
|
106
95
|
});
|
|
107
96
|
}
|
|
97
|
+
async initializeStorage() {
|
|
98
|
+
if (this.#storageInitialized) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await this.db.initializeStreamStorage(this.group_id);
|
|
102
|
+
await this.initializeVersionStorage();
|
|
103
|
+
this.#storageInitialized = true;
|
|
104
|
+
}
|
|
108
105
|
async createWriter(options) {
|
|
106
|
+
await this.initializeStorage();
|
|
109
107
|
const doc = await this.db.sync_rules.findOne({
|
|
110
108
|
_id: this.group_id
|
|
111
109
|
}, { projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1, snapshot_lsn: 1 } });
|
|
112
110
|
const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
|
|
113
|
-
const
|
|
114
|
-
logger: options.logger,
|
|
111
|
+
const batchOptions = {
|
|
112
|
+
logger: options.logger ?? this.logger,
|
|
115
113
|
db: this.db,
|
|
116
114
|
syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
|
|
115
|
+
mapping: this.sync_rules.mapping,
|
|
117
116
|
groupId: this.group_id,
|
|
118
117
|
slotName: this.slot_name,
|
|
119
118
|
lastCheckpointLsn: checkpoint_lsn,
|
|
@@ -121,14 +120,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
121
120
|
keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
|
|
122
121
|
storeCurrentData: options.storeCurrentData,
|
|
123
122
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
124
|
-
markRecordUnavailable: options.markRecordUnavailable
|
|
125
|
-
|
|
123
|
+
markRecordUnavailable: options.markRecordUnavailable,
|
|
124
|
+
tracer: options.tracer
|
|
125
|
+
};
|
|
126
|
+
const writer = this.createWriterImpl(batchOptions);
|
|
126
127
|
this.iterateListeners((cb) => cb.batchStarted?.(writer));
|
|
127
128
|
return writer;
|
|
128
129
|
}
|
|
129
|
-
/**
|
|
130
|
-
* @deprecated Use `createWriter()` with `await using` instead.
|
|
131
|
-
*/
|
|
132
130
|
async startBatch(options, callback) {
|
|
133
131
|
await using writer = await this.createWriter(options);
|
|
134
132
|
await callback(writer);
|
|
@@ -144,10 +142,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
144
142
|
type_oid: column.typeId
|
|
145
143
|
}));
|
|
146
144
|
let result = null;
|
|
145
|
+
let initializeSourceRecordsFor = null;
|
|
146
|
+
const baseId = this.sourceTableBaseId();
|
|
147
147
|
await this.db.client.withSession(async (session) => {
|
|
148
|
-
const col = this.db.
|
|
148
|
+
const col = this.db.commonSourceTables(group_id);
|
|
149
149
|
let filter = {
|
|
150
|
-
|
|
150
|
+
...baseId,
|
|
151
151
|
connection_id: connection_id,
|
|
152
152
|
schema_name: schema,
|
|
153
153
|
table_name: name,
|
|
@@ -158,9 +158,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
158
158
|
}
|
|
159
159
|
let doc = await col.findOne(filter, { session });
|
|
160
160
|
if (doc == null) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
const candidateSourceTable = new storage.SourceTable({
|
|
162
|
+
id: new bson.ObjectId(),
|
|
163
|
+
connectionTag: connection_tag,
|
|
164
|
+
objectId: objectId,
|
|
165
|
+
schema: schema,
|
|
166
|
+
name: name,
|
|
167
|
+
replicaIdColumns: replicaIdColumns,
|
|
168
|
+
snapshotComplete: false
|
|
169
|
+
});
|
|
170
|
+
const createDoc = {
|
|
171
|
+
_id: candidateSourceTable.id,
|
|
172
|
+
...baseId,
|
|
164
173
|
connection_id: connection_id,
|
|
165
174
|
relation_id: objectId,
|
|
166
175
|
schema_name: schema,
|
|
@@ -170,7 +179,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
170
179
|
snapshot_done: false,
|
|
171
180
|
snapshot_status: undefined
|
|
172
181
|
};
|
|
182
|
+
this.augmentCreatedSourceTableDocument(createDoc, options, candidateSourceTable);
|
|
183
|
+
doc = createDoc;
|
|
173
184
|
await col.insertOne(doc, { session });
|
|
185
|
+
initializeSourceRecordsFor = doc._id;
|
|
174
186
|
}
|
|
175
187
|
const sourceTable = new storage.SourceTable({
|
|
176
188
|
id: doc._id,
|
|
@@ -193,15 +205,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
193
205
|
replicatedCount: doc.snapshot_status.replicated_count
|
|
194
206
|
};
|
|
195
207
|
let dropTables = [];
|
|
196
|
-
// Detect tables that are either renamed, or have different replica_id_columns
|
|
197
208
|
let truncateFilter = [{ schema_name: schema, table_name: name }];
|
|
198
209
|
if (objectId != null) {
|
|
199
|
-
// Only detect renames if the source uses relation ids.
|
|
200
210
|
truncateFilter.push({ relation_id: objectId });
|
|
201
211
|
}
|
|
202
212
|
const truncate = await col
|
|
203
213
|
.find({
|
|
204
|
-
|
|
214
|
+
...baseId,
|
|
205
215
|
connection_id: connection_id,
|
|
206
216
|
_id: { $ne: doc._id },
|
|
207
217
|
$or: truncateFilter
|
|
@@ -221,192 +231,16 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
221
231
|
dropTables: dropTables
|
|
222
232
|
};
|
|
223
233
|
});
|
|
234
|
+
if (initializeSourceRecordsFor != null) {
|
|
235
|
+
await this.initializeResolvedSourceRecords(initializeSourceRecordsFor);
|
|
236
|
+
}
|
|
224
237
|
return result;
|
|
225
238
|
}
|
|
226
|
-
async getParameterSets(checkpoint, lookups) {
|
|
227
|
-
return this.
|
|
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
|
-
});
|
|
239
|
+
async getParameterSets(checkpoint, lookups, limit) {
|
|
240
|
+
return this.getParameterSetsImpl(checkpoint, lookups, limit);
|
|
284
241
|
}
|
|
285
242
|
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
|
-
}
|
|
243
|
+
yield* this.getBucketDataBatchImpl(checkpoint, dataBuckets, options);
|
|
410
244
|
}
|
|
411
245
|
async getChecksums(checkpoint, buckets) {
|
|
412
246
|
return this.checksums.getChecksums(checkpoint, buckets);
|
|
@@ -415,7 +249,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
415
249
|
this.checksums.clearCache();
|
|
416
250
|
}
|
|
417
251
|
async terminate(options) {
|
|
418
|
-
// Default is to clear the storage except when explicitly requested not to.
|
|
419
252
|
if (!options || options?.clearStorage) {
|
|
420
253
|
await this.clear(options);
|
|
421
254
|
}
|
|
@@ -442,7 +275,7 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
442
275
|
}
|
|
443
276
|
});
|
|
444
277
|
if (doc == null) {
|
|
445
|
-
throw new ServiceAssertionError('Cannot find
|
|
278
|
+
throw new ServiceAssertionError('Cannot find replication stream status');
|
|
446
279
|
}
|
|
447
280
|
return {
|
|
448
281
|
snapshot_done: doc.snapshot_done,
|
|
@@ -452,29 +285,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
452
285
|
};
|
|
453
286
|
}
|
|
454
287
|
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
|
-
}
|
|
288
|
+
const signal = options?.signal;
|
|
289
|
+
if (signal?.aborted) {
|
|
290
|
+
throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
|
|
473
291
|
}
|
|
474
|
-
}
|
|
475
|
-
async clearIteration() {
|
|
476
|
-
// Individual operations here may time out with the maxTimeMS option.
|
|
477
|
-
// It is expected to still make progress, and continue on the next try.
|
|
478
292
|
await this.db.sync_rules.updateOne({
|
|
479
293
|
_id: this.group_id
|
|
480
294
|
}, {
|
|
@@ -489,21 +303,22 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
489
303
|
snapshot_lsn: 1
|
|
490
304
|
}
|
|
491
305
|
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
492
|
-
await this.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
await this.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
306
|
+
await this.clearBucketData(signal);
|
|
307
|
+
await this.clearParameterIndexes(signal);
|
|
308
|
+
await this.clearSourceRecords(signal);
|
|
309
|
+
await this.clearBucketState(signal);
|
|
310
|
+
await this.clearSourceTables(signal);
|
|
311
|
+
this.#storageInitialized = false;
|
|
312
|
+
}
|
|
313
|
+
async clearDeleteMany(label, operation, signal) {
|
|
314
|
+
await retryOnMongoMaxTimeMSExpired(operation, {
|
|
315
|
+
signal,
|
|
316
|
+
abortMessage: 'Aborted clearing data',
|
|
317
|
+
retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5,
|
|
318
|
+
onRetry: () => {
|
|
319
|
+
this.logger.info(`Cleared batch of ${label} in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
507
322
|
}
|
|
508
323
|
async reportError(e) {
|
|
509
324
|
const message = String(e.message ?? 'Replication failure');
|
|
@@ -522,83 +337,52 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
522
337
|
const checkpoint = await this.getCheckpointInternal();
|
|
523
338
|
maxOpId = checkpoint?.checkpoint ?? undefined;
|
|
524
339
|
}
|
|
525
|
-
await
|
|
340
|
+
await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
|
|
526
341
|
if (maxOpId != null && options?.compactParameterData) {
|
|
527
|
-
await
|
|
342
|
+
await this.createMongoParameterCompactor(maxOpId, options).compact();
|
|
528
343
|
}
|
|
529
344
|
}
|
|
530
345
|
async populatePersistentChecksumCache(options) {
|
|
531
|
-
logger.info(`Populating persistent checksum cache...`);
|
|
346
|
+
this.logger.info(`Populating persistent checksum cache...`);
|
|
532
347
|
const start = Date.now();
|
|
533
|
-
|
|
534
|
-
// We can optimize this in the future.
|
|
535
|
-
const compactor = new MongoCompactor(this, this.db, {
|
|
348
|
+
const compactor = this.createMongoCompactor({
|
|
536
349
|
...options,
|
|
537
|
-
|
|
538
|
-
|
|
350
|
+
memoryLimitMB: 0,
|
|
351
|
+
logger: this.logger
|
|
539
352
|
});
|
|
540
353
|
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
354
|
minBucketChanges: options.minBucketChanges ?? 10
|
|
544
355
|
});
|
|
545
356
|
const duration = Date.now() - start;
|
|
546
|
-
logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
|
|
357
|
+
this.logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
|
|
547
358
|
return result;
|
|
548
359
|
}
|
|
549
|
-
/**
|
|
550
|
-
* Instance-wide watch on the latest available checkpoint (op_id + lsn).
|
|
551
|
-
*/
|
|
552
360
|
async *watchActiveCheckpoint(signal) {
|
|
553
361
|
if (signal.aborted) {
|
|
554
362
|
return;
|
|
555
363
|
}
|
|
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
364
|
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
365
|
for await (const _ of stream) {
|
|
564
366
|
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
367
|
break;
|
|
567
368
|
}
|
|
568
369
|
const op = await this.getCheckpointInternal();
|
|
569
370
|
if (op == null) {
|
|
570
|
-
// Sync rules have changed - abort and restart.
|
|
571
|
-
// We do a soft close of the stream here - no error
|
|
572
371
|
break;
|
|
573
372
|
}
|
|
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
373
|
yield op;
|
|
579
374
|
}
|
|
580
375
|
}
|
|
581
|
-
// Nothing is done here until a subscriber starts to iterate
|
|
582
376
|
sharedIter = new BroadcastIterable((signal) => {
|
|
583
377
|
return this.watchActiveCheckpoint(signal);
|
|
584
378
|
});
|
|
585
|
-
/**
|
|
586
|
-
* User-specific watch on the latest checkpoint and/or write checkpoint.
|
|
587
|
-
*/
|
|
588
379
|
async *watchCheckpointChanges(options) {
|
|
589
380
|
let lastCheckpoint = null;
|
|
590
381
|
const iter = this.sharedIter[Symbol.asyncIterator](options.signal);
|
|
591
382
|
let writeCheckpoint = null;
|
|
592
|
-
// true if we queried the initial write checkpoint, even if it doesn't exist
|
|
593
383
|
let queriedInitialWriteCheckpoint = false;
|
|
594
384
|
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
385
|
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
386
|
writeCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
603
387
|
sync_rules_id: this.group_id,
|
|
604
388
|
user_id: options.user_id,
|
|
@@ -611,14 +395,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
611
395
|
if (lastCheckpoint != null &&
|
|
612
396
|
lastCheckpoint.checkpoint == nextCheckpoint.checkpoint &&
|
|
613
397
|
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
398
|
await timers.setTimeout(20 + 10 * Math.random());
|
|
618
399
|
continue;
|
|
619
400
|
}
|
|
620
401
|
if (lastCheckpoint == null) {
|
|
621
|
-
// First message for this stream - "INVALIDATE_ALL" means it will lookup all data
|
|
622
402
|
yield {
|
|
623
403
|
base: nextCheckpoint,
|
|
624
404
|
writeCheckpoint,
|
|
@@ -632,8 +412,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
632
412
|
});
|
|
633
413
|
let updatedWriteCheckpoint = updates.updatedWriteCheckpoints.get(options.user_id) ?? null;
|
|
634
414
|
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
415
|
updatedWriteCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
638
416
|
sync_rules_id: this.group_id,
|
|
639
417
|
user_id: options.user_id,
|
|
@@ -644,8 +422,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
644
422
|
}
|
|
645
423
|
if (updatedWriteCheckpoint != null && (writeCheckpoint == null || updatedWriteCheckpoint > writeCheckpoint)) {
|
|
646
424
|
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
425
|
queriedInitialWriteCheckpoint = true;
|
|
650
426
|
}
|
|
651
427
|
yield {
|
|
@@ -662,12 +438,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
662
438
|
lastCheckpoint = nextCheckpoint;
|
|
663
439
|
}
|
|
664
440
|
}
|
|
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
441
|
async *checkpointChangesStream(signal) {
|
|
672
442
|
if (signal.aborted) {
|
|
673
443
|
return;
|
|
@@ -679,16 +449,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
679
449
|
signal.addEventListener('abort', () => {
|
|
680
450
|
cursor.close().catch(() => { });
|
|
681
451
|
});
|
|
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
452
|
yield;
|
|
685
453
|
try {
|
|
686
454
|
while (!signal.aborted) {
|
|
687
455
|
const doc = await cursor.tryNext().catch((e) => {
|
|
688
456
|
if (lib_mongo.isMongoServerError(e) && e.codeName === 'CappedPositionLost') {
|
|
689
|
-
// Cursor position lost, potentially due to a high rate of notifications
|
|
690
457
|
cursor = query();
|
|
691
|
-
// Treat as an event found, before querying the new cursor again
|
|
692
458
|
return {};
|
|
693
459
|
}
|
|
694
460
|
else {
|
|
@@ -698,8 +464,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
698
464
|
if (cursor.closed) {
|
|
699
465
|
return;
|
|
700
466
|
}
|
|
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
467
|
cursor.readBufferedDocuments();
|
|
704
468
|
if (doc != null) {
|
|
705
469
|
yield;
|
|
@@ -723,7 +487,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
723
487
|
}
|
|
724
488
|
catch (e) {
|
|
725
489
|
if (e.name == 'AbortError') {
|
|
726
|
-
// This is how we typically abort this stream, when all listeners are done
|
|
727
490
|
return;
|
|
728
491
|
}
|
|
729
492
|
throw e;
|
|
@@ -734,74 +497,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
734
497
|
}
|
|
735
498
|
}
|
|
736
499
|
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
|
-
};
|
|
500
|
+
return this.getDataBucketChangesImpl(options);
|
|
760
501
|
}
|
|
761
502
|
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
|
-
};
|
|
503
|
+
return this.getParameterBucketChangesImpl(options);
|
|
785
504
|
}
|
|
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
505
|
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
506
|
max: 50,
|
|
799
507
|
maxSize: 12 * 1024 * 1024,
|
|
800
508
|
sizeCalculation: (value) => {
|
|
801
|
-
// Estimate of memory usage
|
|
802
509
|
const paramSize = [...value.updatedParameterLookups].reduce((a, b) => a + b.length, 0);
|
|
803
510
|
const bucketSize = [...value.updatedDataBuckets].reduce((a, b) => a + b.length, 0);
|
|
804
|
-
const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
|
|
511
|
+
const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
|
|
805
512
|
return 100 + paramSize + bucketSize + writeCheckpointSize;
|
|
806
513
|
},
|
|
807
514
|
fetchMethod: async (_key, _staleValue, options) => {
|
|
@@ -835,14 +542,14 @@ class MongoReplicationCheckpoint {
|
|
|
835
542
|
this.snapshotTime = snapshotTime;
|
|
836
543
|
this.#storage = storage;
|
|
837
544
|
}
|
|
838
|
-
async getParameterSets(lookups) {
|
|
839
|
-
return this.#storage.getParameterSets(this, lookups);
|
|
545
|
+
async getParameterSets(lookups, limit) {
|
|
546
|
+
return this.#storage.getParameterSets(this, lookups, limit);
|
|
840
547
|
}
|
|
841
548
|
}
|
|
842
549
|
class EmptyReplicationCheckpoint {
|
|
843
550
|
checkpoint = 0n;
|
|
844
551
|
lsn = null;
|
|
845
|
-
async getParameterSets(
|
|
552
|
+
async getParameterSets(_lookups) {
|
|
846
553
|
return [];
|
|
847
554
|
}
|
|
848
555
|
}
|