@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716
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 +129 -7
- package/dist/storage/MongoBucketStorage.d.ts +42 -5
- package/dist/storage/MongoBucketStorage.js +404 -215
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
- package/dist/storage/implementation/MongoBucketBatch.js +169 -98
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- 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/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +35 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +8 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
- 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.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
- package/dist/storage/implementation/common/PersistedBatch.js +57 -14
- 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 +9 -1
- package/dist/storage/implementation/db.js +28 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +35 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +11 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
- package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +66 -17
- 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/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +14 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +108 -0
- package/dist/storage/implementation/v3/compact-utils.js +223 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +90 -6
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
- package/dist/storage/implementation/v3/source-table-utils.js +49 -57
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +28 -1
- package/dist/types/types.js +28 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -2
- package/dist/utils/test-utils.js +7 -4
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +545 -277
- package/src/storage/implementation/MongoBucketBatch.ts +258 -124
- package/src/storage/implementation/MongoCompactor.ts +37 -346
- package/src/storage/implementation/MongoIdSequence.ts +21 -1
- package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
- package/src/storage/implementation/MongoStorageProvider.ts +16 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +76 -20
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +36 -2
- package/src/storage/implementation/models.ts +39 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
- package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +339 -247
- package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +329 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +97 -4
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +9 -5
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +794 -205
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
- package/test/src/compact-utils.test.ts +251 -0
- package/test/src/concurrent_writers.test.ts +599 -0
- package/test/src/connection_config.test.ts +159 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/helpers/concurrentWriter.mjs +29 -0
- package/test/src/helpers/s3TestFactory.ts +12 -12
- package/test/src/object_storage_usage.test.ts +268 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +280 -24
- package/test/src/storage_compacting.test.ts +1097 -253
- package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
- package/test/src/storage_s3_checksums.test.ts +6 -7
- package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
- package/test/src/storage_s3_reading.test.ts +294 -6
- package/test/src/storage_s3_writing.test.ts +4 -3
- package/test/src/storage_sync.test.ts +412 -80
- package/test/src/storeCurrentData.test.ts +5 -5
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EventDefinitionId,
|
|
4
|
+
HydratedEventDescriptor,
|
|
5
|
+
HydratedSyncConfig,
|
|
6
|
+
SqliteRow,
|
|
7
|
+
SqliteValue
|
|
8
|
+
} from '@powersync/service-sync-rules';
|
|
3
9
|
import * as bson from 'bson';
|
|
4
10
|
|
|
5
11
|
import {
|
|
@@ -8,6 +14,7 @@ import {
|
|
|
8
14
|
ErrorCode,
|
|
9
15
|
errors,
|
|
10
16
|
Logger,
|
|
17
|
+
ReplicationAbortedError,
|
|
11
18
|
ReplicationAssertionError,
|
|
12
19
|
ServiceError
|
|
13
20
|
} from '@powersync/lib-services-framework';
|
|
@@ -27,21 +34,20 @@ import { mongoTableId } from '../../utils/util.js';
|
|
|
27
34
|
import { PersistedBatch } from './common/PersistedBatch.js';
|
|
28
35
|
import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
|
|
29
36
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
37
|
+
import { SyncRuleDocumentBase } from './models.js';
|
|
30
38
|
import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
|
|
31
|
-
import { MongoIdSequence } from './MongoIdSequence.js';
|
|
39
|
+
import { MongoIdSequence, OpIdRangeExhausted } from './MongoIdSequence.js';
|
|
40
|
+
import { MongoOpIdAllocator } from './MongoOpIdAllocator.js';
|
|
32
41
|
import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
|
|
33
|
-
import {
|
|
42
|
+
import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
|
|
43
|
+
import { MongoWriteBatch } from './MongoWriteBatch.js';
|
|
34
44
|
import { OperationBatch, RecordOperation } from './OperationBatch.js';
|
|
35
45
|
import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
|
|
36
|
-
|
|
37
|
-
// Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
|
|
38
|
-
// While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
|
|
39
|
-
// makes it more fair and has less overhead.
|
|
40
|
-
//
|
|
41
|
-
// In the future, we can investigate allowing multiple replication streams operating independently.
|
|
42
|
-
const replicationMutex = new utils.Mutex();
|
|
46
|
+
import { createObjectStorageUsageWriterId } from './v3/object-storage/ObjectStorageUsage.js';
|
|
43
47
|
|
|
44
48
|
export interface MongoBucketBatchOptions {
|
|
49
|
+
replicationLock: MongoSyncRulesLock;
|
|
50
|
+
opIdAllocator: MongoOpIdAllocator;
|
|
45
51
|
db: VersionedPowerSyncMongo;
|
|
46
52
|
/**
|
|
47
53
|
* The parsed sync config set for this batch.
|
|
@@ -72,6 +78,10 @@ export interface MongoBucketBatchOptions {
|
|
|
72
78
|
|
|
73
79
|
logger: Logger;
|
|
74
80
|
tracer?: PerformanceTracer<'storage' | 'evaluate'>;
|
|
81
|
+
/**
|
|
82
|
+
* Aborts in-flight object storage uploads when replication stops.
|
|
83
|
+
*/
|
|
84
|
+
signal?: AbortSignal;
|
|
75
85
|
|
|
76
86
|
objectStorage?: ObjectStorage;
|
|
77
87
|
inlineThresholdBytes?: number;
|
|
@@ -104,9 +114,10 @@ export abstract class MongoBucketBatch
|
|
|
104
114
|
private readonly storeCurrentData: boolean;
|
|
105
115
|
public readonly skipExistingRows: boolean;
|
|
106
116
|
protected readonly mapping: BucketDefinitionMapping;
|
|
117
|
+
protected readonly objectStorageUsageWriterId = createObjectStorageUsageWriterId();
|
|
107
118
|
|
|
108
119
|
private batch: OperationBatch | null = null;
|
|
109
|
-
|
|
120
|
+
protected write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
|
|
110
121
|
private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
111
122
|
private hooks: storage.StorageHooks | undefined;
|
|
112
123
|
private clearedError = false;
|
|
@@ -133,7 +144,13 @@ export abstract class MongoBucketBatch
|
|
|
133
144
|
constructor(options: MongoBucketBatchOptions) {
|
|
134
145
|
super();
|
|
135
146
|
this.logger = options.logger;
|
|
136
|
-
this.options =
|
|
147
|
+
this.options = {
|
|
148
|
+
...options,
|
|
149
|
+
signal:
|
|
150
|
+
options.signal == null
|
|
151
|
+
? options.replicationLock.signal
|
|
152
|
+
: AbortSignal.any([options.signal, options.replicationLock.signal])
|
|
153
|
+
};
|
|
137
154
|
this.client = options.db.client;
|
|
138
155
|
this.db = options.db;
|
|
139
156
|
this.replicationStreamId = options.replicationStreamId;
|
|
@@ -185,6 +202,11 @@ export abstract class MongoBucketBatch
|
|
|
185
202
|
no_checkpoint_before_lsn?: string
|
|
186
203
|
): Promise<storage.SourceTable[]>;
|
|
187
204
|
|
|
205
|
+
protected abstract batchCreateCustomWriteCheckpoints(session: mongo.ClientSession, opId: InternalOpId): Promise<void>;
|
|
206
|
+
|
|
207
|
+
/** Perform any version-specific setup required before writing custom checkpoints. */
|
|
208
|
+
protected async prepareCustomWriteCheckpoints(): Promise<void> {}
|
|
209
|
+
|
|
188
210
|
async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
|
|
189
211
|
let result: storage.FlushedResult | null = null;
|
|
190
212
|
// One flush may be split over multiple transactions.
|
|
@@ -198,7 +220,34 @@ export abstract class MongoBucketBatch
|
|
|
198
220
|
return result;
|
|
199
221
|
}
|
|
200
222
|
|
|
201
|
-
|
|
223
|
+
/** Publish the checkpoint in the final flush transaction, or use the empty-commit path. */
|
|
224
|
+
protected async flushAndCommit<T>(
|
|
225
|
+
checkpoint: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<T>,
|
|
226
|
+
commitWithoutFlush: () => Promise<T>,
|
|
227
|
+
options?: storage.BatchBucketFlushOptions,
|
|
228
|
+
projection?: mongo.Document
|
|
229
|
+
): Promise<T> {
|
|
230
|
+
if (this.batch == null && this.write_checkpoint_batch.length === 0) {
|
|
231
|
+
return commitWithoutFlush();
|
|
232
|
+
}
|
|
233
|
+
let result!: T;
|
|
234
|
+
while (this.batch != null || this.write_checkpoint_batch.length > 0) {
|
|
235
|
+
await this.flushInner(
|
|
236
|
+
options,
|
|
237
|
+
async (stream, lastOp) => {
|
|
238
|
+
result = await checkpoint(stream, lastOp);
|
|
239
|
+
},
|
|
240
|
+
projection
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async flushInner(
|
|
247
|
+
options?: storage.BatchBucketFlushOptions,
|
|
248
|
+
checkpoint?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<void>,
|
|
249
|
+
projection?: mongo.Document
|
|
250
|
+
): Promise<storage.FlushedResult | null> {
|
|
202
251
|
const batch = this.batch;
|
|
203
252
|
let last_op: InternalOpId | null = null;
|
|
204
253
|
let resumeBatch: OperationBatch | null = null;
|
|
@@ -207,23 +256,41 @@ export abstract class MongoBucketBatch
|
|
|
207
256
|
using _ = this.tracer.span('storage', 'flush');
|
|
208
257
|
|
|
209
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
|
+
}
|
|
210
263
|
|
|
211
|
-
await this.withReplicationTransaction(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
273
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
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
|
+
}
|
|
224
278
|
|
|
225
|
-
|
|
226
|
-
|
|
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 = [];
|
|
227
294
|
|
|
228
295
|
if (clearedError) {
|
|
229
296
|
this.clearedError = true;
|
|
@@ -620,17 +687,46 @@ export abstract class MongoBucketBatch
|
|
|
620
687
|
return result;
|
|
621
688
|
}
|
|
622
689
|
|
|
623
|
-
protected async
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
690
|
+
protected async fence(session = this.session, projection?: mongo.Document) {
|
|
691
|
+
// Graceful cancellation belongs to the source connector's page/batch boundary.
|
|
692
|
+
// It must still be able to persist progress for rows already flushed. Lease
|
|
693
|
+
// loss is different: the fence rejects every subsequent writer transaction.
|
|
694
|
+
return MongoSyncRulesLock.fence(
|
|
695
|
+
this.db,
|
|
696
|
+
this.replicationStreamId,
|
|
697
|
+
this.options.replicationLock,
|
|
698
|
+
session,
|
|
699
|
+
projection
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
protected async withTransaction<T>(
|
|
704
|
+
cb: (stream: SyncRuleDocumentBase) => Promise<T>,
|
|
705
|
+
session = this.session
|
|
706
|
+
): Promise<T> {
|
|
707
|
+
return this.withFencedTransaction(() => this.fence(session), cb, session);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** The first operation must modify the stream document conditional on lease ownership. */
|
|
711
|
+
protected async withFencedTransaction<S, T>(
|
|
712
|
+
acquireFence: () => Promise<S>,
|
|
713
|
+
cb: (stream: S) => Promise<T>,
|
|
714
|
+
session = this.session
|
|
715
|
+
): Promise<T> {
|
|
716
|
+
return this.withWriterLock(() =>
|
|
717
|
+
session.withTransaction(
|
|
628
718
|
async () => {
|
|
719
|
+
const stream = await acquireFence();
|
|
629
720
|
try {
|
|
630
|
-
await cb();
|
|
721
|
+
const result = await cb(stream);
|
|
722
|
+
this.options.replicationLock.throwIfAborted();
|
|
723
|
+
return result;
|
|
631
724
|
} catch (e: unknown) {
|
|
725
|
+
if (e instanceof OpIdRangeExhausted) {
|
|
726
|
+
throw e;
|
|
727
|
+
}
|
|
632
728
|
if (e instanceof mongo.MongoError && e.hasErrorLabel('TransientTransactionError')) {
|
|
633
|
-
// Likely write conflict caused by concurrent
|
|
729
|
+
// Likely write conflict caused by concurrent writes to this replication stream.
|
|
634
730
|
} else {
|
|
635
731
|
this.logger.warn('Transaction error', e as Error);
|
|
636
732
|
}
|
|
@@ -640,102 +736,118 @@ export abstract class MongoBucketBatch
|
|
|
640
736
|
throw e;
|
|
641
737
|
}
|
|
642
738
|
},
|
|
643
|
-
{ maxCommitTimeMS: 10000 }
|
|
644
|
-
)
|
|
739
|
+
{ maxCommitTimeMS: 10000, writeConcern: { w: 'majority' } }
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private async withWriterLock<T>(callback: () => Promise<T>): Promise<T> {
|
|
745
|
+
using lockSpan = this.tracer.span('storage', 'internal_lock');
|
|
746
|
+
return this.options.replicationLock.writerMutex.exclusiveLock(async () => {
|
|
747
|
+
lockSpan.end();
|
|
748
|
+
this.options.replicationLock.throwIfAborted();
|
|
749
|
+
return callback();
|
|
645
750
|
});
|
|
646
751
|
}
|
|
647
752
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
753
|
+
/** Single-document updates enforce ownership atomically, without a separate transaction. */
|
|
754
|
+
protected async updateStreamMetadata(
|
|
755
|
+
set: mongo.Document,
|
|
756
|
+
filter: mongo.Document = {},
|
|
757
|
+
writeConcern: mongo.WriteConcernSettings = { w: 'majority' }
|
|
651
758
|
): Promise<void> {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const start = Date.now();
|
|
655
|
-
const lastTry = start + 90000;
|
|
656
|
-
|
|
657
|
-
const session = this.session;
|
|
658
|
-
|
|
659
|
-
await this.withTransaction(async () => {
|
|
660
|
-
flushTry += 1;
|
|
661
|
-
if (flushTry % 10 == 0) {
|
|
662
|
-
this.logger.info(`${description} - try ${flushTry}`);
|
|
663
|
-
}
|
|
664
|
-
if (flushTry > 20 && Date.now() > lastTry) {
|
|
665
|
-
throw new ServiceError(ErrorCode.PSYNC_S1402, 'Max transaction tries exceeded');
|
|
666
|
-
}
|
|
759
|
+
await this.withWriterLock(() => this.writeStreamMetadata(set, filter, writeConcern));
|
|
760
|
+
}
|
|
667
761
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
{
|
|
673
|
-
$setOnInsert: { op_id: 0n },
|
|
674
|
-
$set: {
|
|
675
|
-
// Force update to ensure we get a mongo lock
|
|
676
|
-
ts: Date.now()
|
|
677
|
-
}
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
upsert: true,
|
|
681
|
-
returnDocument: 'after',
|
|
682
|
-
session
|
|
683
|
-
}
|
|
684
|
-
);
|
|
685
|
-
const opSeq = new MongoIdSequence(next_op_id_doc?.op_id ?? 0n);
|
|
762
|
+
/** Caller must already be inside a fenced transaction holding the writer mutex. */
|
|
763
|
+
protected async updateStreamMetadataInTransaction(set: mongo.Document, filter: mongo.Document = {}): Promise<void> {
|
|
764
|
+
await this.writeStreamMetadata(set, filter);
|
|
765
|
+
}
|
|
686
766
|
|
|
687
|
-
|
|
767
|
+
private async writeStreamMetadata(
|
|
768
|
+
set: mongo.Document,
|
|
769
|
+
filter: mongo.Document,
|
|
770
|
+
writeConcern?: mongo.WriteConcernSettings
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
const result = await this.db.sync_rules.updateOne(
|
|
773
|
+
{ ...filter, ...MongoSyncRulesLock.ownerFilter(this.replicationStreamId, this.options.replicationLock) },
|
|
774
|
+
[{ $set: { ...set, ...MongoSyncRulesLock.heartbeatUpdate() } }],
|
|
775
|
+
{ session: this.session, ...(writeConcern == null ? {} : { writeConcern }) }
|
|
776
|
+
);
|
|
777
|
+
if (result.matchedCount === 0) {
|
|
778
|
+
throw new ReplicationAbortedError('Replication writer no longer owns the stream');
|
|
779
|
+
}
|
|
780
|
+
}
|
|
688
781
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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.
|
|
696
821
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
822
|
+
);
|
|
823
|
+
allocator.committed(lastOp);
|
|
824
|
+
return;
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (!(error instanceof OpIdRangeExhausted)) {
|
|
827
|
+
throw error;
|
|
700
828
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
$set: {
|
|
709
|
-
last_keepalive_ts: new Date()
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
{ session }
|
|
713
|
-
);
|
|
714
|
-
|
|
715
|
-
// Allow subclasses to persist additional flush-time state in the same transaction
|
|
716
|
-
// (e.g. v3 advances the stream-level last_persisted_op).
|
|
717
|
-
await this.onReplicationTransactionFlush(session, opSeq.last());
|
|
718
|
-
// We don't notify checkpoint here - we don't make any checkpoint updates directly
|
|
719
|
-
});
|
|
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
|
+
}
|
|
835
|
+
}
|
|
720
836
|
}
|
|
721
837
|
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
*/
|
|
728
|
-
protected async onReplicationTransactionFlush(_session: mongo.ClientSession, _lastOp: InternalOpId): Promise<void> {
|
|
729
|
-
// No-op by default (v1 behaviour unchanged).
|
|
730
|
-
}
|
|
838
|
+
/** Highest durably persisted operation, including operations not yet checkpointed. */
|
|
839
|
+
protected abstract persistedOpHead(stream: SyncRuleDocumentBase): InternalOpId;
|
|
840
|
+
|
|
841
|
+
/** Advance the persisted head in the same transaction as the operations. */
|
|
842
|
+
protected abstract onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: InternalOpId): void;
|
|
731
843
|
|
|
732
844
|
/**
|
|
733
845
|
* Called after a replication transaction has successfully committed, with the last persisted op id.
|
|
734
846
|
*
|
|
735
847
|
* v1 storage tracks this in memory to fold into the next checkpoint. v3 storage does not need it:
|
|
736
848
|
* the stream-level `last_persisted_op` is already advanced durably by
|
|
737
|
-
* {@link onReplicationTransactionFlush}
|
|
738
|
-
* from the document.
|
|
849
|
+
* {@link onReplicationTransactionFlush} or the combined checkpoint update within the same
|
|
850
|
+
* transaction, and empty commits read it from the document.
|
|
739
851
|
*
|
|
740
852
|
* Keep calls to this adjacent to {@link withReplicationTransaction} usage - both must observe
|
|
741
853
|
* every path that persists ops.
|
|
@@ -762,11 +874,12 @@ export abstract class MongoBucketBatch
|
|
|
762
874
|
async save(record: storage.SaveOptions): Promise<storage.FlushedResult | null> {
|
|
763
875
|
const { after, before, sourceTable, tag } = record;
|
|
764
876
|
const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
|
|
765
|
-
//
|
|
766
|
-
// SourceTables
|
|
767
|
-
//
|
|
877
|
+
// V3 source tables own disjoint event-definition ids for each physical table. Multiple
|
|
878
|
+
// SourceTables may evaluate different events, but each definition is evaluated through
|
|
879
|
+
// at most one record for this row change.
|
|
880
|
+
// Legacy storage leaves eventDefinitionIds undefined and selects by table ref.
|
|
768
881
|
if (sourceTable.syncEvent) {
|
|
769
|
-
for (const event of this.getTableEvents(sourceTable)) {
|
|
882
|
+
for (const { event, eventId } of this.getTableEvents(sourceTable)) {
|
|
770
883
|
this.iterateListeners((cb) =>
|
|
771
884
|
cb.replicationEvent?.({
|
|
772
885
|
batch: this,
|
|
@@ -776,7 +889,8 @@ export abstract class MongoBucketBatch
|
|
|
776
889
|
after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
|
|
777
890
|
before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
|
|
778
891
|
},
|
|
779
|
-
event
|
|
892
|
+
event,
|
|
893
|
+
event_id: eventId
|
|
780
894
|
})
|
|
781
895
|
);
|
|
782
896
|
}
|
|
@@ -812,7 +926,9 @@ export abstract class MongoBucketBatch
|
|
|
812
926
|
|
|
813
927
|
await this.withTransaction(async () => {
|
|
814
928
|
for (let table of sourceTables) {
|
|
815
|
-
await this.db
|
|
929
|
+
await this.db
|
|
930
|
+
.commonSourceTables(this.replicationStreamId)
|
|
931
|
+
.deleteOne({ _id: mongoTableId(table.id) }, { session: this.session });
|
|
816
932
|
}
|
|
817
933
|
});
|
|
818
934
|
|
|
@@ -934,11 +1050,29 @@ export abstract class MongoBucketBatch
|
|
|
934
1050
|
}
|
|
935
1051
|
|
|
936
1052
|
/**
|
|
937
|
-
* Gets relevant {@link
|
|
1053
|
+
* Gets relevant {@link HydratedEventDescriptor}s for the given {@link SourceTable}
|
|
938
1054
|
*/
|
|
939
|
-
protected getTableEvents(
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1055
|
+
protected getTableEvents(
|
|
1056
|
+
table: storage.SourceTable
|
|
1057
|
+
): { event: HydratedEventDescriptor; eventId?: EventDefinitionId }[] {
|
|
1058
|
+
// V3 storage assigns event-definition ids to each source table, so membership is authoritative.
|
|
1059
|
+
// Iterate the table's distinct ids and resolve each through the stream's deduped event map, so a
|
|
1060
|
+
// definition reused across configs has one evaluator for that id. The evaluator may still return
|
|
1061
|
+
// multiple payloads from matching queries. Legacy storage leaves this undefined and selects by table ref.
|
|
1062
|
+
if (table.eventDefinitionIds != null) {
|
|
1063
|
+
const eventById = this.options.parsedSyncConfig.eventById;
|
|
1064
|
+
const events: { event: HydratedEventDescriptor; eventId: EventDefinitionId }[] = [];
|
|
1065
|
+
for (const id of table.eventDefinitionIds) {
|
|
1066
|
+
const event = eventById.get(id);
|
|
1067
|
+
if (event != null && event.tableTriggersEvent(table.ref)) {
|
|
1068
|
+
events.push({ event, eventId: id });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return events;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return this.sync_rules.eventDescriptors
|
|
1075
|
+
.filter((event) => event.tableTriggersEvent(table.ref))
|
|
1076
|
+
.map((event) => ({ event }));
|
|
943
1077
|
}
|
|
944
1078
|
}
|