@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/storage/MongoBucketStorage.js +16 -3
  3. package/dist/storage/MongoBucketStorage.js.map +1 -1
  4. package/dist/storage/implementation/MongoBucketBatch.d.ts +13 -11
  5. package/dist/storage/implementation/MongoBucketBatch.js +208 -127
  6. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  7. package/dist/storage/implementation/MongoChecksums.d.ts +4 -4
  8. package/dist/storage/implementation/MongoChecksums.js +1 -0
  9. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  10. package/dist/storage/implementation/MongoCompactor.d.ts +8 -2
  11. package/dist/storage/implementation/MongoCompactor.js +50 -21
  12. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  13. package/dist/storage/implementation/MongoParameterCompactor.d.ts +2 -2
  14. package/dist/storage/implementation/MongoParameterCompactor.js +13 -1
  15. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  16. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +2 -7
  17. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +9 -4
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js +35 -33
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +3 -3
  22. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  23. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
  24. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  25. package/dist/storage/implementation/OperationBatch.js +3 -2
  26. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  27. package/dist/storage/implementation/PersistedBatch.d.ts +11 -4
  28. package/dist/storage/implementation/PersistedBatch.js +42 -11
  29. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  30. package/dist/storage/implementation/db.d.ts +35 -1
  31. package/dist/storage/implementation/db.js +99 -0
  32. package/dist/storage/implementation/db.js.map +1 -1
  33. package/dist/storage/implementation/models.d.ts +15 -3
  34. package/dist/storage/implementation/models.js +2 -1
  35. package/dist/storage/implementation/models.js.map +1 -1
  36. package/dist/utils/test-utils.d.ts +4 -1
  37. package/dist/utils/test-utils.js +15 -12
  38. package/dist/utils/test-utils.js.map +1 -1
  39. package/dist/utils/util.d.ts +2 -1
  40. package/dist/utils/util.js +15 -1
  41. package/dist/utils/util.js.map +1 -1
  42. package/package.json +6 -6
  43. package/src/storage/MongoBucketStorage.ts +29 -8
  44. package/src/storage/implementation/MongoBucketBatch.ts +263 -177
  45. package/src/storage/implementation/MongoChecksums.ts +5 -3
  46. package/src/storage/implementation/MongoCompactor.ts +53 -24
  47. package/src/storage/implementation/MongoParameterCompactor.ts +17 -4
  48. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +3 -11
  49. package/src/storage/implementation/MongoSyncBucketStorage.ts +33 -26
  50. package/src/storage/implementation/MongoSyncRulesLock.ts +3 -3
  51. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -4
  52. package/src/storage/implementation/OperationBatch.ts +3 -2
  53. package/src/storage/implementation/PersistedBatch.ts +42 -11
  54. package/src/storage/implementation/db.ts +129 -1
  55. package/src/storage/implementation/models.ts +18 -4
  56. package/src/utils/test-utils.ts +15 -12
  57. package/src/utils/util.ts +17 -2
  58. package/test/src/__snapshots__/storage.test.ts.snap +201 -0
  59. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  60. package/test/src/__snapshots__/storage_sync.test.ts.snap +1111 -16
  61. package/test/src/storage.test.ts +9 -7
  62. package/test/src/storage_compacting.test.ts +117 -45
  63. package/test/src/storage_sync.test.ts +53 -51
  64. package/test/src/util.ts +3 -3
  65. package/tsconfig.tsbuildinfo +1 -1
