@powersync/service-module-mongodb-storage 0.0.0-dev-20260313100403 → 0.0.0-dev-20260515144844
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 +96 -8
- 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 +3 -3
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +5 -3
- package/dist/storage/MongoBucketStorage.js +50 -36
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/MongoReportStorage.d.ts +3 -3
- package/dist/storage/MongoReportStorage.js +5 -5
- package/dist/storage/MongoReportStorage.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 -52
- package/dist/storage/implementation/MongoCompactor.js +256 -295
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +24 -9
- 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/MongoStorageProvider.js +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +49 -30
- package/dist/storage/implementation/MongoSyncBucketStorage.js +100 -456
- 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 +34 -34
- package/dist/storage/implementation/db.js +78 -98
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +65 -36
- package/dist/storage/implementation/models.js +21 -2
- 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 +8 -5
- package/dist/storage/storage-index.js +8 -5
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/util.d.ts +11 -4
- package/dist/utils/util.js +25 -4
- 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/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
- package/src/storage/MongoBucketStorage.ts +97 -62
- package/src/storage/MongoReportStorage.ts +7 -7
- 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 +53 -76
- package/src/storage/implementation/MongoCompactor.ts +401 -403
- package/src/storage/implementation/MongoParameterCompactor.ts +51 -25
- package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +18 -1
- package/src/storage/implementation/MongoStorageProvider.ts +1 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +190 -457
- package/src/storage/implementation/MongoSyncRulesLock.ts +12 -14
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
- 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 +107 -128
- package/src/storage/implementation/models.ts +86 -40
- 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 +8 -5
- package/src/utils/util.ts +36 -7
- package/test/src/__snapshots__/{client-connections-storage.test.ts.snap → connection-report-storage.test.ts.snap} +68 -68
- package/test/src/__snapshots__/storage_sync.test.ts.snap +282 -0
- package/test/src/{client-connections-storage.test.ts → connection-report-storage.test.ts} +4 -4
- package/test/src/setup.ts +1 -1
- package/test/src/storage.test.ts +2 -2
- package/test/src/storage_compacting.test.ts +94 -3
- 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,67 +1,10 @@
|
|
|
1
|
-
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
-
if (value !== null && value !== void 0) {
|
|
3
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
-
var dispose, inner;
|
|
5
|
-
if (async) {
|
|
6
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
-
dispose = value[Symbol.asyncDispose];
|
|
8
|
-
}
|
|
9
|
-
if (dispose === void 0) {
|
|
10
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
-
dispose = value[Symbol.dispose];
|
|
12
|
-
if (async) inner = dispose;
|
|
13
|
-
}
|
|
14
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
-
}
|
|
18
|
-
else if (async) {
|
|
19
|
-
env.stack.push({ async: true });
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
};
|
|
23
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
-
return function (env) {
|
|
25
|
-
function fail(e) {
|
|
26
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
-
env.hasError = true;
|
|
28
|
-
}
|
|
29
|
-
var r, s = 0;
|
|
30
|
-
function next() {
|
|
31
|
-
while (r = env.stack.pop()) {
|
|
32
|
-
try {
|
|
33
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
-
if (r.dispose) {
|
|
35
|
-
var result = r.dispose.call(r.value);
|
|
36
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
-
}
|
|
38
|
-
else s |= 1;
|
|
39
|
-
}
|
|
40
|
-
catch (e) {
|
|
41
|
-
fail(e);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
-
if (env.hasError) throw env.error;
|
|
46
|
-
}
|
|
47
|
-
return next();
|
|
48
|
-
};
|
|
49
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
-
var e = new Error(message);
|
|
51
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
-
});
|
|
53
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
54
|
-
import { BaseObserver,
|
|
55
|
-
import { BroadcastIterable, CHECKPOINT_INVALIDATE_ALL,
|
|
56
|
-
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';
|
|
57
4
|
import * as bson from 'bson';
|
|
58
5
|
import { LRUCache } from 'lru-cache';
|
|
59
6
|
import * as timers from 'timers/promises';
|
|
60
|
-
import {
|
|
61
|
-
import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
62
|
-
import { MongoChecksums } from './MongoChecksums.js';
|
|
63
|
-
import { MongoCompactor } from './MongoCompactor.js';
|
|
64
|
-
import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
7
|
+
import { retryOnMongoMaxTimeMSExpired } from '../../utils/util.js';
|
|
65
8
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
66
9
|
/**
|
|
67
10
|
* Only keep checkpoints around for a minute, before fetching a fresh one.
|
|
@@ -79,9 +22,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
79
22
|
sync_rules;
|
|
80
23
|
slot_name;
|
|
81
24
|
db;
|
|
25
|
+
[DO_NOT_LOG] = true;
|
|
82
26
|
checksums;
|
|
83
27
|
parsedSyncRulesCache;
|
|
84
28
|
writeCheckpointAPI;
|
|
29
|
+
logger;
|
|
30
|
+
#storageInitialized = false;
|
|
85
31
|
constructor(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options) {
|
|
86
32
|
super();
|
|
87
33
|
this.factory = factory;
|
|
@@ -89,19 +35,27 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
89
35
|
this.sync_rules = sync_rules;
|
|
90
36
|
this.slot_name = slot_name;
|
|
91
37
|
this.db = factory.db.versioned(sync_rules.getStorageConfig());
|
|
92
|
-
this.checksums =
|
|
93
|
-
...options.checksumOptions,
|
|
94
|
-
storageConfig: options?.storageConfig
|
|
95
|
-
});
|
|
38
|
+
this.checksums = this.createMongoChecksums(options);
|
|
96
39
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
97
40
|
db: this.db,
|
|
98
41
|
mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
|
|
99
42
|
sync_rules_id: group_id
|
|
100
43
|
});
|
|
44
|
+
this.logger = sync_rules.logger;
|
|
101
45
|
}
|
|
102
46
|
get writeCheckpointMode() {
|
|
103
47
|
return this.writeCheckpointAPI.writeCheckpointMode;
|
|
104
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
|
+
}
|
|
105
59
|
setWriteCheckpointMode(mode) {
|
|
106
60
|
this.writeCheckpointAPI.setWriteCheckpointMode(mode);
|
|
107
61
|
}
|
|
@@ -116,10 +70,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
116
70
|
}
|
|
117
71
|
getParsedSyncRules(options) {
|
|
118
72
|
const { parsed, options: cachedOptions } = this.parsedSyncRulesCache ?? {};
|
|
119
|
-
/**
|
|
120
|
-
* Check if the cached sync rules, if present, had the same options.
|
|
121
|
-
* Parse sync rules if the options are different or if there is no cached value.
|
|
122
|
-
*/
|
|
123
73
|
if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
|
|
124
74
|
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncRules(), options };
|
|
125
75
|
}
|
|
@@ -135,36 +85,34 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
135
85
|
projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
|
|
136
86
|
});
|
|
137
87
|
if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
|
|
138
|
-
// Sync rules not active - return null
|
|
139
88
|
return null;
|
|
140
89
|
}
|
|
141
|
-
// Specifically using operationTime instead of clusterTime
|
|
142
|
-
// There are 3 fields in the response:
|
|
143
|
-
// 1. operationTime, not exposed for snapshot sessions (used for causal consistency)
|
|
144
|
-
// 2. clusterTime (used for connection management)
|
|
145
|
-
// 3. atClusterTime, which is session.snapshotTime
|
|
146
|
-
// We use atClusterTime, to match the driver's internal snapshot handling.
|
|
147
|
-
// There are cases where clusterTime > operationTime and atClusterTime,
|
|
148
|
-
// which could cause snapshot queries using this as the snapshotTime to timeout.
|
|
149
|
-
// This was specifically observed on MongoDB 6.0 and 7.0.
|
|
150
90
|
const snapshotTime = session.snapshotTime;
|
|
151
91
|
if (snapshotTime == null) {
|
|
152
92
|
throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
|
|
153
93
|
}
|
|
154
|
-
return new MongoReplicationCheckpoint(this,
|
|
155
|
-
// null/0n is a valid checkpoint in some cases, for example if the initial snapshot was empty
|
|
156
|
-
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);
|
|
157
95
|
});
|
|
158
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
|
+
}
|
|
159
105
|
async createWriter(options) {
|
|
106
|
+
await this.initializeStorage();
|
|
160
107
|
const doc = await this.db.sync_rules.findOne({
|
|
161
108
|
_id: this.group_id
|
|
162
109
|
}, { projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1, snapshot_lsn: 1 } });
|
|
163
110
|
const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
|
|
164
|
-
const
|
|
165
|
-
logger: options.logger,
|
|
111
|
+
const batchOptions = {
|
|
112
|
+
logger: options.logger ?? this.logger,
|
|
166
113
|
db: this.db,
|
|
167
114
|
syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
|
|
115
|
+
mapping: this.sync_rules.mapping,
|
|
168
116
|
groupId: this.group_id,
|
|
169
117
|
slotName: this.slot_name,
|
|
170
118
|
lastCheckpointLsn: checkpoint_lsn,
|
|
@@ -172,31 +120,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
172
120
|
keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
|
|
173
121
|
storeCurrentData: options.storeCurrentData,
|
|
174
122
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
175
|
-
markRecordUnavailable: options.markRecordUnavailable
|
|
176
|
-
|
|
123
|
+
markRecordUnavailable: options.markRecordUnavailable,
|
|
124
|
+
tracer: options.tracer
|
|
125
|
+
};
|
|
126
|
+
const writer = this.createWriterImpl(batchOptions);
|
|
177
127
|
this.iterateListeners((cb) => cb.batchStarted?.(writer));
|
|
178
128
|
return writer;
|
|
179
129
|
}
|
|
180
|
-
/**
|
|
181
|
-
* @deprecated Use `createWriter()` with `await using` instead.
|
|
182
|
-
*/
|
|
183
130
|
async startBatch(options, callback) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await writer.flush();
|
|
189
|
-
return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
|
|
190
|
-
}
|
|
191
|
-
catch (e_1) {
|
|
192
|
-
env_1.error = e_1;
|
|
193
|
-
env_1.hasError = true;
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
const result_1 = __disposeResources(env_1);
|
|
197
|
-
if (result_1)
|
|
198
|
-
await result_1;
|
|
199
|
-
}
|
|
131
|
+
await using writer = await this.createWriter(options);
|
|
132
|
+
await callback(writer);
|
|
133
|
+
await writer.flush();
|
|
134
|
+
return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
|
|
200
135
|
}
|
|
201
136
|
async resolveTable(options) {
|
|
202
137
|
const { group_id, connection_id, connection_tag, entity_descriptor } = options;
|
|
@@ -207,10 +142,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
207
142
|
type_oid: column.typeId
|
|
208
143
|
}));
|
|
209
144
|
let result = null;
|
|
145
|
+
let initializeSourceRecordsFor = null;
|
|
146
|
+
const baseId = this.sourceTableBaseId();
|
|
210
147
|
await this.db.client.withSession(async (session) => {
|
|
211
|
-
const col = this.db.
|
|
148
|
+
const col = this.db.commonSourceTables(group_id);
|
|
212
149
|
let filter = {
|
|
213
|
-
|
|
150
|
+
...baseId,
|
|
214
151
|
connection_id: connection_id,
|
|
215
152
|
schema_name: schema,
|
|
216
153
|
table_name: name,
|
|
@@ -221,9 +158,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
221
158
|
}
|
|
222
159
|
let doc = await col.findOne(filter, { session });
|
|
223
160
|
if (doc == null) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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,
|
|
227
173
|
connection_id: connection_id,
|
|
228
174
|
relation_id: objectId,
|
|
229
175
|
schema_name: schema,
|
|
@@ -233,7 +179,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
233
179
|
snapshot_done: false,
|
|
234
180
|
snapshot_status: undefined
|
|
235
181
|
};
|
|
182
|
+
this.augmentCreatedSourceTableDocument(createDoc, options, candidateSourceTable);
|
|
183
|
+
doc = createDoc;
|
|
236
184
|
await col.insertOne(doc, { session });
|
|
185
|
+
initializeSourceRecordsFor = doc._id;
|
|
237
186
|
}
|
|
238
187
|
const sourceTable = new storage.SourceTable({
|
|
239
188
|
id: doc._id,
|
|
@@ -256,15 +205,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
256
205
|
replicatedCount: doc.snapshot_status.replicated_count
|
|
257
206
|
};
|
|
258
207
|
let dropTables = [];
|
|
259
|
-
// Detect tables that are either renamed, or have different replica_id_columns
|
|
260
208
|
let truncateFilter = [{ schema_name: schema, table_name: name }];
|
|
261
209
|
if (objectId != null) {
|
|
262
|
-
// Only detect renames if the source uses relation ids.
|
|
263
210
|
truncateFilter.push({ relation_id: objectId });
|
|
264
211
|
}
|
|
265
212
|
const truncate = await col
|
|
266
213
|
.find({
|
|
267
|
-
|
|
214
|
+
...baseId,
|
|
268
215
|
connection_id: connection_id,
|
|
269
216
|
_id: { $ne: doc._id },
|
|
270
217
|
$or: truncateFilter
|
|
@@ -284,192 +231,16 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
284
231
|
dropTables: dropTables
|
|
285
232
|
};
|
|
286
233
|
});
|
|
234
|
+
if (initializeSourceRecordsFor != null) {
|
|
235
|
+
await this.initializeResolvedSourceRecords(initializeSourceRecordsFor);
|
|
236
|
+
}
|
|
287
237
|
return result;
|
|
288
238
|
}
|
|
289
|
-
async getParameterSets(checkpoint, lookups) {
|
|
290
|
-
return this.
|
|
291
|
-
// Set the session's snapshot time to the checkpoint's snapshot time.
|
|
292
|
-
// An alternative would be to create the session when the checkpoint is created, but managing
|
|
293
|
-
// the session lifetime would become more complex.
|
|
294
|
-
// Starting and ending sessions are cheap (synchronous when no transactions are used),
|
|
295
|
-
// so this should be fine.
|
|
296
|
-
// This is a roundabout way of setting {readConcern: {atClusterTime: clusterTime}}, since
|
|
297
|
-
// that is not exposed directly by the driver.
|
|
298
|
-
// Future versions of the driver may change the snapshotTime behavior, so we need tests to
|
|
299
|
-
// validate that this works as expected. We test this in the compacting tests.
|
|
300
|
-
setSessionSnapshotTime(session, checkpoint.snapshotTime);
|
|
301
|
-
const lookupFilter = lookups.map((lookup) => {
|
|
302
|
-
return storage.serializeLookup(lookup);
|
|
303
|
-
});
|
|
304
|
-
// This query does not use indexes super efficiently, apart from the lookup filter.
|
|
305
|
-
// From some experimentation I could do individual lookups more efficient using an index
|
|
306
|
-
// on {'key.g': 1, lookup: 1, 'key.t': 1, 'key.k': 1, _id: -1},
|
|
307
|
-
// but could not do the same using $group.
|
|
308
|
-
// For now, just rely on compacting to remove extraneous data.
|
|
309
|
-
// For a description of the data format, see the `/docs/parameters-lookups.md` file.
|
|
310
|
-
const rows = await this.db.bucket_parameters
|
|
311
|
-
.aggregate([
|
|
312
|
-
{
|
|
313
|
-
$match: {
|
|
314
|
-
'key.g': this.group_id,
|
|
315
|
-
lookup: { $in: lookupFilter },
|
|
316
|
-
_id: { $lte: checkpoint.checkpoint }
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
$sort: {
|
|
321
|
-
_id: -1
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
$group: {
|
|
326
|
-
_id: { key: '$key', lookup: '$lookup' },
|
|
327
|
-
bucket_parameters: {
|
|
328
|
-
$first: '$bucket_parameters'
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
], {
|
|
333
|
-
session,
|
|
334
|
-
readConcern: 'snapshot',
|
|
335
|
-
// Limit the time for the operation to complete, to avoid getting connection timeouts
|
|
336
|
-
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
|
|
337
|
-
})
|
|
338
|
-
.toArray()
|
|
339
|
-
.catch((e) => {
|
|
340
|
-
throw lib_mongo.mapQueryError(e, 'while evaluating parameter queries');
|
|
341
|
-
});
|
|
342
|
-
const groupedParameters = rows.map((row) => {
|
|
343
|
-
return row.bucket_parameters;
|
|
344
|
-
});
|
|
345
|
-
return groupedParameters.flat();
|
|
346
|
-
});
|
|
239
|
+
async getParameterSets(checkpoint, lookups, limit) {
|
|
240
|
+
return this.getParameterSetsImpl(checkpoint, lookups, limit);
|
|
347
241
|
}
|
|
348
242
|
async *getBucketDataBatch(checkpoint, dataBuckets, options) {
|
|
349
|
-
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
let filters = [];
|
|
353
|
-
const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
354
|
-
if (checkpoint == null) {
|
|
355
|
-
throw new ServiceAssertionError('checkpoint is null');
|
|
356
|
-
}
|
|
357
|
-
const end = checkpoint;
|
|
358
|
-
for (let { bucket: name, start } of dataBuckets) {
|
|
359
|
-
filters.push({
|
|
360
|
-
_id: {
|
|
361
|
-
$gt: {
|
|
362
|
-
g: this.group_id,
|
|
363
|
-
b: name,
|
|
364
|
-
o: start
|
|
365
|
-
},
|
|
366
|
-
$lte: {
|
|
367
|
-
g: this.group_id,
|
|
368
|
-
b: name,
|
|
369
|
-
o: end
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
// Internal naming:
|
|
375
|
-
// We do a query for one "batch", which may consist of multiple "chunks".
|
|
376
|
-
// Each chunk is limited to single bucket, and is limited in length and size.
|
|
377
|
-
// There are also overall batch length and size limits.
|
|
378
|
-
const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
|
|
379
|
-
const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
|
|
380
|
-
const cursor = this.db.bucket_data.find({
|
|
381
|
-
$or: filters
|
|
382
|
-
}, {
|
|
383
|
-
session: undefined,
|
|
384
|
-
sort: { _id: 1 },
|
|
385
|
-
limit: batchLimit,
|
|
386
|
-
// Increase batch size above the default 101, so that we can fill an entire batch in
|
|
387
|
-
// one go.
|
|
388
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
389
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
390
|
-
batchSize: batchLimit + 1,
|
|
391
|
-
// Raw mode is returns an array of Buffer instead of parsed documents.
|
|
392
|
-
// We use it so that:
|
|
393
|
-
// 1. We can calculate the document size accurately without serializing again.
|
|
394
|
-
// 2. We can delay parsing the results until it's needed.
|
|
395
|
-
// We manually use bson.deserialize below
|
|
396
|
-
raw: true,
|
|
397
|
-
// Limit the time for the operation to complete, to avoid getting connection timeouts
|
|
398
|
-
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
|
|
399
|
-
});
|
|
400
|
-
// We want to limit results to a single batch to avoid high memory usage.
|
|
401
|
-
// This approach uses MongoDB's batch limits to limit the data here, which limits
|
|
402
|
-
// to the lower of the batch count and size limits.
|
|
403
|
-
// This is similar to using `singleBatch: true` in the find options, but allows
|
|
404
|
-
// detecting "hasMore".
|
|
405
|
-
let { data, hasMore: batchHasMore } = await readSingleBatch(cursor).catch((e) => {
|
|
406
|
-
throw lib_mongo.mapQueryError(e, 'while reading bucket data');
|
|
407
|
-
});
|
|
408
|
-
if (data.length == batchLimit) {
|
|
409
|
-
// Limit reached - could have more data, despite the cursor being drained.
|
|
410
|
-
batchHasMore = true;
|
|
411
|
-
}
|
|
412
|
-
let chunkSizeBytes = 0;
|
|
413
|
-
let currentChunk = null;
|
|
414
|
-
let targetOp = null;
|
|
415
|
-
// Ordered by _id, meaning buckets are grouped together
|
|
416
|
-
for (let rawData of data) {
|
|
417
|
-
const row = bson.deserialize(rawData, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS);
|
|
418
|
-
const bucket = row._id.b;
|
|
419
|
-
if (currentChunk == null || currentChunk.bucket != bucket || chunkSizeBytes >= chunkSizeLimitBytes) {
|
|
420
|
-
// We need to start a new chunk
|
|
421
|
-
let start = undefined;
|
|
422
|
-
if (currentChunk != null) {
|
|
423
|
-
// There is an existing chunk we need to yield
|
|
424
|
-
if (currentChunk.bucket == bucket) {
|
|
425
|
-
// Current and new chunk have the same bucket, so need has_more on the current one.
|
|
426
|
-
// If currentChunk.bucket != bucket, then we reached the end of the previous bucket,
|
|
427
|
-
// and has_more = false in that case.
|
|
428
|
-
currentChunk.has_more = true;
|
|
429
|
-
start = currentChunk.next_after;
|
|
430
|
-
}
|
|
431
|
-
const yieldChunk = currentChunk;
|
|
432
|
-
currentChunk = null;
|
|
433
|
-
chunkSizeBytes = 0;
|
|
434
|
-
yield { chunkData: yieldChunk, targetOp: targetOp };
|
|
435
|
-
targetOp = null;
|
|
436
|
-
}
|
|
437
|
-
if (start == null) {
|
|
438
|
-
const startOpId = bucketMap.get(bucket);
|
|
439
|
-
if (startOpId == null) {
|
|
440
|
-
throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
|
|
441
|
-
}
|
|
442
|
-
start = internalToExternalOpId(startOpId);
|
|
443
|
-
}
|
|
444
|
-
currentChunk = {
|
|
445
|
-
bucket,
|
|
446
|
-
after: start,
|
|
447
|
-
has_more: false,
|
|
448
|
-
data: [],
|
|
449
|
-
next_after: start
|
|
450
|
-
};
|
|
451
|
-
targetOp = null;
|
|
452
|
-
}
|
|
453
|
-
const entry = mapOpEntry(row);
|
|
454
|
-
if (row.target_op != null) {
|
|
455
|
-
// MOVE, CLEAR
|
|
456
|
-
if (targetOp == null || row.target_op > targetOp) {
|
|
457
|
-
targetOp = row.target_op;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
currentChunk.data.push(entry);
|
|
461
|
-
currentChunk.next_after = entry.op_id;
|
|
462
|
-
chunkSizeBytes += rawData.byteLength;
|
|
463
|
-
}
|
|
464
|
-
if (currentChunk != null) {
|
|
465
|
-
const yieldChunk = currentChunk;
|
|
466
|
-
currentChunk = null;
|
|
467
|
-
// This is the final chunk in the batch.
|
|
468
|
-
// There may be more data if and only if the batch we retrieved isn't complete.
|
|
469
|
-
yieldChunk.has_more = batchHasMore;
|
|
470
|
-
yield { chunkData: yieldChunk, targetOp: targetOp };
|
|
471
|
-
targetOp = null;
|
|
472
|
-
}
|
|
243
|
+
yield* this.getBucketDataBatchImpl(checkpoint, dataBuckets, options);
|
|
473
244
|
}
|
|
474
245
|
async getChecksums(checkpoint, buckets) {
|
|
475
246
|
return this.checksums.getChecksums(checkpoint, buckets);
|
|
@@ -478,7 +249,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
478
249
|
this.checksums.clearCache();
|
|
479
250
|
}
|
|
480
251
|
async terminate(options) {
|
|
481
|
-
// Default is to clear the storage except when explicitly requested not to.
|
|
482
252
|
if (!options || options?.clearStorage) {
|
|
483
253
|
await this.clear(options);
|
|
484
254
|
}
|
|
@@ -505,7 +275,7 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
505
275
|
}
|
|
506
276
|
});
|
|
507
277
|
if (doc == null) {
|
|
508
|
-
throw new ServiceAssertionError('Cannot find
|
|
278
|
+
throw new ServiceAssertionError('Cannot find replication stream status');
|
|
509
279
|
}
|
|
510
280
|
return {
|
|
511
281
|
snapshot_done: doc.snapshot_done,
|
|
@@ -515,29 +285,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
515
285
|
};
|
|
516
286
|
}
|
|
517
287
|
async clear(options) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
try {
|
|
523
|
-
await this.clearIteration();
|
|
524
|
-
logger.info(`${this.slot_name} Done clearing data`);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
catch (e) {
|
|
528
|
-
if (lib_mongo.isMongoServerError(e) && e.codeName == 'MaxTimeMSExpired') {
|
|
529
|
-
logger.info(`${this.slot_name} Cleared batch of data in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`);
|
|
530
|
-
await timers.setTimeout(lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5);
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
throw e;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
288
|
+
const signal = options?.signal;
|
|
289
|
+
if (signal?.aborted) {
|
|
290
|
+
throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
|
|
536
291
|
}
|
|
537
|
-
}
|
|
538
|
-
async clearIteration() {
|
|
539
|
-
// Individual operations here may time out with the maxTimeMS option.
|
|
540
|
-
// It is expected to still make progress, and continue on the next try.
|
|
541
292
|
await this.db.sync_rules.updateOne({
|
|
542
293
|
_id: this.group_id
|
|
543
294
|
}, {
|
|
@@ -552,21 +303,22 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
552
303
|
snapshot_lsn: 1
|
|
553
304
|
}
|
|
554
305
|
}, { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS });
|
|
555
|
-
await this.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
await this.
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
});
|
|
570
322
|
}
|
|
571
323
|
async reportError(e) {
|
|
572
324
|
const message = String(e.message ?? 'Replication failure');
|
|
@@ -585,83 +337,52 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
585
337
|
const checkpoint = await this.getCheckpointInternal();
|
|
586
338
|
maxOpId = checkpoint?.checkpoint ?? undefined;
|
|
587
339
|
}
|
|
588
|
-
await
|
|
340
|
+
await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
|
|
589
341
|
if (maxOpId != null && options?.compactParameterData) {
|
|
590
|
-
await
|
|
342
|
+
await this.createMongoParameterCompactor(maxOpId, options).compact();
|
|
591
343
|
}
|
|
592
344
|
}
|
|
593
345
|
async populatePersistentChecksumCache(options) {
|
|
594
|
-
logger.info(`Populating persistent checksum cache...`);
|
|
346
|
+
this.logger.info(`Populating persistent checksum cache...`);
|
|
595
347
|
const start = Date.now();
|
|
596
|
-
|
|
597
|
-
// We can optimize this in the future.
|
|
598
|
-
const compactor = new MongoCompactor(this, this.db, {
|
|
348
|
+
const compactor = this.createMongoCompactor({
|
|
599
349
|
...options,
|
|
600
|
-
|
|
601
|
-
|
|
350
|
+
memoryLimitMB: 0,
|
|
351
|
+
logger: this.logger
|
|
602
352
|
});
|
|
603
353
|
const result = await compactor.populateChecksums({
|
|
604
|
-
// There are cases with millions of small buckets, in which case it can take very long to
|
|
605
|
-
// populate the checksums, with minimal benefit. We skip the small buckets here.
|
|
606
354
|
minBucketChanges: options.minBucketChanges ?? 10
|
|
607
355
|
});
|
|
608
356
|
const duration = Date.now() - start;
|
|
609
|
-
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`);
|
|
610
358
|
return result;
|
|
611
359
|
}
|
|
612
|
-
/**
|
|
613
|
-
* Instance-wide watch on the latest available checkpoint (op_id + lsn).
|
|
614
|
-
*/
|
|
615
360
|
async *watchActiveCheckpoint(signal) {
|
|
616
361
|
if (signal.aborted) {
|
|
617
362
|
return;
|
|
618
363
|
}
|
|
619
|
-
// If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS) before we get another checkpoint,
|
|
620
|
-
// to avoid stale checkpoint snapshots. This is what checkpointTimeoutStream() is for.
|
|
621
|
-
// Essentially, even if there are no actual checkpoint changes, we want a new snapshotTime every minute or so,
|
|
622
|
-
// to ensure that any new clients connecting will get a valid snapshotTime.
|
|
623
364
|
const stream = mergeAsyncIterables([this.checkpointChangesStream(signal), this.checkpointTimeoutStream(signal)], signal);
|
|
624
|
-
// We only watch changes to the active sync rules.
|
|
625
|
-
// If it changes to inactive, we abort and restart with the new sync rules.
|
|
626
365
|
for await (const _ of stream) {
|
|
627
366
|
if (signal.aborted) {
|
|
628
|
-
// Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
|
|
629
367
|
break;
|
|
630
368
|
}
|
|
631
369
|
const op = await this.getCheckpointInternal();
|
|
632
370
|
if (op == null) {
|
|
633
|
-
// Sync rules have changed - abort and restart.
|
|
634
|
-
// We do a soft close of the stream here - no error
|
|
635
371
|
break;
|
|
636
372
|
}
|
|
637
|
-
// Previously, we only yielded when the checkpoint or lsn changed.
|
|
638
|
-
// However, we always want to use the latest snapshotTime, so we skip that filtering here.
|
|
639
|
-
// That filtering could be added in the per-user streams if needed, but in general the capped collection
|
|
640
|
-
// should already only contain useful changes in most cases.
|
|
641
373
|
yield op;
|
|
642
374
|
}
|
|
643
375
|
}
|
|
644
|
-
// Nothing is done here until a subscriber starts to iterate
|
|
645
376
|
sharedIter = new BroadcastIterable((signal) => {
|
|
646
377
|
return this.watchActiveCheckpoint(signal);
|
|
647
378
|
});
|
|
648
|
-
/**
|
|
649
|
-
* User-specific watch on the latest checkpoint and/or write checkpoint.
|
|
650
|
-
*/
|
|
651
379
|
async *watchCheckpointChanges(options) {
|
|
652
380
|
let lastCheckpoint = null;
|
|
653
381
|
const iter = this.sharedIter[Symbol.asyncIterator](options.signal);
|
|
654
382
|
let writeCheckpoint = null;
|
|
655
|
-
// true if we queried the initial write checkpoint, even if it doesn't exist
|
|
656
383
|
let queriedInitialWriteCheckpoint = false;
|
|
657
384
|
for await (const nextCheckpoint of iter) {
|
|
658
|
-
// lsn changes are not important by itself.
|
|
659
|
-
// What is important is:
|
|
660
|
-
// 1. checkpoint (op_id) changes.
|
|
661
|
-
// 2. write checkpoint changes for the specific user
|
|
662
385
|
if (nextCheckpoint.lsn != null && !queriedInitialWriteCheckpoint) {
|
|
663
|
-
// Lookup the first write checkpoint for the user when we can.
|
|
664
|
-
// There will not actually be one in all cases.
|
|
665
386
|
writeCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
666
387
|
sync_rules_id: this.group_id,
|
|
667
388
|
user_id: options.user_id,
|
|
@@ -674,14 +395,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
674
395
|
if (lastCheckpoint != null &&
|
|
675
396
|
lastCheckpoint.checkpoint == nextCheckpoint.checkpoint &&
|
|
676
397
|
lastCheckpoint.lsn == nextCheckpoint.lsn) {
|
|
677
|
-
// No change - wait for next one
|
|
678
|
-
// In some cases, many LSNs may be produced in a short time.
|
|
679
|
-
// Add a delay to throttle the loop a bit.
|
|
680
398
|
await timers.setTimeout(20 + 10 * Math.random());
|
|
681
399
|
continue;
|
|
682
400
|
}
|
|
683
401
|
if (lastCheckpoint == null) {
|
|
684
|
-
// First message for this stream - "INVALIDATE_ALL" means it will lookup all data
|
|
685
402
|
yield {
|
|
686
403
|
base: nextCheckpoint,
|
|
687
404
|
writeCheckpoint,
|
|
@@ -695,8 +412,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
695
412
|
});
|
|
696
413
|
let updatedWriteCheckpoint = updates.updatedWriteCheckpoints.get(options.user_id) ?? null;
|
|
697
414
|
if (updates.invalidateWriteCheckpoints) {
|
|
698
|
-
// Invalidated means there were too many updates to track the individual ones,
|
|
699
|
-
// so we switch to "polling" (querying directly in each stream).
|
|
700
415
|
updatedWriteCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
701
416
|
sync_rules_id: this.group_id,
|
|
702
417
|
user_id: options.user_id,
|
|
@@ -707,8 +422,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
707
422
|
}
|
|
708
423
|
if (updatedWriteCheckpoint != null && (writeCheckpoint == null || updatedWriteCheckpoint > writeCheckpoint)) {
|
|
709
424
|
writeCheckpoint = updatedWriteCheckpoint;
|
|
710
|
-
// If it happened that we haven't queried a write checkpoint at this point,
|
|
711
|
-
// then we don't need to anymore, since we got an updated one.
|
|
712
425
|
queriedInitialWriteCheckpoint = true;
|
|
713
426
|
}
|
|
714
427
|
yield {
|
|
@@ -725,12 +438,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
725
438
|
lastCheckpoint = nextCheckpoint;
|
|
726
439
|
}
|
|
727
440
|
}
|
|
728
|
-
/**
|
|
729
|
-
* This watches the checkpoint_events capped collection for new documents inserted,
|
|
730
|
-
* and yields whenever one or more documents are inserted.
|
|
731
|
-
*
|
|
732
|
-
* The actual checkpoint must be queried on the sync_rules collection after this.
|
|
733
|
-
*/
|
|
734
441
|
async *checkpointChangesStream(signal) {
|
|
735
442
|
if (signal.aborted) {
|
|
736
443
|
return;
|
|
@@ -742,16 +449,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
742
449
|
signal.addEventListener('abort', () => {
|
|
743
450
|
cursor.close().catch(() => { });
|
|
744
451
|
});
|
|
745
|
-
// Yield once on start, regardless of whether there are documents in the cursor.
|
|
746
|
-
// This is to ensure that the first iteration of the generator yields immediately.
|
|
747
452
|
yield;
|
|
748
453
|
try {
|
|
749
454
|
while (!signal.aborted) {
|
|
750
455
|
const doc = await cursor.tryNext().catch((e) => {
|
|
751
456
|
if (lib_mongo.isMongoServerError(e) && e.codeName === 'CappedPositionLost') {
|
|
752
|
-
// Cursor position lost, potentially due to a high rate of notifications
|
|
753
457
|
cursor = query();
|
|
754
|
-
// Treat as an event found, before querying the new cursor again
|
|
755
458
|
return {};
|
|
756
459
|
}
|
|
757
460
|
else {
|
|
@@ -761,8 +464,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
761
464
|
if (cursor.closed) {
|
|
762
465
|
return;
|
|
763
466
|
}
|
|
764
|
-
// Skip buffered documents, if any. We don't care about the contents,
|
|
765
|
-
// we only want to know when new documents are inserted.
|
|
766
467
|
cursor.readBufferedDocuments();
|
|
767
468
|
if (doc != null) {
|
|
768
469
|
yield;
|
|
@@ -786,7 +487,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
786
487
|
}
|
|
787
488
|
catch (e) {
|
|
788
489
|
if (e.name == 'AbortError') {
|
|
789
|
-
// This is how we typically abort this stream, when all listeners are done
|
|
790
490
|
return;
|
|
791
491
|
}
|
|
792
492
|
throw e;
|
|
@@ -797,74 +497,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
797
497
|
}
|
|
798
498
|
}
|
|
799
499
|
async getDataBucketChanges(options) {
|
|
800
|
-
|
|
801
|
-
const bucketStateUpdates = await this.db.bucket_state
|
|
802
|
-
.find({
|
|
803
|
-
// We have an index on (_id.g, last_op).
|
|
804
|
-
'_id.g': this.group_id,
|
|
805
|
-
last_op: { $gt: options.lastCheckpoint.checkpoint }
|
|
806
|
-
}, {
|
|
807
|
-
projection: {
|
|
808
|
-
'_id.b': 1
|
|
809
|
-
},
|
|
810
|
-
limit: limit + 1,
|
|
811
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
812
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
813
|
-
batchSize: limit + 2,
|
|
814
|
-
singleBatch: true
|
|
815
|
-
})
|
|
816
|
-
.toArray();
|
|
817
|
-
const buckets = bucketStateUpdates.map((doc) => doc._id.b);
|
|
818
|
-
const invalidateDataBuckets = buckets.length > limit;
|
|
819
|
-
return {
|
|
820
|
-
invalidateDataBuckets: invalidateDataBuckets,
|
|
821
|
-
updatedDataBuckets: invalidateDataBuckets ? new Set() : new Set(buckets)
|
|
822
|
-
};
|
|
500
|
+
return this.getDataBucketChangesImpl(options);
|
|
823
501
|
}
|
|
824
502
|
async getParameterBucketChanges(options) {
|
|
825
|
-
|
|
826
|
-
const parameterUpdates = await this.db.bucket_parameters
|
|
827
|
-
.find({
|
|
828
|
-
_id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint },
|
|
829
|
-
'key.g': this.group_id
|
|
830
|
-
}, {
|
|
831
|
-
projection: {
|
|
832
|
-
lookup: 1
|
|
833
|
-
},
|
|
834
|
-
limit: limit + 1,
|
|
835
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
836
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
837
|
-
batchSize: limit + 2,
|
|
838
|
-
singleBatch: true
|
|
839
|
-
})
|
|
840
|
-
.toArray();
|
|
841
|
-
const invalidateParameterUpdates = parameterUpdates.length > limit;
|
|
842
|
-
return {
|
|
843
|
-
invalidateParameterBuckets: invalidateParameterUpdates,
|
|
844
|
-
updatedParameterLookups: invalidateParameterUpdates
|
|
845
|
-
? new Set()
|
|
846
|
-
: new Set(parameterUpdates.map((p) => JSONBig.stringify(deserializeParameterLookup(p.lookup))))
|
|
847
|
-
};
|
|
503
|
+
return this.getParameterBucketChangesImpl(options);
|
|
848
504
|
}
|
|
849
|
-
// If we processed all connections together for each checkpoint, we could do a single lookup for all connections.
|
|
850
|
-
// In practice, specific connections may fall behind. So instead, we just cache the results of each specific lookup.
|
|
851
|
-
// TODO (later):
|
|
852
|
-
// We can optimize this by implementing it like ChecksumCache: We can use partial cache results to do
|
|
853
|
-
// more efficient lookups in some cases.
|
|
854
505
|
checkpointChangesCache = new LRUCache({
|
|
855
|
-
// Limit to 50 cache entries, or 10MB, whichever comes first.
|
|
856
|
-
// Some rough calculations:
|
|
857
|
-
// If we process 10 checkpoints per second, and a connection may be 2 seconds behind, we could have
|
|
858
|
-
// up to 20 relevant checkpoints. That gives us 20*20 = 400 potentially-relevant cache entries.
|
|
859
|
-
// That is a worst-case scenario, so we don't actually store that many. In real life, the cache keys
|
|
860
|
-
// would likely be clustered around a few values, rather than spread over all 400 potential values.
|
|
861
506
|
max: 50,
|
|
862
507
|
maxSize: 12 * 1024 * 1024,
|
|
863
508
|
sizeCalculation: (value) => {
|
|
864
|
-
// Estimate of memory usage
|
|
865
509
|
const paramSize = [...value.updatedParameterLookups].reduce((a, b) => a + b.length, 0);
|
|
866
510
|
const bucketSize = [...value.updatedDataBuckets].reduce((a, b) => a + b.length, 0);
|
|
867
|
-
const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
|
|
511
|
+
const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
|
|
868
512
|
return 100 + paramSize + bucketSize + writeCheckpointSize;
|
|
869
513
|
},
|
|
870
514
|
fetchMethod: async (_key, _staleValue, options) => {
|
|
@@ -888,24 +532,24 @@ export class MongoSyncBucketStorage extends BaseObserver {
|
|
|
888
532
|
}
|
|
889
533
|
}
|
|
890
534
|
class MongoReplicationCheckpoint {
|
|
891
|
-
storage;
|
|
892
535
|
checkpoint;
|
|
893
536
|
lsn;
|
|
894
537
|
snapshotTime;
|
|
538
|
+
#storage;
|
|
895
539
|
constructor(storage, checkpoint, lsn, snapshotTime) {
|
|
896
|
-
this.storage = storage;
|
|
897
540
|
this.checkpoint = checkpoint;
|
|
898
541
|
this.lsn = lsn;
|
|
899
542
|
this.snapshotTime = snapshotTime;
|
|
543
|
+
this.#storage = storage;
|
|
900
544
|
}
|
|
901
|
-
async getParameterSets(lookups) {
|
|
902
|
-
return this
|
|
545
|
+
async getParameterSets(lookups, limit) {
|
|
546
|
+
return this.#storage.getParameterSets(this, lookups, limit);
|
|
903
547
|
}
|
|
904
548
|
}
|
|
905
549
|
class EmptyReplicationCheckpoint {
|
|
906
550
|
checkpoint = 0n;
|
|
907
551
|
lsn = null;
|
|
908
|
-
async getParameterSets(
|
|
552
|
+
async getParameterSets(_lookups) {
|
|
909
553
|
return [];
|
|
910
554
|
}
|
|
911
555
|
}
|