@powersync/service-module-mongodb-storage 0.18.2 → 0.19.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 +48 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +2 -0
- package/dist/storage/implementation/MongoChecksums.js +3 -2
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -5
- package/dist/storage/implementation/MongoSyncBucketStorage.js +25 -14
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +16 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +20 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +3 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +109 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +38 -1
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoChecksums.ts +5 -2
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +34 -24
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +16 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +54 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +3 -1
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +123 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +40 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +95 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { storage } from '@powersync/service-core';
|
|
3
|
+
import { addChecksums, storage, utils } from '@powersync/service-core';
|
|
4
4
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
|
-
import {
|
|
5
|
+
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
6
|
import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
|
|
7
|
-
import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
|
|
8
|
-
import {
|
|
7
|
+
import { CurrentBucketState, DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
|
|
8
|
+
import { cacheKey } from '../OperationBatch.js';
|
|
9
|
+
import { BucketDataDocumentV1, BucketStateDocumentV1 } from './models.js';
|
|
9
10
|
import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
|
|
10
11
|
import { SingleBucketStoreV1 } from './SingleBucketStoreV1.js';
|
|
11
12
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
12
13
|
|
|
14
|
+
type CompactClearProperties = 'op' | 'checksum' | 'target_op';
|
|
15
|
+
|
|
13
16
|
export class MongoCompactorV1 extends MongoCompactor {
|
|
14
17
|
// Override types to the more specific ones
|
|
15
18
|
declare protected readonly db: VersionedPowerSyncMongoV1;
|
|
16
19
|
declare protected readonly storage: MongoSyncBucketStorageV1;
|
|
17
20
|
|
|
21
|
+
private updates: mongo.AnyBulkWriteOperation<BucketDataDocumentV1>[] = [];
|
|
22
|
+
|
|
18
23
|
public async *dirtyBucketBatches(options: {
|
|
19
24
|
minBucketChanges: number;
|
|
20
25
|
minChangeRatio: number;
|
|
@@ -81,14 +86,293 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
bucket: string,
|
|
86
|
-
_definitionId: BucketDefinitionId | null
|
|
87
|
-
): Promise<SingleBucketStore | null> {
|
|
89
|
+
private getBucketDataContext(bucket: string): SingleBucketStoreV1 {
|
|
88
90
|
return new SingleBucketStoreV1(this.db, {
|
|
89
91
|
replicationStreamId: this.group_id,
|
|
90
92
|
definitionId: LEGACY_BUCKET_DATA_DEFINITION_ID,
|
|
91
93
|
bucket
|
|
92
94
|
});
|
|
93
95
|
}
|
|
96
|
+
|
|
97
|
+
protected async compactSingleBucket(bucket: string) {
|
|
98
|
+
// Do not carry queued writes from a failed attempt into the rescan
|
|
99
|
+
this.updates = [];
|
|
100
|
+
|
|
101
|
+
const idLimitBytes = this.idLimitBytes;
|
|
102
|
+
const bucketContext = this.getBucketDataContext(bucket);
|
|
103
|
+
const currentState: CurrentBucketState = {
|
|
104
|
+
bucket,
|
|
105
|
+
definitionId: bucketContext.key.definitionId,
|
|
106
|
+
seen: new Map(),
|
|
107
|
+
trackingSize: 0,
|
|
108
|
+
lastNotPut: null,
|
|
109
|
+
opsSincePut: 0,
|
|
110
|
+
checksum: 0,
|
|
111
|
+
opCount: 0,
|
|
112
|
+
opBytes: 0
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Constant lower bound.
|
|
116
|
+
const lowerBound = bucketContext.minId;
|
|
117
|
+
// Upper bound is adjusted for each batch.
|
|
118
|
+
let upperBound = bucketContext.maxId;
|
|
119
|
+
|
|
120
|
+
// Paginate through bucket data in batches to avoid cursor timeouts.
|
|
121
|
+
while (true) {
|
|
122
|
+
this.signal?.throwIfAborted();
|
|
123
|
+
|
|
124
|
+
// Query one batch at a time, to avoid cursor timeouts.
|
|
125
|
+
const pipeline = [
|
|
126
|
+
{
|
|
127
|
+
$match: {
|
|
128
|
+
_id: {
|
|
129
|
+
$gte: lowerBound,
|
|
130
|
+
$lt: upperBound
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ $sort: { _id: -1 } },
|
|
135
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
136
|
+
{
|
|
137
|
+
$project: {
|
|
138
|
+
_id: 1,
|
|
139
|
+
op: 1,
|
|
140
|
+
table: 1,
|
|
141
|
+
row_id: 1,
|
|
142
|
+
source_table: 1,
|
|
143
|
+
source_key: 1,
|
|
144
|
+
checksum: 1,
|
|
145
|
+
size: { $bsonSize: '$$ROOT' }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const cursor = bucketContext.collection.aggregate<BucketDataDocumentV1 & { size: number | bigint }>(pipeline, {
|
|
151
|
+
// batchSize is 1 more than limit to auto-close the cursor.
|
|
152
|
+
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
153
|
+
batchSize: this.moveBatchQueryLimit + 1
|
|
154
|
+
});
|
|
155
|
+
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
156
|
+
// Instead, we load up to the limit.
|
|
157
|
+
const rawBatch = await cursor.toArray();
|
|
158
|
+
const batch = rawBatch.map((document) => {
|
|
159
|
+
const { size, ...rest } = document;
|
|
160
|
+
return {
|
|
161
|
+
doc: bucketContext.fromPersistedDocument(rest),
|
|
162
|
+
size
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (batch.length == 0) {
|
|
167
|
+
// We've reached the end.
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Reuse the exact collection _id value from Mongo for the next bound.
|
|
172
|
+
upperBound = rawBatch[rawBatch.length - 1]._id;
|
|
173
|
+
|
|
174
|
+
for (const { doc, size } of batch) {
|
|
175
|
+
if (doc.o > this.maxOpId) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
currentState.checksum = addChecksums(currentState.checksum, Number(doc.checksum));
|
|
180
|
+
currentState.opCount += 1;
|
|
181
|
+
|
|
182
|
+
let isPersistentPut = doc.op == 'PUT';
|
|
183
|
+
|
|
184
|
+
currentState.opBytes += Number(size);
|
|
185
|
+
if (doc.op == 'REMOVE' || doc.op == 'PUT') {
|
|
186
|
+
const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table!, doc.source_key!)}`;
|
|
187
|
+
const targetOp = currentState.seen.get(key);
|
|
188
|
+
if (targetOp) {
|
|
189
|
+
// Will convert to MOVE, so don't count as PUT.
|
|
190
|
+
isPersistentPut = false;
|
|
191
|
+
|
|
192
|
+
this.updates.push({
|
|
193
|
+
updateOne: {
|
|
194
|
+
filter: { _id: bucketContext.docId(doc.o) },
|
|
195
|
+
update: {
|
|
196
|
+
$set: {
|
|
197
|
+
op: 'MOVE',
|
|
198
|
+
target_op: targetOp
|
|
199
|
+
},
|
|
200
|
+
$unset: {
|
|
201
|
+
source_table: 1,
|
|
202
|
+
source_key: 1,
|
|
203
|
+
table: 1,
|
|
204
|
+
row_id: 1,
|
|
205
|
+
data: 1
|
|
206
|
+
}
|
|
207
|
+
} satisfies mongo.UpdateFilter<BucketDataDocumentV1>
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// TODO: better estimate for this.
|
|
212
|
+
currentState.opBytes += 200 - Number(size);
|
|
213
|
+
} else if (currentState.trackingSize < idLimitBytes) {
|
|
214
|
+
// flatstr reduces the memory usage by flattening the string.
|
|
215
|
+
currentState.seen.set(utils.flatstr(key), doc.o);
|
|
216
|
+
// length + 16 for the string
|
|
217
|
+
// 24 for the bigint
|
|
218
|
+
// 50 for map overhead
|
|
219
|
+
// 50 for additional overhead
|
|
220
|
+
currentState.trackingSize += key.length + 140;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (isPersistentPut) {
|
|
225
|
+
currentState.lastNotPut = null;
|
|
226
|
+
currentState.opsSincePut = 0;
|
|
227
|
+
} else if (doc.op != 'CLEAR') {
|
|
228
|
+
if (currentState.lastNotPut == null) {
|
|
229
|
+
currentState.lastNotPut = doc.o;
|
|
230
|
+
}
|
|
231
|
+
currentState.opsSincePut += 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.updates.length + this.bucketStateUpdates.length >= this.moveBatchLimit) {
|
|
235
|
+
await this.flush(bucketContext);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Free memory before clearing the bucket.
|
|
243
|
+
currentState.seen.clear();
|
|
244
|
+
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
245
|
+
this.logger.info(
|
|
246
|
+
`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
247
|
+
);
|
|
248
|
+
// Need flush() before clear().
|
|
249
|
+
await this.flush(bucketContext);
|
|
250
|
+
await this.clearBucket(currentState, bucketContext);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Do this after clearBucket so we have accurate counts.
|
|
254
|
+
this.updateBucketChecksums(currentState, this.maxOpId);
|
|
255
|
+
// Need another flush after updateBucketChecksums().
|
|
256
|
+
await this.flush(bucketContext);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async flush(bucketContext: SingleBucketStoreV1) {
|
|
260
|
+
if (this.updates.length > 0) {
|
|
261
|
+
this.logger.info(`Compacting ${this.updates.length} ops`);
|
|
262
|
+
await bucketContext.collection.bulkWrite(this.updates, {
|
|
263
|
+
// Order is not important. Since checksums are not affected, these operations can happen in any order,
|
|
264
|
+
// and it's fine if the operations are partially applied. Each individual operation is atomic.
|
|
265
|
+
ordered: false
|
|
266
|
+
});
|
|
267
|
+
this.updates = [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await this.flushBucketStateUpdates();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Perform a CLEAR compact for a bucket.
|
|
275
|
+
*
|
|
276
|
+
* @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
|
|
277
|
+
*/
|
|
278
|
+
private async clearBucket(currentState: CurrentBucketState, bucketContext: SingleBucketStoreV1) {
|
|
279
|
+
const clearOp = currentState.lastNotPut!;
|
|
280
|
+
|
|
281
|
+
const opFilter = {
|
|
282
|
+
_id: {
|
|
283
|
+
$gte: bucketContext.minId,
|
|
284
|
+
$lte: bucketContext.docId(clearOp)
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const session = this.db.client.startSession();
|
|
289
|
+
try {
|
|
290
|
+
let done = false;
|
|
291
|
+
while (!done) {
|
|
292
|
+
this.signal?.throwIfAborted();
|
|
293
|
+
let opCountDiff = 0;
|
|
294
|
+
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
295
|
+
// The state after each batch is fully consistent.
|
|
296
|
+
// We need a transaction per batch to make sure checksums stay consistent.
|
|
297
|
+
await session.withTransaction(
|
|
298
|
+
async () => {
|
|
299
|
+
const query = bucketContext.collection.find<Pick<BucketDataDocumentV1, '_id' | CompactClearProperties>>(
|
|
300
|
+
opFilter,
|
|
301
|
+
{
|
|
302
|
+
session,
|
|
303
|
+
sort: { _id: 1 },
|
|
304
|
+
projection: {
|
|
305
|
+
_id: 1,
|
|
306
|
+
op: 1,
|
|
307
|
+
checksum: 1,
|
|
308
|
+
target_op: 1
|
|
309
|
+
},
|
|
310
|
+
limit: this.clearBatchLimit
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
let checksum = 0;
|
|
314
|
+
let lastOp: Pick<BucketDataDoc, 'o' | CompactClearProperties> | null = null;
|
|
315
|
+
let targetOp: bigint | null = null;
|
|
316
|
+
let gotAnOp = false;
|
|
317
|
+
let numberOfOpsToClear = 0;
|
|
318
|
+
for await (const rawOp of query.stream()) {
|
|
319
|
+
const op = bucketContext.fromPartialPersistedDocument(rawOp);
|
|
320
|
+
|
|
321
|
+
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
322
|
+
checksum = utils.addChecksums(checksum, Number(op.checksum));
|
|
323
|
+
lastOp = op;
|
|
324
|
+
numberOfOpsToClear += 1;
|
|
325
|
+
if (op.op != 'CLEAR') {
|
|
326
|
+
gotAnOp = true;
|
|
327
|
+
}
|
|
328
|
+
if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
|
|
329
|
+
targetOp = op.target_op;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!gotAnOp) {
|
|
336
|
+
done = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
341
|
+
await bucketContext.collection.deleteMany(
|
|
342
|
+
{
|
|
343
|
+
_id: {
|
|
344
|
+
$gte: bucketContext.minId,
|
|
345
|
+
$lte: bucketContext.docId(lastOp!.o)
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
{ session }
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const op = bucketContext.toPersistedDocument({
|
|
352
|
+
o: lastOp!.o,
|
|
353
|
+
op: 'CLEAR',
|
|
354
|
+
checksum: BigInt(checksum),
|
|
355
|
+
data: null,
|
|
356
|
+
target_op: targetOp
|
|
357
|
+
});
|
|
358
|
+
await bucketContext.collection.insertOne(op, { session });
|
|
359
|
+
|
|
360
|
+
opCountDiff = -numberOfOpsToClear + 1;
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
writeConcern: { w: 'majority' },
|
|
364
|
+
readConcern: { level: 'snapshot' }
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
// Update outside the transaction, since the transaction can be retried multiple times.
|
|
368
|
+
currentState.opCount += opCountDiff;
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
await session.endSession();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private formatBucketDataKey(doc: Pick<BucketDataDoc, 'bucketKey' | 'o'>) {
|
|
376
|
+
return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
|
|
377
|
+
}
|
|
94
378
|
}
|
|
@@ -16,7 +16,15 @@ import {
|
|
|
16
16
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
17
17
|
import { ParameterLookupRows, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
|
|
18
18
|
import * as bson from 'bson';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
clearCollectionInIdBatches,
|
|
21
|
+
clearCollectionInIdRanges,
|
|
22
|
+
idPrefixFilter,
|
|
23
|
+
mapOpEntry,
|
|
24
|
+
readSingleBatch,
|
|
25
|
+
retryOnMongoMaxTimeMSExpired,
|
|
26
|
+
setSessionSnapshotTime
|
|
27
|
+
} from '../../../utils/util.js';
|
|
20
28
|
import { MongoBucketStorage } from '../../MongoBucketStorage.js';
|
|
21
29
|
import { MongoSyncBucketStorageCheckpoint } from '../common/MongoSyncBucketStorageCheckpoint.js';
|
|
22
30
|
import { SourceKey } from '../models.js';
|
|
@@ -214,64 +222,59 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
protected async clearBucketData(signal?: AbortSignal): Promise<void> {
|
|
217
|
-
await
|
|
225
|
+
await clearCollectionInIdRanges(
|
|
226
|
+
this.logger,
|
|
218
227
|
'bucket data',
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
),
|
|
226
|
-
signal
|
|
228
|
+
this.db.bucket_data,
|
|
229
|
+
{
|
|
230
|
+
_id: idPrefixFilter<BucketDataKeyV1>({ g: this.replicationStreamId }, ['b', 'o'])
|
|
231
|
+
},
|
|
232
|
+
signal,
|
|
233
|
+
this.clearBatchThrottleRate
|
|
227
234
|
);
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
protected async clearParameterIndexes(signal?: AbortSignal): Promise<void> {
|
|
231
|
-
await
|
|
238
|
+
await clearCollectionInIdBatches(
|
|
239
|
+
this.logger,
|
|
232
240
|
'parameter index',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
),
|
|
240
|
-
signal
|
|
241
|
+
this.db.parameterIndexV1,
|
|
242
|
+
{
|
|
243
|
+
'key.g': this.replicationStreamId
|
|
244
|
+
},
|
|
245
|
+
signal,
|
|
246
|
+
this.clearBatchThrottleRate
|
|
241
247
|
);
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
protected async clearSourceRecords(signal?: AbortSignal): Promise<void> {
|
|
245
|
-
await
|
|
251
|
+
await clearCollectionInIdRanges(
|
|
252
|
+
this.logger,
|
|
246
253
|
'source records',
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
),
|
|
254
|
-
signal
|
|
254
|
+
this.db.sourceRecordsV1,
|
|
255
|
+
{
|
|
256
|
+
_id: idPrefixFilter<SourceKey>({ g: this.replicationStreamId }, ['t', 'k'])
|
|
257
|
+
},
|
|
258
|
+
signal,
|
|
259
|
+
this.clearBatchThrottleRate
|
|
255
260
|
);
|
|
256
261
|
}
|
|
257
262
|
|
|
258
263
|
protected async clearBucketState(signal?: AbortSignal): Promise<void> {
|
|
259
|
-
await
|
|
264
|
+
await clearCollectionInIdRanges(
|
|
265
|
+
this.logger,
|
|
260
266
|
'bucket state',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
),
|
|
268
|
-
signal
|
|
267
|
+
this.db.bucketStateV1,
|
|
268
|
+
{
|
|
269
|
+
_id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.replicationStreamId }, ['b'])
|
|
270
|
+
},
|
|
271
|
+
signal,
|
|
272
|
+
this.clearBatchThrottleRate
|
|
269
273
|
);
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
protected async clearSourceTables(signal?: AbortSignal): Promise<void> {
|
|
273
|
-
await
|
|
274
|
-
'source tables',
|
|
277
|
+
await retryOnMongoMaxTimeMSExpired(
|
|
275
278
|
() =>
|
|
276
279
|
this.db.sourceTablesV1(this.replicationStreamId).deleteMany(
|
|
277
280
|
{
|
|
@@ -279,7 +282,17 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
279
282
|
},
|
|
280
283
|
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
281
284
|
),
|
|
282
|
-
|
|
285
|
+
{
|
|
286
|
+
signal,
|
|
287
|
+
abortMessage: 'Aborted clearing data',
|
|
288
|
+
// This is a fairly long delay - only expected to hit this when the storage database is under high load.
|
|
289
|
+
retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS,
|
|
290
|
+
onRetry: () => {
|
|
291
|
+
this.logger.info(
|
|
292
|
+
`Clearing batch of source tables timed out after ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, retrying...`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
283
296
|
);
|
|
284
297
|
}
|
|
285
298
|
|
|
@@ -389,6 +402,7 @@ export async function* getBucketDataBatchV1(
|
|
|
389
402
|
|
|
390
403
|
if (session != null) {
|
|
391
404
|
session.advanceOperationTime(checkpoint.snapshotTime);
|
|
405
|
+
session.advanceClusterTime(checkpoint.clusterTime);
|
|
392
406
|
}
|
|
393
407
|
|
|
394
408
|
let filters: mongo.Filter<BucketDataDocumentV1>[] = [];
|
|
@@ -1,58 +1,50 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { InternalOpId } from '@powersync/service-core';
|
|
3
3
|
import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
|
|
4
|
-
import {
|
|
5
|
-
BucketDataDocumentGeneric,
|
|
6
|
-
BucketDataDocumentGenericId,
|
|
7
|
-
SingleBucketStore
|
|
8
|
-
} from '../common/SingleBucketStore.js';
|
|
9
4
|
import { BucketDataProperties } from '../models.js';
|
|
10
5
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
11
6
|
import { BucketDataDocumentV1, BucketDataKeyV1, serializeBucketDataV1 } from './models.js';
|
|
12
7
|
|
|
13
|
-
export class SingleBucketStoreV1
|
|
14
|
-
public readonly collection: mongo.Collection<
|
|
8
|
+
export class SingleBucketStoreV1 {
|
|
9
|
+
public readonly collection: mongo.Collection<BucketDataDocumentV1>;
|
|
15
10
|
|
|
16
11
|
constructor(
|
|
17
|
-
|
|
12
|
+
db: VersionedPowerSyncMongoV1,
|
|
18
13
|
public readonly key: BucketKey
|
|
19
14
|
) {
|
|
20
|
-
this.collection = db.bucketDataV1
|
|
15
|
+
this.collection = db.bucketDataV1;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
docId(o: InternalOpId):
|
|
24
|
-
// `satisfies BucketDataKeyV1` checks that we use the correct type for V1 storage
|
|
25
|
-
// `as anyt` is to allow casting to the interface virtual type
|
|
18
|
+
docId(o: InternalOpId): BucketDataKeyV1 {
|
|
26
19
|
return {
|
|
27
20
|
g: this.key.replicationStreamId,
|
|
28
21
|
b: this.key.bucket,
|
|
29
22
|
o
|
|
30
|
-
}
|
|
23
|
+
};
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
get minId():
|
|
26
|
+
get minId(): BucketDataKeyV1 {
|
|
34
27
|
return {
|
|
35
28
|
g: this.key.replicationStreamId,
|
|
36
29
|
b: this.key.bucket,
|
|
37
|
-
o: new mongo.MinKey()
|
|
38
|
-
}
|
|
30
|
+
o: new mongo.MinKey() as any
|
|
31
|
+
};
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
get maxId():
|
|
34
|
+
get maxId(): BucketDataKeyV1 {
|
|
42
35
|
return {
|
|
43
36
|
g: this.key.replicationStreamId,
|
|
44
37
|
b: this.key.bucket,
|
|
45
|
-
o: new mongo.MaxKey()
|
|
46
|
-
}
|
|
38
|
+
o: new mongo.MaxKey() as any
|
|
39
|
+
};
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>):
|
|
50
|
-
return serializeBucketDataV1({ bucketKey: this.key, ...source })
|
|
42
|
+
toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentV1 {
|
|
43
|
+
return serializeBucketDataV1({ bucketKey: this.key, ...source });
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
fromPersistedDocument(doc:
|
|
54
|
-
const
|
|
55
|
-
const { _id, ...rest } = document;
|
|
46
|
+
fromPersistedDocument(doc: BucketDataDocumentV1): BucketDataDoc {
|
|
47
|
+
const { _id, ...rest } = doc;
|
|
56
48
|
return {
|
|
57
49
|
bucketKey: this.key,
|
|
58
50
|
o: _id.o,
|
|
@@ -61,10 +53,9 @@ export class SingleBucketStoreV1 implements SingleBucketStore {
|
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
|
|
64
|
-
doc: Pick<
|
|
56
|
+
doc: Pick<BucketDataDocumentV1, '_id' | T>
|
|
65
57
|
): Pick<BucketDataDoc, 'bucketKey' | 'o' | T> {
|
|
66
|
-
const
|
|
67
|
-
const { _id, ...rest } = document;
|
|
58
|
+
const { _id, ...rest } = doc;
|
|
68
59
|
return {
|
|
69
60
|
bucketKey: this.key,
|
|
70
61
|
o: _id.o,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { InternalOpId } from '@powersync/service-core';
|
|
3
|
+
import { BucketKey } from '../common/BucketDataDoc.js';
|
|
4
|
+
import { BucketDataKey } from '../models.js';
|
|
5
|
+
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
6
|
+
import { BucketDataDocumentV3 } from './models.js';
|
|
7
|
+
|
|
8
|
+
export class BucketDataContextV3 {
|
|
9
|
+
public readonly collection: mongo.Collection<BucketDataDocumentV3>;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
db: VersionedPowerSyncMongoV3,
|
|
13
|
+
public readonly key: BucketKey
|
|
14
|
+
) {
|
|
15
|
+
this.collection = db.bucketData(key.replicationStreamId, key.definitionId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
docId(o: InternalOpId): BucketDataKey {
|
|
19
|
+
return {
|
|
20
|
+
b: this.key.bucket,
|
|
21
|
+
o
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get minId(): BucketDataKey {
|
|
26
|
+
// MongoDB's MinKey sentinel does not match the bigint type for _id.o.
|
|
27
|
+
return {
|
|
28
|
+
b: this.key.bucket,
|
|
29
|
+
o: new mongo.MinKey()
|
|
30
|
+
} as unknown as BucketDataKey;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -43,7 +43,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
43
43
|
|
|
44
44
|
protected createPersistedBatch(writtenSize: number): PersistedBatch {
|
|
45
45
|
return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
|
|
46
|
-
logger: this.logger
|
|
46
|
+
logger: this.logger,
|
|
47
|
+
objectStorage: this.options.objectStorage,
|
|
48
|
+
inlineThresholdBytes: this.options.inlineThresholdBytes
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|