@@ -1,9 +1,9 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import * as bson from 'bson';
3
3
  import { BaseObserver, container, logger as defaultLogger, ErrorCode, errors, ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
4
- import { deserializeBson, isCompleteRow, SaveOperationTag, storage, utils } from '@powersync/service-core';
4
+ import { deserializeBson, isCompleteRow, SaveOperationTag, storage, SyncRuleState, utils } from '@powersync/service-core';
5
5
  import * as timers from 'node:timers/promises';
6
- import { idPrefixFilter } from '../../utils/util.js';
6
+ import { idPrefixFilter, mongoTableId } from '../../utils/util.js';
7
7
  import { MongoIdSequence } from './MongoIdSequence.js';
8
8
  import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
9
9
  import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
@@ -18,6 +18,7 @@ export const MAX_ROW_SIZE = 15 * 1024 * 1024;
18
18
  //
19
19
  // In the future, we can investigate allowing multiple replication streams operating independently.
20
20
  const replicationMutex = new utils.Mutex();
21
+ export const EMPTY_DATA = new bson.Binary(bson.serialize({}));
21
22
  export class MongoBucketBatch extends BaseObserver {
22
23
  logger;
23
24
  client;
@@ -40,7 +41,6 @@ export class MongoBucketBatch extends BaseObserver {
40
41
  * 2. A keepalive message LSN.
41
42
  */
42
43
  last_checkpoint_lsn = null;
43
- no_checkpoint_before_lsn;
44
44
  persisted_op = null;
45
45
  /**
46
46
  * Last written op, if any. This may not reflect a consistent checkpoint.
@@ -65,7 +65,6 @@ export class MongoBucketBatch extends BaseObserver {
65
65
  this.db = options.db;
66
66
  this.group_id = options.groupId;
67
67
  this.last_checkpoint_lsn = options.lastCheckpointLsn;
68
- this.no_checkpoint_before_lsn = options.noCheckpointBeforeLsn;
69
68
  this.resumeFromLsn = options.resumeFromLsn;
70
69
  this.session = this.client.startSession();
71
70
  this.slot_name = options.slotName;
@@ -85,9 +84,6 @@ export class MongoBucketBatch extends BaseObserver {
85
84
  get lastCheckpointLsn() {
86
85
  return this.last_checkpoint_lsn;
87
86
  }
88
- get noCheckpointBeforeLsn() {
89
- return this.no_checkpoint_before_lsn;
90
- }
91
87
  async flush(options) {
92
88
  let result = null;
93
89
  // One flush may be split over multiple transactions.
@@ -139,10 +135,10 @@ export class MongoBucketBatch extends BaseObserver {
139
135
  // the order of processing, which then becomes really tricky to manage.
140
136
  // This now takes 2+ queries, but doesn't have any issues with order of operations.
141
137
  const sizeLookups = batch.batch.map((r) => {
142
- return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
138
+ return { g: this.group_id, t: mongoTableId(r.record.sourceTable.id), k: r.beforeId };
143
139
  });
144
140
  sizes = new Map();
145
- const sizeCursor = this.db.current_data.aggregate([
141
+ const sizeCursor = this.db.common_current_data.aggregate([
146
142
  {
147
143
  $match: {
148
144
  _id: { $in: sizeLookups }
@@ -174,18 +170,18 @@ export class MongoBucketBatch extends BaseObserver {
174
170
  continue;
175
171
  }
176
172
  const lookups = b.map((r) => {
177
- return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
173
+ return { g: this.group_id, t: mongoTableId(r.record.sourceTable.id), k: r.beforeId };
178
174
  });
179
175
  let current_data_lookup = new Map();
180
176
  // With skipExistingRows, we only need to know whether or not the row exists.
181
177
  const projection = this.skipExistingRows ? { _id: 1 } : undefined;
182
- const cursor = this.db.current_data.find({
178
+ const cursor = this.db.common_current_data.find({
183
179
  _id: { $in: lookups }
184
180
  }, { session, projection });
185
181
  for await (let doc of cursor.stream()) {
186
182
  current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
187
183
  }
188
- let persistedBatch = new PersistedBatch(this.group_id, transactionSize, {
184
+ let persistedBatch = new PersistedBatch(this.db, this.group_id, transactionSize, {
189
185
  logger: this.logger
190
186
  });
191
187
  for (let op of b) {
@@ -207,7 +203,7 @@ export class MongoBucketBatch extends BaseObserver {
207
203
  if (persistedBatch.shouldFlushTransaction()) {
208
204
  // Transaction is getting big.
209
205
  // Flush, and resume in a new transaction.
210
- const { flushedAny } = await persistedBatch.flush(this.db, this.session, options);
206
+ const { flushedAny } = await persistedBatch.flush(this.session, options);
211
207
  didFlush ||= flushedAny;
212
208
  persistedBatch = null;
213
209
  // Computing our current progress is a little tricky here, since
@@ -218,7 +214,7 @@ export class MongoBucketBatch extends BaseObserver {
218
214
  }
219
215
  if (persistedBatch) {
220
216
  transactionSize = persistedBatch.currentSize;
221
- const { flushedAny } = await persistedBatch.flush(this.db, this.session, options);
217
+ const { flushedAny } = await persistedBatch.flush(this.session, options);
222
218
  didFlush ||= flushedAny;
223
219
  }
224
220
  }
@@ -237,7 +233,7 @@ export class MongoBucketBatch extends BaseObserver {
237
233
  let new_buckets = [];
238
234
  let existing_lookups = [];
239
235
  let new_lookups = [];
240
- const before_key = { g: this.group_id, t: record.sourceTable.id, k: beforeId };
236
+ const before_key = { g: this.group_id, t: mongoTableId(record.sourceTable.id), k: beforeId };
241
237
  if (this.skipExistingRows) {
242
238
  if (record.tag == SaveOperationTag.INSERT) {
243
239
  if (current_data != null) {
@@ -298,7 +294,7 @@ export class MongoBucketBatch extends BaseObserver {
298
294
  }
299
295
  let afterData;
300
296
  if (afterId != null && !this.storeCurrentData) {
301
- afterData = new bson.Binary(bson.serialize({}));
297
+ afterData = EMPTY_DATA;
302
298
  }
303
299
  else if (afterId != null) {
304
300
  try {
@@ -419,7 +415,7 @@ export class MongoBucketBatch extends BaseObserver {
419
415
  // 5. TOAST: Update current data and bucket list.
420
416
  if (afterId) {
421
417
  // Insert or update
422
- const after_key = { g: this.group_id, t: sourceTable.id, k: afterId };
418
+ const after_key = { g: this.group_id, t: mongoTableId(sourceTable.id), k: afterId };
423
419
  batch.upsertCurrentData(after_key, {
424
420
  data: afterData,
425
421
  buckets: new_buckets,
@@ -434,7 +430,10 @@ export class MongoBucketBatch extends BaseObserver {
434
430
  }
435
431
  if (afterId == null || !storage.replicaIdEquals(beforeId, afterId)) {
436
432
  // Either a delete (afterId == null), or replaced the old replication id
437
- batch.deleteCurrentData(before_key);
433
+ // Note that this is a soft delete.
434
+ // We don't specifically need a new or unique op_id here, but it must be greater than the
435
+ // last checkpoint, so we use next().
436
+ batch.softDeleteCurrentData(before_key, opSeq.next());
438
437
  }
439
438
  return result;
440
439
  }
@@ -505,59 +504,23 @@ export class MongoBucketBatch extends BaseObserver {
505
504
  });
506
505
  }
507
506
  async [Symbol.asyncDispose]() {
507
+ if (this.batch != null || this.write_checkpoint_batch.length > 0) {
508
+ // We don't error here, since:
509
+ // 1. In error states, this is expected (we can't distinguish between disposing after success or error).
510
+ // 2. SuppressedError is messy to deal with.
511
+ this.logger.warn('Disposing writer with unflushed changes');
512
+ }
508
513
  await this.session.endSession();
509
514
  super.clearListeners();
510
515
  }
516
+ async dispose() {
517
+ await this[Symbol.asyncDispose]();
518
+ }
511
519
  lastWaitingLogThottled = 0;
512
520
  async commit(lsn, options) {
513
521
  const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
514
522
  await this.flush(options);
515
- if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
516
- // When re-applying transactions, don't create a new checkpoint until
517
- // we are past the last transaction.
518
- this.logger.info(`Re-applied transaction ${lsn} - skipping checkpoint`);
519
- // Cannot create a checkpoint yet - return false
520
- return false;
521
- }
522
- if (lsn < this.no_checkpoint_before_lsn) {
523
- if (Date.now() - this.lastWaitingLogThottled > 5_000) {
524
- this.logger.info(`Waiting until ${this.no_checkpoint_before_lsn} before creating checkpoint, currently at ${lsn}. Persisted op: ${this.persisted_op}`);
525
- this.lastWaitingLogThottled = Date.now();
526
- }
527
- // Edge case: During initial replication, we have a no_checkpoint_before_lsn set,
528
- // and don't actually commit the snapshot.
529
- // The first commit can happen from an implicit keepalive message.
530
- // That needs the persisted_op to get an accurate checkpoint, so
531
- // we persist that in keepalive_op.
532
- await this.db.sync_rules.updateOne({
533
- _id: this.group_id
534
- }, {
535
- $set: {
536
- keepalive_op: this.persisted_op == null ? null : String(this.persisted_op)
537
- }
538
- }, { session: this.session });
539
- await this.db.notifyCheckpoint();
540
- // Cannot create a checkpoint yet - return false
541
- return false;
542
- }
543
- if (!createEmptyCheckpoints && this.persisted_op == null) {
544
- // Nothing to commit - also return true
545
- await this.autoActivate(lsn);
546
- return true;
547
- }
548
523
  const now = new Date();
549
- const update = {
550
- last_checkpoint_lsn: lsn,
551
- last_checkpoint_ts: now,
552
- last_keepalive_ts: now,
553
- snapshot_done: true,
554
- last_fatal_error: null,
555
- last_fatal_error_ts: null,
556
- keepalive_op: null
557
- };
558
- if (this.persisted_op != null) {
559
- update.last_checkpoint = this.persisted_op;
560
- }
561
524
  // Mark relevant write checkpoints as "processed".
562
525
  // This makes it easier to identify write checkpoints that are "valid" in order.
563
526
  await this.db.write_checkpoints.updateMany({
@@ -570,17 +533,143 @@ export class MongoBucketBatch extends BaseObserver {
570
533
  }, {
571
534
  session: this.session
572
535
  });
573
- await this.db.sync_rules.updateOne({
574
- _id: this.group_id
575
- }, {
576
- $set: update,
577
- $unset: { snapshot_lsn: 1 }
578
- }, { session: this.session });
579
- await this.autoActivate(lsn);
580
- await this.db.notifyCheckpoint();
581
- this.persisted_op = null;
582
- this.last_checkpoint_lsn = lsn;
583
- return true;
536
+ const can_checkpoint = {
537
+ $and: [
538
+ { $eq: ['$snapshot_done', true] },
539
+ {
540
+ $or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
541
+ },
542
+ {
543
+ $or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
544
+ }
545
+ ]
546
+ };
547
+ const new_keepalive_op = {
548
+ $cond: [
549
+ can_checkpoint,
550
+ { $literal: null },
551
+ {
552
+ $toString: {
553
+ $max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
554
+ }
555
+ }
556
+ ]
557
+ };
558
+ const new_last_checkpoint = {
559
+ $cond: [
560
+ can_checkpoint,
561
+ {
562
+ $max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
563
+ },
564
+ '$last_checkpoint'
565
+ ]
566
+ };
567
+ // For this query, we need to handle multiple cases, depending on the state:
568
+ // 1. Normal commit - advance last_checkpoint to this.persisted_op.
569
+ // 2. Commit delayed by no_checkpoint_before due to snapshot. In this case we only advance keepalive_op.
570
+ // 3. Commit with no new data - here may may set last_checkpoint = keepalive_op, if a delayed commit is relevant.
571
+ // We want to do as much as possible in a single atomic database operation, which makes this somewhat complex.
572
+ let preUpdateDocument = await this.db.sync_rules.findOneAndUpdate({ _id: this.group_id }, [
573
+ {
574
+ $set: {
575
+ _can_checkpoint: can_checkpoint,
576
+ _not_empty: createEmptyCheckpoints
577
+ ? true
578
+ : {
579
+ $or: [
580
+ { $literal: createEmptyCheckpoints },
581
+ { $ne: ['$keepalive_op', new_keepalive_op] },
582
+ { $ne: ['$last_checkpoint', new_last_checkpoint] }
583
+ ]
584
+ }
585
+ }
586
+ },
587
+ {
588
+ $set: {
589
+ last_checkpoint_lsn: {
590
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
591
+ },
592
+ last_checkpoint_ts: {
593
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
594
+ },
595
+ last_keepalive_ts: { $literal: now },
596
+ last_fatal_error: { $literal: null },
597
+ last_fatal_error_ts: { $literal: null },
598
+ keepalive_op: new_keepalive_op,
599
+ last_checkpoint: new_last_checkpoint,
600
+ // Unset snapshot_lsn on checkpoint
601
+ snapshot_lsn: {
602
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
603
+ }
604
+ }
605
+ },
606
+ {
607
+ $unset: ['_can_checkpoint', '_not_empty']
608
+ }
609
+ ], {
610
+ session: this.session,
611
+ // We return the before document, so that we can check the previous state to determine if a checkpoint was actually created or if we were blocked by snapshot/no_checkpoint_before.
612
+ returnDocument: 'before',
613
+ projection: {
614
+ snapshot_done: 1,
615
+ last_checkpoint_lsn: 1,
616
+ no_checkpoint_before: 1,
617
+ keepalive_op: 1,
618
+ last_checkpoint: 1
619
+ }
620
+ });
621
+ if (preUpdateDocument == null) {
622
+ throw new ReplicationAssertionError('Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id);
623
+ }
624
+ // This re-implements the same logic as in the pipeline, to determine what was actually updated.
625
+ // Unfortunately we cannot return these from the pipeline directly, so we need to re-implement the logic.
626
+ const canCheckpoint = preUpdateDocument.snapshot_done === true &&
627
+ (preUpdateDocument.last_checkpoint_lsn == null || preUpdateDocument.last_checkpoint_lsn <= lsn) &&
628
+ (preUpdateDocument.no_checkpoint_before == null || preUpdateDocument.no_checkpoint_before <= lsn);
629
+ const keepaliveOp = preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op);
630
+ const maxKeepalive = [keepaliveOp ?? 0n, this.persisted_op ?? 0n, 0n].reduce((a, b) => (a > b ? a : b));
631
+ const newKeepaliveOp = canCheckpoint ? null : maxKeepalive.toString();
632
+ const newLastCheckpoint = canCheckpoint
633
+ ? [preUpdateDocument.last_checkpoint ?? 0n, this.persisted_op ?? 0n, keepaliveOp ?? 0n, 0n].reduce((a, b) => a > b ? a : b)
634
+ : preUpdateDocument.last_checkpoint;
635
+ const notEmpty = createEmptyCheckpoints ||
636
+ preUpdateDocument.keepalive_op !== newKeepaliveOp ||
637
+ preUpdateDocument.last_checkpoint !== newLastCheckpoint;
638
+ const checkpointCreated = canCheckpoint && notEmpty;
639
+ const checkpointBlocked = !canCheckpoint;
640
+ if (checkpointBlocked) {
641
+ // Failed on snapshot_done or no_checkpoint_before.
642
+ if (Date.now() - this.lastWaitingLogThottled > 5_000) {
643
+ this.logger.info(`Waiting before creating checkpoint, currently at ${lsn} / ${preUpdateDocument.keepalive_op}. Current state: ${JSON.stringify({
644
+ snapshot_done: preUpdateDocument.snapshot_done,
645
+ last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
646
+ no_checkpoint_before: preUpdateDocument.no_checkpoint_before
647
+ })}`);
648
+ this.lastWaitingLogThottled = Date.now();
649
+ }
650
+ }
651
+ else {
652
+ if (checkpointCreated) {
653
+ this.logger.debug(`Created checkpoint at ${lsn} / ${newLastCheckpoint}`);
654
+ }
655
+ await this.autoActivate(lsn);
656
+ await this.db.notifyCheckpoint();
657
+ this.persisted_op = null;
658
+ this.last_checkpoint_lsn = lsn;
659
+ if (this.db.storageConfig.softDeleteCurrentData && newLastCheckpoint != null) {
660
+ await this.cleanupCurrentData(newLastCheckpoint);
661
+ }
662
+ }
663
+ return { checkpointBlocked, checkpointCreated };
664
+ }
665
+ async cleanupCurrentData(lastCheckpoint) {
666
+ const result = await this.db.v3_current_data.deleteMany({
667
+ '_id.g': this.group_id,
668
+ pending_delete: { $exists: true, $lte: lastCheckpoint }
669
+ });
670
+ if (result.deletedCount > 0) {
671
+ this.logger.info(`Cleaned up ${result.deletedCount} pending delete current_data records for checkpoint ${lastCheckpoint}`);
672
+ }
584
673
  }
585
674
  /**
586
675
  * Switch from processing -> active if relevant.
@@ -597,7 +686,7 @@ export class MongoBucketBatch extends BaseObserver {
597
686
  let activated = false;
598
687
  await session.withTransaction(async () => {
599
688
  const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
600
- if (doc && doc.state == 'PROCESSING') {
689
+ if (doc && doc.state == SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
601
690
  await this.db.sync_rules.updateOne({
602
691
  _id: this.group_id
603
692
  }, {
@@ -615,53 +704,18 @@ export class MongoBucketBatch extends BaseObserver {
615
704
  }, { session });
616
705
  activated = true;
617
706
  }
707
+ else if (doc?.state != SyncRuleState.PROCESSING) {
708
+ this.needsActivation = false;
709
+ }
618
710
  });
619
711
  if (activated) {
620
712
  this.logger.info(`Activated new sync rules at ${lsn}`);
621
713
  await this.db.notifyCheckpoint();
714
+ this.needsActivation = false;
622
715
  }
623
- this.needsActivation = false;
624
716
  }
625
717
  async keepalive(lsn) {
626
- if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
627
- // No-op
628
- return false;
629
- }
630
- if (lsn < this.no_checkpoint_before_lsn) {
631
- return false;
632
- }
633
- if (this.persisted_op != null) {
634
- // The commit may have been skipped due to "no_checkpoint_before_lsn".
635
- // Apply it now if relevant
636
- this.logger.info(`Commit due to keepalive at ${lsn} / ${this.persisted_op}`);
637
- return await this.commit(lsn);
638
- }
639
- await this.db.write_checkpoints.updateMany({
640
- processed_at_lsn: null,
641
- 'lsns.1': { $lte: lsn }
642
- }, {
643
- $set: {
644
- processed_at_lsn: lsn
645
- }
646
- }, {
647
- session: this.session
648
- });
649
- await this.db.sync_rules.updateOne({
650
- _id: this.group_id
651
- }, {
652
- $set: {
653
- last_checkpoint_lsn: lsn,
654
- snapshot_done: true,
655
- last_fatal_error: null,
656
- last_fatal_error_ts: null,
657
- last_keepalive_ts: new Date()
658
- },
659
- $unset: { snapshot_lsn: 1 }
660
- }, { session: this.session });
661
- await this.autoActivate(lsn);
662
- await this.db.notifyCheckpoint();
663
- this.last_checkpoint_lsn = lsn;
664
- return true;
718
+ return await this.commit(lsn, { createEmptyCheckpoints: true });
665
719
  }
666
720
  async setResumeLsn(lsn) {
667
721
  const update = {
@@ -712,7 +766,7 @@ export class MongoBucketBatch extends BaseObserver {
712
766
  const result = await this.flush();
713
767
  await this.withTransaction(async () => {
714
768
  for (let table of sourceTables) {
715
- await this.db.source_tables.deleteOne({ _id: table.id });
769
+ await this.db.source_tables.deleteOne({ _id: mongoTableId(table.id) });
716
770
  }
717
771
  });
718
772
  return result;
@@ -742,9 +796,12 @@ export class MongoBucketBatch extends BaseObserver {
742
796
  while (lastBatchCount == BATCH_LIMIT) {
743
797
  await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
744
798
  const current_data_filter = {
745
- _id: idPrefixFilter({ g: this.group_id, t: sourceTable.id }, ['k'])
799
+ _id: idPrefixFilter({ g: this.group_id, t: mongoTableId(sourceTable.id) }, ['k']),
800
+ // Skip soft-deleted data
801
+ // Works for both v1 and v3 current_data schemas
802
+ pending_delete: { $exists: false }
746
803
  };
747
- const cursor = this.db.current_data.find(current_data_filter, {
804
+ const cursor = this.db.common_current_data.find(current_data_filter, {
748
805
  projection: {
749
806
  _id: 1,
750
807
  buckets: 1,
@@ -754,7 +811,7 @@ export class MongoBucketBatch extends BaseObserver {
754
811
  session: session
755
812
  });
756
813
  const batch = await cursor.toArray();
757
- const persistedBatch = new PersistedBatch(this.group_id, 0, { logger: this.logger });
814
+ const persistedBatch = new PersistedBatch(this.db, this.group_id, 0, { logger: this.logger });
758
815
  for (let value of batch) {
759
816
  persistedBatch.saveBucketData({
760
817
  op_seq: opSeq,
@@ -770,9 +827,10 @@ export class MongoBucketBatch extends BaseObserver {
770
827
  sourceTable: sourceTable,
771
828
  sourceKey: value._id.k
772
829
  });
773
- persistedBatch.deleteCurrentData(value._id);
830
+ // Since this is not from streaming replication, we can do a hard delete
831
+ persistedBatch.hardDeleteCurrentData(value._id);
774
832
  }
775
- await persistedBatch.flush(this.db, session);
833
+ await persistedBatch.flush(session);
776
834
  lastBatchCount = batch.length;
777
835
  last_op = opSeq.last();
778
836
  });
@@ -788,7 +846,7 @@ export class MongoBucketBatch extends BaseObserver {
788
846
  };
789
847
  copy.snapshotStatus = snapshotStatus;
790
848
  await this.withTransaction(async () => {
791
- await this.db.source_tables.updateOne({ _id: table.id }, {
849
+ await this.db.source_tables.updateOne({ _id: mongoTableId(table.id) }, {
792
850
  $set: {
793
851
  snapshot_status: {
794
852
  last_key: snapshotStatus.lastKey == null ? null : new bson.Binary(snapshotStatus.lastKey),
@@ -800,9 +858,31 @@ export class MongoBucketBatch extends BaseObserver {
800
858
  });
801
859
  return copy;
802
860
  }
803
- async markSnapshotDone(tables, no_checkpoint_before_lsn) {
861
+ async markAllSnapshotDone(no_checkpoint_before_lsn) {
862
+ await this.db.sync_rules.updateOne({
863
+ _id: this.group_id
864
+ }, {
865
+ $set: {
866
+ snapshot_done: true,
867
+ last_keepalive_ts: new Date()
868
+ },
869
+ $max: {
870
+ no_checkpoint_before: no_checkpoint_before_lsn
871
+ }
872
+ }, { session: this.session });
873
+ }
874
+ async markTableSnapshotRequired(table) {
875
+ await this.db.sync_rules.updateOne({
876
+ _id: this.group_id
877
+ }, {
878
+ $set: {
879
+ snapshot_done: false
880
+ }
881
+ }, { session: this.session });
882
+ }
883
+ async markTableSnapshotDone(tables, no_checkpoint_before_lsn) {
804
884
  const session = this.session;
805
- const ids = tables.map((table) => table.id);
885
+ const ids = tables.map((table) => mongoTableId(table.id));
806
886
  await this.withTransaction(async () => {
807
887
  await this.db.source_tables.updateMany({ _id: { $in: ids } }, {
808
888
  $set: {
@@ -812,14 +892,15 @@ export class MongoBucketBatch extends BaseObserver {
812
892
  snapshot_status: 1
813
893
  }
814
894
  }, { session });
815
- if (no_checkpoint_before_lsn > this.no_checkpoint_before_lsn) {
816
- this.no_checkpoint_before_lsn = no_checkpoint_before_lsn;
895
+ if (no_checkpoint_before_lsn != null) {
817
896
  await this.db.sync_rules.updateOne({
818
897
  _id: this.group_id
819
898
  }, {
820
899
  $set: {
821
- no_checkpoint_before: no_checkpoint_before_lsn,
822
900
  last_keepalive_ts: new Date()
901
+ },
902
+ $max: {
903
+ no_checkpoint_before: no_checkpoint_before_lsn
823
904
  }
824
905
  }, { session: this.session });
825
906
  }