@powersync/service-module-mongodb-storage 0.10.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/LICENSE +3 -3
- package/dist/storage/implementation/MongoBucketBatch.d.ts +21 -2
- package/dist/storage/implementation/MongoBucketBatch.js +66 -7
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +7 -0
- package/dist/storage/implementation/MongoCompactor.js +122 -44
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +17 -0
- package/dist/storage/implementation/MongoParameterCompactor.js +92 -0
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +14 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +229 -115
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +1 -0
- package/dist/storage/implementation/PersistedBatch.js +12 -5
- package/dist/storage/implementation/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -0
- package/dist/storage/implementation/util.d.ts +2 -1
- package/dist/storage/implementation/util.js +13 -0
- package/dist/storage/implementation/util.js.map +1 -1
- package/package.json +9 -9
- package/src/storage/implementation/MongoBucketBatch.ts +82 -8
- package/src/storage/implementation/MongoCompactor.ts +147 -47
- package/src/storage/implementation/MongoParameterCompactor.ts +105 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +257 -157
- package/src/storage/implementation/PersistedBatch.ts +13 -5
- package/src/storage/implementation/models.ts +21 -0
- package/src/storage/implementation/util.ts +14 -1
- package/test/src/__snapshots__/storage_sync.test.ts.snap +319 -11
- package/test/src/storage_compacting.test.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,20 +2,23 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
|
2
2
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
3
3
|
import {
|
|
4
4
|
BaseObserver,
|
|
5
|
-
ErrorCode,
|
|
6
5
|
logger,
|
|
7
6
|
ReplicationAbortedError,
|
|
8
|
-
ServiceAssertionError
|
|
9
|
-
ServiceError
|
|
7
|
+
ServiceAssertionError
|
|
10
8
|
} from '@powersync/lib-services-framework';
|
|
11
9
|
import {
|
|
10
|
+
addPartialChecksums,
|
|
12
11
|
BroadcastIterable,
|
|
12
|
+
BucketChecksum,
|
|
13
13
|
CHECKPOINT_INVALIDATE_ALL,
|
|
14
14
|
CheckpointChanges,
|
|
15
|
+
CompactOptions,
|
|
15
16
|
deserializeParameterLookup,
|
|
16
17
|
GetCheckpointChangesOptions,
|
|
17
18
|
InternalOpId,
|
|
18
19
|
internalToExternalOpId,
|
|
20
|
+
maxLsn,
|
|
21
|
+
PartialChecksum,
|
|
19
22
|
ProtocolOpId,
|
|
20
23
|
ReplicationCheckpoint,
|
|
21
24
|
storage,
|
|
@@ -29,18 +32,12 @@ import { LRUCache } from 'lru-cache';
|
|
|
29
32
|
import * as timers from 'timers/promises';
|
|
30
33
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
31
34
|
import { PowerSyncMongo } from './db.js';
|
|
32
|
-
import {
|
|
33
|
-
BucketDataDocument,
|
|
34
|
-
BucketDataKey,
|
|
35
|
-
BucketStateDocument,
|
|
36
|
-
SourceKey,
|
|
37
|
-
SourceTableDocument,
|
|
38
|
-
SyncRuleCheckpointState
|
|
39
|
-
} from './models.js';
|
|
35
|
+
import { BucketDataDocument, BucketDataKey, BucketStateDocument, SourceKey, SourceTableDocument } from './models.js';
|
|
40
36
|
import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
41
37
|
import { MongoCompactor } from './MongoCompactor.js';
|
|
38
|
+
import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
42
39
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
43
|
-
import { idPrefixFilter, mapOpEntry, readSingleBatch } from './util.js';
|
|
40
|
+
import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from './util.js';
|
|
44
41
|
|
|
45
42
|
export class MongoSyncBucketStorage
|
|
46
43
|
extends BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
@@ -105,22 +102,44 @@ export class MongoSyncBucketStorage
|
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
async getCheckpoint(): Promise<storage.ReplicationCheckpoint> {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
return (await this.getCheckpointInternal()) ?? new EmptyReplicationCheckpoint();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getCheckpointInternal(): Promise<storage.ReplicationCheckpoint | null> {
|
|
109
|
+
return await this.db.client.withSession({ snapshot: true }, async (session) => {
|
|
110
|
+
const doc = await this.db.sync_rules.findOne(
|
|
111
|
+
{ _id: this.group_id },
|
|
112
|
+
{
|
|
113
|
+
session,
|
|
114
|
+
projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
|
|
118
|
+
// Sync rules not active - return null
|
|
119
|
+
return null;
|
|
112
120
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
|
|
122
|
+
// Specifically using operationTime instead of clusterTime
|
|
123
|
+
// There are 3 fields in the response:
|
|
124
|
+
// 1. operationTime, not exposed for snapshot sessions (used for causal consistency)
|
|
125
|
+
// 2. clusterTime (used for connection management)
|
|
126
|
+
// 3. atClusterTime, which is session.snapshotTime
|
|
127
|
+
// We use atClusterTime, to match the driver's internal snapshot handling.
|
|
128
|
+
// There are cases where clusterTime > operationTime and atClusterTime,
|
|
129
|
+
// which could cause snapshot queries using this as the snapshotTime to timeout.
|
|
130
|
+
// This was specifically observed on MongoDB 6.0 and 7.0.
|
|
131
|
+
const snapshotTime = (session as any).snapshotTime as bson.Timestamp | undefined;
|
|
132
|
+
if (snapshotTime == null) {
|
|
133
|
+
throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
|
|
134
|
+
}
|
|
135
|
+
return new MongoReplicationCheckpoint(
|
|
136
|
+
this,
|
|
137
|
+
// null/0n is a valid checkpoint in some cases, for example if the initial snapshot was empty
|
|
138
|
+
doc.last_checkpoint ?? 0n,
|
|
139
|
+
doc.last_checkpoint_lsn ?? null,
|
|
140
|
+
snapshotTime
|
|
141
|
+
);
|
|
142
|
+
});
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
async startBatch(
|
|
@@ -131,7 +150,7 @@ export class MongoSyncBucketStorage
|
|
|
131
150
|
{
|
|
132
151
|
_id: this.group_id
|
|
133
152
|
},
|
|
134
|
-
{ projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1 } }
|
|
153
|
+
{ projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1, snapshot_lsn: 1 } }
|
|
135
154
|
);
|
|
136
155
|
const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
|
|
137
156
|
|
|
@@ -142,6 +161,7 @@ export class MongoSyncBucketStorage
|
|
|
142
161
|
groupId: this.group_id,
|
|
143
162
|
slotName: this.slot_name,
|
|
144
163
|
lastCheckpointLsn: checkpoint_lsn,
|
|
164
|
+
resumeFromLsn: maxLsn(checkpoint_lsn, doc?.snapshot_lsn),
|
|
145
165
|
noCheckpointBeforeLsn: doc?.no_checkpoint_before ?? options.zeroLSN,
|
|
146
166
|
keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
|
|
147
167
|
storeCurrentData: options.storeCurrentData,
|
|
@@ -162,9 +182,9 @@ export class MongoSyncBucketStorage
|
|
|
162
182
|
async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
|
|
163
183
|
const { group_id, connection_id, connection_tag, entity_descriptor } = options;
|
|
164
184
|
|
|
165
|
-
const { schema, name
|
|
185
|
+
const { schema, name, objectId, replicaIdColumns } = entity_descriptor;
|
|
166
186
|
|
|
167
|
-
const
|
|
187
|
+
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
168
188
|
name: column.name,
|
|
169
189
|
type: column.type,
|
|
170
190
|
type_oid: column.typeId
|
|
@@ -176,8 +196,8 @@ export class MongoSyncBucketStorage
|
|
|
176
196
|
group_id: group_id,
|
|
177
197
|
connection_id: connection_id,
|
|
178
198
|
schema_name: schema,
|
|
179
|
-
table_name:
|
|
180
|
-
replica_id_columns2:
|
|
199
|
+
table_name: name,
|
|
200
|
+
replica_id_columns2: normalizedReplicaIdColumns
|
|
181
201
|
};
|
|
182
202
|
if (objectId != null) {
|
|
183
203
|
filter.relation_id = objectId;
|
|
@@ -190,24 +210,24 @@ export class MongoSyncBucketStorage
|
|
|
190
210
|
connection_id: connection_id,
|
|
191
211
|
relation_id: objectId,
|
|
192
212
|
schema_name: schema,
|
|
193
|
-
table_name:
|
|
213
|
+
table_name: name,
|
|
194
214
|
replica_id_columns: null,
|
|
195
|
-
replica_id_columns2:
|
|
215
|
+
replica_id_columns2: normalizedReplicaIdColumns,
|
|
196
216
|
snapshot_done: false,
|
|
197
217
|
snapshot_status: undefined
|
|
198
218
|
};
|
|
199
219
|
|
|
200
220
|
await col.insertOne(doc, { session });
|
|
201
221
|
}
|
|
202
|
-
const sourceTable = new storage.SourceTable(
|
|
203
|
-
doc._id,
|
|
204
|
-
connection_tag,
|
|
205
|
-
objectId,
|
|
206
|
-
schema,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
doc.snapshot_done ?? true
|
|
210
|
-
);
|
|
222
|
+
const sourceTable = new storage.SourceTable({
|
|
223
|
+
id: doc._id,
|
|
224
|
+
connectionTag: connection_tag,
|
|
225
|
+
objectId: objectId,
|
|
226
|
+
schema: schema,
|
|
227
|
+
name: name,
|
|
228
|
+
replicaIdColumns: replicaIdColumns,
|
|
229
|
+
snapshotComplete: doc.snapshot_done ?? true
|
|
230
|
+
});
|
|
211
231
|
sourceTable.syncEvent = options.sync_rules.tableTriggersEvent(sourceTable);
|
|
212
232
|
sourceTable.syncData = options.sync_rules.tableSyncsData(sourceTable);
|
|
213
233
|
sourceTable.syncParameters = options.sync_rules.tableSyncsParameters(sourceTable);
|
|
@@ -222,7 +242,7 @@ export class MongoSyncBucketStorage
|
|
|
222
242
|
|
|
223
243
|
let dropTables: storage.SourceTable[] = [];
|
|
224
244
|
// Detect tables that are either renamed, or have different replica_id_columns
|
|
225
|
-
let truncateFilter = [{ schema_name: schema, table_name:
|
|
245
|
+
let truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
|
|
226
246
|
if (objectId != null) {
|
|
227
247
|
// Only detect renames if the source uses relation ids.
|
|
228
248
|
truncateFilter.push({ relation_id: objectId });
|
|
@@ -240,15 +260,16 @@ export class MongoSyncBucketStorage
|
|
|
240
260
|
.toArray();
|
|
241
261
|
dropTables = truncate.map(
|
|
242
262
|
(doc) =>
|
|
243
|
-
new storage.SourceTable(
|
|
244
|
-
doc._id,
|
|
245
|
-
connection_tag,
|
|
246
|
-
doc.relation_id,
|
|
247
|
-
doc.schema_name,
|
|
248
|
-
doc.table_name,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
new storage.SourceTable({
|
|
264
|
+
id: doc._id,
|
|
265
|
+
connectionTag: connection_tag,
|
|
266
|
+
objectId: doc.relation_id,
|
|
267
|
+
schema: doc.schema_name,
|
|
268
|
+
name: doc.table_name,
|
|
269
|
+
replicaIdColumns:
|
|
270
|
+
doc.replica_id_columns2?.map((c) => ({ name: c.name, typeOid: c.type_oid, type: c.type })) ?? [],
|
|
271
|
+
snapshotComplete: doc.snapshot_done ?? true
|
|
272
|
+
})
|
|
252
273
|
);
|
|
253
274
|
|
|
254
275
|
result = {
|
|
@@ -259,38 +280,67 @@ export class MongoSyncBucketStorage
|
|
|
259
280
|
return result!;
|
|
260
281
|
}
|
|
261
282
|
|
|
262
|
-
async getParameterSets(checkpoint:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
async getParameterSets(checkpoint: MongoReplicationCheckpoint, lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
|
|
284
|
+
return this.db.client.withSession({ snapshot: true }, async (session) => {
|
|
285
|
+
// Set the session's snapshot time to the checkpoint's snapshot time.
|
|
286
|
+
// An alternative would be to create the session when the checkpoint is created, but managing
|
|
287
|
+
// the session lifetime would become more complex.
|
|
288
|
+
// Starting and ending sessions are cheap (synchronous when no transactions are used),
|
|
289
|
+
// so this should be fine.
|
|
290
|
+
// This is a roundabout way of setting {readConcern: {atClusterTime: clusterTime}}, since
|
|
291
|
+
// that is not exposed directly by the driver.
|
|
292
|
+
// Future versions of the driver may change the snapshotTime behavior, so we need tests to
|
|
293
|
+
// validate that this works as expected. We test this in the compacting tests.
|
|
294
|
+
setSessionSnapshotTime(session, checkpoint.snapshotTime);
|
|
295
|
+
const lookupFilter = lookups.map((lookup) => {
|
|
296
|
+
return storage.serializeLookup(lookup);
|
|
297
|
+
});
|
|
298
|
+
// This query does not use indexes super efficiently, apart from the lookup filter.
|
|
299
|
+
// From some experimentation I could do individual lookups more efficient using an index
|
|
300
|
+
// on {'key.g': 1, lookup: 1, 'key.t': 1, 'key.k': 1, _id: -1},
|
|
301
|
+
// but could not do the same using $group.
|
|
302
|
+
// For now, just rely on compacting to remove extraneous data.
|
|
303
|
+
// For a description of the data format, see the `/docs/parameters-lookups.md` file.
|
|
304
|
+
const rows = await this.db.bucket_parameters
|
|
305
|
+
.aggregate(
|
|
306
|
+
[
|
|
307
|
+
{
|
|
308
|
+
$match: {
|
|
309
|
+
'key.g': this.group_id,
|
|
310
|
+
lookup: { $in: lookupFilter },
|
|
311
|
+
_id: { $lte: checkpoint.checkpoint }
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
$sort: {
|
|
316
|
+
_id: -1
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
$group: {
|
|
321
|
+
_id: { key: '$key', lookup: '$lookup' },
|
|
322
|
+
bucket_parameters: {
|
|
323
|
+
$first: '$bucket_parameters'
|
|
324
|
+
}
|
|
325
|
+
}
|
|
285
326
|
}
|
|
327
|
+
],
|
|
328
|
+
{
|
|
329
|
+
session,
|
|
330
|
+
readConcern: 'snapshot',
|
|
331
|
+
// Limit the time for the operation to complete, to avoid getting connection timeouts
|
|
332
|
+
maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
|
|
286
333
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
334
|
+
)
|
|
335
|
+
.toArray()
|
|
336
|
+
.catch((e) => {
|
|
337
|
+
throw lib_mongo.mapQueryError(e, 'while evaluating parameter queries');
|
|
338
|
+
});
|
|
339
|
+
const groupedParameters = rows.map((row) => {
|
|
340
|
+
return row.bucket_parameters;
|
|
341
|
+
});
|
|
342
|
+
return groupedParameters.flat();
|
|
292
343
|
});
|
|
293
|
-
return groupedParameters.flat();
|
|
294
344
|
}
|
|
295
345
|
|
|
296
346
|
async *getBucketDataBatch(
|
|
@@ -444,24 +494,71 @@ export class MongoSyncBucketStorage
|
|
|
444
494
|
return this.checksumCache.getChecksumMap(checkpoint, buckets);
|
|
445
495
|
}
|
|
446
496
|
|
|
497
|
+
clearChecksumCache() {
|
|
498
|
+
this.checksumCache.clear();
|
|
499
|
+
}
|
|
500
|
+
|
|
447
501
|
private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
|
|
448
502
|
if (batch.length == 0) {
|
|
449
503
|
return new Map();
|
|
450
504
|
}
|
|
451
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
|
+
|
|
452
541
|
const filters: any[] = [];
|
|
453
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
|
+
|
|
454
551
|
filters.push({
|
|
455
552
|
_id: {
|
|
456
553
|
$gt: {
|
|
457
554
|
g: this.group_id,
|
|
458
555
|
b: request.bucket,
|
|
459
|
-
o:
|
|
556
|
+
o: start ?? new bson.MinKey()
|
|
460
557
|
},
|
|
461
558
|
$lte: {
|
|
462
559
|
g: this.group_id,
|
|
463
560
|
b: request.bucket,
|
|
464
|
-
o:
|
|
561
|
+
o: request.end
|
|
465
562
|
}
|
|
466
563
|
}
|
|
467
564
|
});
|
|
@@ -491,26 +588,48 @@ export class MongoSyncBucketStorage
|
|
|
491
588
|
}
|
|
492
589
|
}
|
|
493
590
|
],
|
|
494
|
-
{ session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.db.
|
|
591
|
+
{ session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.db.MONGO_CHECKSUM_TIMEOUT_MS }
|
|
495
592
|
)
|
|
496
593
|
.toArray()
|
|
497
594
|
.catch((e) => {
|
|
498
595
|
throw lib_mongo.mapQueryError(e, 'while reading checksums');
|
|
499
596
|
});
|
|
500
597
|
|
|
501
|
-
|
|
598
|
+
const partialChecksums = new Map<string, storage.PartialOrFullChecksum>(
|
|
502
599
|
aggregate.map((doc) => {
|
|
600
|
+
const partialChecksum = Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff;
|
|
601
|
+
const bucket = doc._id;
|
|
503
602
|
return [
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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)
|
|
511
617
|
];
|
|
512
618
|
})
|
|
513
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
|
+
);
|
|
514
633
|
}
|
|
515
634
|
|
|
516
635
|
async terminate(options?: storage.TerminateOptions) {
|
|
@@ -575,7 +694,6 @@ export class MongoSyncBucketStorage
|
|
|
575
694
|
`${this.slot_name} Cleared batch of data in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`
|
|
576
695
|
);
|
|
577
696
|
await timers.setTimeout(lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5);
|
|
578
|
-
continue;
|
|
579
697
|
} else {
|
|
580
698
|
throw e;
|
|
581
699
|
}
|
|
@@ -640,41 +758,6 @@ export class MongoSyncBucketStorage
|
|
|
640
758
|
);
|
|
641
759
|
}
|
|
642
760
|
|
|
643
|
-
async autoActivate(): Promise<void> {
|
|
644
|
-
await this.db.client.withSession(async (session) => {
|
|
645
|
-
await session.withTransaction(async () => {
|
|
646
|
-
const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
|
|
647
|
-
if (doc && doc.state == 'PROCESSING') {
|
|
648
|
-
await this.db.sync_rules.updateOne(
|
|
649
|
-
{
|
|
650
|
-
_id: this.group_id
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
$set: {
|
|
654
|
-
state: storage.SyncRuleState.ACTIVE
|
|
655
|
-
}
|
|
656
|
-
},
|
|
657
|
-
{ session }
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
await this.db.sync_rules.updateMany(
|
|
661
|
-
{
|
|
662
|
-
_id: { $ne: this.group_id },
|
|
663
|
-
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
664
|
-
},
|
|
665
|
-
{
|
|
666
|
-
$set: {
|
|
667
|
-
state: storage.SyncRuleState.STOP
|
|
668
|
-
}
|
|
669
|
-
},
|
|
670
|
-
{ session }
|
|
671
|
-
);
|
|
672
|
-
await this.db.notifyCheckpoint();
|
|
673
|
-
}
|
|
674
|
-
});
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
|
|
678
761
|
async reportError(e: any): Promise<void> {
|
|
679
762
|
const message = String(e.message ?? 'Replication failure');
|
|
680
763
|
await this.db.sync_rules.updateOne(
|
|
@@ -691,14 +774,29 @@ export class MongoSyncBucketStorage
|
|
|
691
774
|
}
|
|
692
775
|
|
|
693
776
|
async compact(options?: storage.CompactOptions) {
|
|
694
|
-
|
|
777
|
+
let maxOpId = options?.maxOpId;
|
|
778
|
+
if (maxOpId == null) {
|
|
779
|
+
const checkpoint = await this.getCheckpointInternal();
|
|
780
|
+
maxOpId = checkpoint?.checkpoint ?? undefined;
|
|
781
|
+
}
|
|
782
|
+
await new MongoCompactor(this.db, this.group_id, { ...options, maxOpId }).compact();
|
|
783
|
+
if (maxOpId != null && options?.compactParameterData) {
|
|
784
|
+
await new MongoParameterCompactor(this.db, this.group_id, maxOpId, options).compact();
|
|
785
|
+
}
|
|
695
786
|
}
|
|
696
787
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
788
|
+
async populatePersistentChecksumCache(options: Pick<CompactOptions, 'signal' | 'maxOpId'>): Promise<void> {
|
|
789
|
+
const start = Date.now();
|
|
790
|
+
// We do a minimal compact, primarily to populate the checksum cache
|
|
791
|
+
await this.compact({
|
|
792
|
+
...options,
|
|
793
|
+
// Skip parameter data
|
|
794
|
+
compactParameterData: false,
|
|
795
|
+
// Don't track updates for MOVE compacting
|
|
796
|
+
memoryLimitMB: 0
|
|
797
|
+
});
|
|
798
|
+
const duration = Date.now() - start;
|
|
799
|
+
logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
|
|
702
800
|
}
|
|
703
801
|
|
|
704
802
|
/**
|
|
@@ -720,33 +818,13 @@ export class MongoSyncBucketStorage
|
|
|
720
818
|
break;
|
|
721
819
|
}
|
|
722
820
|
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
_id: this.group_id,
|
|
726
|
-
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
727
|
-
},
|
|
728
|
-
{
|
|
729
|
-
limit: 1,
|
|
730
|
-
projection: {
|
|
731
|
-
_id: 1,
|
|
732
|
-
state: 1,
|
|
733
|
-
last_checkpoint: 1,
|
|
734
|
-
last_checkpoint_lsn: 1
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
if (doc == null) {
|
|
740
|
-
// Sync rules not present or not active.
|
|
741
|
-
// Abort the connections - clients will have to retry later.
|
|
742
|
-
throw new ServiceError(ErrorCode.PSYNC_S2302, 'No active sync rules available');
|
|
743
|
-
} else if (doc.state != storage.SyncRuleState.ACTIVE && doc.state != storage.SyncRuleState.ERRORED) {
|
|
821
|
+
const op = await this.getCheckpointInternal();
|
|
822
|
+
if (op == null) {
|
|
744
823
|
// Sync rules have changed - abort and restart.
|
|
745
824
|
// We do a soft close of the stream here - no error
|
|
746
825
|
break;
|
|
747
826
|
}
|
|
748
827
|
|
|
749
|
-
const op = this.makeActiveCheckpoint(doc);
|
|
750
828
|
// Check for LSN / checkpoint changes - ignore other metadata changes
|
|
751
829
|
if (lastOp == null || op.lsn != lastOp.lsn || op.checkpoint != lastOp.checkpoint) {
|
|
752
830
|
lastOp = op;
|
|
@@ -1013,3 +1091,25 @@ interface InternalCheckpointChanges extends CheckpointChanges {
|
|
|
1013
1091
|
updatedWriteCheckpoints: Map<string, bigint>;
|
|
1014
1092
|
invalidateWriteCheckpoints: boolean;
|
|
1015
1093
|
}
|
|
1094
|
+
|
|
1095
|
+
class MongoReplicationCheckpoint implements ReplicationCheckpoint {
|
|
1096
|
+
constructor(
|
|
1097
|
+
private storage: MongoSyncBucketStorage,
|
|
1098
|
+
public readonly checkpoint: InternalOpId,
|
|
1099
|
+
public readonly lsn: string | null,
|
|
1100
|
+
public snapshotTime: mongo.Timestamp
|
|
1101
|
+
) {}
|
|
1102
|
+
|
|
1103
|
+
async getParameterSets(lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
|
|
1104
|
+
return this.storage.getParameterSets(this, lookups);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
class EmptyReplicationCheckpoint implements ReplicationCheckpoint {
|
|
1109
|
+
readonly checkpoint: InternalOpId = 0n;
|
|
1110
|
+
readonly lsn: string | null = null;
|
|
1111
|
+
|
|
1112
|
+
async getParameterSets(lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
|
|
1113
|
+
return [];
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
@@ -71,15 +71,17 @@ export class PersistedBatch {
|
|
|
71
71
|
this.logger = options?.logger ?? defaultLogger;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
private incrementBucket(bucket: string, op_id: InternalOpId) {
|
|
74
|
+
private incrementBucket(bucket: string, op_id: InternalOpId, bytes: number) {
|
|
75
75
|
let existingState = this.bucketStates.get(bucket);
|
|
76
76
|
if (existingState) {
|
|
77
77
|
existingState.lastOp = op_id;
|
|
78
78
|
existingState.incrementCount += 1;
|
|
79
|
+
existingState.incrementBytes += bytes;
|
|
79
80
|
} else {
|
|
80
81
|
this.bucketStates.set(bucket, {
|
|
81
82
|
lastOp: op_id,
|
|
82
|
-
incrementCount: 1
|
|
83
|
+
incrementCount: 1,
|
|
84
|
+
incrementBytes: bytes
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
}
|
|
@@ -115,7 +117,8 @@ export class PersistedBatch {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
remaining_buckets.delete(key);
|
|
118
|
-
|
|
120
|
+
const byteEstimate = recordData.length + 200;
|
|
121
|
+
this.currentSize += byteEstimate;
|
|
119
122
|
|
|
120
123
|
const op_id = options.op_seq.next();
|
|
121
124
|
this.debugLastOpId = op_id;
|
|
@@ -138,7 +141,7 @@ export class PersistedBatch {
|
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
});
|
|
141
|
-
this.incrementBucket(k.bucket, op_id);
|
|
144
|
+
this.incrementBucket(k.bucket, op_id, byteEstimate);
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
for (let bd of remaining_buckets.values()) {
|
|
@@ -166,7 +169,7 @@ export class PersistedBatch {
|
|
|
166
169
|
}
|
|
167
170
|
});
|
|
168
171
|
this.currentSize += 200;
|
|
169
|
-
this.incrementBucket(bd.bucket, op_id);
|
|
172
|
+
this.incrementBucket(bd.bucket, op_id, 200);
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -369,6 +372,10 @@ export class PersistedBatch {
|
|
|
369
372
|
update: {
|
|
370
373
|
$set: {
|
|
371
374
|
last_op: state.lastOp
|
|
375
|
+
},
|
|
376
|
+
$inc: {
|
|
377
|
+
'estimate_since_compact.count': state.incrementCount,
|
|
378
|
+
'estimate_since_compact.bytes': state.incrementBytes
|
|
372
379
|
}
|
|
373
380
|
},
|
|
374
381
|
upsert: true
|
|
@@ -381,4 +388,5 @@ export class PersistedBatch {
|
|
|
381
388
|
interface BucketStateUpdate {
|
|
382
389
|
lastOp: InternalOpId;
|
|
383
390
|
incrementCount: number;
|
|
391
|
+
incrementBytes: number;
|
|
384
392
|
}
|