@powersync/service-module-mongodb-storage 0.14.0 → 0.15.1
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 +45 -0
- package/dist/storage/MongoBucketStorage.js +16 -3
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +13 -11
- package/dist/storage/implementation/MongoBucketBatch.js +208 -127
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +4 -4
- package/dist/storage/implementation/MongoChecksums.js +1 -0
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +8 -2
- package/dist/storage/implementation/MongoCompactor.js +50 -21
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +2 -2
- package/dist/storage/implementation/MongoParameterCompactor.js +13 -1
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +2 -7
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +9 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +35 -33
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +3 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +3 -2
- package/dist/storage/implementation/OperationBatch.js.map +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +11 -4
- package/dist/storage/implementation/PersistedBatch.js +42 -11
- package/dist/storage/implementation/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +35 -1
- package/dist/storage/implementation/db.js +99 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +15 -3
- package/dist/storage/implementation/models.js +2 -1
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -1
- package/dist/utils/test-utils.js +15 -12
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +2 -1
- package/dist/utils/util.js +15 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +6 -6
- package/src/storage/MongoBucketStorage.ts +29 -8
- package/src/storage/implementation/MongoBucketBatch.ts +263 -177
- package/src/storage/implementation/MongoChecksums.ts +5 -3
- package/src/storage/implementation/MongoCompactor.ts +53 -24
- package/src/storage/implementation/MongoParameterCompactor.ts +17 -4
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +3 -11
- package/src/storage/implementation/MongoSyncBucketStorage.ts +33 -26
- package/src/storage/implementation/MongoSyncRulesLock.ts +3 -3
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -4
- package/src/storage/implementation/OperationBatch.ts +3 -2
- package/src/storage/implementation/PersistedBatch.ts +42 -11
- package/src/storage/implementation/db.ts +129 -1
- package/src/storage/implementation/models.ts +18 -4
- package/src/utils/test-utils.ts +15 -12
- package/src/utils/util.ts +17 -2
- package/test/src/__snapshots__/storage.test.ts.snap +201 -0
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1111 -16
- package/test/src/storage.test.ts +9 -7
- package/test/src/storage_compacting.test.ts +117 -45
- package/test/src/storage_sync.test.ts +53 -51
- package/test/src/util.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,6 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
|
2
2
|
import {
|
|
3
3
|
addPartialChecksums,
|
|
4
4
|
bson,
|
|
5
|
+
BucketChecksumRequest,
|
|
5
6
|
BucketChecksum,
|
|
6
7
|
ChecksumCache,
|
|
7
8
|
ChecksumMap,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
PartialChecksumMap,
|
|
13
14
|
PartialOrFullChecksum
|
|
14
15
|
} from '@powersync/service-core';
|
|
15
|
-
import {
|
|
16
|
+
import { VersionedPowerSyncMongo } from './db.js';
|
|
16
17
|
import { StorageConfig } from './models.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -49,7 +50,7 @@ export class MongoChecksums {
|
|
|
49
50
|
private readonly storageConfig: StorageConfig;
|
|
50
51
|
|
|
51
52
|
constructor(
|
|
52
|
-
private db:
|
|
53
|
+
private db: VersionedPowerSyncMongo,
|
|
53
54
|
private group_id: number,
|
|
54
55
|
private options: MongoChecksumOptions
|
|
55
56
|
) {
|
|
@@ -74,7 +75,7 @@ export class MongoChecksums {
|
|
|
74
75
|
* Calculate checksums, utilizing the cache for partial checkums, and querying the remainder from
|
|
75
76
|
* the database (bucket_state + bucket_data).
|
|
76
77
|
*/
|
|
77
|
-
async getChecksums(checkpoint: InternalOpId, buckets:
|
|
78
|
+
async getChecksums(checkpoint: InternalOpId, buckets: BucketChecksumRequest[]): Promise<ChecksumMap> {
|
|
78
79
|
return this.cache.getChecksumMap(checkpoint, buckets);
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -298,6 +299,7 @@ export class MongoChecksums {
|
|
|
298
299
|
const req = requests.get(bucket);
|
|
299
300
|
requests.set(bucket, {
|
|
300
301
|
bucket,
|
|
302
|
+
source: req!.source,
|
|
301
303
|
start: doc.last_op,
|
|
302
304
|
end: req!.end
|
|
303
305
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
1
|
+
import { isMongoServerError, mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
3
|
import {
|
|
4
4
|
addChecksums,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
utils
|
|
10
10
|
} from '@powersync/service-core';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { VersionedPowerSyncMongo } from './db.js';
|
|
13
13
|
import { BucketDataDocument, BucketDataKey, BucketStateDocument } from './models.js';
|
|
14
14
|
import { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
|
|
15
15
|
import { cacheKey } from './OperationBatch.js';
|
|
@@ -85,19 +85,19 @@ export class MongoCompactor {
|
|
|
85
85
|
|
|
86
86
|
constructor(
|
|
87
87
|
private storage: MongoSyncBucketStorage,
|
|
88
|
-
private db:
|
|
89
|
-
options
|
|
88
|
+
private db: VersionedPowerSyncMongo,
|
|
89
|
+
options: MongoCompactOptions
|
|
90
90
|
) {
|
|
91
91
|
this.group_id = storage.group_id;
|
|
92
|
-
this.idLimitBytes = (options
|
|
93
|
-
this.moveBatchLimit = options
|
|
94
|
-
this.moveBatchQueryLimit = options
|
|
95
|
-
this.clearBatchLimit = options
|
|
96
|
-
this.minBucketChanges = options
|
|
97
|
-
this.minChangeRatio = options
|
|
98
|
-
this.maxOpId = options
|
|
99
|
-
this.buckets = options
|
|
100
|
-
this.signal = options
|
|
92
|
+
this.idLimitBytes = (options.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
|
|
93
|
+
this.moveBatchLimit = options.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
|
|
94
|
+
this.moveBatchQueryLimit = options.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
|
|
95
|
+
this.clearBatchLimit = options.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
|
|
96
|
+
this.minBucketChanges = options.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
|
|
97
|
+
this.minChangeRatio = options.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
|
|
98
|
+
this.maxOpId = options.maxOpId ?? 0n;
|
|
99
|
+
this.buckets = options.compactBuckets;
|
|
100
|
+
this.signal = options.signal;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
@@ -111,7 +111,7 @@ export class MongoCompactor {
|
|
|
111
111
|
// We can make this more efficient later on by iterating
|
|
112
112
|
// through the buckets in a single query.
|
|
113
113
|
// That makes batching more tricky, so we leave for later.
|
|
114
|
-
await this.
|
|
114
|
+
await this.compactSingleBucketRetried(bucket);
|
|
115
115
|
}
|
|
116
116
|
} else {
|
|
117
117
|
await this.compactDirtyBuckets();
|
|
@@ -123,15 +123,36 @@ export class MongoCompactor {
|
|
|
123
123
|
minBucketChanges: this.minBucketChanges,
|
|
124
124
|
minChangeRatio: this.minChangeRatio
|
|
125
125
|
})) {
|
|
126
|
-
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
126
|
+
this.signal?.throwIfAborted();
|
|
129
127
|
if (buckets.length == 0) {
|
|
130
128
|
continue;
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
for (let { bucket } of buckets) {
|
|
132
|
+
await this.compactSingleBucketRetried(bucket);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Compaction for a single bucket, with retries on failure.
|
|
139
|
+
*
|
|
140
|
+
* This covers against occasional network or other database errors during a long compact job.
|
|
141
|
+
*/
|
|
142
|
+
private async compactSingleBucketRetried(bucket: string) {
|
|
143
|
+
let retryCount = 0;
|
|
144
|
+
while (true) {
|
|
145
|
+
try {
|
|
134
146
|
await this.compactSingleBucket(bucket);
|
|
147
|
+
break;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (retryCount < 3 && isMongoServerError(e)) {
|
|
150
|
+
logger.warn(`Error compacting bucket ${bucket}, retrying...`, e);
|
|
151
|
+
retryCount++;
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount));
|
|
153
|
+
} else {
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
158
|
}
|
|
@@ -165,7 +186,9 @@ export class MongoCompactor {
|
|
|
165
186
|
o: new mongo.MaxKey() as any
|
|
166
187
|
};
|
|
167
188
|
|
|
168
|
-
while (
|
|
189
|
+
while (true) {
|
|
190
|
+
this.signal?.throwIfAborted();
|
|
191
|
+
|
|
169
192
|
// Query one batch at a time, to avoid cursor timeouts
|
|
170
193
|
const cursor = this.db.bucket_data.aggregate<BucketDataDocument & { size: number | bigint }>(
|
|
171
194
|
[
|
|
@@ -393,7 +416,8 @@ export class MongoCompactor {
|
|
|
393
416
|
const session = this.db.client.startSession();
|
|
394
417
|
try {
|
|
395
418
|
let done = false;
|
|
396
|
-
while (!done
|
|
419
|
+
while (!done) {
|
|
420
|
+
this.signal?.throwIfAborted();
|
|
397
421
|
let opCountDiff = 0;
|
|
398
422
|
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
399
423
|
// The state after each batch is fully consistent.
|
|
@@ -486,12 +510,14 @@ export class MongoCompactor {
|
|
|
486
510
|
*/
|
|
487
511
|
async populateChecksums(options: { minBucketChanges: number }): Promise<PopulateChecksumCacheResults> {
|
|
488
512
|
let count = 0;
|
|
489
|
-
while (
|
|
513
|
+
while (true) {
|
|
514
|
+
this.signal?.throwIfAborted();
|
|
490
515
|
const buckets = await this.dirtyBucketBatchForChecksums(options);
|
|
491
|
-
if (buckets.length == 0
|
|
516
|
+
if (buckets.length == 0) {
|
|
492
517
|
// All done
|
|
493
518
|
break;
|
|
494
519
|
}
|
|
520
|
+
this.signal?.throwIfAborted();
|
|
495
521
|
|
|
496
522
|
const start = Date.now();
|
|
497
523
|
|
|
@@ -593,10 +619,12 @@ export class MongoCompactor {
|
|
|
593
619
|
lastId = cursor._id;
|
|
594
620
|
|
|
595
621
|
const mapped = (result?.buckets ?? []).map((b) => {
|
|
622
|
+
// The numbers, specifically the bytes, could be a bigint. We convert to Number to allow calculating the ratios.
|
|
623
|
+
// BigInt precision is not needed here since it's just an estimate.
|
|
596
624
|
const updatedCount = b.estimate_since_compact?.count ?? 0;
|
|
597
625
|
const totalCount = (b.compacted_state?.count ?? 0) + updatedCount;
|
|
598
|
-
const updatedBytes = b.estimate_since_compact?.bytes ?? 0;
|
|
599
|
-
const totalBytes = (b.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
626
|
+
const updatedBytes = Number(b.estimate_since_compact?.bytes ?? 0);
|
|
627
|
+
const totalBytes = Number(b.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
600
628
|
const dirtyChangeNumber = totalCount > 0 ? updatedCount / totalCount : 0;
|
|
601
629
|
const dirtyChangeBytes = totalBytes > 0 ? updatedBytes / totalBytes : 0;
|
|
602
630
|
return {
|
|
@@ -653,7 +681,7 @@ export class MongoCompactor {
|
|
|
653
681
|
|
|
654
682
|
return dirtyBuckets.map((bucket) => ({
|
|
655
683
|
bucket: bucket._id.b,
|
|
656
|
-
estimatedCount: bucket.estimate_since_compact!.count + (bucket.compacted_state?.count ?? 0)
|
|
684
|
+
estimatedCount: Number(bucket.estimate_since_compact!.count) + Number(bucket.compacted_state?.count ?? 0)
|
|
657
685
|
}));
|
|
658
686
|
}
|
|
659
687
|
|
|
@@ -662,6 +690,7 @@ export class MongoCompactor {
|
|
|
662
690
|
buckets.map((bucket) => {
|
|
663
691
|
return {
|
|
664
692
|
bucket,
|
|
693
|
+
source: {} as any,
|
|
665
694
|
end: this.maxOpId
|
|
666
695
|
};
|
|
667
696
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
1
2
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
3
|
import { bson, CompactOptions, InternalOpId } from '@powersync/service-core';
|
|
3
4
|
import { LRUCache } from 'lru-cache';
|
|
4
|
-
import {
|
|
5
|
-
import { mongo } from '@powersync/lib-service-mongodb';
|
|
5
|
+
import { VersionedPowerSyncMongo } from './db.js';
|
|
6
6
|
import { BucketParameterDocument } from './models.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -14,14 +14,14 @@ import { BucketParameterDocument } from './models.js';
|
|
|
14
14
|
*/
|
|
15
15
|
export class MongoParameterCompactor {
|
|
16
16
|
constructor(
|
|
17
|
-
private db:
|
|
17
|
+
private db: VersionedPowerSyncMongo,
|
|
18
18
|
private group_id: number,
|
|
19
19
|
private checkpoint: InternalOpId,
|
|
20
20
|
private options: CompactOptions
|
|
21
21
|
) {}
|
|
22
22
|
|
|
23
23
|
async compact() {
|
|
24
|
-
logger.info(`Compacting parameters for
|
|
24
|
+
logger.info(`Compacting parameters for sync config ${this.group_id} up to checkpoint ${this.checkpoint}`);
|
|
25
25
|
// This is the currently-active checkpoint.
|
|
26
26
|
// We do not remove any data that may be used by this checkpoint.
|
|
27
27
|
// snapshot queries ensure that if any clients are still using older checkpoints, they would
|
|
@@ -49,6 +49,9 @@ export class MongoParameterCompactor {
|
|
|
49
49
|
});
|
|
50
50
|
let removeIds: InternalOpId[] = [];
|
|
51
51
|
let removeDeleted: mongo.AnyBulkWriteOperation<BucketParameterDocument>[] = [];
|
|
52
|
+
let checkedEntries = 0;
|
|
53
|
+
let checkedEntriesAtLastLog = 0;
|
|
54
|
+
let lastProgressLogTime = Date.now();
|
|
52
55
|
|
|
53
56
|
const flush = async (force: boolean) => {
|
|
54
57
|
if (removeIds.length >= 1000 || (force && removeIds.length > 0)) {
|
|
@@ -66,6 +69,16 @@ export class MongoParameterCompactor {
|
|
|
66
69
|
|
|
67
70
|
while (await cursor.hasNext()) {
|
|
68
71
|
const batch = cursor.readBufferedDocuments();
|
|
72
|
+
checkedEntries += batch.length;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (now - lastProgressLogTime >= 60_000) {
|
|
75
|
+
const elapsedSeconds = (now - lastProgressLogTime) / 1000;
|
|
76
|
+
const rate = (checkedEntries - checkedEntriesAtLastLog) / elapsedSeconds;
|
|
77
|
+
logger.info(`Checked ${checkedEntries} parameter index entries for compaction (${rate.toFixed(1)} entries/s)`);
|
|
78
|
+
lastProgressLogTime = now;
|
|
79
|
+
checkedEntriesAtLastLog = checkedEntries;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
for (let doc of batch) {
|
|
70
83
|
if (doc._id >= checkpoint) {
|
|
71
84
|
continue;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { storage } from '@powersync/service-core';
|
|
3
3
|
import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
|
|
4
|
-
import { PowerSyncMongo } from './db.js';
|
|
4
|
+
import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
|
|
5
5
|
import { getMongoStorageConfig, SyncRuleDocument } from './models.js';
|
|
6
|
-
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
7
6
|
|
|
8
7
|
export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesContent {
|
|
9
8
|
public current_lock: MongoSyncRulesLock | null = null;
|
|
@@ -29,18 +28,11 @@ export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesCo
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
getStorageConfig() {
|
|
32
|
-
|
|
33
|
-
if (storageConfig == null) {
|
|
34
|
-
throw new ServiceError(
|
|
35
|
-
ErrorCode.PSYNC_S1005,
|
|
36
|
-
`Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return storageConfig;
|
|
31
|
+
return getMongoStorageConfig(this.storageVersion);
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
async lock() {
|
|
43
|
-
const lock = await MongoSyncRulesLock.createLock(this.db, this);
|
|
35
|
+
const lock = await MongoSyncRulesLock.createLock(this.db.versioned(this.getStorageConfig()), this);
|
|
44
36
|
this.current_lock = lock;
|
|
45
37
|
return lock;
|
|
46
38
|
}
|
|
@@ -31,7 +31,7 @@ import { LRUCache } from 'lru-cache';
|
|
|
31
31
|
import * as timers from 'timers/promises';
|
|
32
32
|
import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
|
|
33
33
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
34
|
-
import {
|
|
34
|
+
import { VersionedPowerSyncMongo } from './db.js';
|
|
35
35
|
import {
|
|
36
36
|
BucketDataDocument,
|
|
37
37
|
BucketDataKey,
|
|
@@ -44,6 +44,7 @@ import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
|
44
44
|
import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
|
|
45
45
|
import { MongoCompactor } from './MongoCompactor.js';
|
|
46
46
|
import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
47
|
+
import { MongoPersistedSyncRulesContent } from './MongoPersistedSyncRulesContent.js';
|
|
47
48
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
48
49
|
|
|
49
50
|
export interface MongoSyncBucketStorageOptions {
|
|
@@ -66,7 +67,7 @@ export class MongoSyncBucketStorage
|
|
|
66
67
|
extends BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
67
68
|
implements storage.SyncRulesBucketStorage
|
|
68
69
|
{
|
|
69
|
-
private readonly db:
|
|
70
|
+
private readonly db: VersionedPowerSyncMongo;
|
|
70
71
|
readonly checksums: MongoChecksums;
|
|
71
72
|
|
|
72
73
|
private parsedSyncRulesCache: { parsed: HydratedSyncRules; options: storage.ParseSyncRulesOptions } | undefined;
|
|
@@ -75,13 +76,13 @@ export class MongoSyncBucketStorage
|
|
|
75
76
|
constructor(
|
|
76
77
|
public readonly factory: MongoBucketStorage,
|
|
77
78
|
public readonly group_id: number,
|
|
78
|
-
private readonly sync_rules:
|
|
79
|
+
private readonly sync_rules: MongoPersistedSyncRulesContent,
|
|
79
80
|
public readonly slot_name: string,
|
|
80
81
|
writeCheckpointMode: storage.WriteCheckpointMode | undefined,
|
|
81
82
|
options: MongoSyncBucketStorageOptions
|
|
82
83
|
) {
|
|
83
84
|
super();
|
|
84
|
-
this.db = factory.db;
|
|
85
|
+
this.db = factory.db.versioned(sync_rules.getStorageConfig());
|
|
85
86
|
this.checksums = new MongoChecksums(this.db, this.group_id, {
|
|
86
87
|
...options.checksumOptions,
|
|
87
88
|
storageConfig: options?.storageConfig
|
|
@@ -166,10 +167,7 @@ export class MongoSyncBucketStorage
|
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
async
|
|
170
|
-
options: storage.StartBatchOptions,
|
|
171
|
-
callback: (batch: storage.BucketStorageBatch) => Promise<void>
|
|
172
|
-
): Promise<storage.FlushedResult | null> {
|
|
170
|
+
async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
|
|
173
171
|
const doc = await this.db.sync_rules.findOne(
|
|
174
172
|
{
|
|
175
173
|
_id: this.group_id
|
|
@@ -178,7 +176,7 @@ export class MongoSyncBucketStorage
|
|
|
178
176
|
);
|
|
179
177
|
const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
|
|
180
178
|
|
|
181
|
-
|
|
179
|
+
const writer = new MongoBucketBatch({
|
|
182
180
|
logger: options.logger,
|
|
183
181
|
db: this.db,
|
|
184
182
|
syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
|
|
@@ -186,21 +184,26 @@ export class MongoSyncBucketStorage
|
|
|
186
184
|
slotName: this.slot_name,
|
|
187
185
|
lastCheckpointLsn: checkpoint_lsn,
|
|
188
186
|
resumeFromLsn: maxLsn(checkpoint_lsn, doc?.snapshot_lsn),
|
|
189
|
-
noCheckpointBeforeLsn: doc?.no_checkpoint_before ?? options.zeroLSN,
|
|
190
187
|
keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
|
|
191
188
|
storeCurrentData: options.storeCurrentData,
|
|
192
189
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
193
190
|
markRecordUnavailable: options.markRecordUnavailable
|
|
194
191
|
});
|
|
195
|
-
this.iterateListeners((cb) => cb.batchStarted?.(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
192
|
+
this.iterateListeners((cb) => cb.batchStarted?.(writer));
|
|
193
|
+
return writer;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated Use `createWriter()` with `await using` instead.
|
|
198
|
+
*/
|
|
199
|
+
async startBatch(
|
|
200
|
+
options: storage.CreateWriterOptions,
|
|
201
|
+
callback: (batch: storage.BucketStorageBatch) => Promise<void>
|
|
202
|
+
): Promise<storage.FlushedResult | null> {
|
|
203
|
+
await using writer = await this.createWriter(options);
|
|
204
|
+
await callback(writer);
|
|
205
|
+
await writer.flush();
|
|
206
|
+
return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
|
|
@@ -372,19 +375,20 @@ export class MongoSyncBucketStorage
|
|
|
372
375
|
|
|
373
376
|
async *getBucketDataBatch(
|
|
374
377
|
checkpoint: utils.InternalOpId,
|
|
375
|
-
dataBuckets:
|
|
378
|
+
dataBuckets: storage.BucketDataRequest[],
|
|
376
379
|
options?: storage.BucketDataBatchOptions
|
|
377
380
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
378
|
-
if (dataBuckets.
|
|
381
|
+
if (dataBuckets.length == 0) {
|
|
379
382
|
return;
|
|
380
383
|
}
|
|
381
384
|
let filters: mongo.Filter<BucketDataDocument>[] = [];
|
|
385
|
+
const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
382
386
|
|
|
383
387
|
if (checkpoint == null) {
|
|
384
388
|
throw new ServiceAssertionError('checkpoint is null');
|
|
385
389
|
}
|
|
386
390
|
const end = checkpoint;
|
|
387
|
-
for (let
|
|
391
|
+
for (let { bucket: name, start } of dataBuckets) {
|
|
388
392
|
filters.push({
|
|
389
393
|
_id: {
|
|
390
394
|
$gt: {
|
|
@@ -477,7 +481,7 @@ export class MongoSyncBucketStorage
|
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
if (start == null) {
|
|
480
|
-
const startOpId =
|
|
484
|
+
const startOpId = bucketMap.get(bucket);
|
|
481
485
|
if (startOpId == null) {
|
|
482
486
|
throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
|
|
483
487
|
}
|
|
@@ -519,7 +523,10 @@ export class MongoSyncBucketStorage
|
|
|
519
523
|
}
|
|
520
524
|
}
|
|
521
525
|
|
|
522
|
-
async getChecksums(
|
|
526
|
+
async getChecksums(
|
|
527
|
+
checkpoint: utils.InternalOpId,
|
|
528
|
+
buckets: storage.BucketChecksumRequest[]
|
|
529
|
+
): Promise<utils.ChecksumMap> {
|
|
523
530
|
return this.checksums.getChecksums(checkpoint, buckets);
|
|
524
531
|
}
|
|
525
532
|
|
|
@@ -576,7 +583,7 @@ export class MongoSyncBucketStorage
|
|
|
576
583
|
async clear(options?: storage.ClearStorageOptions): Promise<void> {
|
|
577
584
|
while (true) {
|
|
578
585
|
if (options?.signal?.aborted) {
|
|
579
|
-
throw new ReplicationAbortedError('Aborted clearing data');
|
|
586
|
+
throw new ReplicationAbortedError('Aborted clearing data', options.signal.reason);
|
|
580
587
|
}
|
|
581
588
|
try {
|
|
582
589
|
await this.clearIteration();
|
|
@@ -631,7 +638,7 @@ export class MongoSyncBucketStorage
|
|
|
631
638
|
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
632
639
|
);
|
|
633
640
|
|
|
634
|
-
await this.db.
|
|
641
|
+
await this.db.common_current_data.deleteMany(
|
|
635
642
|
{
|
|
636
643
|
_id: idPrefixFilter<SourceKey>({ g: this.group_id }, ['t', 'k'])
|
|
637
644
|
},
|
|
@@ -2,7 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
|
|
3
3
|
import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
|
|
4
4
|
import { storage } from '@powersync/service-core';
|
|
5
|
-
import { PowerSyncMongo } from './db.js';
|
|
5
|
+
import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Manages a lock on a sync rules document, so that only one process
|
|
@@ -12,7 +12,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
12
12
|
private readonly refreshInterval: NodeJS.Timeout;
|
|
13
13
|
|
|
14
14
|
static async createLock(
|
|
15
|
-
db:
|
|
15
|
+
db: VersionedPowerSyncMongo,
|
|
16
16
|
sync_rules: storage.PersistedSyncRulesContent
|
|
17
17
|
): Promise<MongoSyncRulesLock> {
|
|
18
18
|
const lockId = crypto.randomBytes(8).toString('hex');
|
|
@@ -52,7 +52,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
constructor(
|
|
55
|
-
private db:
|
|
55
|
+
private db: VersionedPowerSyncMongo,
|
|
56
56
|
public sync_rules_id: number,
|
|
57
57
|
private lock_id: string
|
|
58
58
|
) {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import * as framework from '@powersync/lib-services-framework';
|
|
3
3
|
import { GetCheckpointChangesOptions, InternalOpId, storage } from '@powersync/service-core';
|
|
4
|
-
import { PowerSyncMongo } from './db.js';
|
|
4
|
+
import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
|
|
5
5
|
|
|
6
6
|
export type MongoCheckpointAPIOptions = {
|
|
7
|
-
db:
|
|
7
|
+
db: VersionedPowerSyncMongo;
|
|
8
8
|
mode: storage.WriteCheckpointMode;
|
|
9
9
|
sync_rules_id: number;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
13
|
-
readonly db:
|
|
13
|
+
readonly db: VersionedPowerSyncMongo;
|
|
14
14
|
private _mode: storage.WriteCheckpointMode;
|
|
15
15
|
|
|
16
16
|
constructor(options: MongoCheckpointAPIOptions) {
|
|
@@ -166,7 +166,7 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
export async function batchCreateCustomWriteCheckpoints(
|
|
169
|
-
db:
|
|
169
|
+
db: VersionedPowerSyncMongo,
|
|
170
170
|
session: mongo.ClientSession,
|
|
171
171
|
checkpoints: storage.CustomWriteCheckpointOptions[],
|
|
172
172
|
opId: InternalOpId
|
|
@@ -2,6 +2,7 @@ import { ToastableSqliteRow } from '@powersync/service-sync-rules';
|
|
|
2
2
|
import * as bson from 'bson';
|
|
3
3
|
|
|
4
4
|
import { storage } from '@powersync/service-core';
|
|
5
|
+
import { mongoTableId } from '../storage-index.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Maximum number of operations in a batch.
|
|
@@ -86,8 +87,8 @@ export class RecordOperation {
|
|
|
86
87
|
const beforeId = record.beforeReplicaId ?? record.afterReplicaId;
|
|
87
88
|
this.afterId = afterId;
|
|
88
89
|
this.beforeId = beforeId;
|
|
89
|
-
this.internalBeforeKey = cacheKey(record.sourceTable.id, beforeId);
|
|
90
|
-
this.internalAfterKey = afterId ? cacheKey(record.sourceTable.id, afterId) : null;
|
|
90
|
+
this.internalBeforeKey = cacheKey(mongoTableId(record.sourceTable.id), beforeId);
|
|
91
|
+
this.internalAfterKey = afterId ? cacheKey(mongoTableId(record.sourceTable.id), afterId) : null;
|
|
91
92
|
|
|
92
93
|
this.estimatedSize = estimateRowSize(record.before) + estimateRowSize(record.after);
|
|
93
94
|
}
|
|
@@ -5,9 +5,9 @@ import * as bson from 'bson';
|
|
|
5
5
|
|
|
6
6
|
import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
7
7
|
import { InternalOpId, storage, utils } from '@powersync/service-core';
|
|
8
|
-
import { currentBucketKey, MAX_ROW_SIZE } from './MongoBucketBatch.js';
|
|
8
|
+
import { currentBucketKey, EMPTY_DATA, MAX_ROW_SIZE } from './MongoBucketBatch.js';
|
|
9
9
|
import { MongoIdSequence } from './MongoIdSequence.js';
|
|
10
|
-
import { PowerSyncMongo } from './db.js';
|
|
10
|
+
import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
|
|
11
11
|
import {
|
|
12
12
|
BucketDataDocument,
|
|
13
13
|
BucketParameterDocument,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
CurrentDataDocument,
|
|
17
17
|
SourceKey
|
|
18
18
|
} from './models.js';
|
|
19
|
-
import { replicaIdToSubkey } from '../../utils/util.js';
|
|
19
|
+
import { mongoTableId, replicaIdToSubkey } from '../../utils/util.js';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Maximum size of operations we write in a single transaction.
|
|
@@ -63,6 +63,7 @@ export class PersistedBatch {
|
|
|
63
63
|
currentSize = 0;
|
|
64
64
|
|
|
65
65
|
constructor(
|
|
66
|
+
private db: VersionedPowerSyncMongo,
|
|
66
67
|
private group_id: number,
|
|
67
68
|
writtenSize: number,
|
|
68
69
|
options?: { logger?: Logger }
|
|
@@ -132,7 +133,7 @@ export class PersistedBatch {
|
|
|
132
133
|
o: op_id
|
|
133
134
|
},
|
|
134
135
|
op: 'PUT',
|
|
135
|
-
source_table: options.table.id,
|
|
136
|
+
source_table: mongoTableId(options.table.id),
|
|
136
137
|
source_key: options.sourceKey,
|
|
137
138
|
table: k.table,
|
|
138
139
|
row_id: k.id,
|
|
@@ -159,7 +160,7 @@ export class PersistedBatch {
|
|
|
159
160
|
o: op_id
|
|
160
161
|
},
|
|
161
162
|
op: 'REMOVE',
|
|
162
|
-
source_table: options.table.id,
|
|
163
|
+
source_table: mongoTableId(options.table.id),
|
|
163
164
|
source_key: options.sourceKey,
|
|
164
165
|
table: bd.table,
|
|
165
166
|
row_id: bd.id,
|
|
@@ -208,7 +209,7 @@ export class PersistedBatch {
|
|
|
208
209
|
_id: op_id,
|
|
209
210
|
key: {
|
|
210
211
|
g: this.group_id,
|
|
211
|
-
t: sourceTable.id,
|
|
212
|
+
t: mongoTableId(sourceTable.id),
|
|
212
213
|
k: sourceKey
|
|
213
214
|
},
|
|
214
215
|
lookup: binLookup,
|
|
@@ -230,7 +231,7 @@ export class PersistedBatch {
|
|
|
230
231
|
_id: op_id,
|
|
231
232
|
key: {
|
|
232
233
|
g: this.group_id,
|
|
233
|
-
t: sourceTable.id,
|
|
234
|
+
t: mongoTableId(sourceTable.id),
|
|
234
235
|
k: sourceKey
|
|
235
236
|
},
|
|
236
237
|
lookup: lookup,
|
|
@@ -243,7 +244,7 @@ export class PersistedBatch {
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
|
|
247
|
+
hardDeleteCurrentData(id: SourceKey) {
|
|
247
248
|
const op: mongo.AnyBulkWriteOperation<CurrentDataDocument> = {
|
|
248
249
|
deleteOne: {
|
|
249
250
|
filter: { _id: id }
|
|
@@ -253,12 +254,41 @@ export class PersistedBatch {
|
|
|
253
254
|
this.currentSize += 50;
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Mark a current_data document as soft deleted, to delete on the next commit.
|
|
259
|
+
*
|
|
260
|
+
* If softDeleteCurrentData is not enabled, this falls back to a hard delete.
|
|
261
|
+
*/
|
|
262
|
+
softDeleteCurrentData(id: SourceKey, checkpointGreaterThan: bigint) {
|
|
263
|
+
if (!this.db.storageConfig.softDeleteCurrentData) {
|
|
264
|
+
this.hardDeleteCurrentData(id);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const op: mongo.AnyBulkWriteOperation<CurrentDataDocument> = {
|
|
268
|
+
updateOne: {
|
|
269
|
+
filter: { _id: id },
|
|
270
|
+
update: {
|
|
271
|
+
$set: {
|
|
272
|
+
data: EMPTY_DATA,
|
|
273
|
+
buckets: [],
|
|
274
|
+
lookups: [],
|
|
275
|
+
pending_delete: checkpointGreaterThan
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
upsert: true
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
this.currentData.push(op);
|
|
282
|
+
this.currentSize += 50;
|
|
283
|
+
}
|
|
284
|
+
|
|
256
285
|
upsertCurrentData(id: SourceKey, values: Partial<CurrentDataDocument>) {
|
|
257
286
|
const op: mongo.AnyBulkWriteOperation<CurrentDataDocument> = {
|
|
258
287
|
updateOne: {
|
|
259
288
|
filter: { _id: id },
|
|
260
289
|
update: {
|
|
261
|
-
$set: values
|
|
290
|
+
$set: values,
|
|
291
|
+
$unset: { pending_delete: 1 }
|
|
262
292
|
},
|
|
263
293
|
upsert: true
|
|
264
294
|
}
|
|
@@ -276,7 +306,8 @@ export class PersistedBatch {
|
|
|
276
306
|
);
|
|
277
307
|
}
|
|
278
308
|
|
|
279
|
-
async flush(
|
|
309
|
+
async flush(session: mongo.ClientSession, options?: storage.BucketBatchCommitOptions) {
|
|
310
|
+
const db = this.db;
|
|
280
311
|
const startAt = performance.now();
|
|
281
312
|
let flushedSomething = false;
|
|
282
313
|
if (this.bucketData.length > 0) {
|
|
@@ -297,7 +328,7 @@ export class PersistedBatch {
|
|
|
297
328
|
}
|
|
298
329
|
if (this.currentData.length > 0) {
|
|
299
330
|
flushedSomething = true;
|
|
300
|
-
await db.
|
|
331
|
+
await db.common_current_data.bulkWrite(this.currentData, {
|
|
301
332
|
session,
|
|
302
333
|
// may update and delete data within the same batch - order matters
|
|
303
334
|
ordered: true
|