@powersync/service-module-mongodb-storage 0.21.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 +43 -0
- package/dist/storage/MongoBucketStorage.d.ts +10 -1
- package/dist/storage/MongoBucketStorage.js +37 -7
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +24 -10
- package/dist/storage/implementation/MongoBucketBatch.js +134 -89
- 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/MongoStorageProvider.js +4 -2
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- 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/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
- package/dist/storage/implementation/common/PersistedBatch.js +6 -4
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +5 -0
- package/dist/storage/implementation/db.js +14 -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 +149 -139
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
- package/dist/storage/implementation/v1/MongoCompactorV1.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/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +6 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +148 -162
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +10 -4
- package/dist/storage/implementation/v3/MongoCompactorV3.js +175 -76
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
- package/dist/storage/implementation/v3/PersistedBatchV3.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/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/types/types.d.ts +5 -0
- package/dist/types/types.js +12 -0
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +2 -1
- package/dist/utils/test-utils.js +5 -3
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/MongoBucketStorage.ts +47 -8
- package/src/storage/implementation/MongoBucketBatch.ts +205 -111
- 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/MongoStorageProvider.ts +12 -2
- package/src/storage/implementation/MongoSyncBucketStorage.ts +19 -0
- package/src/storage/implementation/MongoSyncRulesLock.ts +56 -3
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
- package/src/storage/implementation/common/PersistedBatch.ts +12 -8
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +15 -0
- package/src/storage/implementation/models.ts +1 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +167 -160
- package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +1 -1
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +217 -242
- package/src/storage/implementation/v3/MongoCompactorV3.ts +284 -108
- package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
- package/src/storage/implementation/v3/compact-utils.ts +16 -7
- package/src/storage/implementation/v3/models.ts +1 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
- package/src/types/types.ts +18 -0
- package/src/utils/test-utils.ts +6 -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/chunk_compaction_config.test.ts +38 -0
- 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 +18 -5
- 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 +402 -24
- 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 +51 -5
- 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
|
@@ -7,9 +7,11 @@ import { mongoTableId } from '../../../utils/util.js';
|
|
|
7
7
|
import { canCheckpointState } from '../CheckpointState.js';
|
|
8
8
|
import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
|
|
9
9
|
import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
|
|
10
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
10
11
|
import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
|
|
11
12
|
import { PersistedBatch } from '../common/PersistedBatch.js';
|
|
12
13
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
14
|
+
import { SyncRuleDocumentBase } from '../models.js';
|
|
13
15
|
import { PersistedBatchV3 } from './PersistedBatchV3.js';
|
|
14
16
|
import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
|
|
15
17
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
@@ -60,14 +62,16 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
60
62
|
return this.store;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
protected override
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
protected override persistedOpHead(stream: SyncRuleDocumentBase): InternalOpId {
|
|
66
|
+
return (stream as ReplicationStreamDocumentV3).last_persisted_op ?? 0n;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected override onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: InternalOpId): void {
|
|
67
70
|
// Durably advance the stream-level head of persisted ops within the flush transaction.
|
|
68
71
|
// This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
|
|
69
72
|
// config that replicates nothing) covers all ops persisted before a potential crash.
|
|
70
|
-
|
|
73
|
+
writes.updateOne(
|
|
74
|
+
this.db.sync_rules,
|
|
71
75
|
{
|
|
72
76
|
_id: this.replicationStreamId
|
|
73
77
|
},
|
|
@@ -75,8 +79,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
75
79
|
$max: {
|
|
76
80
|
last_persisted_op: lastOp
|
|
77
81
|
}
|
|
78
|
-
}
|
|
79
|
-
{ session }
|
|
82
|
+
}
|
|
80
83
|
);
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -173,7 +176,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
173
176
|
const session = this.db.client.startSession();
|
|
174
177
|
await using _ = { [Symbol.asyncDispose]: () => session.endSession() };
|
|
175
178
|
|
|
176
|
-
await
|
|
179
|
+
await this.withTransaction(async () => {
|
|
177
180
|
const col = this.db.sourceTables(this.replicationStreamId);
|
|
178
181
|
|
|
179
182
|
// Find records that overlap by name or relation id.
|
|
@@ -243,7 +246,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
243
246
|
sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping, eventById)
|
|
244
247
|
)
|
|
245
248
|
};
|
|
246
|
-
});
|
|
249
|
+
}, session);
|
|
247
250
|
|
|
248
251
|
return result!;
|
|
249
252
|
}
|
|
@@ -268,156 +271,143 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
268
271
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
269
272
|
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
270
273
|
|
|
271
|
-
await this.flush(options);
|
|
272
|
-
|
|
273
274
|
using _ = this.tracer.span('storage', 'commit');
|
|
274
275
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
$set: {
|
|
284
|
-
processed_at_lsn: lsn
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
session: this.session
|
|
289
|
-
}
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
const preUpdateDocument = (await this.db.sync_rules.findOne(
|
|
293
|
-
{
|
|
294
|
-
_id: this.replicationStreamId,
|
|
295
|
-
'sync_configs._id': { $in: this.syncConfigIds }
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
session: this.session,
|
|
299
|
-
projection: {
|
|
300
|
-
sync_configs: 1,
|
|
301
|
-
last_persisted_op: 1
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
)) as ReplicationStreamDocumentV3 | null;
|
|
305
|
-
|
|
306
|
-
const states =
|
|
307
|
-
preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
|
|
308
|
-
if (states.length == 0) {
|
|
309
|
-
throw new ReplicationAssertionError(
|
|
310
|
-
`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
|
|
311
|
-
.map((id) => id.toHexString())
|
|
312
|
-
.join(',')}`
|
|
276
|
+
const checkpoint = async (stream: SyncRuleDocumentBase, lastOp?: InternalOpId) => {
|
|
277
|
+
const now = new Date();
|
|
278
|
+
const preUpdateDocument = stream as ReplicationStreamDocumentV3;
|
|
279
|
+
const writes = this.db.createWriteBatch(this.session, { ordered: false });
|
|
280
|
+
writes.updateMany(
|
|
281
|
+
this.db.write_checkpoints,
|
|
282
|
+
{ processed_at_lsn: null, 'lsns.1': { $lte: lsn } },
|
|
283
|
+
{ $set: { processed_at_lsn: lsn } }
|
|
313
284
|
);
|
|
314
|
-
}
|
|
315
|
-
// The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
|
|
316
|
-
// sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
|
|
317
|
-
// remain embedded in the document and are ignored here.
|
|
318
|
-
const missingSyncConfig = preUpdateDocument!.sync_configs.find(
|
|
319
|
-
(config) =>
|
|
320
|
-
[storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
|
|
321
|
-
!this.syncConfigIds.some((id) => id.equals(config._id))
|
|
322
|
-
);
|
|
323
|
-
if (missingSyncConfig != null) {
|
|
324
|
-
throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
|
|
325
|
-
}
|
|
326
285
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
preUpdateDocument?.last_persisted_op == null ? 0n : BigInt(preUpdateDocument.last_persisted_op);
|
|
332
|
-
|
|
333
|
-
let checkpointBlocked = false;
|
|
334
|
-
let checkpointCreated = false;
|
|
335
|
-
let checkpointLogState: unknown = null;
|
|
336
|
-
const unblockedConfigIds: bson.ObjectId[] = [];
|
|
337
|
-
|
|
338
|
-
for (const state of states) {
|
|
339
|
-
if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
|
|
340
|
-
// last_persisted_op is $max-advanced durably in the same transaction as every flush, and
|
|
341
|
-
// checkpoints are only ever created at that head, so a checkpoint past the head means the
|
|
342
|
-
// op sequence or the stored state is corrupt.
|
|
286
|
+
const states =
|
|
287
|
+
preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ??
|
|
288
|
+
[];
|
|
289
|
+
if (states.length == 0) {
|
|
343
290
|
throw new ReplicationAssertionError(
|
|
344
|
-
`
|
|
291
|
+
`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
|
|
292
|
+
.map((id) => id.toHexString())
|
|
293
|
+
.join(',')}`
|
|
345
294
|
);
|
|
346
295
|
}
|
|
296
|
+
// The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
|
|
297
|
+
// sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
|
|
298
|
+
// remain embedded in the document and are ignored here.
|
|
299
|
+
const missingSyncConfig = preUpdateDocument!.sync_configs.find(
|
|
300
|
+
(config) =>
|
|
301
|
+
[storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
|
|
302
|
+
!this.syncConfigIds.some((id) => id.equals(config._id))
|
|
303
|
+
);
|
|
304
|
+
if (missingSyncConfig != null) {
|
|
305
|
+
throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
|
|
306
|
+
}
|
|
347
307
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
noCheckpointBefore: state.no_checkpoint_before
|
|
352
|
-
});
|
|
308
|
+
// Effective head of the stream's op sequence.
|
|
309
|
+
// A combined flush supplies its new head; an empty commit uses the fenced persisted head.
|
|
310
|
+
const newCheckpoint = lastOp ?? this.persistedOpHead(preUpdateDocument);
|
|
353
311
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
snapshot_done: state.snapshot_done,
|
|
359
|
-
last_checkpoint_lsn: state.last_checkpoint_lsn,
|
|
360
|
-
no_checkpoint_before: state.no_checkpoint_before
|
|
361
|
-
};
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
312
|
+
let checkpointBlocked = false;
|
|
313
|
+
let checkpointCreated = false;
|
|
314
|
+
let checkpointLogState: unknown = null;
|
|
315
|
+
const unblockedConfigIds: bson.ObjectId[] = [];
|
|
364
316
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
317
|
+
for (const state of states) {
|
|
318
|
+
if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
|
|
319
|
+
// last_persisted_op is $max-advanced durably in the same transaction as every flush, and
|
|
320
|
+
// checkpoints are only ever created at that head, so a checkpoint past the head means the
|
|
321
|
+
// op sequence or the stored state is corrupt.
|
|
322
|
+
throw new ReplicationAssertionError(
|
|
323
|
+
`Invariant violation: sync config ${state._id} has last_checkpoint ${state.last_checkpoint} > stream head ${newCheckpoint}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
368
326
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
updateSet['last_checkpoint_ts'] = now;
|
|
327
|
+
const canCheckpoint = canCheckpointState(lsn, {
|
|
328
|
+
snapshotDone: state.snapshot_done === true,
|
|
329
|
+
lastCheckpointLsn: state.last_checkpoint_lsn,
|
|
330
|
+
noCheckpointBefore: state.no_checkpoint_before
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (!canCheckpoint) {
|
|
334
|
+
checkpointBlocked = true;
|
|
335
|
+
// Log the first blocked config's state.
|
|
336
|
+
checkpointLogState ??= {
|
|
337
|
+
snapshot_done: state.snapshot_done,
|
|
338
|
+
last_checkpoint_lsn: state.last_checkpoint_lsn,
|
|
339
|
+
no_checkpoint_before: state.no_checkpoint_before
|
|
340
|
+
};
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
checkpointCreated ||= createEmptyCheckpoints || state.last_checkpoint !== newCheckpoint;
|
|
345
|
+
unblockedConfigIds.push(state._id);
|
|
389
346
|
}
|
|
390
347
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
|
|
348
|
+
// Every commit advances the stream's resume position: commit() flushes first, so all
|
|
349
|
+
// source changes up to this lsn have been persisted, even when checkpoints are blocked.
|
|
350
|
+
// In the future we could also advance this on flush, when the connector provides the
|
|
351
|
+
// current position (see setResumeLsn, which connectors may already call after flushing).
|
|
352
|
+
const resumeLsnUpdate = {
|
|
353
|
+
resume_lsn: lsn,
|
|
354
|
+
...(lastOp == null ? {} : { last_persisted_op: lastOp })
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (unblockedConfigIds.length > 0) {
|
|
358
|
+
// All unblocked configs get the SAME new value, so we apply it with a single updateOne
|
|
359
|
+
// (single-document atomicity).
|
|
360
|
+
const updateSet: Record<string, any> = {
|
|
361
|
+
last_fatal_error: null,
|
|
362
|
+
last_fatal_error_ts: null
|
|
363
|
+
};
|
|
364
|
+
// Only advance checkpoint fields when an actual (non-empty) checkpoint is created, matching
|
|
365
|
+
// the previous per-config / v1 behaviour.
|
|
366
|
+
if (checkpointCreated) {
|
|
367
|
+
updateSet['sync_configs.$[config].last_checkpoint'] = newCheckpoint;
|
|
368
|
+
updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
|
|
369
|
+
updateSet['last_checkpoint_ts'] = now;
|
|
400
370
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
_id: this.replicationStreamId
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
$set: {
|
|
411
|
-
last_keepalive_ts: now,
|
|
412
|
-
last_fatal_error: null,
|
|
413
|
-
last_fatal_error_ts: null
|
|
371
|
+
|
|
372
|
+
writes.updateOne(
|
|
373
|
+
this.db.sync_rules,
|
|
374
|
+
{
|
|
375
|
+
_id: this.replicationStreamId,
|
|
376
|
+
'sync_configs._id': { $in: unblockedConfigIds }
|
|
414
377
|
},
|
|
415
|
-
$max: resumeLsnUpdate
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
378
|
+
{ $set: updateSet, $max: resumeLsnUpdate },
|
|
379
|
+
{
|
|
380
|
+
arrayFilters: checkpointCreated ? [{ 'config._id': { $in: unblockedConfigIds } }] : undefined
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
// All selected configs are blocked - only update keepalive/error tracking and the
|
|
385
|
+
// resume position.
|
|
386
|
+
writes.updateOne(
|
|
387
|
+
this.db.sync_rules,
|
|
388
|
+
{
|
|
389
|
+
_id: this.replicationStreamId
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
$set: {
|
|
393
|
+
last_fatal_error: null,
|
|
394
|
+
last_fatal_error_ts: null
|
|
395
|
+
},
|
|
396
|
+
$max: resumeLsnUpdate
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
}
|
|
420
400
|
|
|
401
|
+
await writes.execute();
|
|
402
|
+
return { checkpointBlocked, checkpointCreated, checkpointLogState, newCheckpoint };
|
|
403
|
+
};
|
|
404
|
+
const projection = { sync_configs: 1, last_persisted_op: 1 };
|
|
405
|
+
const { checkpointBlocked, checkpointCreated, checkpointLogState, newCheckpoint } = await this.flushAndCommit(
|
|
406
|
+
checkpoint,
|
|
407
|
+
() => this.withFencedTransaction(() => this.fence(this.session, projection), checkpoint),
|
|
408
|
+
options,
|
|
409
|
+
projection
|
|
410
|
+
);
|
|
421
411
|
if (checkpointBlocked) {
|
|
422
412
|
if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
|
|
423
413
|
this.logger.info(
|
|
@@ -448,23 +438,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
448
438
|
|
|
449
439
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
450
440
|
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
$set: {
|
|
457
|
-
resume_lsn: lsn
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
session: this.session,
|
|
462
|
-
// Losing occasional resume LSN is fine. That may mean reprocessing
|
|
463
|
-
// some source changes in some edge cases, which is not an issue since
|
|
464
|
-
// changes are processed in an idempotent way.
|
|
465
|
-
writeConcern: { w: 1 }
|
|
466
|
-
}
|
|
467
|
-
);
|
|
441
|
+
// Losing occasional resume LSN would only reprocess source changes.
|
|
442
|
+
// Keep the lease check atomic, but do not wait for majority replication of this resume hint.
|
|
443
|
+
await this.updateStreamMetadata({ resume_lsn: { $literal: lsn } }, {}, { w: 1 });
|
|
468
444
|
}
|
|
469
445
|
|
|
470
446
|
private async autoActivateV3(lsn: string): Promise<void> {
|
|
@@ -475,7 +451,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
475
451
|
const session = this.session;
|
|
476
452
|
let activated = false;
|
|
477
453
|
let needsFutureActivationCheck = true;
|
|
478
|
-
await
|
|
454
|
+
await this.withTransaction(async () => {
|
|
479
455
|
// Reset on transaction retries.
|
|
480
456
|
needsFutureActivationCheck = true;
|
|
481
457
|
activated = false;
|
|
@@ -563,7 +539,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
563
539
|
} else if (doc.state == storage.SyncRuleState.ACTIVE && processingStates.length == 0) {
|
|
564
540
|
needsFutureActivationCheck = false;
|
|
565
541
|
}
|
|
566
|
-
});
|
|
542
|
+
}, session);
|
|
567
543
|
if (activated) {
|
|
568
544
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
569
545
|
await this.db.notifyCheckpoint();
|
|
@@ -573,26 +549,35 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
573
549
|
}
|
|
574
550
|
}
|
|
575
551
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
{
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
552
|
+
private syncConfigMetadataUpdate(syncConfigIds: bson.ObjectId[], set: lib_mongo.mongo.Document) {
|
|
553
|
+
return {
|
|
554
|
+
sync_configs: {
|
|
555
|
+
$map: {
|
|
556
|
+
input: '$sync_configs',
|
|
557
|
+
as: 'config',
|
|
558
|
+
in: {
|
|
559
|
+
$cond: [
|
|
560
|
+
{ $in: ['$$config._id', { $literal: syncConfigIds }] },
|
|
561
|
+
{ $mergeObjects: ['$$config', set] },
|
|
562
|
+
'$$config'
|
|
563
|
+
]
|
|
564
|
+
}
|
|
589
565
|
}
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
session: this.session,
|
|
593
|
-
arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
|
|
594
566
|
}
|
|
595
|
-
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private snapshotDoneUpdate(no_checkpoint_before_lsn: string) {
|
|
571
|
+
return this.syncConfigMetadataUpdate(this.syncConfigIds, {
|
|
572
|
+
snapshot_done: true,
|
|
573
|
+
no_checkpoint_before: { $max: ['$$config.no_checkpoint_before', { $literal: no_checkpoint_before_lsn }] }
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
578
|
+
await this.updateStreamMetadata(this.snapshotDoneUpdate(no_checkpoint_before_lsn), {
|
|
579
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
580
|
+
});
|
|
596
581
|
}
|
|
597
582
|
|
|
598
583
|
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
@@ -616,7 +601,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
616
601
|
}
|
|
617
602
|
}
|
|
618
603
|
|
|
619
|
-
await this.
|
|
604
|
+
await this.updateStreamMetadataInTransaction(this.snapshotDoneUpdate(no_checkpoint_before_lsn), {
|
|
605
|
+
'sync_configs._id': { $in: this.syncConfigIds }
|
|
606
|
+
});
|
|
620
607
|
});
|
|
621
608
|
}
|
|
622
609
|
|
|
@@ -625,22 +612,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
625
612
|
if (syncConfigIds.length == 0) {
|
|
626
613
|
return;
|
|
627
614
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
_id: this.replicationStreamId,
|
|
632
|
-
'sync_configs._id': { $in: syncConfigIds }
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
$set: {
|
|
636
|
-
'sync_configs.$[config].snapshot_done': false
|
|
637
|
-
}
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
session: this.session,
|
|
641
|
-
arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
|
|
642
|
-
}
|
|
643
|
-
);
|
|
615
|
+
await this.updateStreamMetadata(this.syncConfigMetadataUpdate(syncConfigIds, { snapshot_done: false }), {
|
|
616
|
+
'sync_configs._id': { $in: syncConfigIds }
|
|
617
|
+
});
|
|
644
618
|
}
|
|
645
619
|
|
|
646
620
|
async markTableSnapshotDone(
|
|
@@ -652,7 +626,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
652
626
|
const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
|
|
653
627
|
|
|
654
628
|
await this.withTransaction(async () => {
|
|
655
|
-
|
|
629
|
+
const writes = this.db.createWriteBatch(session, { ordered: false });
|
|
630
|
+
writes.updateMany(
|
|
631
|
+
this.db.sourceTables(this.replicationStreamId),
|
|
656
632
|
{ _id: { $in: ids } },
|
|
657
633
|
{
|
|
658
634
|
$set: {
|
|
@@ -661,31 +637,28 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
661
637
|
$unset: {
|
|
662
638
|
snapshot_status: 1
|
|
663
639
|
}
|
|
664
|
-
}
|
|
665
|
-
{ session }
|
|
640
|
+
}
|
|
666
641
|
);
|
|
667
642
|
|
|
668
643
|
if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
|
|
669
|
-
|
|
644
|
+
writes.updateOne(
|
|
645
|
+
this.db.sync_rules,
|
|
670
646
|
{
|
|
671
647
|
_id: this.replicationStreamId,
|
|
672
648
|
'sync_configs._id': { $in: syncConfigIds }
|
|
673
649
|
},
|
|
674
650
|
{
|
|
675
|
-
$set: {
|
|
676
|
-
last_keepalive_ts: new Date()
|
|
677
|
-
},
|
|
678
651
|
$max: {
|
|
679
652
|
'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
|
|
680
653
|
}
|
|
681
654
|
},
|
|
682
655
|
{
|
|
683
|
-
session: this.session,
|
|
684
656
|
// Only set for sync configs that use this table
|
|
685
657
|
arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
|
|
686
658
|
}
|
|
687
659
|
);
|
|
688
660
|
}
|
|
661
|
+
await writes.execute();
|
|
689
662
|
});
|
|
690
663
|
return tables.map((table) => {
|
|
691
664
|
const copy = table.clone();
|
|
@@ -706,9 +679,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
706
679
|
this.validateCustomCheckpointEventId(checkpoint.event_id)
|
|
707
680
|
);
|
|
708
681
|
|
|
682
|
+
const writes = this.db.createWriteBatch(session, { ordered: false });
|
|
709
683
|
for (const [eventId, checkpoints] of checkpointsByEvent) {
|
|
710
|
-
|
|
684
|
+
this.batchCreateEventCustomWriteCheckpoints(writes, opId, eventId, checkpoints);
|
|
711
685
|
}
|
|
686
|
+
await writes.execute();
|
|
712
687
|
}
|
|
713
688
|
|
|
714
689
|
protected override async prepareCustomWriteCheckpoints(): Promise<void> {
|
|
@@ -741,48 +716,48 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
741
716
|
return eventId;
|
|
742
717
|
}
|
|
743
718
|
|
|
744
|
-
private
|
|
745
|
-
|
|
719
|
+
private batchCreateEventCustomWriteCheckpoints(
|
|
720
|
+
writes: MongoWriteBatch,
|
|
746
721
|
opId: InternalOpId,
|
|
747
722
|
eventId: EventDefinitionId,
|
|
748
723
|
checkpoints: storage.CustomWriteCheckpointOptions[]
|
|
749
|
-
):
|
|
750
|
-
|
|
751
|
-
|
|
724
|
+
): void {
|
|
725
|
+
// A repeated user within an event replaces the complete checkpoint state.
|
|
726
|
+
const uniqueCheckpoints = new Map(checkpoints.map((checkpoint) => [checkpoint.user_id, checkpoint]));
|
|
727
|
+
writes.bulkWriteUnordered(
|
|
728
|
+
this.db.customCheckpointRequests({
|
|
752
729
|
eventId,
|
|
753
730
|
replicationStreamId: this.replicationStreamId
|
|
754
|
-
})
|
|
755
|
-
.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
731
|
+
}),
|
|
732
|
+
[...uniqueCheckpoints.values()].map((checkpointOptions) => {
|
|
733
|
+
const set: Partial<CustomCheckpointRequestDocumentV3> = {
|
|
734
|
+
user_id: checkpointOptions.user_id,
|
|
735
|
+
checkpoint: checkpointOptions.checkpoint,
|
|
736
|
+
op_id: opId
|
|
737
|
+
};
|
|
738
|
+
if (checkpointOptions.checkpoint_requested_at != null) {
|
|
739
|
+
set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
|
|
740
|
+
}
|
|
765
741
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
742
|
+
return {
|
|
743
|
+
updateOne: {
|
|
744
|
+
filter: {
|
|
745
|
+
user_id: checkpointOptions.user_id
|
|
746
|
+
},
|
|
747
|
+
update: {
|
|
748
|
+
$set: set,
|
|
749
|
+
...(checkpointOptions.checkpoint_requested_at == null
|
|
750
|
+
? {
|
|
751
|
+
$unset: {
|
|
752
|
+
checkpoint_requested_at: 1
|
|
778
753
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
754
|
+
}
|
|
755
|
+
: {})
|
|
756
|
+
},
|
|
757
|
+
upsert: true
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
})
|
|
761
|
+
);
|
|
787
762
|
}
|
|
788
763
|
}
|