@powersync/service-module-mongodb-storage 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js +1 -4
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +3 -2
- package/dist/storage/MongoBucketStorage.js +4 -2
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +66 -0
- package/dist/storage/implementation/MongoChecksums.js +287 -0
- package/dist/storage/implementation/MongoChecksums.js.map +1 -0
- package/dist/storage/implementation/MongoCompactor.d.ts +9 -2
- package/dist/storage/implementation/MongoCompactor.js +116 -39
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +7 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +14 -132
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.d.ts +2 -0
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js +4 -3
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -0
- package/dist/storage/implementation/db.js +10 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +5 -1
- package/dist/storage/implementation/util.js.map +1 -1
- package/package.json +4 -4
- package/src/migrations/db/migrations/1741697235857-bucket-state-index.ts +1 -7
- package/src/storage/MongoBucketStorage.ts +4 -3
- package/src/storage/implementation/MongoChecksums.ts +342 -0
- package/src/storage/implementation/MongoCompactor.ts +156 -64
- package/src/storage/implementation/MongoSyncBucketStorage.ts +21 -152
- package/src/storage/implementation/MongoTestStorageFactoryGenerator.ts +7 -4
- package/src/storage/implementation/db.ts +14 -0
- package/src/storage/implementation/models.ts +5 -1
- package/src/storage/implementation/util.ts +1 -1
- package/test/src/__snapshots__/storage.test.ts.snap +17 -1
- package/test/src/storage.test.ts +38 -1
- package/test/src/storage_compacting.test.ts +120 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { mongo } from '@powersync/lib-service-mongodb';
|
|
1
|
+
import { mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { addChecksums, InternalOpId, storage, utils } from '@powersync/service-core';
|
|
3
|
+
import { addChecksums, InternalOpId, isPartialChecksum, storage, utils } from '@powersync/service-core';
|
|
4
4
|
|
|
5
5
|
import { PowerSyncMongo } from './db.js';
|
|
6
6
|
import { BucketDataDocument, BucketDataKey, BucketStateDocument } from './models.js';
|
|
7
|
+
import { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
|
|
7
8
|
import { cacheKey } from './OperationBatch.js';
|
|
8
|
-
import { readSingleBatch } from './util.js';
|
|
9
9
|
|
|
10
10
|
interface CurrentBucketState {
|
|
11
11
|
/** Bucket name */
|
|
@@ -68,12 +68,14 @@ export class MongoCompactor {
|
|
|
68
68
|
private maxOpId: bigint;
|
|
69
69
|
private buckets: string[] | undefined;
|
|
70
70
|
private signal?: AbortSignal;
|
|
71
|
+
private group_id: number;
|
|
71
72
|
|
|
72
73
|
constructor(
|
|
74
|
+
private storage: MongoSyncBucketStorage,
|
|
73
75
|
private db: PowerSyncMongo,
|
|
74
|
-
private group_id: number,
|
|
75
76
|
options?: MongoCompactOptions
|
|
76
77
|
) {
|
|
78
|
+
this.group_id = storage.group_id;
|
|
77
79
|
this.idLimitBytes = (options?.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
|
|
78
80
|
this.moveBatchLimit = options?.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
|
|
79
81
|
this.moveBatchQueryLimit = options?.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
|
|
@@ -136,33 +138,57 @@ export class MongoCompactor {
|
|
|
136
138
|
o: new mongo.MaxKey() as any
|
|
137
139
|
};
|
|
138
140
|
|
|
141
|
+
const doneWithBucket = async () => {
|
|
142
|
+
if (currentState == null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Free memory before clearing bucket
|
|
146
|
+
currentState.seen.clear();
|
|
147
|
+
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
148
|
+
logger.info(
|
|
149
|
+
`Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
150
|
+
);
|
|
151
|
+
// Need flush() before clear()
|
|
152
|
+
await this.flush();
|
|
153
|
+
await this.clearBucket(currentState);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Do this _after_ clearBucket so that we have accurate counts.
|
|
157
|
+
this.updateBucketChecksums(currentState);
|
|
158
|
+
};
|
|
159
|
+
|
|
139
160
|
while (!this.signal?.aborted) {
|
|
140
161
|
// Query one batch at a time, to avoid cursor timeouts
|
|
141
|
-
const cursor = this.db.bucket_data.aggregate<BucketDataDocument & { size: number | bigint }>(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
const cursor = this.db.bucket_data.aggregate<BucketDataDocument & { size: number | bigint }>(
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
$match: {
|
|
166
|
+
_id: {
|
|
167
|
+
$gte: lowerBound,
|
|
168
|
+
$lt: upperBound
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{ $sort: { _id: -1 } },
|
|
173
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
174
|
+
{
|
|
175
|
+
$project: {
|
|
176
|
+
_id: 1,
|
|
177
|
+
op: 1,
|
|
178
|
+
table: 1,
|
|
179
|
+
row_id: 1,
|
|
180
|
+
source_table: 1,
|
|
181
|
+
source_key: 1,
|
|
182
|
+
checksum: 1,
|
|
183
|
+
size: { $bsonSize: '$$ROOT' }
|
|
147
184
|
}
|
|
148
185
|
}
|
|
149
|
-
|
|
150
|
-
{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
op: 1,
|
|
156
|
-
table: 1,
|
|
157
|
-
row_id: 1,
|
|
158
|
-
source_table: 1,
|
|
159
|
-
source_key: 1,
|
|
160
|
-
checksum: 1,
|
|
161
|
-
size: { $bsonSize: '$$ROOT' }
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
]);
|
|
165
|
-
const { data: batch } = await readSingleBatch(cursor);
|
|
186
|
+
],
|
|
187
|
+
{ batchSize: this.moveBatchQueryLimit }
|
|
188
|
+
);
|
|
189
|
+
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
190
|
+
// Instead, we load up to the limit.
|
|
191
|
+
const batch = await cursor.toArray();
|
|
166
192
|
|
|
167
193
|
if (batch.length == 0) {
|
|
168
194
|
// We've reached the end
|
|
@@ -174,24 +200,8 @@ export class MongoCompactor {
|
|
|
174
200
|
|
|
175
201
|
for (let doc of batch) {
|
|
176
202
|
if (currentState == null || doc._id.b != currentState.bucket) {
|
|
177
|
-
|
|
178
|
-
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
179
|
-
// Important to flush before clearBucket()
|
|
180
|
-
// Does not have to happen before flushBucketChecksums()
|
|
181
|
-
await this.flush();
|
|
182
|
-
logger.info(
|
|
183
|
-
`Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Free memory before clearing bucket
|
|
187
|
-
currentState!.seen.clear();
|
|
188
|
-
|
|
189
|
-
await this.clearBucket(currentState);
|
|
190
|
-
}
|
|
203
|
+
await doneWithBucket();
|
|
191
204
|
|
|
192
|
-
// Should happen after clearBucket() for accurate stats
|
|
193
|
-
this.updateBucketChecksums(currentState);
|
|
194
|
-
}
|
|
195
205
|
currentState = {
|
|
196
206
|
bucket: doc._id.b,
|
|
197
207
|
seen: new Map(),
|
|
@@ -274,21 +284,14 @@ export class MongoCompactor {
|
|
|
274
284
|
await this.flush();
|
|
275
285
|
}
|
|
276
286
|
}
|
|
277
|
-
}
|
|
278
287
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
283
|
-
);
|
|
284
|
-
// Need flush() before clear()
|
|
285
|
-
await this.flush();
|
|
286
|
-
await this.clearBucket(currentState);
|
|
287
|
-
}
|
|
288
|
-
if (currentState != null) {
|
|
289
|
-
// Do this _after_ clearBucket so that we have accurate counts.
|
|
290
|
-
this.updateBucketChecksums(currentState);
|
|
288
|
+
if (currentState != null) {
|
|
289
|
+
logger.info(`Processed batch of length ${batch.length} current bucket: ${currentState.bucket}`);
|
|
290
|
+
}
|
|
291
291
|
}
|
|
292
|
+
|
|
293
|
+
await doneWithBucket();
|
|
294
|
+
|
|
292
295
|
// Need another flush after updateBucketChecksums()
|
|
293
296
|
await this.flush();
|
|
294
297
|
}
|
|
@@ -325,15 +328,11 @@ export class MongoCompactor {
|
|
|
325
328
|
count: 0,
|
|
326
329
|
bytes: 0
|
|
327
330
|
}
|
|
328
|
-
},
|
|
329
|
-
$setOnInsert: {
|
|
330
|
-
// Only set this if we're creating the document.
|
|
331
|
-
// In all other cases, the replication process will have a set a more accurate id.
|
|
332
|
-
last_op: this.maxOpId
|
|
333
331
|
}
|
|
334
332
|
},
|
|
335
|
-
// We generally expect this to have been created before
|
|
336
|
-
|
|
333
|
+
// We generally expect this to have been created before.
|
|
334
|
+
// We don't create new ones here, to avoid issues with the unique index on bucket_updates.
|
|
335
|
+
upsert: false
|
|
337
336
|
}
|
|
338
337
|
});
|
|
339
338
|
}
|
|
@@ -475,4 +474,97 @@ export class MongoCompactor {
|
|
|
475
474
|
await session.endSession();
|
|
476
475
|
}
|
|
477
476
|
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Subset of compact, only populating checksums where relevant.
|
|
480
|
+
*/
|
|
481
|
+
async populateChecksums() {
|
|
482
|
+
// This is updated after each batch
|
|
483
|
+
let lowerBound: BucketStateDocument['_id'] = {
|
|
484
|
+
g: this.group_id,
|
|
485
|
+
b: new mongo.MinKey() as any
|
|
486
|
+
};
|
|
487
|
+
// This is static
|
|
488
|
+
const upperBound: BucketStateDocument['_id'] = {
|
|
489
|
+
g: this.group_id,
|
|
490
|
+
b: new mongo.MaxKey() as any
|
|
491
|
+
};
|
|
492
|
+
while (!this.signal?.aborted) {
|
|
493
|
+
// By filtering buckets, we effectively make this "resumeable".
|
|
494
|
+
const filter: mongo.Filter<BucketStateDocument> = {
|
|
495
|
+
_id: {
|
|
496
|
+
$gt: lowerBound,
|
|
497
|
+
$lt: upperBound
|
|
498
|
+
},
|
|
499
|
+
compacted_state: { $exists: false }
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const bucketsWithoutChecksums = await this.db.bucket_state
|
|
503
|
+
.find(filter, {
|
|
504
|
+
projection: {
|
|
505
|
+
_id: 1
|
|
506
|
+
},
|
|
507
|
+
sort: {
|
|
508
|
+
_id: 1
|
|
509
|
+
},
|
|
510
|
+
limit: 5_000,
|
|
511
|
+
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
512
|
+
})
|
|
513
|
+
.toArray();
|
|
514
|
+
if (bucketsWithoutChecksums.length == 0) {
|
|
515
|
+
// All done
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
logger.info(`Calculating checksums for batch of ${bucketsWithoutChecksums.length} buckets`);
|
|
520
|
+
|
|
521
|
+
await this.updateChecksumsBatch(bucketsWithoutChecksums.map((b) => b._id.b));
|
|
522
|
+
|
|
523
|
+
lowerBound = bucketsWithoutChecksums[bucketsWithoutChecksums.length - 1]._id;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private async updateChecksumsBatch(buckets: string[]) {
|
|
528
|
+
const checksums = await this.storage.checksums.computePartialChecksumsDirect(
|
|
529
|
+
buckets.map((bucket) => {
|
|
530
|
+
return {
|
|
531
|
+
bucket,
|
|
532
|
+
end: this.maxOpId
|
|
533
|
+
};
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
for (let bucketChecksum of checksums.values()) {
|
|
538
|
+
if (isPartialChecksum(bucketChecksum)) {
|
|
539
|
+
// Should never happen since we don't specify `start`
|
|
540
|
+
throw new ServiceAssertionError(`Full checksum expected, got ${JSON.stringify(bucketChecksum)}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.bucketStateUpdates.push({
|
|
544
|
+
updateOne: {
|
|
545
|
+
filter: {
|
|
546
|
+
_id: {
|
|
547
|
+
g: this.group_id,
|
|
548
|
+
b: bucketChecksum.bucket
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
update: {
|
|
552
|
+
$set: {
|
|
553
|
+
compacted_state: {
|
|
554
|
+
op_id: this.maxOpId,
|
|
555
|
+
count: bucketChecksum.count,
|
|
556
|
+
checksum: BigInt(bucketChecksum.checksum),
|
|
557
|
+
bytes: null
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
// We don't create new ones here - it gets tricky to get the last_op right with the unique index on:
|
|
562
|
+
// bucket_updates: {'id.g': 1, 'last_op': 1}
|
|
563
|
+
upsert: false
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
await this.flush();
|
|
569
|
+
}
|
|
478
570
|
}
|
|
@@ -7,9 +7,7 @@ import {
|
|
|
7
7
|
ServiceAssertionError
|
|
8
8
|
} from '@powersync/lib-services-framework';
|
|
9
9
|
import {
|
|
10
|
-
addPartialChecksums,
|
|
11
10
|
BroadcastIterable,
|
|
12
|
-
BucketChecksum,
|
|
13
11
|
CHECKPOINT_INVALIDATE_ALL,
|
|
14
12
|
CheckpointChanges,
|
|
15
13
|
CompactOptions,
|
|
@@ -18,7 +16,6 @@ import {
|
|
|
18
16
|
InternalOpId,
|
|
19
17
|
internalToExternalOpId,
|
|
20
18
|
maxLsn,
|
|
21
|
-
PartialChecksum,
|
|
22
19
|
ProtocolOpId,
|
|
23
20
|
ReplicationCheckpoint,
|
|
24
21
|
storage,
|
|
@@ -34,21 +31,22 @@ import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
|
34
31
|
import { PowerSyncMongo } from './db.js';
|
|
35
32
|
import { BucketDataDocument, BucketDataKey, BucketStateDocument, SourceKey, SourceTableDocument } from './models.js';
|
|
36
33
|
import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
34
|
+
import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
|
|
37
35
|
import { MongoCompactor } from './MongoCompactor.js';
|
|
38
36
|
import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
39
37
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
40
38
|
import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from './util.js';
|
|
41
39
|
|
|
40
|
+
export interface MongoSyncBucketStorageOptions {
|
|
41
|
+
checksumOptions?: MongoChecksumOptions;
|
|
42
|
+
}
|
|
43
|
+
|
|
42
44
|
export class MongoSyncBucketStorage
|
|
43
45
|
extends BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
44
46
|
implements storage.SyncRulesBucketStorage
|
|
45
47
|
{
|
|
46
48
|
private readonly db: PowerSyncMongo;
|
|
47
|
-
|
|
48
|
-
fetchChecksums: (batch) => {
|
|
49
|
-
return this.getChecksumsInternal(batch);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
49
|
+
readonly checksums: MongoChecksums;
|
|
52
50
|
|
|
53
51
|
private parsedSyncRulesCache: { parsed: SqlSyncRules; options: storage.ParseSyncRulesOptions } | undefined;
|
|
54
52
|
private writeCheckpointAPI: MongoWriteCheckpointAPI;
|
|
@@ -58,13 +56,15 @@ export class MongoSyncBucketStorage
|
|
|
58
56
|
public readonly group_id: number,
|
|
59
57
|
private readonly sync_rules: storage.PersistedSyncRulesContent,
|
|
60
58
|
public readonly slot_name: string,
|
|
61
|
-
writeCheckpointMode
|
|
59
|
+
writeCheckpointMode?: storage.WriteCheckpointMode,
|
|
60
|
+
options?: MongoSyncBucketStorageOptions
|
|
62
61
|
) {
|
|
63
62
|
super();
|
|
64
63
|
this.db = factory.db;
|
|
64
|
+
this.checksums = new MongoChecksums(this.db, this.group_id, options?.checksumOptions);
|
|
65
65
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
66
66
|
db: this.db,
|
|
67
|
-
mode: writeCheckpointMode,
|
|
67
|
+
mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
|
|
68
68
|
sync_rules_id: group_id
|
|
69
69
|
});
|
|
70
70
|
}
|
|
@@ -491,145 +491,11 @@ export class MongoSyncBucketStorage
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
async getChecksums(checkpoint: utils.InternalOpId, buckets: string[]): Promise<utils.ChecksumMap> {
|
|
494
|
-
return this.
|
|
494
|
+
return this.checksums.getChecksums(checkpoint, buckets);
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
clearChecksumCache() {
|
|
498
|
-
this.
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
|
|
502
|
-
if (batch.length == 0) {
|
|
503
|
-
return new Map();
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const preFilters: any[] = [];
|
|
507
|
-
for (let request of batch) {
|
|
508
|
-
if (request.start == null) {
|
|
509
|
-
preFilters.push({
|
|
510
|
-
_id: {
|
|
511
|
-
g: this.group_id,
|
|
512
|
-
b: request.bucket
|
|
513
|
-
},
|
|
514
|
-
'compacted_state.op_id': { $exists: true, $lte: request.end }
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const preStates = new Map<string, { opId: InternalOpId; checksum: BucketChecksum }>();
|
|
520
|
-
|
|
521
|
-
if (preFilters.length > 0) {
|
|
522
|
-
// For un-cached bucket checksums, attempt to use the compacted state first.
|
|
523
|
-
const states = await this.db.bucket_state
|
|
524
|
-
.find({
|
|
525
|
-
$or: preFilters
|
|
526
|
-
})
|
|
527
|
-
.toArray();
|
|
528
|
-
for (let state of states) {
|
|
529
|
-
const compactedState = state.compacted_state!;
|
|
530
|
-
preStates.set(state._id.b, {
|
|
531
|
-
opId: compactedState.op_id,
|
|
532
|
-
checksum: {
|
|
533
|
-
bucket: state._id.b,
|
|
534
|
-
checksum: Number(compactedState.checksum),
|
|
535
|
-
count: compactedState.count
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const filters: any[] = [];
|
|
542
|
-
for (let request of batch) {
|
|
543
|
-
let start = request.start;
|
|
544
|
-
if (start == null) {
|
|
545
|
-
const preState = preStates.get(request.bucket);
|
|
546
|
-
if (preState != null) {
|
|
547
|
-
start = preState.opId;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
filters.push({
|
|
552
|
-
_id: {
|
|
553
|
-
$gt: {
|
|
554
|
-
g: this.group_id,
|
|
555
|
-
b: request.bucket,
|
|
556
|
-
o: start ?? new bson.MinKey()
|
|
557
|
-
},
|
|
558
|
-
$lte: {
|
|
559
|
-
g: this.group_id,
|
|
560
|
-
b: request.bucket,
|
|
561
|
-
o: request.end
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const aggregate = await this.db.bucket_data
|
|
568
|
-
.aggregate(
|
|
569
|
-
[
|
|
570
|
-
{
|
|
571
|
-
$match: {
|
|
572
|
-
$or: filters
|
|
573
|
-
}
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
$group: {
|
|
577
|
-
_id: '$_id.b',
|
|
578
|
-
// Historically, checksum may be stored as 'int' or 'double'.
|
|
579
|
-
// More recently, this should be a 'long'.
|
|
580
|
-
// $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
|
|
581
|
-
checksum_total: { $sum: { $toLong: '$checksum' } },
|
|
582
|
-
count: { $sum: 1 },
|
|
583
|
-
has_clear_op: {
|
|
584
|
-
$max: {
|
|
585
|
-
$cond: [{ $eq: ['$op', 'CLEAR'] }, 1, 0]
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
],
|
|
591
|
-
{ session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.db.MONGO_CHECKSUM_TIMEOUT_MS }
|
|
592
|
-
)
|
|
593
|
-
.toArray()
|
|
594
|
-
.catch((e) => {
|
|
595
|
-
throw lib_mongo.mapQueryError(e, 'while reading checksums');
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
const partialChecksums = new Map<string, storage.PartialOrFullChecksum>(
|
|
599
|
-
aggregate.map((doc) => {
|
|
600
|
-
const partialChecksum = Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff;
|
|
601
|
-
const bucket = doc._id;
|
|
602
|
-
return [
|
|
603
|
-
bucket,
|
|
604
|
-
doc.has_clear_op == 1
|
|
605
|
-
? ({
|
|
606
|
-
// full checksum - replaces any previous one
|
|
607
|
-
bucket,
|
|
608
|
-
checksum: partialChecksum,
|
|
609
|
-
count: doc.count
|
|
610
|
-
} satisfies BucketChecksum)
|
|
611
|
-
: ({
|
|
612
|
-
// partial checksum - is added to a previous one
|
|
613
|
-
bucket,
|
|
614
|
-
partialCount: doc.count,
|
|
615
|
-
partialChecksum
|
|
616
|
-
} satisfies PartialChecksum)
|
|
617
|
-
];
|
|
618
|
-
})
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
return new Map<string, storage.PartialOrFullChecksum>(
|
|
622
|
-
batch.map((request) => {
|
|
623
|
-
const bucket = request.bucket;
|
|
624
|
-
// Could be null if this is either (1) a partial request, or (2) no compacted checksum was available
|
|
625
|
-
const preState = preStates.get(bucket);
|
|
626
|
-
// Could be null if we got no data
|
|
627
|
-
const partialChecksum = partialChecksums.get(bucket);
|
|
628
|
-
const merged = addPartialChecksums(bucket, preState?.checksum ?? null, partialChecksum ?? null);
|
|
629
|
-
|
|
630
|
-
return [bucket, merged];
|
|
631
|
-
})
|
|
632
|
-
);
|
|
498
|
+
this.checksums.clearCache();
|
|
633
499
|
}
|
|
634
500
|
|
|
635
501
|
async terminate(options?: storage.TerminateOptions) {
|
|
@@ -779,22 +645,25 @@ export class MongoSyncBucketStorage
|
|
|
779
645
|
const checkpoint = await this.getCheckpointInternal();
|
|
780
646
|
maxOpId = checkpoint?.checkpoint ?? undefined;
|
|
781
647
|
}
|
|
782
|
-
await new MongoCompactor(this
|
|
648
|
+
await new MongoCompactor(this, this.db, { ...options, maxOpId }).compact();
|
|
649
|
+
|
|
783
650
|
if (maxOpId != null && options?.compactParameterData) {
|
|
784
651
|
await new MongoParameterCompactor(this.db, this.group_id, maxOpId, options).compact();
|
|
785
652
|
}
|
|
786
653
|
}
|
|
787
654
|
|
|
788
|
-
async populatePersistentChecksumCache(options: Pick<CompactOptions, 'signal' | 'maxOpId'
|
|
655
|
+
async populatePersistentChecksumCache(options: Required<Pick<CompactOptions, 'signal' | 'maxOpId'>>): Promise<void> {
|
|
656
|
+
logger.info(`Populating persistent checksum cache...`);
|
|
789
657
|
const start = Date.now();
|
|
790
|
-
// We do a minimal compact
|
|
791
|
-
|
|
658
|
+
// We do a minimal compact here.
|
|
659
|
+
// We can optimize this in the future.
|
|
660
|
+
const compactor = new MongoCompactor(this, this.db, {
|
|
792
661
|
...options,
|
|
793
|
-
// Skip parameter data
|
|
794
|
-
compactParameterData: false,
|
|
795
662
|
// Don't track updates for MOVE compacting
|
|
796
663
|
memoryLimitMB: 0
|
|
797
664
|
});
|
|
665
|
+
|
|
666
|
+
await compactor.populateChecksums();
|
|
798
667
|
const duration = Date.now() - start;
|
|
799
668
|
logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
|
|
800
669
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { TestStorageOptions } from '@powersync/service-core';
|
|
2
2
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
3
3
|
import { connectMongoForTests } from './util.js';
|
|
4
|
+
import { MongoSyncBucketStorageOptions } from './MongoSyncBucketStorage.js';
|
|
4
5
|
|
|
5
6
|
export type MongoTestStorageOptions = {
|
|
6
7
|
url: string;
|
|
7
8
|
isCI: boolean;
|
|
9
|
+
internalOptions?: MongoSyncBucketStorageOptions;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
export const MongoTestStorageFactoryGenerator = (factoryOptions: MongoTestStorageOptions) => {
|
|
@@ -16,13 +18,14 @@ export const MongoTestStorageFactoryGenerator = (factoryOptions: MongoTestStorag
|
|
|
16
18
|
await db.db.createCollection('bucket_parameters');
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
// Full migrations are not currently run for tests, so we manually create this
|
|
20
|
-
await db.createCheckpointEventsCollection();
|
|
21
|
-
|
|
22
21
|
if (!options?.doNotClear) {
|
|
23
22
|
await db.clear();
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
// Full migrations are not currently run for tests, so we manually create the important ones
|
|
26
|
+
await db.createCheckpointEventsCollection();
|
|
27
|
+
await db.createBucketStateIndex();
|
|
28
|
+
|
|
29
|
+
return new MongoBucketStorage(db, { slot_name_prefix: 'test_' }, factoryOptions.internalOptions);
|
|
27
30
|
};
|
|
28
31
|
};
|
|
@@ -127,6 +127,20 @@ export class PowerSyncMongo {
|
|
|
127
127
|
max: 50 // max number of documents
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Only use in migrations and tests.
|
|
133
|
+
*/
|
|
134
|
+
async createBucketStateIndex() {
|
|
135
|
+
// TODO: Implement a better mechanism to use migrations in tests
|
|
136
|
+
await this.bucket_state.createIndex(
|
|
137
|
+
{
|
|
138
|
+
'_id.g': 1,
|
|
139
|
+
last_op: 1
|
|
140
|
+
},
|
|
141
|
+
{ name: 'bucket_updates', unique: true }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
export function createPowerSyncMongo(config: MongoStorageConfig, options?: lib_mongo.MongoConnectionOptions) {
|
|
@@ -97,6 +97,10 @@ export interface BucketStateDocument {
|
|
|
97
97
|
g: number;
|
|
98
98
|
b: string;
|
|
99
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Important: There is an unique index on {'_id.g': 1, last_op: 1}.
|
|
102
|
+
* That means the last_op must match an actual op in the bucket, and not the commit checkpoint.
|
|
103
|
+
*/
|
|
100
104
|
last_op: bigint;
|
|
101
105
|
/**
|
|
102
106
|
* If set, this can be treated as "cache" of a checksum at a specific point.
|
|
@@ -106,7 +110,7 @@ export interface BucketStateDocument {
|
|
|
106
110
|
op_id: InternalOpId;
|
|
107
111
|
count: number;
|
|
108
112
|
checksum: bigint;
|
|
109
|
-
bytes: number;
|
|
113
|
+
bytes: number | null;
|
|
110
114
|
};
|
|
111
115
|
|
|
112
116
|
estimate_since_compact?: {
|
|
@@ -3,7 +3,7 @@ import * as crypto from 'crypto';
|
|
|
3
3
|
import * as uuid from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
6
|
-
import { storage, utils } from '@powersync/service-core';
|
|
6
|
+
import { BucketChecksum, PartialChecksum, PartialOrFullChecksum, storage, utils } from '@powersync/service-core';
|
|
7
7
|
|
|
8
8
|
import { PowerSyncMongo } from './db.js';
|
|
9
9
|
import { BucketDataDocument } from './models.js';
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`Mongo Sync Bucket Storage > empty storage metrics 1`] = `
|
|
3
|
+
exports[`Mongo Sync Bucket Storage - Data > empty storage metrics 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"operations_size_bytes": 0,
|
|
6
|
+
"parameters_size_bytes": 0,
|
|
7
|
+
"replication_size_bytes": 0,
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`Mongo Sync Bucket Storage - split buckets > empty storage metrics 1`] = `
|
|
12
|
+
{
|
|
13
|
+
"operations_size_bytes": 0,
|
|
14
|
+
"parameters_size_bytes": 0,
|
|
15
|
+
"replication_size_bytes": 0,
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
exports[`Mongo Sync Bucket Storage - split operations > empty storage metrics 1`] = `
|
|
4
20
|
{
|
|
5
21
|
"operations_size_bytes": 0,
|
|
6
22
|
"parameters_size_bytes": 0,
|
package/test/src/storage.test.ts
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import { register } from '@powersync/service-core-tests';
|
|
2
2
|
import { describe } from 'vitest';
|
|
3
3
|
import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
|
|
4
|
+
import { env } from './env.js';
|
|
5
|
+
import { MongoTestStorageFactoryGenerator } from '@module/storage/implementation/MongoTestStorageFactoryGenerator.js';
|
|
4
6
|
|
|
5
|
-
describe('Mongo Sync Bucket Storage', () =>
|
|
7
|
+
describe('Mongo Sync Bucket Storage - Parameters', () =>
|
|
8
|
+
register.registerDataStorageParameterTests(INITIALIZED_MONGO_STORAGE_FACTORY));
|
|
9
|
+
|
|
10
|
+
describe('Mongo Sync Bucket Storage - Data', () =>
|
|
11
|
+
register.registerDataStorageDataTests(INITIALIZED_MONGO_STORAGE_FACTORY));
|
|
12
|
+
|
|
13
|
+
describe('Mongo Sync Bucket Storage - Checkpoints', () =>
|
|
14
|
+
register.registerDataStorageCheckpointTests(INITIALIZED_MONGO_STORAGE_FACTORY));
|
|
6
15
|
|
|
7
16
|
describe('Sync Bucket Validation', register.registerBucketValidationTests);
|
|
17
|
+
|
|
18
|
+
describe('Mongo Sync Bucket Storage - split operations', () =>
|
|
19
|
+
register.registerDataStorageDataTests(
|
|
20
|
+
MongoTestStorageFactoryGenerator({
|
|
21
|
+
url: env.MONGO_TEST_URL,
|
|
22
|
+
isCI: env.CI,
|
|
23
|
+
internalOptions: {
|
|
24
|
+
checksumOptions: {
|
|
25
|
+
bucketBatchLimit: 100,
|
|
26
|
+
operationBatchLimit: 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
describe('Mongo Sync Bucket Storage - split buckets', () =>
|
|
33
|
+
register.registerDataStorageDataTests(
|
|
34
|
+
MongoTestStorageFactoryGenerator({
|
|
35
|
+
url: env.MONGO_TEST_URL,
|
|
36
|
+
isCI: env.CI,
|
|
37
|
+
internalOptions: {
|
|
38
|
+
checksumOptions: {
|
|
39
|
+
bucketBatchLimit: 1,
|
|
40
|
+
operationBatchLimit: 100
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
));
|