@powersync/service-module-mongodb-storage 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/storage/MongoBucketStorage.js +16 -3
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +13 -11
- package/dist/storage/implementation/MongoBucketBatch.js +208 -127
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +4 -4
- package/dist/storage/implementation/MongoChecksums.js +1 -0
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +8 -2
- package/dist/storage/implementation/MongoCompactor.js +50 -21
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +2 -2
- package/dist/storage/implementation/MongoParameterCompactor.js +13 -1
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +2 -7
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +9 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +35 -33
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +3 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +3 -2
- package/dist/storage/implementation/OperationBatch.js.map +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +11 -4
- package/dist/storage/implementation/PersistedBatch.js +42 -11
- package/dist/storage/implementation/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +35 -1
- package/dist/storage/implementation/db.js +99 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +15 -3
- package/dist/storage/implementation/models.js +2 -1
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -1
- package/dist/utils/test-utils.js +15 -12
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +2 -1
- package/dist/utils/util.js +15 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +6 -6
- package/src/storage/MongoBucketStorage.ts +29 -8
- package/src/storage/implementation/MongoBucketBatch.ts +263 -177
- package/src/storage/implementation/MongoChecksums.ts +5 -3
- package/src/storage/implementation/MongoCompactor.ts +53 -24
- package/src/storage/implementation/MongoParameterCompactor.ts +17 -4
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +3 -11
- package/src/storage/implementation/MongoSyncBucketStorage.ts +33 -26
- package/src/storage/implementation/MongoSyncRulesLock.ts +3 -3
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -4
- package/src/storage/implementation/OperationBatch.ts +3 -2
- package/src/storage/implementation/PersistedBatch.ts +42 -11
- package/src/storage/implementation/db.ts +129 -1
- package/src/storage/implementation/models.ts +18 -4
- package/src/utils/test-utils.ts +15 -12
- package/src/utils/util.ts +17 -2
- package/test/src/__snapshots__/storage.test.ts.snap +201 -0
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1111 -16
- package/test/src/storage.test.ts +9 -7
- package/test/src/storage_compacting.test.ts +117 -45
- package/test/src/storage_sync.test.ts +53 -51
- package/test/src/util.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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 ==
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
}
|