@powersync/service-module-mongodb-storage 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +201 -67
- package/dist/storage/MongoBucketStorage.d.ts +1 -0
- package/dist/storage/MongoBucketStorage.js +45 -83
- 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 +55 -122
- 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 +101 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -39,6 +39,7 @@ import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
|
|
|
39
39
|
import { MongoIdSequence, OpIdRangeExhausted } from './MongoIdSequence.js';
|
|
40
40
|
import { MongoOpIdAllocator } from './MongoOpIdAllocator.js';
|
|
41
41
|
import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
|
|
42
|
+
import { REPLICATION_WRITE_OPTIONS } from './MongoReplicationOptions.js';
|
|
42
43
|
import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
|
|
43
44
|
import { MongoWriteBatch } from './MongoWriteBatch.js';
|
|
44
45
|
import { OperationBatch, RecordOperation } from './OperationBatch.js';
|
|
@@ -48,7 +49,6 @@ import { createObjectStorageUsageWriterId } from './v3/object-storage/ObjectStor
|
|
|
48
49
|
export interface MongoBucketBatchOptions {
|
|
49
50
|
replicationLock: MongoSyncRulesLock;
|
|
50
51
|
opIdAllocator: MongoOpIdAllocator;
|
|
51
|
-
db: VersionedPowerSyncMongo;
|
|
52
52
|
/**
|
|
53
53
|
* The parsed sync config set for this batch.
|
|
54
54
|
*
|
|
@@ -94,8 +94,8 @@ export abstract class MongoBucketBatch
|
|
|
94
94
|
protected readonly options: MongoBucketBatchOptions;
|
|
95
95
|
protected logger: Logger;
|
|
96
96
|
|
|
97
|
-
private readonly client: mongo.MongoClient;
|
|
98
97
|
readonly db: VersionedPowerSyncMongo;
|
|
98
|
+
/** Shared by this lease's writers; only use inside a serialized public operation. */
|
|
99
99
|
public readonly session: mongo.ClientSession;
|
|
100
100
|
protected readonly sync_rules: HydratedSyncConfig;
|
|
101
101
|
|
|
@@ -111,16 +111,16 @@ export abstract class MongoBucketBatch
|
|
|
111
111
|
* via SourceTable.storeCurrentData. The effective per-record value is the conjunction of
|
|
112
112
|
* the two.
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
protected readonly storeCurrentData: boolean;
|
|
115
115
|
public readonly skipExistingRows: boolean;
|
|
116
116
|
protected readonly mapping: BucketDefinitionMapping;
|
|
117
117
|
protected readonly objectStorageUsageWriterId = createObjectStorageUsageWriterId();
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
protected batch: OperationBatch | null = null;
|
|
120
120
|
protected write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
protected readonly markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
122
|
+
protected hooks: storage.StorageHooks | undefined;
|
|
123
|
+
protected clearedError = false;
|
|
124
124
|
|
|
125
125
|
protected tracer: PerformanceTracer<'storage' | 'evaluate'>;
|
|
126
126
|
|
|
@@ -151,11 +151,12 @@ export abstract class MongoBucketBatch
|
|
|
151
151
|
? options.replicationLock.signal
|
|
152
152
|
: AbortSignal.any([options.signal, options.replicationLock.signal])
|
|
153
153
|
};
|
|
154
|
-
|
|
155
|
-
this
|
|
154
|
+
// A MongoDB session can only be used with its originating client. Writers
|
|
155
|
+
// created through another factory must still use this lease's database.
|
|
156
|
+
this.db = options.replicationLock.db;
|
|
156
157
|
this.replicationStreamId = options.replicationStreamId;
|
|
157
158
|
this.resumeFromLsn = options.resumeFromLsn;
|
|
158
|
-
this.session =
|
|
159
|
+
this.session = options.replicationLock.session;
|
|
159
160
|
this.replicationStreamName = options.replicationStreamName;
|
|
160
161
|
this.sync_rules = options.parsedSyncConfig.hydratedSyncConfig;
|
|
161
162
|
this.storeCurrentData = options.storeCurrentData;
|
|
@@ -175,7 +176,11 @@ export abstract class MongoBucketBatch
|
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
|
|
179
|
+
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
180
|
+
return this.withWriterLock(() => this.resolveTablesInternal(options));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
protected abstract resolveTablesInternal(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult>;
|
|
179
184
|
|
|
180
185
|
protected abstract createPersistedBatch(writtenSize: number): PersistedBatch;
|
|
181
186
|
|
|
@@ -183,31 +188,73 @@ export abstract class MongoBucketBatch
|
|
|
183
188
|
|
|
184
189
|
protected abstract cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]): Promise<void>;
|
|
185
190
|
|
|
186
|
-
|
|
191
|
+
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
192
|
+
return this.withWriterLock(() => this.commitInternal(lsn, options));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected abstract commitInternal(
|
|
196
|
+
lsn: string,
|
|
197
|
+
options?: storage.BucketBatchCommitOptions
|
|
198
|
+
): Promise<storage.CheckpointResult>;
|
|
199
|
+
|
|
200
|
+
async keepalive(lsn: string): Promise<storage.CheckpointResult> {
|
|
201
|
+
return this.commit(lsn, { createEmptyCheckpoints: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async setResumeLsn(lsn: string): Promise<void> {
|
|
205
|
+
return this.withWriterLock(() => this.setResumeLsnInternal(lsn));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
protected abstract setResumeLsnInternal(lsn: string): Promise<void>;
|
|
187
209
|
|
|
188
|
-
|
|
210
|
+
async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
|
|
211
|
+
return this.withWriterLock(() => this.getSourceTableStatusInternal(table));
|
|
212
|
+
}
|
|
189
213
|
|
|
190
|
-
abstract
|
|
214
|
+
protected abstract getSourceTableStatusInternal(table: storage.SourceTable): Promise<storage.SourceTable | null>;
|
|
191
215
|
|
|
192
|
-
|
|
216
|
+
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
217
|
+
return this.withWriterLock(() => this.markAllSnapshotDoneInternal(no_checkpoint_before_lsn));
|
|
218
|
+
}
|
|
193
219
|
|
|
194
|
-
abstract
|
|
220
|
+
protected abstract markAllSnapshotDoneInternal(no_checkpoint_before_lsn: string): Promise<void>;
|
|
195
221
|
|
|
196
|
-
|
|
222
|
+
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
223
|
+
return this.withWriterLock(() => this.markSnapshotDoneInternal(no_checkpoint_before_lsn, options));
|
|
224
|
+
}
|
|
197
225
|
|
|
198
|
-
abstract
|
|
226
|
+
protected abstract markSnapshotDoneInternal(
|
|
227
|
+
no_checkpoint_before_lsn: string,
|
|
228
|
+
options?: { throwOnConflict?: boolean }
|
|
229
|
+
): Promise<void>;
|
|
199
230
|
|
|
200
|
-
|
|
231
|
+
async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
|
|
232
|
+
return this.withWriterLock(() => this.markTableSnapshotRequiredInternal(table));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected abstract markTableSnapshotRequiredInternal(table: storage.SourceTable): Promise<void>;
|
|
236
|
+
|
|
237
|
+
async markTableSnapshotDone(
|
|
201
238
|
tables: storage.SourceTable[],
|
|
202
239
|
no_checkpoint_before_lsn?: string
|
|
203
|
-
): Promise<storage.SourceTable[]
|
|
240
|
+
): Promise<storage.SourceTable[]> {
|
|
241
|
+
return this.withWriterLock(() => this.markTableSnapshotDoneInternal(tables, no_checkpoint_before_lsn));
|
|
242
|
+
}
|
|
204
243
|
|
|
205
|
-
protected abstract
|
|
244
|
+
protected abstract markTableSnapshotDoneInternal(
|
|
245
|
+
tables: storage.SourceTable[],
|
|
246
|
+
no_checkpoint_before_lsn?: string
|
|
247
|
+
): Promise<storage.SourceTable[]>;
|
|
206
248
|
|
|
207
249
|
/** Perform any version-specific setup required before writing custom checkpoints. */
|
|
208
250
|
protected async prepareCustomWriteCheckpoints(): Promise<void> {}
|
|
209
251
|
|
|
210
252
|
async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
|
|
253
|
+
return this.withWriterLock(() => this.flushInternal(options));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Internal helpers require the public operation to hold the writer mutex. */
|
|
257
|
+
protected async flushInternal(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
|
|
211
258
|
let result: storage.FlushedResult | null = null;
|
|
212
259
|
// One flush may be split over multiple transactions.
|
|
213
260
|
// Each flushInner() is one transaction.
|
|
@@ -222,7 +269,7 @@ export abstract class MongoBucketBatch
|
|
|
222
269
|
|
|
223
270
|
/** Publish the checkpoint in the final flush transaction, or use the empty-commit path. */
|
|
224
271
|
protected async flushAndCommit<T>(
|
|
225
|
-
checkpoint: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<T>,
|
|
272
|
+
checkpoint: (stream: SyncRuleDocumentBase, lastOp: InternalOpId, writes?: MongoWriteBatch) => Promise<T>,
|
|
226
273
|
commitWithoutFlush: () => Promise<T>,
|
|
227
274
|
options?: storage.BatchBucketFlushOptions,
|
|
228
275
|
projection?: mongo.Document
|
|
@@ -234,8 +281,8 @@ export abstract class MongoBucketBatch
|
|
|
234
281
|
while (this.batch != null || this.write_checkpoint_batch.length > 0) {
|
|
235
282
|
await this.flushInner(
|
|
236
283
|
options,
|
|
237
|
-
async (stream, lastOp) => {
|
|
238
|
-
result = await checkpoint(stream, lastOp);
|
|
284
|
+
async (stream, lastOp, writes) => {
|
|
285
|
+
result = await checkpoint(stream, lastOp, writes);
|
|
239
286
|
},
|
|
240
287
|
projection
|
|
241
288
|
);
|
|
@@ -243,200 +290,26 @@ export abstract class MongoBucketBatch
|
|
|
243
290
|
return result;
|
|
244
291
|
}
|
|
245
292
|
|
|
246
|
-
|
|
293
|
+
protected abstract flushInner(
|
|
247
294
|
options?: storage.BatchBucketFlushOptions,
|
|
248
|
-
checkpoint?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<void>,
|
|
295
|
+
checkpoint?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId, writes?: MongoWriteBatch) => Promise<void>,
|
|
249
296
|
projection?: mongo.Document
|
|
250
|
-
): Promise<storage.FlushedResult | null
|
|
251
|
-
const batch = this.batch;
|
|
252
|
-
let last_op: InternalOpId | null = null;
|
|
253
|
-
let resumeBatch: OperationBatch | null = null;
|
|
254
|
-
let clearedError = false;
|
|
255
|
-
|
|
256
|
-
using _ = this.tracer.span('storage', 'flush');
|
|
257
|
-
|
|
258
|
-
await this.hooks?.beforeBatchFlush?.(this);
|
|
259
|
-
if (this.write_checkpoint_batch.length > 0) {
|
|
260
|
-
// Collection/index creation cannot run inside the replication transaction.
|
|
261
|
-
await this.prepareCustomWriteCheckpoints();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
await this.withReplicationTransaction(
|
|
265
|
-
`Flushing ${batch?.length ?? 0} ops`,
|
|
266
|
-
async (session, opSeq) => {
|
|
267
|
-
clearedError = false;
|
|
268
|
-
if (batch != null) {
|
|
269
|
-
const result = await this.replicateBatch(session, batch, opSeq, options);
|
|
270
|
-
resumeBatch = result.resumeBatch;
|
|
271
|
-
clearedError ||= result.clearedError;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (this.write_checkpoint_batch.length > 0) {
|
|
275
|
-
this.logger.info(`Writing ${this.write_checkpoint_batch.length} custom write checkpoints`);
|
|
276
|
-
await this.batchCreateCustomWriteCheckpoints(session, opSeq.next());
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
last_op = opSeq.last();
|
|
280
|
-
},
|
|
281
|
-
async (stream, lastOp) => {
|
|
282
|
-
if (checkpoint != null && resumeBatch == null) {
|
|
283
|
-
// The checkpoint must also persist the head, including when visibility is blocked.
|
|
284
|
-
await checkpoint(stream, lastOp);
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
return false;
|
|
288
|
-
},
|
|
289
|
-
projection
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// Keep checkpoints available if the transaction retries after writing them.
|
|
293
|
-
this.write_checkpoint_batch = [];
|
|
294
|
-
|
|
295
|
-
if (clearedError) {
|
|
296
|
-
this.clearedError = true;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// null if done, set if we need another flush
|
|
300
|
-
this.batch = resumeBatch;
|
|
301
|
-
|
|
302
|
-
if (last_op == null) {
|
|
303
|
-
throw new ReplicationAssertionError('Unexpected last_op == null');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
this.recordPersistedOp(last_op);
|
|
307
|
-
this.last_flushed_op = last_op;
|
|
308
|
-
await this.hooks?.afterBatchFlush?.(this);
|
|
309
|
-
return { flushed_op: last_op };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private async replicateBatch(
|
|
313
|
-
session: mongo.ClientSession,
|
|
314
|
-
batch: OperationBatch,
|
|
315
|
-
op_seq: MongoIdSequence,
|
|
316
|
-
options?: storage.BucketBatchCommitOptions
|
|
317
|
-
): Promise<{ resumeBatch: OperationBatch | null; clearedError: boolean }> {
|
|
318
|
-
let sizes: Map<string, number> | undefined = undefined;
|
|
319
|
-
using _ = this.tracer.span('storage', 'replicate_batch');
|
|
320
|
-
// Only look up current_data sizes if the batch stores current_data and at least one
|
|
321
|
-
// table in it does too (per-table can disable it, e.g. Postgres REPLICA IDENTITY FULL).
|
|
322
|
-
const anyTableStoresCurrentData =
|
|
323
|
-
this.storeCurrentData && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
|
|
324
|
-
if (anyTableStoresCurrentData && !this.skipExistingRows) {
|
|
325
|
-
// We skip this step if no tables store current_data, since the sizes will
|
|
326
|
-
// always be small in that case.
|
|
327
|
-
|
|
328
|
-
// With skipExistingRows, we don't load the full documents into memory,
|
|
329
|
-
// so we can also skip the size lookup step.
|
|
330
|
-
|
|
331
|
-
// Find sizes of current_data documents, to assist in intelligent batching without
|
|
332
|
-
// exceeding memory limits.
|
|
333
|
-
//
|
|
334
|
-
// A previous attempt tried to do batching by the results of the current_data query
|
|
335
|
-
// (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
|
|
336
|
-
// the order of processing, which then becomes really tricky to manage.
|
|
337
|
-
// This now takes 2+ queries, but doesn't have any issues with order of operations.
|
|
338
|
-
// Within this branch this.storeCurrentData is true, so the per-table flag is the
|
|
339
|
-
// effective value - only look up sizes for tables that actually store current_data.
|
|
340
|
-
const sizeLookups = batch.batch
|
|
341
|
-
.filter((r) => r.record.sourceTable.storeCurrentData)
|
|
342
|
-
.map((r) => ({
|
|
343
|
-
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
344
|
-
replicaId: r.beforeId
|
|
345
|
-
}));
|
|
346
|
-
|
|
347
|
-
sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// If set, we need to start a new transaction with this batch.
|
|
351
|
-
let resumeBatch: OperationBatch | null = null;
|
|
352
|
-
let transactionSize = 0;
|
|
297
|
+
): Promise<storage.FlushedResult | null>;
|
|
353
298
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
using lookupSpan = this.tracer.span('storage', 'lookup');
|
|
366
|
-
const lookups = b.map((r) => ({
|
|
367
|
-
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
368
|
-
replicaId: r.beforeId
|
|
369
|
-
}));
|
|
370
|
-
let sourceRecordLookup = await this.sourceRecordStore.loadDocuments(session, lookups, this.skipExistingRows);
|
|
371
|
-
lookupSpan.end();
|
|
372
|
-
|
|
373
|
-
let persistedBatch: PersistedBatch | null = this.createPersistedBatch(transactionSize);
|
|
374
|
-
|
|
375
|
-
// The current code structure makes it tricky to cleanly split this span from the one
|
|
376
|
-
// where fluhsing. So we manually end and re-create this span whenever we flush.
|
|
377
|
-
let evalSpan = this.tracer.span('evaluate');
|
|
378
|
-
for (let op of b) {
|
|
379
|
-
if (resumeBatch) {
|
|
380
|
-
resumeBatch.push(op);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const sourceRecord = sourceRecordLookup.get(op.internalBeforeKey) ?? null;
|
|
384
|
-
if (sourceRecord != null) {
|
|
385
|
-
// If it will be used again later, it will be set again using nextData below
|
|
386
|
-
sourceRecordLookup.delete(op.internalBeforeKey);
|
|
387
|
-
}
|
|
388
|
-
const nextData = this.saveOperation(persistedBatch!, op, sourceRecord, op_seq);
|
|
389
|
-
if (nextData != null) {
|
|
390
|
-
// Update our current_data and size cache
|
|
391
|
-
sourceRecordLookup.set(op.internalAfterKey!, nextData);
|
|
392
|
-
sizes?.set(op.internalAfterKey!, nextData.data?.length() ?? 0);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (persistedBatch!.shouldFlushTransaction()) {
|
|
396
|
-
evalSpan.end();
|
|
397
|
-
// Transaction is getting big.
|
|
398
|
-
// Flush, and resume in a new transaction.
|
|
399
|
-
using persistSpan = this.tracer.span('storage', 'persist_flush');
|
|
400
|
-
const { flushedAny } = await persistedBatch!.flush(this.session, options);
|
|
401
|
-
|
|
402
|
-
didFlush ||= flushedAny;
|
|
403
|
-
persistedBatch = null;
|
|
404
|
-
// Computing our current progress is a little tricky here, since
|
|
405
|
-
// we're stopping in the middle of a batch.
|
|
406
|
-
// We create a new batch, and push any remaining operations to it.
|
|
407
|
-
resumeBatch = new OperationBatch();
|
|
408
|
-
persistSpan.end();
|
|
409
|
-
evalSpan = this.tracer.span('evaluate');
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
evalSpan.end();
|
|
413
|
-
|
|
414
|
-
if (persistedBatch) {
|
|
415
|
-
transactionSize = persistedBatch.currentSize;
|
|
416
|
-
using _ = this.tracer.span('storage', 'persist_flush');
|
|
417
|
-
const { flushedAny } = await persistedBatch.flush(this.session, options);
|
|
418
|
-
didFlush ||= flushedAny;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const clearedError = didFlush && !this.clearedError;
|
|
423
|
-
if (clearedError) {
|
|
424
|
-
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
425
|
-
using _ = this.tracer.span('storage', 'clear_error');
|
|
426
|
-
await this.clearError(this.session);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
resumeBatch: resumeBatch?.hasData() ? resumeBatch : null,
|
|
431
|
-
clearedError
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
private saveOperation(
|
|
299
|
+
/**
|
|
300
|
+
* Evaluate one source operation and append its storage writes to batch.
|
|
301
|
+
*
|
|
302
|
+
* Diagnostics run during evaluation. V1 calls markRecordUnavailable immediately;
|
|
303
|
+
* v3/v4 supply a collector and deliver repair notifications only after a
|
|
304
|
+
* confirmed publication. A failed row attempt may have registered a repair,
|
|
305
|
+
* so its caller must discard that row's collected notification on failure.
|
|
306
|
+
*/
|
|
307
|
+
protected saveOperation(
|
|
436
308
|
batch: PersistedBatch,
|
|
437
309
|
operation: RecordOperation,
|
|
438
310
|
sourceRecord: LoadedSourceRecord | null,
|
|
439
|
-
opSeq: MongoIdSequence
|
|
311
|
+
opSeq: MongoIdSequence,
|
|
312
|
+
onRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined
|
|
440
313
|
) {
|
|
441
314
|
const record = operation.record;
|
|
442
315
|
const beforeId = operation.beforeId;
|
|
@@ -474,19 +347,6 @@ export abstract class MongoBucketBatch
|
|
|
474
347
|
// Not an error if we re-apply a transaction
|
|
475
348
|
existing_buckets = [];
|
|
476
349
|
existing_lookups = [];
|
|
477
|
-
if (!isCompleteRow(storeCurrentData, after!)) {
|
|
478
|
-
if (this.markRecordUnavailable != null) {
|
|
479
|
-
// This will trigger a "resnapshot" of the record.
|
|
480
|
-
// This is not relevant if storeCurrentData is false, since we'll get the full row
|
|
481
|
-
// directly in the replication stream.
|
|
482
|
-
this.markRecordUnavailable(record);
|
|
483
|
-
} else {
|
|
484
|
-
// Log to help with debugging if there was a consistency issue
|
|
485
|
-
this.logger.warn(
|
|
486
|
-
`Cannot find previous record for update on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
350
|
} else {
|
|
491
351
|
existing_buckets = result.buckets;
|
|
492
352
|
existing_lookups = result.lookups;
|
|
@@ -495,6 +355,24 @@ export abstract class MongoBucketBatch
|
|
|
495
355
|
after = storage.mergeToast<SqliteValue>(after!, data);
|
|
496
356
|
}
|
|
497
357
|
}
|
|
358
|
+
if (!isCompleteRow(storeCurrentData, after!)) {
|
|
359
|
+
if (onRecordUnavailable != null) {
|
|
360
|
+
// This will trigger a "resnapshot" of the record.
|
|
361
|
+
// This is not relevant if storeCurrentData is false, since we'll get the full row
|
|
362
|
+
// directly in the replication stream.
|
|
363
|
+
// Check after merging, including existing incomplete rows/tombstones.
|
|
364
|
+
// A flush can commit before a process dies or notification delivery
|
|
365
|
+
// fails. Its source position has not advanced, so WAL replay must
|
|
366
|
+
// request repair again rather than treating mere record existence as
|
|
367
|
+
// proof that the missing TOAST values have been recovered.
|
|
368
|
+
onRecordUnavailable(record);
|
|
369
|
+
} else if (result == null) {
|
|
370
|
+
// Log to help with debugging if there was a consistency issue
|
|
371
|
+
this.logger.warn(
|
|
372
|
+
`Cannot find previous record for update on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
498
376
|
} else if (record.tag == SaveOperationTag.DELETE) {
|
|
499
377
|
const result = sourceRecord;
|
|
500
378
|
if (result == null) {
|
|
@@ -504,7 +382,7 @@ export abstract class MongoBucketBatch
|
|
|
504
382
|
// Log to help with debugging if there was a consistency issue.
|
|
505
383
|
// Gate on the batch-level flag: FULL tables (per-record flag false) still get a
|
|
506
384
|
// current_data entry, so a missing record on DELETE is meaningful for them too.
|
|
507
|
-
if (this.storeCurrentData &&
|
|
385
|
+
if (this.storeCurrentData && onRecordUnavailable == null) {
|
|
508
386
|
this.logger.warn(
|
|
509
387
|
`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
|
|
510
388
|
);
|
|
@@ -707,42 +585,73 @@ export abstract class MongoBucketBatch
|
|
|
707
585
|
return this.withFencedTransaction(() => this.fence(session), cb, session);
|
|
708
586
|
}
|
|
709
587
|
|
|
710
|
-
/**
|
|
588
|
+
/**
|
|
589
|
+
* Internal transaction helper: the public writer operation already holds the
|
|
590
|
+
* mutex. The first operation must modify the stream document conditional on
|
|
591
|
+
* lease ownership. This database fence is required on every retry even though
|
|
592
|
+
* local writers are serialized; another process may have taken over the lease.
|
|
593
|
+
*/
|
|
711
594
|
protected async withFencedTransaction<S, T>(
|
|
712
595
|
acquireFence: () => Promise<S>,
|
|
713
596
|
cb: (stream: S) => Promise<T>,
|
|
714
|
-
session = this.session
|
|
597
|
+
session = this.session,
|
|
598
|
+
description?: string
|
|
715
599
|
): Promise<T> {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
600
|
+
this.options.replicationLock.throwIfAborted();
|
|
601
|
+
let attempts = 0;
|
|
602
|
+
// Includes driver commit retries and the delays between transaction attempts.
|
|
603
|
+
using _transactionSpan = this.tracer.span('storage', 'transaction');
|
|
604
|
+
const result = await session.withTransaction(
|
|
605
|
+
async () => {
|
|
606
|
+
attempts++;
|
|
607
|
+
// One callback invocation per transaction attempt. Commit happens after
|
|
608
|
+
// this span; command monitoring is needed to measure the full wire lifetime.
|
|
609
|
+
using attemptSpan = this.tracer.span('storage', 'transaction_body');
|
|
610
|
+
try {
|
|
719
611
|
const stream = await acquireFence();
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
throw e;
|
|
727
|
-
}
|
|
728
|
-
if (e instanceof mongo.MongoError && e.hasErrorLabel('TransientTransactionError')) {
|
|
729
|
-
// Likely write conflict caused by concurrent writes to this replication stream.
|
|
730
|
-
} else {
|
|
731
|
-
this.logger.warn('Transaction error', e as Error);
|
|
732
|
-
}
|
|
733
|
-
const delay = Math.random() * 50;
|
|
734
|
-
using _ = this.tracer.span('storage', 'retry_delay');
|
|
735
|
-
await timers.setTimeout(delay);
|
|
612
|
+
const result = await cb(stream);
|
|
613
|
+
this.options.replicationLock.throwIfAborted();
|
|
614
|
+
return result;
|
|
615
|
+
} catch (e: unknown) {
|
|
616
|
+
attemptSpan.end();
|
|
617
|
+
if (e instanceof OpIdRangeExhausted) {
|
|
736
618
|
throw e;
|
|
737
619
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
620
|
+
if (e instanceof mongo.MongoError && e.hasErrorLabel('TransientTransactionError')) {
|
|
621
|
+
// Log every tenth attempt, including failures of the fence, without
|
|
622
|
+
// logging a stack or bulk result for every driver retry.
|
|
623
|
+
if ((attempts + 1) % 10 === 0) {
|
|
624
|
+
this.logger.info(`${description ?? 'Replication transaction'} - try ${attempts + 1}`, {
|
|
625
|
+
error: e.message,
|
|
626
|
+
code: e.code,
|
|
627
|
+
codeName: e instanceof mongo.MongoServerError ? e.codeName : undefined,
|
|
628
|
+
errorLabels: e.errorLabels
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
this.logger.warn('Transaction error', e as Error);
|
|
633
|
+
}
|
|
634
|
+
const delay = Math.random() * 50;
|
|
635
|
+
using _ = this.tracer.span('storage', 'retry_delay');
|
|
636
|
+
await timers.setTimeout(delay);
|
|
637
|
+
throw e;
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
{ maxCommitTimeMS: 10000, ...REPLICATION_WRITE_OPTIONS }
|
|
741
641
|
);
|
|
642
|
+
return result;
|
|
742
643
|
}
|
|
743
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Public database operations acquire the lease's mutex once, then call only
|
|
647
|
+
* internal helpers until they finish. Those helpers must never call a public
|
|
648
|
+
* database operation or reacquire this mutex. All session use, including reads,
|
|
649
|
+
* belongs inside this boundary so it cannot join another writer's transaction.
|
|
650
|
+
* Transaction helpers still fence database ownership on every attempt.
|
|
651
|
+
*/
|
|
744
652
|
private async withWriterLock<T>(callback: () => Promise<T>): Promise<T> {
|
|
745
653
|
using lockSpan = this.tracer.span('storage', 'internal_lock');
|
|
654
|
+
this.options.replicationLock.throwIfAborted();
|
|
746
655
|
return this.options.replicationLock.writerMutex.exclusiveLock(async () => {
|
|
747
656
|
lockSpan.end();
|
|
748
657
|
this.options.replicationLock.throwIfAborted();
|
|
@@ -750,13 +659,13 @@ export abstract class MongoBucketBatch
|
|
|
750
659
|
});
|
|
751
660
|
}
|
|
752
661
|
|
|
753
|
-
/**
|
|
662
|
+
/** Internal single-document update; the public operation already holds the mutex. */
|
|
754
663
|
protected async updateStreamMetadata(
|
|
755
664
|
set: mongo.Document,
|
|
756
665
|
filter: mongo.Document = {},
|
|
757
|
-
writeConcern: mongo.WriteConcernSettings =
|
|
666
|
+
writeConcern: mongo.WriteConcernSettings = REPLICATION_WRITE_OPTIONS.writeConcern
|
|
758
667
|
): Promise<void> {
|
|
759
|
-
await this.
|
|
668
|
+
await this.writeStreamMetadata(set, filter, writeConcern);
|
|
760
669
|
}
|
|
761
670
|
|
|
762
671
|
/** Caller must already be inside a fenced transaction holding the writer mutex. */
|
|
@@ -775,63 +684,7 @@ export abstract class MongoBucketBatch
|
|
|
775
684
|
{ session: this.session, ...(writeConcern == null ? {} : { writeConcern }) }
|
|
776
685
|
);
|
|
777
686
|
if (result.matchedCount === 0) {
|
|
778
|
-
throw new ReplicationAbortedError('Replication writer
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
private async withReplicationTransaction(
|
|
783
|
-
description: string,
|
|
784
|
-
callback: (session: mongo.ClientSession, opSeq: MongoIdSequence) => Promise<void>,
|
|
785
|
-
finish?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<boolean>,
|
|
786
|
-
projection?: mongo.Document
|
|
787
|
-
): Promise<void> {
|
|
788
|
-
let flushTry = 0;
|
|
789
|
-
const lastTry = Date.now() + 90000;
|
|
790
|
-
const allocator = this.options.opIdAllocator;
|
|
791
|
-
let lastOp = 0n;
|
|
792
|
-
// Refill outside the publication transaction, before evaluating any rows.
|
|
793
|
-
// Exhaustion remains a fallback for large batches or a newer stream head.
|
|
794
|
-
this.options.replicationLock.throwIfAborted();
|
|
795
|
-
await allocator.ensureCapacity();
|
|
796
|
-
for (;;) {
|
|
797
|
-
try {
|
|
798
|
-
await this.withFencedTransaction(
|
|
799
|
-
() => this.fence(this.session, projection),
|
|
800
|
-
async (stream) => {
|
|
801
|
-
flushTry += 1;
|
|
802
|
-
if (flushTry % 10 == 0) {
|
|
803
|
-
this.logger.info(`${description} - try ${flushTry}`);
|
|
804
|
-
}
|
|
805
|
-
if (flushTry > 20 && Date.now() > lastTry) {
|
|
806
|
-
throw new ServiceError(ErrorCode.PSYNC_S1402, 'Max transaction tries exceeded');
|
|
807
|
-
}
|
|
808
|
-
// The fence already holds this stream document for the transaction.
|
|
809
|
-
// A different writer may have used higher IDs since our last flush.
|
|
810
|
-
const opSeq = allocator.sequence(this.persistedOpHead(stream));
|
|
811
|
-
await callback(this.session, opSeq);
|
|
812
|
-
lastOp = opSeq.last();
|
|
813
|
-
|
|
814
|
-
if (!(await finish?.(stream, lastOp))) {
|
|
815
|
-
const writes = this.db.createWriteBatch(this.session, { ordered: false });
|
|
816
|
-
// Allow subclasses to persist additional flush-time state in the same transaction.
|
|
817
|
-
this.onReplicationTransactionFlush(writes, lastOp);
|
|
818
|
-
await writes.execute();
|
|
819
|
-
}
|
|
820
|
-
// Notifications and cleanup must wait until the transaction commits.
|
|
821
|
-
}
|
|
822
|
-
);
|
|
823
|
-
allocator.committed(lastOp);
|
|
824
|
-
return;
|
|
825
|
-
} catch (error) {
|
|
826
|
-
if (!(error instanceof OpIdRangeExhausted)) {
|
|
827
|
-
throw error;
|
|
828
|
-
}
|
|
829
|
-
// withTransaction has aborted every write. Reserve outside that transaction,
|
|
830
|
-
// then replay evaluation from the same committed stream head. Existing
|
|
831
|
-
// ranges can be reused on retry because no operation from this attempt committed.
|
|
832
|
-
this.options.replicationLock.throwIfAborted();
|
|
833
|
-
await allocator.reserve();
|
|
834
|
-
}
|
|
687
|
+
throw new ReplicationAbortedError('Replication writer lost its lease or metadata target changed');
|
|
835
688
|
}
|
|
836
689
|
}
|
|
837
690
|
|
|
@@ -841,21 +694,6 @@ export abstract class MongoBucketBatch
|
|
|
841
694
|
/** Advance the persisted head in the same transaction as the operations. */
|
|
842
695
|
protected abstract onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: InternalOpId): void;
|
|
843
696
|
|
|
844
|
-
/**
|
|
845
|
-
* Called after a replication transaction has successfully committed, with the last persisted op id.
|
|
846
|
-
*
|
|
847
|
-
* v1 storage tracks this in memory to fold into the next checkpoint. v3 storage does not need it:
|
|
848
|
-
* the stream-level `last_persisted_op` is already advanced durably by
|
|
849
|
-
* {@link onReplicationTransactionFlush} or the combined checkpoint update within the same
|
|
850
|
-
* transaction, and empty commits read it from the document.
|
|
851
|
-
*
|
|
852
|
-
* Keep calls to this adjacent to {@link withReplicationTransaction} usage - both must observe
|
|
853
|
-
* every path that persists ops.
|
|
854
|
-
*/
|
|
855
|
-
protected recordPersistedOp(_lastOp: InternalOpId): void {
|
|
856
|
-
// No-op by default.
|
|
857
|
-
}
|
|
858
|
-
|
|
859
697
|
async [Symbol.asyncDispose]() {
|
|
860
698
|
if (this.batch?.hasData() || this.write_checkpoint_batch.length > 0) {
|
|
861
699
|
// We don't error here, since:
|
|
@@ -863,7 +701,7 @@ export abstract class MongoBucketBatch
|
|
|
863
701
|
// 2. SuppressedError is messy to deal with.
|
|
864
702
|
this.logger.warn('Disposing writer with unflushed changes');
|
|
865
703
|
}
|
|
866
|
-
|
|
704
|
+
// The lease owns the shared session and ends it after all writers stop.
|
|
867
705
|
super.clearListeners();
|
|
868
706
|
}
|
|
869
707
|
|
|
@@ -921,8 +759,11 @@ export abstract class MongoBucketBatch
|
|
|
921
759
|
* Drop is equivalent to TRUNCATE, plus removing our record of the table.
|
|
922
760
|
*/
|
|
923
761
|
async drop(sourceTables: storage.SourceTable[]): Promise<storage.FlushedResult | null> {
|
|
924
|
-
|
|
925
|
-
|
|
762
|
+
return this.withWriterLock(() => this.dropInternal(sourceTables));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
protected async dropInternal(sourceTables: storage.SourceTable[]): Promise<storage.FlushedResult | null> {
|
|
766
|
+
await this.truncateInternal(sourceTables);
|
|
926
767
|
|
|
927
768
|
await this.withTransaction(async () => {
|
|
928
769
|
for (let table of sourceTables) {
|
|
@@ -933,19 +774,22 @@ export abstract class MongoBucketBatch
|
|
|
933
774
|
});
|
|
934
775
|
|
|
935
776
|
await this.cleanupDroppedSourceTables(sourceTables);
|
|
936
|
-
return
|
|
777
|
+
return null;
|
|
937
778
|
}
|
|
938
779
|
|
|
939
780
|
async truncate(sourceTables: storage.SourceTable[]): Promise<storage.FlushedResult | null> {
|
|
940
|
-
|
|
781
|
+
return this.withWriterLock(() => this.truncateInternal(sourceTables));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
protected async truncateInternal(sourceTables: storage.SourceTable[]): Promise<storage.FlushedResult | null> {
|
|
785
|
+
await this.flushInternal();
|
|
941
786
|
|
|
942
787
|
let last_op: InternalOpId | null = null;
|
|
943
788
|
for (let table of sourceTables) {
|
|
944
|
-
last_op = await this.
|
|
789
|
+
last_op = await this.truncateSingleInternal(table);
|
|
945
790
|
}
|
|
946
791
|
|
|
947
792
|
if (last_op) {
|
|
948
|
-
this.recordPersistedOp(last_op);
|
|
949
793
|
return {
|
|
950
794
|
flushed_op: last_op
|
|
951
795
|
};
|
|
@@ -955,53 +799,19 @@ export abstract class MongoBucketBatch
|
|
|
955
799
|
}
|
|
956
800
|
|
|
957
801
|
async truncateSingle(sourceTable: storage.SourceTable): Promise<InternalOpId> {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
// To avoid too large transactions, we limit the amount of data we delete per transaction.
|
|
961
|
-
// Since we don't use the record data here, we don't have explicit size limits per batch.
|
|
962
|
-
const BATCH_LIMIT = 2000;
|
|
963
|
-
|
|
964
|
-
let lastBatchCount = BATCH_LIMIT;
|
|
965
|
-
while (lastBatchCount == BATCH_LIMIT) {
|
|
966
|
-
await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
|
|
967
|
-
using evalSpan = this.tracer.span('evaluate');
|
|
968
|
-
const sourceTableId = mongoTableId(sourceTable.id);
|
|
969
|
-
const batch = await this.sourceRecordStore.loadTruncateBatch(session, sourceTableId, BATCH_LIMIT);
|
|
970
|
-
const persistedBatch = this.createPersistedBatch(0);
|
|
971
|
-
|
|
972
|
-
for (let value of batch) {
|
|
973
|
-
persistedBatch.saveBucketData({
|
|
974
|
-
op_seq: opSeq,
|
|
975
|
-
before_buckets: value.buckets,
|
|
976
|
-
evaluated: [],
|
|
977
|
-
table: sourceTable,
|
|
978
|
-
sourceKey: value.replicaId
|
|
979
|
-
});
|
|
980
|
-
persistedBatch.saveParameterData({
|
|
981
|
-
op_seq: opSeq,
|
|
982
|
-
existing_lookups: value.lookups,
|
|
983
|
-
evaluated: [],
|
|
984
|
-
sourceTable: sourceTable,
|
|
985
|
-
sourceKey: value.replicaId
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// Since this is not from streaming replication, we can do a hard delete
|
|
989
|
-
persistedBatch.hardDeleteCurrentData(sourceTableId, value.replicaId);
|
|
990
|
-
}
|
|
991
|
-
evalSpan.end();
|
|
992
|
-
|
|
993
|
-
using _ = this.tracer.span('storage', 'persist_flush');
|
|
994
|
-
await persistedBatch.flush(session);
|
|
995
|
-
lastBatchCount = batch.length;
|
|
802
|
+
return this.withWriterLock(() => this.truncateSingleInternal(sourceTable));
|
|
803
|
+
}
|
|
996
804
|
|
|
997
|
-
|
|
998
|
-
});
|
|
999
|
-
}
|
|
805
|
+
protected abstract truncateSingleInternal(sourceTable: storage.SourceTable): Promise<InternalOpId>;
|
|
1000
806
|
|
|
1001
|
-
|
|
807
|
+
async updateTableProgress(
|
|
808
|
+
table: storage.SourceTable,
|
|
809
|
+
progress: Partial<storage.TableSnapshotStatus>
|
|
810
|
+
): Promise<storage.SourceTable> {
|
|
811
|
+
return this.withWriterLock(() => this.updateTableProgressInternal(table, progress));
|
|
1002
812
|
}
|
|
1003
813
|
|
|
1004
|
-
async
|
|
814
|
+
private async updateTableProgressInternal(
|
|
1005
815
|
table: storage.SourceTable,
|
|
1006
816
|
progress: Partial<storage.TableSnapshotStatus>
|
|
1007
817
|
): Promise<storage.SourceTable> {
|
|
@@ -1032,23 +842,6 @@ export abstract class MongoBucketBatch
|
|
|
1032
842
|
return copy;
|
|
1033
843
|
}
|
|
1034
844
|
|
|
1035
|
-
protected async clearError(session: mongo.ClientSession): Promise<void> {
|
|
1036
|
-
await this.db.sync_rules.updateOne(
|
|
1037
|
-
{
|
|
1038
|
-
_id: this.replicationStreamId
|
|
1039
|
-
},
|
|
1040
|
-
{
|
|
1041
|
-
$set: {
|
|
1042
|
-
last_fatal_error: null,
|
|
1043
|
-
last_fatal_error_ts: null
|
|
1044
|
-
}
|
|
1045
|
-
},
|
|
1046
|
-
{
|
|
1047
|
-
session
|
|
1048
|
-
}
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
845
|
/**
|
|
1053
846
|
* Gets relevant {@link HydratedEventDescriptor}s for the given {@link SourceTable}
|
|
1054
847
|
*/
|