@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
|
@@ -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
|
}
|
|
@@ -55,8 +79,15 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
55
79
|
session: mongo.ClientSession,
|
|
56
80
|
opId: InternalOpId
|
|
57
81
|
): Promise<void> {
|
|
82
|
+
// Only the final checkpoint per user/stream is visible after this transaction.
|
|
83
|
+
const checkpoints = new Map(
|
|
84
|
+
this.write_checkpoint_batch.map((checkpoint) => [
|
|
85
|
+
JSON.stringify([checkpoint.sync_rules_id, checkpoint.user_id]),
|
|
86
|
+
checkpoint
|
|
87
|
+
])
|
|
88
|
+
);
|
|
58
89
|
await this.db.custom_write_checkpoints.bulkWrite(
|
|
59
|
-
|
|
90
|
+
[...checkpoints.values()].map((checkpointOptions) => {
|
|
60
91
|
const set: Record<string, unknown> = {
|
|
61
92
|
checkpoint: checkpointOptions.checkpoint,
|
|
62
93
|
sync_rules_id: checkpointOptions.sync_rules_id,
|
|
@@ -77,7 +108,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
77
108
|
}
|
|
78
109
|
};
|
|
79
110
|
}),
|
|
80
|
-
{ session }
|
|
111
|
+
{ session, ordered: false }
|
|
81
112
|
);
|
|
82
113
|
}
|
|
83
114
|
|
|
@@ -94,7 +125,8 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
94
125
|
}));
|
|
95
126
|
|
|
96
127
|
let result: storage.ResolveTablesResult | null = null;
|
|
97
|
-
|
|
128
|
+
const session = this.session;
|
|
129
|
+
await this.withTransaction(async () => {
|
|
98
130
|
const col = this.db.sourceTablesV1(this.replicationStreamId);
|
|
99
131
|
|
|
100
132
|
// Find records that overlap by name or relation id.
|
|
@@ -212,129 +244,136 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
212
244
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
213
245
|
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
214
246
|
|
|
215
|
-
await this.flush(options);
|
|
216
|
-
|
|
217
247
|
using _ = this.tracer.span('storage', 'commit');
|
|
218
248
|
|
|
219
|
-
const
|
|
249
|
+
const updateCheckpoint = async (persistedOp = this.persisted_op) => {
|
|
250
|
+
const now = new Date();
|
|
220
251
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
session: this.session
|
|
233
|
-
}
|
|
234
|
-
);
|
|
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
|
+
};
|
|
235
263
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
};
|
|
247
275
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
};
|
|
285
|
+
|
|
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
|
+
],
|
|
252
326
|
{
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
255
335
|
}
|
|
256
336
|
}
|
|
257
|
-
|
|
258
|
-
};
|
|
337
|
+
)) as SyncRuleDocumentV1;
|
|
259
338
|
|
|
260
|
-
|
|
261
|
-
$cond: [
|
|
262
|
-
can_checkpoint,
|
|
263
|
-
{
|
|
264
|
-
$max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
|
|
265
|
-
},
|
|
266
|
-
'$last_checkpoint'
|
|
267
|
-
]
|
|
339
|
+
return MongoSyncRulesLock.assertOwned(preUpdateDocument);
|
|
268
340
|
};
|
|
269
341
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
[
|
|
342
|
+
const finishCheckpoint = async (preUpdateDocument: SyncRuleDocumentV1, lastOp?: InternalOpId) => {
|
|
343
|
+
await this.db.write_checkpoints.updateMany(
|
|
273
344
|
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
_not_empty: createEmptyCheckpoints
|
|
277
|
-
? true
|
|
278
|
-
: {
|
|
279
|
-
$or: [
|
|
280
|
-
{ $literal: createEmptyCheckpoints },
|
|
281
|
-
{ $ne: ['$keepalive_op', new_keepalive_op] },
|
|
282
|
-
{ $ne: ['$last_checkpoint', new_last_checkpoint] }
|
|
283
|
-
]
|
|
284
|
-
}
|
|
285
|
-
}
|
|
345
|
+
processed_at_lsn: null,
|
|
346
|
+
'lsns.1': { $lte: lsn }
|
|
286
347
|
},
|
|
287
348
|
{
|
|
288
349
|
$set: {
|
|
289
|
-
|
|
290
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
|
|
291
|
-
},
|
|
292
|
-
last_checkpoint_ts: {
|
|
293
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
|
|
294
|
-
},
|
|
295
|
-
last_keepalive_ts: { $literal: now },
|
|
296
|
-
last_fatal_error: { $literal: null },
|
|
297
|
-
last_fatal_error_ts: { $literal: null },
|
|
298
|
-
keepalive_op: new_keepalive_op,
|
|
299
|
-
last_checkpoint: new_last_checkpoint,
|
|
300
|
-
snapshot_lsn: {
|
|
301
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
|
|
302
|
-
}
|
|
350
|
+
processed_at_lsn: lsn
|
|
303
351
|
}
|
|
304
352
|
},
|
|
305
353
|
{
|
|
306
|
-
|
|
354
|
+
session: this.session
|
|
307
355
|
}
|
|
308
|
-
],
|
|
309
|
-
{
|
|
310
|
-
session: this.session,
|
|
311
|
-
returnDocument: 'before',
|
|
312
|
-
projection: {
|
|
313
|
-
snapshot_done: 1,
|
|
314
|
-
last_checkpoint_lsn: 1,
|
|
315
|
-
no_checkpoint_before: 1,
|
|
316
|
-
keepalive_op: 1,
|
|
317
|
-
last_checkpoint: 1
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
)) as SyncRuleDocumentV1;
|
|
321
|
-
|
|
322
|
-
if (preUpdateDocument == null) {
|
|
323
|
-
throw new ReplicationAssertionError(
|
|
324
|
-
'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.replicationStreamId
|
|
325
356
|
);
|
|
326
|
-
}
|
|
327
357
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
);
|
|
338
377
|
if (checkpointState.checkpointBlocked) {
|
|
339
378
|
if (Date.now() - this.lastWaitingLogThrottled > 5_000) {
|
|
340
379
|
this.logger.info(
|
|
@@ -372,41 +411,20 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
372
411
|
|
|
373
412
|
async setResumeLsn(lsn: string): Promise<void> {
|
|
374
413
|
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
$set: {
|
|
381
|
-
snapshot_lsn: lsn
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
session: this.session,
|
|
386
|
-
// Losing occasional resume LSN is fine. That may mean reprocessing
|
|
387
|
-
// some source changes in some edge cases, which is not an issue since
|
|
388
|
-
// changes are processed in an idempotent way.
|
|
389
|
-
writeConcern: { w: 1 }
|
|
390
|
-
}
|
|
391
|
-
);
|
|
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 });
|
|
392
417
|
}
|
|
393
418
|
|
|
394
419
|
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
395
|
-
await this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
},
|
|
404
|
-
$max: {
|
|
405
|
-
no_checkpoint_before: no_checkpoint_before_lsn
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
{ session: this.session }
|
|
409
|
-
);
|
|
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
|
+
};
|
|
410
428
|
}
|
|
411
429
|
|
|
412
430
|
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
@@ -436,22 +454,12 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
436
454
|
}
|
|
437
455
|
}
|
|
438
456
|
|
|
439
|
-
await this.
|
|
457
|
+
await this.updateStreamMetadataInTransaction(this.snapshotDoneUpdate(no_checkpoint_before_lsn));
|
|
440
458
|
});
|
|
441
459
|
}
|
|
442
460
|
|
|
443
461
|
async markTableSnapshotRequired(_table: storage.SourceTable): Promise<void> {
|
|
444
|
-
await this.
|
|
445
|
-
{
|
|
446
|
-
_id: this.replicationStreamId
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
$set: {
|
|
450
|
-
snapshot_done: false
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
{ session: this.session }
|
|
454
|
-
);
|
|
462
|
+
await this.updateStreamMetadata({ snapshot_done: false });
|
|
455
463
|
}
|
|
456
464
|
|
|
457
465
|
async markTableSnapshotDone(
|
|
@@ -462,7 +470,9 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
462
470
|
const ids = tables.map((table) => mongoTableId(table.id));
|
|
463
471
|
|
|
464
472
|
await this.withTransaction(async () => {
|
|
465
|
-
|
|
473
|
+
const writes = this.db.createWriteBatch(session, { ordered: false });
|
|
474
|
+
writes.updateMany(
|
|
475
|
+
this.db.sourceTablesV1(this.replicationStreamId),
|
|
466
476
|
{ _id: { $in: ids } },
|
|
467
477
|
{
|
|
468
478
|
$set: {
|
|
@@ -471,26 +481,23 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
471
481
|
$unset: {
|
|
472
482
|
snapshot_status: 1
|
|
473
483
|
}
|
|
474
|
-
}
|
|
475
|
-
{ session }
|
|
484
|
+
}
|
|
476
485
|
);
|
|
477
486
|
|
|
478
487
|
if (no_checkpoint_before_lsn != null) {
|
|
479
|
-
|
|
488
|
+
writes.updateOne(
|
|
489
|
+
this.db.sync_rules,
|
|
480
490
|
{
|
|
481
491
|
_id: this.replicationStreamId
|
|
482
492
|
},
|
|
483
493
|
{
|
|
484
|
-
$set: {
|
|
485
|
-
last_keepalive_ts: new Date()
|
|
486
|
-
},
|
|
487
494
|
$max: {
|
|
488
495
|
no_checkpoint_before: no_checkpoint_before_lsn
|
|
489
496
|
}
|
|
490
|
-
}
|
|
491
|
-
{ session: this.session }
|
|
497
|
+
}
|
|
492
498
|
);
|
|
493
499
|
}
|
|
500
|
+
await writes.execute();
|
|
494
501
|
});
|
|
495
502
|
return tables.map((table) => {
|
|
496
503
|
const copy = table.clone();
|
|
@@ -507,7 +514,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
507
514
|
const session = this.session;
|
|
508
515
|
let activated = false;
|
|
509
516
|
let needsFutureActivationCheck = true;
|
|
510
|
-
await
|
|
517
|
+
await this.withTransaction(async () => {
|
|
511
518
|
// Reset on transaction retries.
|
|
512
519
|
activated = false;
|
|
513
520
|
needsFutureActivationCheck = true;
|
|
@@ -545,7 +552,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
545
552
|
} else if (doc?.state != storage.SyncRuleState.PROCESSING) {
|
|
546
553
|
needsFutureActivationCheck = false;
|
|
547
554
|
}
|
|
548
|
-
});
|
|
555
|
+
}, session);
|
|
549
556
|
if (activated) {
|
|
550
557
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
551
558
|
await this.db.notifyCheckpoint();
|
|
@@ -13,6 +13,7 @@ import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
|
13
13
|
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
14
14
|
import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
|
|
15
15
|
import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
|
|
16
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
16
17
|
import { cacheKey } from '../OperationBatch.js';
|
|
17
18
|
import { BucketDataDocumentV1, BucketStateDocumentV1 } from './models.js';
|
|
18
19
|
import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
|
|
@@ -296,11 +297,10 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
private async flushBucketStateUpdates() {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
300
|
+
const writes = this.db.createWriteBatch(undefined, { ordered: false });
|
|
301
|
+
this.writeBucketStateUpdates(writes);
|
|
302
|
+
await writes.execute();
|
|
303
|
+
this.bucketStateUpdates = [];
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
private async updateChecksumsBatch(buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]) {
|
|
@@ -340,10 +340,12 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
340
340
|
await this.flushBucketStateUpdates();
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
protected
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
protected writeBucketStateUpdates(writes: MongoWriteBatch): void {
|
|
344
|
+
if (this.bucketStateUpdates.length === 0) return;
|
|
345
|
+
this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
346
|
+
writes.bulkWriteUnordered(
|
|
347
|
+
this.db.bucketStateV1,
|
|
348
|
+
this.bucketStateUpdates as mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[]
|
|
347
349
|
);
|
|
348
350
|
}
|
|
349
351
|
|
|
@@ -541,17 +543,19 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
541
543
|
}
|
|
542
544
|
|
|
543
545
|
private async flush(bucketContext: SingleBucketStoreV1) {
|
|
546
|
+
// Persist data changes before advertising the resulting bucket state.
|
|
547
|
+
const writes = this.db.createWriteBatch(undefined, { ordered: true });
|
|
544
548
|
if (this.updates.length > 0) {
|
|
545
549
|
this.logger.info(`Compacting ${this.updates.length} ops`);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
ordered: false
|
|
550
|
-
});
|
|
551
|
-
this.updates = [];
|
|
550
|
+
// Order is not important. Since checksums are not affected, these operations can happen in any order,
|
|
551
|
+
// and it's fine if the operations are partially applied. Each individual operation is atomic.
|
|
552
|
+
writes.bulkWriteUnordered(bucketContext.collection, this.updates);
|
|
552
553
|
}
|
|
553
554
|
|
|
554
|
-
|
|
555
|
+
this.writeBucketStateUpdates(writes);
|
|
556
|
+
await writes.execute();
|
|
557
|
+
this.updates = [];
|
|
558
|
+
this.bucketStateUpdates = [];
|
|
555
559
|
}
|
|
556
560
|
|
|
557
561
|
/**
|
|
@@ -622,15 +626,13 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
622
626
|
}
|
|
623
627
|
|
|
624
628
|
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
{ session }
|
|
633
|
-
);
|
|
629
|
+
const writes = this.db.createWriteBatch(session, { ordered: true });
|
|
630
|
+
writes.deleteMany(bucketContext.collection, {
|
|
631
|
+
_id: {
|
|
632
|
+
$gte: bucketContext.minId,
|
|
633
|
+
$lte: bucketContext.docId(lastOp!.o)
|
|
634
|
+
}
|
|
635
|
+
});
|
|
634
636
|
|
|
635
637
|
const op = bucketContext.toPersistedDocument({
|
|
636
638
|
o: lastOp!.o,
|
|
@@ -639,7 +641,8 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
639
641
|
data: null,
|
|
640
642
|
target_op: targetOp
|
|
641
643
|
});
|
|
642
|
-
|
|
644
|
+
writes.insertOne(bucketContext.collection, op);
|
|
645
|
+
await writes.execute();
|
|
643
646
|
|
|
644
647
|
opCountDiff = -numberOfOpsToClear + 1;
|
|
645
648
|
},
|
|
@@ -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> {
|
|
@@ -6,6 +6,7 @@ import * as bson from 'bson';
|
|
|
6
6
|
import { BucketDataSource, BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
7
7
|
import { mongoTableId } from '../../../utils/util.js';
|
|
8
8
|
import { EMPTY_DATA } from '../MongoBucketBatchShared.js';
|
|
9
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
9
10
|
import {
|
|
10
11
|
BucketStateUpdate,
|
|
11
12
|
PersistedBatch,
|
|
@@ -25,7 +26,9 @@ import {
|
|
|
25
26
|
export class PersistedBatchV1 extends PersistedBatch {
|
|
26
27
|
declare protected readonly db: VersionedPowerSyncMongoV1;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// Each upsert supplies the complete source-record state. Only its final
|
|
30
|
+
// state matters within a flush; bucket and parameter history is kept separately.
|
|
31
|
+
currentData = new Map<string, mongo.AnyBulkWriteOperation<CurrentDataDocument>>();
|
|
29
32
|
|
|
30
33
|
protected checkDefinitionId(_definitionId: BucketDefinitionId | null): BucketDefinitionId {
|
|
31
34
|
// V1 storage doesn't persist the id, and we don't use it.
|
|
@@ -94,7 +97,7 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
|
|
97
|
-
this.currentData.
|
|
100
|
+
this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
|
|
98
101
|
deleteOne: {
|
|
99
102
|
filter: { _id: this.currentDataId(sourceTableId, replicaId) }
|
|
100
103
|
}
|
|
@@ -124,7 +127,7 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
124
127
|
return lookup.lookup;
|
|
125
128
|
});
|
|
126
129
|
|
|
127
|
-
this.currentData.
|
|
130
|
+
this.currentData.set(this.currentDataKey(values.sourceTableId, values.replicaId), {
|
|
128
131
|
updateOne: {
|
|
129
132
|
filter: { _id: this.currentDataId(values.sourceTableId, values.replicaId) },
|
|
130
133
|
update: {
|
|
@@ -141,57 +144,45 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
protected get currentDataCount() {
|
|
144
|
-
return this.currentData.
|
|
147
|
+
return this.currentData.size;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
protected async
|
|
148
|
-
|
|
150
|
+
protected async queueBucketData(writes: MongoWriteBatch) {
|
|
151
|
+
writes.bulkWriteUnordered(
|
|
152
|
+
this.db.bucketDataV1,
|
|
149
153
|
this.bucketData.map((document) => ({
|
|
150
154
|
insertOne: {
|
|
151
155
|
document: serializeBucketDataV1(document)
|
|
152
156
|
}
|
|
153
|
-
}))
|
|
154
|
-
{
|
|
155
|
-
session,
|
|
156
|
-
ordered: false
|
|
157
|
-
}
|
|
157
|
+
}))
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
protected
|
|
162
|
-
|
|
161
|
+
protected queueBucketParameters(writes: MongoWriteBatch): void {
|
|
162
|
+
writes.bulkWriteUnordered(
|
|
163
|
+
this.db.parameterIndexV1,
|
|
163
164
|
this.bucketParameters.map((document) => ({
|
|
164
165
|
insertOne: {
|
|
165
166
|
document: taggedBucketParameterDocumentToV1(document)
|
|
166
167
|
}
|
|
167
|
-
}))
|
|
168
|
-
{
|
|
169
|
-
session,
|
|
170
|
-
ordered: false
|
|
171
|
-
}
|
|
168
|
+
}))
|
|
172
169
|
);
|
|
173
170
|
}
|
|
174
171
|
|
|
175
|
-
protected
|
|
176
|
-
if (this.currentData.
|
|
172
|
+
protected queueCurrentData(writes: MongoWriteBatch): void {
|
|
173
|
+
if (this.currentData.size == 0) {
|
|
177
174
|
return;
|
|
178
175
|
}
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
session,
|
|
182
|
-
ordered: true
|
|
183
|
-
});
|
|
177
|
+
writes.bulkWriteUnordered(this.db.sourceRecordsV1, [...this.currentData.values()]);
|
|
184
178
|
}
|
|
185
179
|
|
|
186
|
-
protected
|
|
187
|
-
|
|
188
|
-
session,
|
|
189
|
-
ordered: false
|
|
190
|
-
});
|
|
180
|
+
protected queueBucketStates(writes: MongoWriteBatch): void {
|
|
181
|
+
writes.bulkWriteUnordered(this.db.bucketStateV1, this.getBucketStateUpdates());
|
|
191
182
|
}
|
|
192
183
|
|
|
193
184
|
protected resetCurrentData() {
|
|
194
|
-
this.currentData
|
|
185
|
+
this.currentData.clear();
|
|
195
186
|
}
|
|
196
187
|
|
|
197
188
|
private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[] {
|
|
@@ -219,6 +210,10 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
219
210
|
});
|
|
220
211
|
}
|
|
221
212
|
|
|
213
|
+
private currentDataKey(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): string {
|
|
214
|
+
return Buffer.from(bson.serialize({ t: sourceTableId, k: replicaId })).toString('base64');
|
|
215
|
+
}
|
|
216
|
+
|
|
222
217
|
private currentDataId(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): SourceKey {
|
|
223
218
|
return {
|
|
224
219
|
g: this.group_id,
|