@powersync/service-module-mongodb-storage 0.22.0 → 0.23.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 +26 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +30 -6
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +23 -10
- package/dist/storage/implementation/MongoBucketBatch.js +132 -88
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
- package/dist/storage/implementation/MongoIdSequence.js +14 -1
- package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +5 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.js +17 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +38 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/db.js +1 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +5 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +136 -133
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +5 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +131 -148
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +4 -3
- package/dist/storage/implementation/v3/MongoCompactorV3.js +14 -10
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +3 -2
- package/dist/storage/implementation/v3/compact-utils.js +12 -5
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +1 -1
- package/dist/utils/test-utils.d.ts +2 -1
- package/dist/utils/test-utils.js +4 -3
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/MongoBucketStorage.ts +35 -7
- package/src/storage/implementation/MongoBucketBatch.ts +201 -110
- package/src/storage/implementation/MongoCompactor.ts +2 -0
- package/src/storage/implementation/MongoIdSequence.ts +21 -1
- package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +19 -0
- package/src/storage/implementation/MongoSyncRulesLock.ts +56 -3
- package/src/storage/implementation/db.ts +1 -0
- package/src/storage/implementation/models.ts +1 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +150 -152
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +1 -1
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +167 -194
- package/src/storage/implementation/v3/MongoCompactorV3.ts +19 -22
- package/src/storage/implementation/v3/compact-utils.ts +16 -7
- package/src/storage/implementation/v3/models.ts +1 -1
- package/src/utils/test-utils.ts +5 -3
- package/test/src/__snapshots__/storage.test.ts.snap +102 -2
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +2 -2
- package/test/src/__snapshots__/storage_sync.test.ts.snap +12 -12
- package/test/src/cleanup-stopped-sync-configs.test.ts +19 -13
- package/test/src/compact-utils.test.ts +33 -8
- package/test/src/concurrent_writers.test.ts +599 -0
- package/test/src/helpers/concurrentWriter.mjs +29 -0
- package/test/src/helpers/s3TestFactory.ts +12 -12
- package/test/src/object_storage_usage.test.ts +2 -1
- package/test/src/parameter_compacting_v1.test.ts +2 -2
- package/test/src/parameter_compacting_v3.test.ts +2 -2
- package/test/src/parameter_compaction_fence.test.ts +3 -3
- package/test/src/storage.test.ts +32 -33
- package/test/src/storage_compacting.test.ts +147 -21
- package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
- package/test/src/storage_s3_checksums.test.ts +4 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +8 -8
- package/test/src/storage_s3_reading.test.ts +4 -4
- package/test/src/storage_s3_writing.test.ts +3 -3
- package/test/src/storage_sync.test.ts +52 -46
- package/test/src/storeCurrentData.test.ts +5 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
|
|
3
3
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
4
|
-
import { ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
|
|
5
|
-
import { storage } from '@powersync/service-core';
|
|
4
|
+
import { ErrorCode, Logger, ReplicationAbortedError, ServiceError } from '@powersync/lib-services-framework';
|
|
5
|
+
import { storage, utils } from '@powersync/service-core';
|
|
6
6
|
import { VersionedPowerSyncMongo } from './db.js';
|
|
7
7
|
|
|
8
8
|
const LOCK_DURATION_MS = 60 * 1000;
|
|
@@ -12,6 +12,12 @@ const LOCK_DURATION_MS = 60 * 1000;
|
|
|
12
12
|
* processes that replication stream at a time.
|
|
13
13
|
*/
|
|
14
14
|
export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
15
|
+
/** FIFO admission shared by this lease's writers; database fencing still enforces ownership. */
|
|
16
|
+
readonly writerMutex = new utils.Mutex();
|
|
17
|
+
|
|
18
|
+
private readonly abort = new AbortController();
|
|
19
|
+
readonly signal = this.abort.signal;
|
|
20
|
+
|
|
15
21
|
private readonly refreshInterval: NodeJS.Timeout;
|
|
16
22
|
|
|
17
23
|
/**
|
|
@@ -67,13 +73,14 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
67
73
|
constructor(
|
|
68
74
|
private db: VersionedPowerSyncMongo,
|
|
69
75
|
public sync_rules_id: number,
|
|
70
|
-
|
|
76
|
+
public readonly lock_id: string,
|
|
71
77
|
private logger: Logger
|
|
72
78
|
) {
|
|
73
79
|
this.refreshInterval = setInterval(async () => {
|
|
74
80
|
try {
|
|
75
81
|
await this.refresh();
|
|
76
82
|
} catch (e) {
|
|
83
|
+
this.abort.abort(e);
|
|
77
84
|
this.logger.error('Failed to refresh lock', e);
|
|
78
85
|
clearInterval(this.refreshInterval);
|
|
79
86
|
}
|
|
@@ -81,6 +88,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
async release(): Promise<void> {
|
|
91
|
+
this.abort.abort(new Error('Replication lock released'));
|
|
84
92
|
clearInterval(this.refreshInterval);
|
|
85
93
|
const result = await this.db.sync_rules.updateOne(
|
|
86
94
|
{
|
|
@@ -97,6 +105,51 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
throwIfAborted(): void {
|
|
109
|
+
this.signal.throwIfAborted();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static ownerFilter(streamId: number, lock: MongoSyncRulesLock) {
|
|
113
|
+
lock.throwIfAborted();
|
|
114
|
+
return { _id: streamId, 'lock.id': lock.lock_id };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static heartbeatUpdate() {
|
|
118
|
+
// Always change the existing heartbeat, even for writes in the same
|
|
119
|
+
// millisecond. A no-op update is not sufficient for a transactional fence.
|
|
120
|
+
return {
|
|
121
|
+
last_keepalive_ts: {
|
|
122
|
+
$max: ['$$NOW', { $add: [{ $ifNull: ['$last_keepalive_ts', new Date(0)] }, 1] }]
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static assertOwned<T>(document: T | null): T {
|
|
128
|
+
if (document == null) {
|
|
129
|
+
throw new ReplicationAbortedError('Replication writer no longer owns the stream');
|
|
130
|
+
}
|
|
131
|
+
return document;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A write, not just an ownership read: takeover conflicts with every transaction
|
|
136
|
+
* that publishes under this owner. Every writer must hold a stream lease.
|
|
137
|
+
*/
|
|
138
|
+
static async fence(
|
|
139
|
+
db: VersionedPowerSyncMongo,
|
|
140
|
+
streamId: number,
|
|
141
|
+
lock: MongoSyncRulesLock,
|
|
142
|
+
session: mongo.ClientSession,
|
|
143
|
+
projection: mongo.Document = { last_persisted_op: 1, last_checkpoint: 1, keepalive_op: 1 }
|
|
144
|
+
) {
|
|
145
|
+
const doc = await db.sync_rules.findOneAndUpdate(
|
|
146
|
+
this.ownerFilter(streamId, lock),
|
|
147
|
+
[{ $set: this.heartbeatUpdate() }],
|
|
148
|
+
{ session, returnDocument: 'after', projection }
|
|
149
|
+
);
|
|
150
|
+
return this.assertOwned(doc);
|
|
151
|
+
}
|
|
152
|
+
|
|
100
153
|
private async refresh(): Promise<void> {
|
|
101
154
|
const result = await this.db.sync_rules.findOneAndUpdate(
|
|
102
155
|
{
|
|
@@ -81,6 +81,7 @@ export class PowerSyncMongo {
|
|
|
81
81
|
this.bucket_data = db.collection('bucket_data');
|
|
82
82
|
this.bucket_parameters = db.collection('bucket_parameters');
|
|
83
83
|
this.op_id_sequence = db.collection('op_id_sequence');
|
|
84
|
+
// Uses legacy terminology instead of "sync config" for backwards-compatibility
|
|
84
85
|
this.sync_rules = db.collection('sync_rules');
|
|
85
86
|
this.source_tables = db.collection('source_tables');
|
|
86
87
|
this.custom_write_checkpoints = db.collection('custom_write_checkpoints');
|
|
@@ -308,6 +308,7 @@ export interface CustomWriteCheckpointDocument {
|
|
|
308
308
|
_id: bson.ObjectId;
|
|
309
309
|
user_id: string;
|
|
310
310
|
checkpoint: bigint;
|
|
311
|
+
// Uses the legacy field name "sync_rules_id" for backwards-compatibility. See AGENTS.md.
|
|
311
312
|
sync_rules_id: number;
|
|
312
313
|
/**
|
|
313
314
|
* Unlike managed write checkpoints, custom write checkpoints are flushed together with
|
|
@@ -6,8 +6,11 @@ import * as bson from 'bson';
|
|
|
6
6
|
import { mongoTableId } from '../../../utils/util.js';
|
|
7
7
|
import { calculateCheckpointState } from '../CheckpointState.js';
|
|
8
8
|
import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
|
|
9
|
+
import { MongoSyncRulesLock } from '../MongoSyncRulesLock.js';
|
|
10
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
9
11
|
import { PersistedBatch } from '../common/PersistedBatch.js';
|
|
10
12
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
13
|
+
import { SyncRuleDocumentBase } from '../models.js';
|
|
11
14
|
import { PersistedBatchV1 } from './PersistedBatchV1.js';
|
|
12
15
|
import { SourceRecordStoreV1 } from './SourceRecordStoreV1.js';
|
|
13
16
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
@@ -34,6 +37,27 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
34
37
|
this.store = new SourceRecordStoreV1(this.db, this.replicationStreamId);
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
protected override persistedOpHead(stream: SyncRuleDocumentBase): InternalOpId {
|
|
41
|
+
const legacy = stream as SyncRuleDocumentV1;
|
|
42
|
+
const checkpoint = legacy.last_checkpoint ?? 0n;
|
|
43
|
+
const keepalive = legacy.keepalive_op == null ? 0n : BigInt(legacy.keepalive_op);
|
|
44
|
+
return checkpoint > keepalive ? checkpoint : keepalive;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected override onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: InternalOpId): void {
|
|
48
|
+
// Keep flushed operations recoverable before a checkpoint can be published.
|
|
49
|
+
// keepalive_op remains a decimal string for compatibility with legacy readers.
|
|
50
|
+
writes.updateOne(this.db.sync_rules, { _id: this.replicationStreamId }, [
|
|
51
|
+
{
|
|
52
|
+
$set: {
|
|
53
|
+
keepalive_op: {
|
|
54
|
+
$toString: { $max: [{ $toLong: '$keepalive_op' }, '$last_checkpoint', lastOp] }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
protected override recordPersistedOp(lastOp: InternalOpId): void {
|
|
38
62
|
this.persisted_op = lastOp;
|
|
39
63
|
}
|
|
@@ -101,7 +125,8 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
101
125
|
}));
|
|
102
126
|
|
|
103
127
|
let result: storage.ResolveTablesResult | null = null;
|
|
104
|
-
|
|
128
|
+
const session = this.session;
|
|
129
|
+
await this.withTransaction(async () => {
|
|
105
130
|
const col = this.db.sourceTablesV1(this.replicationStreamId);
|
|
106
131
|
|
|
107
132
|
// Find records that overlap by name or relation id.
|
|
@@ -219,129 +244,136 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
219
244
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
220
245
|
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
221
246
|
|
|
222
|
-
await this.flush(options);
|
|
223
|
-
|
|
224
247
|
using _ = this.tracer.span('storage', 'commit');
|
|
225
248
|
|
|
226
|
-
const
|
|
249
|
+
const updateCheckpoint = async (persistedOp = this.persisted_op) => {
|
|
250
|
+
const now = new Date();
|
|
227
251
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
session: this.session
|
|
240
|
-
}
|
|
241
|
-
);
|
|
252
|
+
const can_checkpoint = {
|
|
253
|
+
$and: [
|
|
254
|
+
{ $eq: ['$snapshot_done', true] },
|
|
255
|
+
{
|
|
256
|
+
$or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
$or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
};
|
|
242
263
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
const new_keepalive_op = {
|
|
265
|
+
$cond: [
|
|
266
|
+
can_checkpoint,
|
|
267
|
+
{ $literal: null },
|
|
268
|
+
{
|
|
269
|
+
$toString: {
|
|
270
|
+
$max: [{ $toLong: '$keepalive_op' }, { $literal: persistedOp }, 0n]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const new_last_checkpoint = {
|
|
277
|
+
$cond: [
|
|
278
|
+
can_checkpoint,
|
|
279
|
+
{
|
|
280
|
+
$max: ['$last_checkpoint', { $literal: persistedOp }, { $toLong: '$keepalive_op' }, 0n]
|
|
281
|
+
},
|
|
282
|
+
'$last_checkpoint'
|
|
283
|
+
]
|
|
284
|
+
};
|
|
254
285
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
286
|
+
const preUpdateDocument = (await this.db.sync_rules.findOneAndUpdate(
|
|
287
|
+
MongoSyncRulesLock.ownerFilter(this.replicationStreamId, this.options.replicationLock),
|
|
288
|
+
[
|
|
289
|
+
{
|
|
290
|
+
$set: {
|
|
291
|
+
_can_checkpoint: can_checkpoint,
|
|
292
|
+
_not_empty: createEmptyCheckpoints
|
|
293
|
+
? true
|
|
294
|
+
: {
|
|
295
|
+
$or: [
|
|
296
|
+
{ $literal: createEmptyCheckpoints },
|
|
297
|
+
{ $ne: ['$keepalive_op', new_keepalive_op] },
|
|
298
|
+
{ $ne: ['$last_checkpoint', new_last_checkpoint] }
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
$set: {
|
|
305
|
+
...MongoSyncRulesLock.heartbeatUpdate(),
|
|
306
|
+
last_checkpoint_lsn: {
|
|
307
|
+
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
|
|
308
|
+
},
|
|
309
|
+
last_checkpoint_ts: {
|
|
310
|
+
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
last_fatal_error: { $literal: null },
|
|
314
|
+
last_fatal_error_ts: { $literal: null },
|
|
315
|
+
keepalive_op: new_keepalive_op,
|
|
316
|
+
last_checkpoint: new_last_checkpoint,
|
|
317
|
+
snapshot_lsn: {
|
|
318
|
+
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
$unset: ['_can_checkpoint', '_not_empty']
|
|
324
|
+
}
|
|
325
|
+
],
|
|
259
326
|
{
|
|
260
|
-
|
|
261
|
-
|
|
327
|
+
session: this.session,
|
|
328
|
+
returnDocument: 'before',
|
|
329
|
+
projection: {
|
|
330
|
+
snapshot_done: 1,
|
|
331
|
+
last_checkpoint_lsn: 1,
|
|
332
|
+
no_checkpoint_before: 1,
|
|
333
|
+
keepalive_op: 1,
|
|
334
|
+
last_checkpoint: 1
|
|
262
335
|
}
|
|
263
336
|
}
|
|
264
|
-
|
|
265
|
-
};
|
|
337
|
+
)) as SyncRuleDocumentV1;
|
|
266
338
|
|
|
267
|
-
|
|
268
|
-
$cond: [
|
|
269
|
-
can_checkpoint,
|
|
270
|
-
{
|
|
271
|
-
$max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
|
|
272
|
-
},
|
|
273
|
-
'$last_checkpoint'
|
|
274
|
-
]
|
|
339
|
+
return MongoSyncRulesLock.assertOwned(preUpdateDocument);
|
|
275
340
|
};
|
|
276
341
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
[
|
|
342
|
+
const finishCheckpoint = async (preUpdateDocument: SyncRuleDocumentV1, lastOp?: InternalOpId) => {
|
|
343
|
+
await this.db.write_checkpoints.updateMany(
|
|
280
344
|
{
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
_not_empty: createEmptyCheckpoints
|
|
284
|
-
? true
|
|
285
|
-
: {
|
|
286
|
-
$or: [
|
|
287
|
-
{ $literal: createEmptyCheckpoints },
|
|
288
|
-
{ $ne: ['$keepalive_op', new_keepalive_op] },
|
|
289
|
-
{ $ne: ['$last_checkpoint', new_last_checkpoint] }
|
|
290
|
-
]
|
|
291
|
-
}
|
|
292
|
-
}
|
|
345
|
+
processed_at_lsn: null,
|
|
346
|
+
'lsns.1': { $lte: lsn }
|
|
293
347
|
},
|
|
294
348
|
{
|
|
295
349
|
$set: {
|
|
296
|
-
|
|
297
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
|
|
298
|
-
},
|
|
299
|
-
last_checkpoint_ts: {
|
|
300
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
|
|
301
|
-
},
|
|
302
|
-
last_keepalive_ts: { $literal: now },
|
|
303
|
-
last_fatal_error: { $literal: null },
|
|
304
|
-
last_fatal_error_ts: { $literal: null },
|
|
305
|
-
keepalive_op: new_keepalive_op,
|
|
306
|
-
last_checkpoint: new_last_checkpoint,
|
|
307
|
-
snapshot_lsn: {
|
|
308
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
|
|
309
|
-
}
|
|
350
|
+
processed_at_lsn: lsn
|
|
310
351
|
}
|
|
311
352
|
},
|
|
312
353
|
{
|
|
313
|
-
|
|
354
|
+
session: this.session
|
|
314
355
|
}
|
|
315
|
-
],
|
|
316
|
-
{
|
|
317
|
-
session: this.session,
|
|
318
|
-
returnDocument: 'before',
|
|
319
|
-
projection: {
|
|
320
|
-
snapshot_done: 1,
|
|
321
|
-
last_checkpoint_lsn: 1,
|
|
322
|
-
no_checkpoint_before: 1,
|
|
323
|
-
keepalive_op: 1,
|
|
324
|
-
last_checkpoint: 1
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
)) as SyncRuleDocumentV1;
|
|
328
|
-
|
|
329
|
-
if (preUpdateDocument == null) {
|
|
330
|
-
throw new ReplicationAssertionError(
|
|
331
|
-
'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.replicationStreamId
|
|
332
356
|
);
|
|
333
|
-
}
|
|
334
357
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
358
|
+
const checkpointState = calculateCheckpointState({
|
|
359
|
+
lsn,
|
|
360
|
+
snapshotDone: preUpdateDocument.snapshot_done === true,
|
|
361
|
+
lastCheckpointLsn: preUpdateDocument.last_checkpoint_lsn,
|
|
362
|
+
noCheckpointBefore: preUpdateDocument.no_checkpoint_before,
|
|
363
|
+
keepaliveOp: preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op),
|
|
364
|
+
lastCheckpoint: preUpdateDocument.last_checkpoint,
|
|
365
|
+
persistedOp: lastOp ?? this.persistedOpHead(preUpdateDocument),
|
|
366
|
+
createEmptyCheckpoints
|
|
367
|
+
});
|
|
368
|
+
return { checkpointState, preUpdateDocument };
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const { checkpointState, preUpdateDocument } = await this.flushAndCommit(
|
|
372
|
+
async (_stream, lastOp) => finishCheckpoint(await updateCheckpoint(lastOp), lastOp),
|
|
373
|
+
// Without a flush, the checkpoint update itself fences the transaction.
|
|
374
|
+
() => this.withFencedTransaction(updateCheckpoint, finishCheckpoint),
|
|
375
|
+
options
|
|
376
|
+
);
|
|
345
377
|
if (checkpointState.checkpointBlocked) {
|
|
346
378
|
if (Date.now() - this.lastWaitingLogThrottled > 5_000) {
|
|
347
379
|
this.logger.info(
|
|
@@ -379,41 +411,20 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
379
411
|
|
|
380
412
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
381
413
|
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
$set: {
|
|
388
|
-
snapshot_lsn: lsn
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
session: this.session,
|
|
393
|
-
// Losing occasional resume LSN is fine. That may mean reprocessing
|
|
394
|
-
// some source changes in some edge cases, which is not an issue since
|
|
395
|
-
// changes are processed in an idempotent way.
|
|
396
|
-
writeConcern: { w: 1 }
|
|
397
|
-
}
|
|
398
|
-
);
|
|
414
|
+
// Losing occasional resume LSN would only reprocess source changes.
|
|
415
|
+
// Keep the lease check atomic, but do not wait for majority replication of this resume hint.
|
|
416
|
+
await this.updateStreamMetadata({ snapshot_lsn: { $literal: lsn } }, {}, { w: 1 });
|
|
399
417
|
}
|
|
400
418
|
|
|
401
419
|
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
402
|
-
await this.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
},
|
|
411
|
-
$max: {
|
|
412
|
-
no_checkpoint_before: no_checkpoint_before_lsn
|
|
413
|
-
}
|
|
414
|
-
},
|
|
415
|
-
{ session: this.session }
|
|
416
|
-
);
|
|
420
|
+
await this.updateStreamMetadata(this.snapshotDoneUpdate(no_checkpoint_before_lsn));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private snapshotDoneUpdate(no_checkpoint_before_lsn: string) {
|
|
424
|
+
return {
|
|
425
|
+
snapshot_done: true,
|
|
426
|
+
no_checkpoint_before: { $max: ['$no_checkpoint_before', { $literal: no_checkpoint_before_lsn }] }
|
|
427
|
+
};
|
|
417
428
|
}
|
|
418
429
|
|
|
419
430
|
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
@@ -443,22 +454,12 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
443
454
|
}
|
|
444
455
|
}
|
|
445
456
|
|
|
446
|
-
await this.
|
|
457
|
+
await this.updateStreamMetadataInTransaction(this.snapshotDoneUpdate(no_checkpoint_before_lsn));
|
|
447
458
|
});
|
|
448
459
|
}
|
|
449
460
|
|
|
450
461
|
async markTableSnapshotRequired(_table: storage.SourceTable): Promise<void> {
|
|
451
|
-
await this.
|
|
452
|
-
{
|
|
453
|
-
_id: this.replicationStreamId
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
$set: {
|
|
457
|
-
snapshot_done: false
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
{ session: this.session }
|
|
461
|
-
);
|
|
462
|
+
await this.updateStreamMetadata({ snapshot_done: false });
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
async markTableSnapshotDone(
|
|
@@ -490,9 +491,6 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
490
491
|
_id: this.replicationStreamId
|
|
491
492
|
},
|
|
492
493
|
{
|
|
493
|
-
$set: {
|
|
494
|
-
last_keepalive_ts: new Date()
|
|
495
|
-
},
|
|
496
494
|
$max: {
|
|
497
495
|
no_checkpoint_before: no_checkpoint_before_lsn
|
|
498
496
|
}
|
|
@@ -516,7 +514,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
516
514
|
const session = this.session;
|
|
517
515
|
let activated = false;
|
|
518
516
|
let needsFutureActivationCheck = true;
|
|
519
|
-
await
|
|
517
|
+
await this.withTransaction(async () => {
|
|
520
518
|
// Reset on transaction retries.
|
|
521
519
|
activated = false;
|
|
522
520
|
needsFutureActivationCheck = true;
|
|
@@ -554,7 +552,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
554
552
|
} else if (doc?.state != storage.SyncRuleState.PROCESSING) {
|
|
555
553
|
needsFutureActivationCheck = false;
|
|
556
554
|
}
|
|
557
|
-
});
|
|
555
|
+
}, session);
|
|
558
556
|
if (activated) {
|
|
559
557
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
560
558
|
await this.db.notifyCheckpoint();
|
|
@@ -90,7 +90,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
90
90
|
if (keepaliveOp == null && lastCheckpoint == null) {
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
|
-
return
|
|
93
|
+
return [keepaliveOp ?? 0n, lastCheckpoint ?? 0n].reduce((a, b) => (a > b ? a : b));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
protected async createWriterImpl(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
|