@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +3 -3
  3. package/dist/storage/implementation/MongoBucketBatch.d.ts +21 -2
  4. package/dist/storage/implementation/MongoBucketBatch.js +66 -7
  5. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  6. package/dist/storage/implementation/MongoCompactor.d.ts +7 -0
  7. package/dist/storage/implementation/MongoCompactor.js +122 -44
  8. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  9. package/dist/storage/implementation/MongoParameterCompactor.d.ts +17 -0
  10. package/dist/storage/implementation/MongoParameterCompactor.js +92 -0
  11. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -0
  12. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +14 -4
  13. package/dist/storage/implementation/MongoSyncBucketStorage.js +229 -115
  14. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  15. package/dist/storage/implementation/PersistedBatch.d.ts +1 -0
  16. package/dist/storage/implementation/PersistedBatch.js +12 -5
  17. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  18. package/dist/storage/implementation/models.d.ts +20 -0
  19. package/dist/storage/implementation/util.d.ts +2 -1
  20. package/dist/storage/implementation/util.js +13 -0
  21. package/dist/storage/implementation/util.js.map +1 -1
  22. package/package.json +9 -9
  23. package/src/storage/implementation/MongoBucketBatch.ts +82 -8
  24. package/src/storage/implementation/MongoCompactor.ts +147 -47
  25. package/src/storage/implementation/MongoParameterCompactor.ts +105 -0
  26. package/src/storage/implementation/MongoSyncBucketStorage.ts +257 -157
  27. package/src/storage/implementation/PersistedBatch.ts +13 -5
  28. package/src/storage/implementation/models.ts +21 -0
  29. package/src/storage/implementation/util.ts +14 -1
  30. package/test/src/__snapshots__/storage_sync.test.ts.snap +319 -11
  31. package/test/src/storage_compacting.test.ts +2 -0
  32. 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
- const doc = await this.db.sync_rules.findOne(
109
- { _id: this.group_id },
110
- {
111
- projection: { last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
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
- if (!doc?.snapshot_done) {
115
- return {
116
- checkpoint: 0n,
117
- lsn: null
118
- };
119
- }
120
- return {
121
- checkpoint: doc?.last_checkpoint ?? 0n,
122
- lsn: doc?.last_checkpoint_lsn ?? null
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: table, objectId, replicationColumns } = entity_descriptor;
185
+ const { schema, name, objectId, replicaIdColumns } = entity_descriptor;
166
186
 
167
- const columns = replicationColumns.map((column) => ({
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: table,
180
- replica_id_columns2: columns
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: table,
213
+ table_name: name,
194
214
  replica_id_columns: null,
195
- replica_id_columns2: columns,
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
- table,
208
- replicationColumns,
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: table }] as any[];
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
- doc.replica_id_columns2?.map((c) => ({ name: c.name, typeOid: c.type_oid, type: c.type })) ?? [],
250
- doc.snapshot_done ?? true
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: utils.InternalOpId, lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
263
- const lookupFilter = lookups.map((lookup) => {
264
- return storage.serializeLookup(lookup);
265
- });
266
- const rows = await this.db.bucket_parameters
267
- .aggregate([
268
- {
269
- $match: {
270
- 'key.g': this.group_id,
271
- lookup: { $in: lookupFilter },
272
- _id: { $lte: checkpoint }
273
- }
274
- },
275
- {
276
- $sort: {
277
- _id: -1
278
- }
279
- },
280
- {
281
- $group: {
282
- _id: { key: '$key', lookup: '$lookup' },
283
- bucket_parameters: {
284
- $first: '$bucket_parameters'
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
- .toArray();
290
- const groupedParameters = rows.map((row) => {
291
- return row.bucket_parameters;
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: request.start ? BigInt(request.start) : new bson.MinKey()
556
+ o: start ?? new bson.MinKey()
460
557
  },
461
558
  $lte: {
462
559
  g: this.group_id,
463
560
  b: request.bucket,
464
- o: BigInt(request.end)
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.MONGO_OPERATION_TIMEOUT_MS }
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
- return new Map<string, storage.PartialChecksum>(
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
- doc._id,
505
- {
506
- bucket: doc._id,
507
- partialCount: doc.count,
508
- partialChecksum: Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff,
509
- isFullChecksum: doc.has_clear_op == 1
510
- } satisfies storage.PartialChecksum
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
- return new MongoCompactor(this.db, this.group_id, options).compact();
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
- private makeActiveCheckpoint(doc: SyncRuleCheckpointState | null) {
698
- return {
699
- checkpoint: doc?.last_checkpoint ?? 0n,
700
- lsn: doc?.last_checkpoint_lsn ?? null
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 doc = await this.db.sync_rules.findOne(
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
- this.currentSize += recordData.length + 200;
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
  }