@powersync/service-module-mongodb-storage 0.23.1 → 0.24.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 +42 -0
- package/LICENSE +201 -67
- package/dist/storage/MongoBucketStorage.d.ts +1 -0
- package/dist/storage/MongoBucketStorage.js +3 -0
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +60 -39
- package/dist/storage/implementation/MongoBucketBatch.js +123 -296
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +6 -1
- package/dist/storage/implementation/MongoOpIdAllocator.js +12 -5
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +7 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +28 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoReplicationOptions.d.ts +27 -0
- package/dist/storage/implementation/MongoReplicationOptions.js +26 -0
- package/dist/storage/implementation/MongoReplicationOptions.js.map +1 -0
- package/dist/storage/implementation/MongoSyncBucketStorage.js +2 -3
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +15 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +36 -14
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/common/BatchOperationBuffer.d.ts +79 -0
- package/dist/storage/implementation/common/BatchOperationBuffer.js +286 -0
- package/dist/storage/implementation/common/BatchOperationBuffer.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +10 -103
- package/dist/storage/implementation/common/PersistedBatch.js +1 -291
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/PreparedPublication.d.ts +43 -0
- package/dist/storage/implementation/common/PreparedPublication.js +2 -0
- package/dist/storage/implementation/common/PreparedPublication.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +0 -2
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +17 -13
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +259 -18
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +21 -15
- package/dist/storage/implementation/v1/PersistedBatchV1.js +58 -24
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +0 -2
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +0 -3
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +25 -14
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +350 -23
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +10 -4
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +39 -28
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +46 -24
- package/dist/storage/implementation/v3/PersistedBatchV3.js +159 -83
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +12 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +41 -13
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +4 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +4 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +27 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +61 -5
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/package.json +9 -10
- package/src/storage/MongoBucketStorage.ts +4 -0
- package/src/storage/implementation/MongoBucketBatch.ts +185 -392
- package/src/storage/implementation/MongoOpIdAllocator.ts +12 -5
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +40 -1
- package/src/storage/implementation/MongoReplicationOptions.ts +28 -0
- package/src/storage/implementation/MongoSyncBucketStorage.ts +2 -3
- package/src/storage/implementation/MongoSyncRulesLock.ts +39 -17
- package/src/storage/implementation/common/BatchOperationBuffer.ts +353 -0
- package/src/storage/implementation/common/PersistedBatch.ts +12 -390
- package/src/storage/implementation/common/PreparedPublication.ts +49 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +0 -2
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +339 -27
- package/src/storage/implementation/v1/PersistedBatchV1.ts +74 -32
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +0 -5
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +430 -32
- package/src/storage/implementation/v3/MongoCompactorV3.ts +13 -4
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +43 -29
- package/src/storage/implementation/v3/PersistedBatchV3.ts +209 -108
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +45 -15
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +9 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +79 -5
- package/test/src/checkpoint_cleanup.test.ts +75 -0
- package/test/src/concurrent_writers.test.ts +127 -53
- package/test/src/helpers/PublicationCommandMonitor.ts +119 -0
- package/test/src/helpers/concurrentWriter.mjs +2 -2
- package/test/src/invalid_sync_config.test.ts +9 -0
- package/test/src/object_storage_lifecycle.test.ts +72 -2
- package/test/src/prepared_publication.test.ts +388 -0
- package/test/src/replication_preparation.test.ts +829 -0
- package/test/src/replication_publication_baseline.test.ts +203 -0
- package/test/src/replication_publication_batching.test.ts +212 -0
- package/test/src/replication_uploads.test.ts +393 -0
- package/test/src/source_table_config.test.ts +4 -4
- package/test/src/storage_object_storage_inline_threshold.test.ts +1 -1
- package/test/src/storage_s3_compaction_lifecycle.test.ts +93 -1
- package/test/src/storage_sync.test.ts +83 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
+
import { ErrorCode, ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { ColumnDescriptor, InternalOpId, SourceTable, storage } from '@powersync/service-core';
|
|
4
4
|
import { HydratedSyncConfig } from '@powersync/service-sync-rules';
|
|
5
5
|
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 { MongoIdSequence, OpIdRangeExhausted } from '../MongoIdSequence.js';
|
|
9
10
|
import { MongoSyncRulesLock } from '../MongoSyncRulesLock.js';
|
|
10
11
|
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
11
|
-
import {
|
|
12
|
+
import { OperationBatch } from '../OperationBatch.js';
|
|
12
13
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
13
14
|
import { SyncRuleDocumentBase } from '../models.js';
|
|
14
15
|
import { PersistedBatchV1 } from './PersistedBatchV1.js';
|
|
@@ -37,6 +38,306 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
37
38
|
this.store = new SourceRecordStoreV1(this.db, this.replicationStreamId);
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
private async withReplicationTransaction(
|
|
42
|
+
description: string,
|
|
43
|
+
callback: (session: mongo.ClientSession, opSeq: MongoIdSequence) => Promise<void>,
|
|
44
|
+
finish?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<boolean>,
|
|
45
|
+
projection?: mongo.Document
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
let flushTry = 0;
|
|
48
|
+
const lastTry = Date.now() + 90000;
|
|
49
|
+
const allocator = this.options.opIdAllocator;
|
|
50
|
+
let lastOp = 0n;
|
|
51
|
+
// Refill outside the publication transaction, before evaluating any rows.
|
|
52
|
+
// Exhaustion remains a fallback for large batches or a newer stream head.
|
|
53
|
+
this.options.replicationLock.throwIfAborted();
|
|
54
|
+
await allocator.ensureCapacity();
|
|
55
|
+
while (true) {
|
|
56
|
+
try {
|
|
57
|
+
await this.withFencedTransaction(
|
|
58
|
+
() => this.fence(this.session, projection),
|
|
59
|
+
async (stream) => {
|
|
60
|
+
flushTry += 1;
|
|
61
|
+
if (flushTry > 20 && Date.now() > lastTry) {
|
|
62
|
+
throw new ServiceError(ErrorCode.PSYNC_S1402, 'Max transaction tries exceeded');
|
|
63
|
+
}
|
|
64
|
+
// The fence already holds this stream document for the transaction.
|
|
65
|
+
// A different writer may have used higher IDs since our last flush.
|
|
66
|
+
const opSeq = allocator.sequence(this.persistedOpHead(stream));
|
|
67
|
+
await callback(this.session, opSeq);
|
|
68
|
+
lastOp = opSeq.last();
|
|
69
|
+
|
|
70
|
+
if (!(await finish?.(stream, lastOp))) {
|
|
71
|
+
const writes = this.db.createWriteBatch(this.session, { ordered: false });
|
|
72
|
+
this.onReplicationTransactionFlush(writes, lastOp);
|
|
73
|
+
await writes.execute();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
this.session,
|
|
77
|
+
description
|
|
78
|
+
);
|
|
79
|
+
allocator.consume(lastOp);
|
|
80
|
+
return;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!(error instanceof OpIdRangeExhausted)) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
// The transaction aborted every write. Reserve outside it and replay
|
|
86
|
+
// evaluation from the same committed stream head.
|
|
87
|
+
this.options.replicationLock.throwIfAborted();
|
|
88
|
+
await allocator.reserve();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async clearError(session: mongo.ClientSession): Promise<void> {
|
|
94
|
+
await this.db.sync_rules.updateOne(
|
|
95
|
+
{ _id: this.replicationStreamId },
|
|
96
|
+
{ $set: { last_fatal_error: null, last_fatal_error_ts: null } },
|
|
97
|
+
{ session }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected override async flushInner(
|
|
102
|
+
options?: storage.BatchBucketFlushOptions,
|
|
103
|
+
checkpoint?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId, writes?: MongoWriteBatch) => Promise<void>,
|
|
104
|
+
projection?: mongo.Document
|
|
105
|
+
): Promise<storage.FlushedResult | null> {
|
|
106
|
+
const batch = this.batch;
|
|
107
|
+
let last_op: InternalOpId | null = null;
|
|
108
|
+
let resumeBatch: OperationBatch | null = null;
|
|
109
|
+
let clearedError = false;
|
|
110
|
+
|
|
111
|
+
using _ = this.tracer.span('storage', 'flush');
|
|
112
|
+
|
|
113
|
+
await this.hooks?.beforeBatchFlush?.(this);
|
|
114
|
+
if (this.write_checkpoint_batch.length > 0) {
|
|
115
|
+
// Collection/index creation cannot run inside the replication transaction.
|
|
116
|
+
await this.prepareCustomWriteCheckpoints();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await this.withReplicationTransaction(
|
|
120
|
+
`Flushing ${batch?.length ?? 0} ops`,
|
|
121
|
+
async (session, opSeq) => {
|
|
122
|
+
clearedError = false;
|
|
123
|
+
if (batch != null) {
|
|
124
|
+
const result = await this.replicateBatch(session, batch, opSeq, options);
|
|
125
|
+
resumeBatch = result.resumeBatch;
|
|
126
|
+
clearedError ||= result.clearedError;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.write_checkpoint_batch.length > 0) {
|
|
130
|
+
this.logger.info(`Writing ${this.write_checkpoint_batch.length} custom write checkpoints`);
|
|
131
|
+
await this.batchCreateCustomWriteCheckpoints(session, opSeq.next());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
last_op = opSeq.last();
|
|
135
|
+
},
|
|
136
|
+
async (stream, lastOp) => {
|
|
137
|
+
if (checkpoint != null && resumeBatch == null) {
|
|
138
|
+
// The checkpoint must also persist the head, including when visibility is blocked.
|
|
139
|
+
await checkpoint(stream, lastOp);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
},
|
|
144
|
+
projection
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Keep checkpoints available if the transaction retries after writing them.
|
|
148
|
+
this.write_checkpoint_batch = [];
|
|
149
|
+
|
|
150
|
+
if (clearedError) {
|
|
151
|
+
this.clearedError = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// null if done, set if we need another flush
|
|
155
|
+
this.batch = resumeBatch;
|
|
156
|
+
|
|
157
|
+
if (last_op == null) {
|
|
158
|
+
throw new ReplicationAssertionError('Unexpected last_op == null');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.recordPersistedOp(last_op);
|
|
162
|
+
this.last_flushed_op = last_op;
|
|
163
|
+
await this.hooks?.afterBatchFlush?.(this);
|
|
164
|
+
return { flushed_op: last_op };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async replicateBatch(
|
|
168
|
+
session: mongo.ClientSession,
|
|
169
|
+
batch: OperationBatch,
|
|
170
|
+
op_seq: MongoIdSequence,
|
|
171
|
+
options?: storage.BucketBatchCommitOptions
|
|
172
|
+
): Promise<{ resumeBatch: OperationBatch | null; clearedError: boolean }> {
|
|
173
|
+
let sizes: Map<string, number> | undefined = undefined;
|
|
174
|
+
using _ = this.tracer.span('storage', 'replicate_batch');
|
|
175
|
+
// Only look up current_data sizes if the batch stores current_data and at least one
|
|
176
|
+
// table in it does too (per-table can disable it, e.g. Postgres REPLICA IDENTITY FULL).
|
|
177
|
+
const anyTableStoresCurrentData =
|
|
178
|
+
this.storeCurrentData && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
|
|
179
|
+
if (anyTableStoresCurrentData && !this.skipExistingRows) {
|
|
180
|
+
// We skip this step if no tables store current_data, since the sizes will
|
|
181
|
+
// always be small in that case.
|
|
182
|
+
|
|
183
|
+
// With skipExistingRows, we don't load the full documents into memory,
|
|
184
|
+
// so we can also skip the size lookup step.
|
|
185
|
+
|
|
186
|
+
// Find sizes of current_data documents, to assist in intelligent batching without
|
|
187
|
+
// exceeding memory limits.
|
|
188
|
+
//
|
|
189
|
+
// A previous attempt tried to do batching by the results of the current_data query
|
|
190
|
+
// (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
|
|
191
|
+
// the order of processing, which then becomes really tricky to manage.
|
|
192
|
+
// This now takes 2+ queries, but doesn't have any issues with order of operations.
|
|
193
|
+
// Within this branch this.storeCurrentData is true, so the per-table flag is the
|
|
194
|
+
// effective value - only look up sizes for tables that actually store current_data.
|
|
195
|
+
const sizeLookups = batch.batch
|
|
196
|
+
.filter((r) => r.record.sourceTable.storeCurrentData)
|
|
197
|
+
.map((r) => ({
|
|
198
|
+
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
199
|
+
replicaId: r.beforeId
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// If set, we need to start a new transaction with this batch.
|
|
206
|
+
let resumeBatch: OperationBatch | null = null;
|
|
207
|
+
let transactionSize = 0;
|
|
208
|
+
|
|
209
|
+
let didFlush = false;
|
|
210
|
+
|
|
211
|
+
// Now batch according to the sizes
|
|
212
|
+
// This is a single batch if storeCurrentData == false
|
|
213
|
+
for await (let b of batch.batched(sizes)) {
|
|
214
|
+
if (resumeBatch) {
|
|
215
|
+
for (let op of b) {
|
|
216
|
+
resumeBatch.push(op);
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
using lookupSpan = this.tracer.span('storage', 'lookup');
|
|
221
|
+
const lookups = b.map((r) => ({
|
|
222
|
+
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
223
|
+
replicaId: r.beforeId
|
|
224
|
+
}));
|
|
225
|
+
let sourceRecordLookup = await this.sourceRecordStore.loadDocuments(session, lookups, this.skipExistingRows);
|
|
226
|
+
lookupSpan.end();
|
|
227
|
+
|
|
228
|
+
let persistedBatch: PersistedBatchV1 | null = this.createPersistedBatch(transactionSize);
|
|
229
|
+
|
|
230
|
+
// The current code structure makes it tricky to cleanly split this span from the one
|
|
231
|
+
// where fluhsing. So we manually end and re-create this span whenever we flush.
|
|
232
|
+
let evalSpan = this.tracer.span('evaluate');
|
|
233
|
+
for (let op of b) {
|
|
234
|
+
if (resumeBatch) {
|
|
235
|
+
resumeBatch.push(op);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const sourceRecord = sourceRecordLookup.get(op.internalBeforeKey) ?? null;
|
|
239
|
+
if (sourceRecord != null) {
|
|
240
|
+
// If it will be used again later, it will be set again using nextData below
|
|
241
|
+
sourceRecordLookup.delete(op.internalBeforeKey);
|
|
242
|
+
}
|
|
243
|
+
const nextData = this.saveOperation(persistedBatch!, op, sourceRecord, op_seq, this.markRecordUnavailable);
|
|
244
|
+
if (nextData != null) {
|
|
245
|
+
// Update our current_data and size cache
|
|
246
|
+
sourceRecordLookup.set(op.internalAfterKey!, nextData);
|
|
247
|
+
sizes?.set(op.internalAfterKey!, nextData.data?.length() ?? 0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (persistedBatch!.shouldFlushTransaction()) {
|
|
251
|
+
evalSpan.end();
|
|
252
|
+
// Transaction is getting big.
|
|
253
|
+
// Flush, and resume in a new transaction.
|
|
254
|
+
using persistSpan = this.tracer.span('storage', 'persist_flush');
|
|
255
|
+
const { flushedAny } = await persistedBatch!.flush(this.session, options);
|
|
256
|
+
|
|
257
|
+
didFlush ||= flushedAny;
|
|
258
|
+
persistedBatch = null;
|
|
259
|
+
// Computing our current progress is a little tricky here, since
|
|
260
|
+
// we're stopping in the middle of a batch.
|
|
261
|
+
// We create a new batch, and push any remaining operations to it.
|
|
262
|
+
resumeBatch = new OperationBatch();
|
|
263
|
+
persistSpan.end();
|
|
264
|
+
evalSpan = this.tracer.span('evaluate');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
evalSpan.end();
|
|
268
|
+
|
|
269
|
+
if (persistedBatch) {
|
|
270
|
+
transactionSize = persistedBatch.currentSize;
|
|
271
|
+
using _ = this.tracer.span('storage', 'persist_flush');
|
|
272
|
+
const { flushedAny } = await persistedBatch.flush(this.session, options);
|
|
273
|
+
didFlush ||= flushedAny;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const clearedError = didFlush && !this.clearedError;
|
|
278
|
+
if (clearedError) {
|
|
279
|
+
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
280
|
+
using _ = this.tracer.span('storage', 'clear_error');
|
|
281
|
+
await this.clearError(this.session);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
resumeBatch: resumeBatch?.hasData() ? resumeBatch : null,
|
|
286
|
+
clearedError
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected override async truncateSingleInternal(sourceTable: storage.SourceTable): Promise<InternalOpId> {
|
|
291
|
+
let last_op: InternalOpId | null = null;
|
|
292
|
+
|
|
293
|
+
// To avoid too large transactions, we limit the amount of data we delete per transaction.
|
|
294
|
+
// Since we don't use the record data here, we don't have explicit size limits per batch.
|
|
295
|
+
const BATCH_LIMIT = 2000;
|
|
296
|
+
|
|
297
|
+
let lastBatchCount = BATCH_LIMIT;
|
|
298
|
+
while (lastBatchCount == BATCH_LIMIT) {
|
|
299
|
+
await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
|
|
300
|
+
using evalSpan = this.tracer.span('evaluate');
|
|
301
|
+
const sourceTableId = mongoTableId(sourceTable.id);
|
|
302
|
+
const batch = await this.sourceRecordStore.loadTruncateBatch(session, sourceTableId, BATCH_LIMIT);
|
|
303
|
+
const persistedBatch = this.createPersistedBatch(0);
|
|
304
|
+
|
|
305
|
+
for (let value of batch) {
|
|
306
|
+
persistedBatch.saveBucketData({
|
|
307
|
+
op_seq: opSeq,
|
|
308
|
+
before_buckets: value.buckets,
|
|
309
|
+
evaluated: [],
|
|
310
|
+
table: sourceTable,
|
|
311
|
+
sourceKey: value.replicaId
|
|
312
|
+
});
|
|
313
|
+
persistedBatch.saveParameterData({
|
|
314
|
+
op_seq: opSeq,
|
|
315
|
+
existing_lookups: value.lookups,
|
|
316
|
+
evaluated: [],
|
|
317
|
+
sourceTable: sourceTable,
|
|
318
|
+
sourceKey: value.replicaId
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Since this is not from streaming replication, we can do a hard delete
|
|
322
|
+
persistedBatch.hardDeleteCurrentData(sourceTableId, value.replicaId);
|
|
323
|
+
}
|
|
324
|
+
evalSpan.end();
|
|
325
|
+
|
|
326
|
+
using _ = this.tracer.span('storage', 'persist_flush');
|
|
327
|
+
await persistedBatch.flush(session);
|
|
328
|
+
lastBatchCount = batch.length;
|
|
329
|
+
|
|
330
|
+
last_op = opSeq.last();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (last_op) {
|
|
335
|
+
this.recordPersistedOp(last_op);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return last_op!;
|
|
339
|
+
}
|
|
340
|
+
|
|
40
341
|
protected override persistedOpHead(stream: SyncRuleDocumentBase): InternalOpId {
|
|
41
342
|
const legacy = stream as SyncRuleDocumentV1;
|
|
42
343
|
const checkpoint = legacy.last_checkpoint ?? 0n;
|
|
@@ -58,12 +359,12 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
58
359
|
]);
|
|
59
360
|
}
|
|
60
361
|
|
|
61
|
-
|
|
362
|
+
private recordPersistedOp(lastOp: InternalOpId): void {
|
|
62
363
|
this.persisted_op = lastOp;
|
|
63
364
|
}
|
|
64
365
|
|
|
65
|
-
protected createPersistedBatch(writtenSize: number):
|
|
66
|
-
return new PersistedBatchV1(this.db, this.replicationStreamId,
|
|
366
|
+
protected createPersistedBatch(writtenSize: number): PersistedBatchV1 {
|
|
367
|
+
return new PersistedBatchV1(this.db, this.replicationStreamId, writtenSize, {
|
|
67
368
|
logger: this.logger
|
|
68
369
|
});
|
|
69
370
|
}
|
|
@@ -75,10 +376,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
75
376
|
// No-op for V1: source records live in a shared collection.
|
|
76
377
|
}
|
|
77
378
|
|
|
78
|
-
|
|
79
|
-
session: mongo.ClientSession,
|
|
80
|
-
opId: InternalOpId
|
|
81
|
-
): Promise<void> {
|
|
379
|
+
private async batchCreateCustomWriteCheckpoints(session: mongo.ClientSession, opId: InternalOpId): Promise<void> {
|
|
82
380
|
// Only the final checkpoint per user/stream is visible after this transaction.
|
|
83
381
|
const checkpoints = new Map(
|
|
84
382
|
this.write_checkpoint_batch.map((checkpoint) => [
|
|
@@ -112,7 +410,9 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
112
410
|
);
|
|
113
411
|
}
|
|
114
412
|
|
|
115
|
-
async
|
|
413
|
+
protected override async resolveTablesInternal(
|
|
414
|
+
options: storage.ResolveTablesOptions
|
|
415
|
+
): Promise<storage.ResolveTablesResult> {
|
|
116
416
|
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
117
417
|
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
118
418
|
const { connection_id, source } = options;
|
|
@@ -145,13 +445,24 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
145
445
|
const resolution = await reconcile({ source, candidates });
|
|
146
446
|
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
147
447
|
|
|
448
|
+
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
449
|
+
const compatibleTables = materializedResolution.compatibleTables;
|
|
450
|
+
|
|
451
|
+
if (!syncRules.tableSyncsAny(source)) {
|
|
452
|
+
// Still reconcile old mappings so renames out of scope remove previously replicated data.
|
|
453
|
+
// Skip updating source_metadata below: these records are dropped by the caller in this
|
|
454
|
+
// same transaction, so persisting new metadata into them first is wasted work.
|
|
455
|
+
result = {
|
|
456
|
+
tables: [],
|
|
457
|
+
dropTables: [...materializedResolution.incompatibleTables, ...compatibleTables]
|
|
458
|
+
};
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
148
462
|
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
149
463
|
await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
|
|
150
464
|
}
|
|
151
465
|
|
|
152
|
-
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
153
|
-
const compatibleTables = materializedResolution.compatibleTables;
|
|
154
|
-
|
|
155
466
|
// V1 keeps one record, preferring one that has already been snapshotted.
|
|
156
467
|
const reuse = compatibleTables.find((table) => table.snapshotComplete) ?? compatibleTables[0] ?? null;
|
|
157
468
|
|
|
@@ -225,7 +536,9 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
225
536
|
return table;
|
|
226
537
|
}
|
|
227
538
|
|
|
228
|
-
async
|
|
539
|
+
protected override async getSourceTableStatusInternal(
|
|
540
|
+
table: storage.SourceTable
|
|
541
|
+
): Promise<storage.SourceTable | null> {
|
|
229
542
|
const doc = await this.db.sourceTablesV1(this.replicationStreamId).findOne(
|
|
230
543
|
{
|
|
231
544
|
group_id: this.replicationStreamId,
|
|
@@ -241,7 +554,10 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
241
554
|
return this.hydrateSourceTable(doc, table.ref.connectionTag, this.sync_rules, undefined);
|
|
242
555
|
}
|
|
243
556
|
|
|
244
|
-
async
|
|
557
|
+
protected override async commitInternal(
|
|
558
|
+
lsn: string,
|
|
559
|
+
options?: storage.BucketBatchCommitOptions
|
|
560
|
+
): Promise<storage.CheckpointResult> {
|
|
245
561
|
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
246
562
|
|
|
247
563
|
using _ = this.tracer.span('storage', 'commit');
|
|
@@ -394,9 +710,6 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
394
710
|
await this.autoActivate(lsn);
|
|
395
711
|
await this.db.notifyCheckpoint();
|
|
396
712
|
this.persisted_op = null;
|
|
397
|
-
if (checkpointState.newLastCheckpoint != null) {
|
|
398
|
-
await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
|
|
399
|
-
}
|
|
400
713
|
}
|
|
401
714
|
|
|
402
715
|
return {
|
|
@@ -405,18 +718,14 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
405
718
|
};
|
|
406
719
|
}
|
|
407
720
|
|
|
408
|
-
async
|
|
409
|
-
return await this.commit(lsn, { createEmptyCheckpoints: true });
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
async setResumeLsn(lsn: string): Promise<void> {
|
|
721
|
+
protected override async setResumeLsnInternal(lsn: string): Promise<void> {
|
|
413
722
|
using _ = this.tracer.span('storage', 'set_resume_lsn');
|
|
414
723
|
// Losing occasional resume LSN would only reprocess source changes.
|
|
415
724
|
// Keep the lease check atomic, but do not wait for majority replication of this resume hint.
|
|
416
725
|
await this.updateStreamMetadata({ snapshot_lsn: { $literal: lsn } }, {}, { w: 1 });
|
|
417
726
|
}
|
|
418
727
|
|
|
419
|
-
async
|
|
728
|
+
protected override async markAllSnapshotDoneInternal(no_checkpoint_before_lsn: string): Promise<void> {
|
|
420
729
|
await this.updateStreamMetadata(this.snapshotDoneUpdate(no_checkpoint_before_lsn));
|
|
421
730
|
}
|
|
422
731
|
|
|
@@ -427,7 +736,10 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
427
736
|
};
|
|
428
737
|
}
|
|
429
738
|
|
|
430
|
-
async
|
|
739
|
+
protected override async markSnapshotDoneInternal(
|
|
740
|
+
no_checkpoint_before_lsn: string,
|
|
741
|
+
options?: { throwOnConflict?: boolean }
|
|
742
|
+
): Promise<void> {
|
|
431
743
|
await this.withTransaction(async () => {
|
|
432
744
|
// Protect against race conditions
|
|
433
745
|
const blockingTables = await this.db
|
|
@@ -458,11 +770,11 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
458
770
|
});
|
|
459
771
|
}
|
|
460
772
|
|
|
461
|
-
async
|
|
773
|
+
protected override async markTableSnapshotRequiredInternal(_table: storage.SourceTable): Promise<void> {
|
|
462
774
|
await this.updateStreamMetadata({ snapshot_done: false });
|
|
463
775
|
}
|
|
464
776
|
|
|
465
|
-
async
|
|
777
|
+
protected override async markTableSnapshotDoneInternal(
|
|
466
778
|
tables: storage.SourceTable[],
|
|
467
779
|
no_checkpoint_before_lsn?: string
|
|
468
780
|
): Promise<storage.SourceTable[]> {
|
|
@@ -2,14 +2,16 @@ import { mongo } from '@powersync/lib-service-mongodb';
|
|
|
2
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { storage } from '@powersync/service-core';
|
|
4
4
|
import * as bson from 'bson';
|
|
5
|
+
import { BatchOperationBuffer } from '../common/BatchOperationBuffer.js';
|
|
5
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
9
|
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
10
10
|
import {
|
|
11
11
|
BucketStateUpdate,
|
|
12
12
|
PersistedBatch,
|
|
13
|
+
PersistedBatchOptions,
|
|
14
|
+
SaveBucketDataOptions,
|
|
13
15
|
SaveParameterDataOptions,
|
|
14
16
|
UpsertCurrentDataOptions
|
|
15
17
|
} from '../common/PersistedBatch.js';
|
|
@@ -23,20 +25,68 @@ import {
|
|
|
23
25
|
taggedBucketParameterDocumentToV1
|
|
24
26
|
} from './models.js';
|
|
25
27
|
|
|
26
|
-
export class PersistedBatchV1
|
|
27
|
-
|
|
28
|
+
export class PersistedBatchV1 implements PersistedBatch {
|
|
29
|
+
private readonly operations: BatchOperationBuffer;
|
|
28
30
|
|
|
29
31
|
// Each upsert supplies the complete source-record state. Only its final
|
|
30
32
|
// state matters within a flush; bucket and parameter history is kept separately.
|
|
31
|
-
currentData = new Map<string, mongo.AnyBulkWriteOperation<CurrentDataDocument>>();
|
|
33
|
+
private readonly currentData = new Map<string, mongo.AnyBulkWriteOperation<CurrentDataDocument>>();
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly db: VersionedPowerSyncMongoV1,
|
|
37
|
+
private readonly group_id: number,
|
|
38
|
+
writtenSize: number,
|
|
39
|
+
options?: PersistedBatchOptions
|
|
40
|
+
) {
|
|
41
|
+
this.operations = new BatchOperationBuffer(
|
|
42
|
+
group_id,
|
|
43
|
+
writtenSize,
|
|
44
|
+
{
|
|
45
|
+
checkDefinitionId: () => LEGACY_BUCKET_DATA_DEFINITION_ID,
|
|
46
|
+
getBucketDefinitionId: () => LEGACY_BUCKET_DATA_DEFINITION_ID
|
|
47
|
+
},
|
|
48
|
+
options?.logger
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get currentSize(): number {
|
|
53
|
+
return this.operations.currentSize;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
saveBucketData(options: SaveBucketDataOptions): void {
|
|
57
|
+
this.operations.saveBucketData(options);
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
return
|
|
60
|
+
shouldFlushTransaction(): boolean {
|
|
61
|
+
return this.operations.shouldFlushTransaction(this.currentData.size);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Keeps track of bulkwrite operations within a transaction.
|
|
66
|
+
*
|
|
67
|
+
* There may be multiple of these batches per transaction, but it may not span
|
|
68
|
+
* multiple transactions.
|
|
69
|
+
*/
|
|
70
|
+
async flush(session: mongo.ClientSession, options?: storage.BucketBatchCommitOptions) {
|
|
71
|
+
const startAt = performance.now();
|
|
72
|
+
const writes = this.db.createWriteBatch(session, { ordered: false });
|
|
73
|
+
if (this.operations.bucketDataCount > 0) {
|
|
74
|
+
await this.queueBucketData(writes);
|
|
75
|
+
}
|
|
76
|
+
if (this.operations.bucketParameters.length > 0) {
|
|
77
|
+
this.queueBucketParameters(writes);
|
|
78
|
+
}
|
|
79
|
+
if (this.currentData.size > 0) {
|
|
80
|
+
this.queueCurrentData(writes);
|
|
81
|
+
}
|
|
82
|
+
if (this.operations.bucketStates.size > 0) {
|
|
83
|
+
this.queueBucketStates(writes);
|
|
84
|
+
}
|
|
85
|
+
await writes.execute();
|
|
86
|
+
const stats = this.operations.logFlush(startAt, this.currentData.size, options);
|
|
87
|
+
this.currentData.clear();
|
|
88
|
+
this.operations.clear();
|
|
89
|
+
return stats;
|
|
40
90
|
}
|
|
41
91
|
|
|
42
92
|
saveParameterData(data: SaveParameterDataOptions) {
|
|
@@ -55,7 +105,7 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
55
105
|
remaining_lookups.delete(binLookup.toString('base64'));
|
|
56
106
|
|
|
57
107
|
const op_id = data.op_seq.next();
|
|
58
|
-
this.debugLastOpId = op_id;
|
|
108
|
+
this.operations.debugLastOpId = op_id;
|
|
59
109
|
const values: BucketParameterDocument = {
|
|
60
110
|
_id: op_id,
|
|
61
111
|
key: {
|
|
@@ -66,17 +116,17 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
66
116
|
lookup: binLookup,
|
|
67
117
|
bucket_parameters: result.bucketParameters
|
|
68
118
|
};
|
|
69
|
-
this.bucketParameters.push({
|
|
119
|
+
this.operations.bucketParameters.push({
|
|
70
120
|
...values,
|
|
71
121
|
index: LEGACY_BUCKET_PARAMETER_INDEX_ID
|
|
72
122
|
});
|
|
73
123
|
|
|
74
|
-
this.currentSize += 200;
|
|
124
|
+
this.operations.currentSize += 200;
|
|
75
125
|
}
|
|
76
126
|
|
|
77
127
|
for (let lookup of remaining_lookups.values()) {
|
|
78
128
|
const op_id = data.op_seq.next();
|
|
79
|
-
this.debugLastOpId = op_id;
|
|
129
|
+
this.operations.debugLastOpId = op_id;
|
|
80
130
|
const values: BucketParameterDocument = {
|
|
81
131
|
_id: op_id,
|
|
82
132
|
key: {
|
|
@@ -87,12 +137,12 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
87
137
|
lookup,
|
|
88
138
|
bucket_parameters: []
|
|
89
139
|
};
|
|
90
|
-
this.bucketParameters.push({
|
|
140
|
+
this.operations.bucketParameters.push({
|
|
91
141
|
...values,
|
|
92
142
|
index: LEGACY_BUCKET_PARAMETER_INDEX_ID
|
|
93
143
|
});
|
|
94
144
|
|
|
95
|
-
this.currentSize += 200;
|
|
145
|
+
this.operations.currentSize += 200;
|
|
96
146
|
}
|
|
97
147
|
}
|
|
98
148
|
|
|
@@ -102,7 +152,7 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
102
152
|
filter: { _id: this.currentDataId(sourceTableId, replicaId) }
|
|
103
153
|
}
|
|
104
154
|
});
|
|
105
|
-
this.currentSize += 50;
|
|
155
|
+
this.operations.currentSize += 50;
|
|
106
156
|
}
|
|
107
157
|
|
|
108
158
|
softDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId, _checkpointGreaterThan: bigint) {
|
|
@@ -140,17 +190,13 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
140
190
|
upsert: true
|
|
141
191
|
}
|
|
142
192
|
});
|
|
143
|
-
this.currentSize += (values.data?.length() ?? 0) + 100;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
protected get currentDataCount() {
|
|
147
|
-
return this.currentData.size;
|
|
193
|
+
this.operations.currentSize += (values.data?.length() ?? 0) + 100;
|
|
148
194
|
}
|
|
149
195
|
|
|
150
|
-
|
|
196
|
+
private async queueBucketData(writes: MongoWriteBatch) {
|
|
151
197
|
writes.bulkWriteUnordered(
|
|
152
198
|
this.db.bucketDataV1,
|
|
153
|
-
this.bucketData.map((document) => ({
|
|
199
|
+
this.operations.bucketData.map((document) => ({
|
|
154
200
|
insertOne: {
|
|
155
201
|
document: serializeBucketDataV1(document)
|
|
156
202
|
}
|
|
@@ -158,10 +204,10 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
158
204
|
);
|
|
159
205
|
}
|
|
160
206
|
|
|
161
|
-
|
|
207
|
+
private queueBucketParameters(writes: MongoWriteBatch): void {
|
|
162
208
|
writes.bulkWriteUnordered(
|
|
163
209
|
this.db.parameterIndexV1,
|
|
164
|
-
this.bucketParameters.map((document) => ({
|
|
210
|
+
this.operations.bucketParameters.map((document) => ({
|
|
165
211
|
insertOne: {
|
|
166
212
|
document: taggedBucketParameterDocumentToV1(document)
|
|
167
213
|
}
|
|
@@ -169,7 +215,7 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
169
215
|
);
|
|
170
216
|
}
|
|
171
217
|
|
|
172
|
-
|
|
218
|
+
private queueCurrentData(writes: MongoWriteBatch): void {
|
|
173
219
|
if (this.currentData.size == 0) {
|
|
174
220
|
return;
|
|
175
221
|
}
|
|
@@ -177,16 +223,12 @@ export class PersistedBatchV1 extends PersistedBatch {
|
|
|
177
223
|
writes.bulkWriteUnordered(this.db.sourceRecordsV1, [...this.currentData.values()]);
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
|
|
226
|
+
private queueBucketStates(writes: MongoWriteBatch): void {
|
|
181
227
|
writes.bulkWriteUnordered(this.db.bucketStateV1, this.getBucketStateUpdates());
|
|
182
228
|
}
|
|
183
229
|
|
|
184
|
-
protected resetCurrentData() {
|
|
185
|
-
this.currentData.clear();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
230
|
private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[] {
|
|
189
|
-
return Array.from(this.bucketStates.values()).map((state: BucketStateUpdate) => {
|
|
231
|
+
return Array.from(this.operations.bucketStates.values()).map((state: BucketStateUpdate) => {
|
|
190
232
|
return {
|
|
191
233
|
updateOne: {
|
|
192
234
|
filter: {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { Logger } from '@powersync/lib-services-framework';
|
|
3
2
|
import { storage } from '@powersync/service-core';
|
|
4
3
|
import { EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
|
|
5
4
|
import * as bson from 'bson';
|
|
@@ -149,8 +148,4 @@ export class SourceRecordStoreV1 implements SourceRecordStore {
|
|
|
149
148
|
this.createLoadedDocument(sourceTableId, doc._id, null, doc.buckets, doc.lookups)
|
|
150
149
|
);
|
|
151
150
|
}
|
|
152
|
-
|
|
153
|
-
async postCommitCleanup(_lastCheckpoint: bigint, _logger: Logger): Promise<void> {
|
|
154
|
-
// No-op for V1.
|
|
155
|
-
}
|
|
156
151
|
}
|