@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,3 +1,5 @@
|
|
|
1
|
+
import * as timers from 'node:timers/promises';
|
|
2
|
+
|
|
1
3
|
import { isMongoServerError, mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
4
|
import {
|
|
3
5
|
logger as defaultLogger,
|
|
@@ -5,24 +7,16 @@ import {
|
|
|
5
7
|
ReplicationAssertionError,
|
|
6
8
|
ServiceAssertionError
|
|
7
9
|
} from '@powersync/lib-services-framework';
|
|
8
|
-
import {
|
|
9
|
-
addChecksums,
|
|
10
|
-
InternalOpId,
|
|
11
|
-
isPartialChecksum,
|
|
12
|
-
PopulateChecksumCacheResults,
|
|
13
|
-
storage,
|
|
14
|
-
utils
|
|
15
|
-
} from '@powersync/service-core';
|
|
10
|
+
import { InternalOpId, isPartialChecksum, PopulateChecksumCacheResults, storage } from '@powersync/service-core';
|
|
16
11
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
17
12
|
|
|
18
|
-
import {
|
|
19
|
-
import { BucketDataDocumentGeneric, SingleBucketStore } from './common/SingleBucketStore.js';
|
|
13
|
+
import { BucketKey } from './common/BucketDataDoc.js';
|
|
20
14
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
21
15
|
import { BucketStateDocumentBase } from './models.js';
|
|
22
16
|
import type { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
|
|
23
|
-
import {
|
|
17
|
+
import { isRetryableObjectStorageError } from './v3/object-storage/ObjectStorage.js';
|
|
24
18
|
|
|
25
|
-
interface CurrentBucketState {
|
|
19
|
+
export interface CurrentBucketState {
|
|
26
20
|
/** Bucket name */
|
|
27
21
|
bucket: string;
|
|
28
22
|
definitionId: BucketDefinitionId;
|
|
@@ -56,8 +50,6 @@ interface CurrentBucketState {
|
|
|
56
50
|
opBytes: number;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
type CompactClearProperties = 'op' | 'checksum' | 'target_op';
|
|
60
|
-
|
|
61
53
|
export interface MongoCompactOptions extends storage.CompactOptions {}
|
|
62
54
|
|
|
63
55
|
const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
|
|
@@ -69,6 +61,15 @@ const DEFAULT_MIN_CHANGE_RATIO = 0.1;
|
|
|
69
61
|
const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
|
|
70
62
|
/** This default is primarily for tests. */
|
|
71
63
|
const DEFAULT_MEMORY_LIMIT_MB = 64;
|
|
64
|
+
const COMPACTION_RETRY_LIMIT = 3;
|
|
65
|
+
const COMPACTION_RETRY_DELAY_MS = 1_000;
|
|
66
|
+
|
|
67
|
+
export class ConcurrentCompactionError extends Error {
|
|
68
|
+
constructor(message: string) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'ConcurrentCompactionError';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
72
73
|
|
|
73
74
|
export interface DirtyBucket {
|
|
74
75
|
bucket: string;
|
|
@@ -78,7 +79,6 @@ export interface DirtyBucket {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export abstract class MongoCompactor {
|
|
81
|
-
protected updates: mongo.AnyBulkWriteOperation<BucketDataDocumentGeneric>[] = [];
|
|
82
82
|
protected bucketStateUpdates: mongo.AnyBulkWriteOperation<BucketStateDocumentBase>[] = [];
|
|
83
83
|
|
|
84
84
|
protected readonly idLimitBytes: number;
|
|
@@ -90,6 +90,7 @@ export abstract class MongoCompactor {
|
|
|
90
90
|
protected readonly minChangeRatio: number;
|
|
91
91
|
protected readonly maxOpId: bigint;
|
|
92
92
|
protected readonly buckets: string[] | undefined;
|
|
93
|
+
protected readonly deleteCheckpointRequestsBefore: Date | undefined;
|
|
93
94
|
protected readonly signal?: AbortSignal;
|
|
94
95
|
protected readonly group_id: number;
|
|
95
96
|
|
|
@@ -113,6 +114,7 @@ export abstract class MongoCompactor {
|
|
|
113
114
|
this.minChangeRatio = options.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
|
|
114
115
|
this.maxOpId = options.maxOpId ?? 0n;
|
|
115
116
|
this.buckets = options.compactBuckets;
|
|
117
|
+
this.deleteCheckpointRequestsBefore = options.deleteCheckpointRequestsBefore;
|
|
116
118
|
this.signal = options.signal;
|
|
117
119
|
this.logger = options.logger ?? defaultLogger;
|
|
118
120
|
}
|
|
@@ -123,6 +125,8 @@ export abstract class MongoCompactor {
|
|
|
123
125
|
* See /docs/storage/compacting-operations.md for details.
|
|
124
126
|
*/
|
|
125
127
|
async compact() {
|
|
128
|
+
await this.deleteOldCheckpointRequests();
|
|
129
|
+
|
|
126
130
|
if (this.buckets) {
|
|
127
131
|
for (const bucket of this.buckets) {
|
|
128
132
|
// We can make this more efficient later on by iterating through the buckets in a single query.
|
|
@@ -134,6 +138,25 @@ export abstract class MongoCompactor {
|
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
private async deleteOldCheckpointRequests() {
|
|
142
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.signal?.throwIfAborted();
|
|
147
|
+
// The explicit $exists guarantees the query is a subset of the
|
|
148
|
+
// checkpoint_requested_at partial index's filter, so the planner can use it.
|
|
149
|
+
// Keep pending requests during replication lag so a retry doesn't advance the associated
|
|
150
|
+
// LSN even further.
|
|
151
|
+
await this.db.write_checkpoints.deleteMany({
|
|
152
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore },
|
|
153
|
+
processed_at_lsn: { $ne: null }
|
|
154
|
+
});
|
|
155
|
+
await this.db.custom_write_checkpoints.deleteMany({
|
|
156
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
/**
|
|
138
161
|
* Subset of compact, only populating checksums where relevant.
|
|
139
162
|
*/
|
|
@@ -308,193 +331,54 @@ export abstract class MongoCompactor {
|
|
|
308
331
|
/**
|
|
309
332
|
* Compaction for a single bucket, with retries on failure.
|
|
310
333
|
*
|
|
311
|
-
*
|
|
334
|
+
* A compaction can race another compactor after its initial scan. Restarting
|
|
335
|
+
* the bucket is safe because each replacement is transactional. Object
|
|
336
|
+
* storage paths are also prepared with lifecycle markers, so a retry can
|
|
337
|
+
* safely overwrite or eventually clean up uploads from the failed attempt.
|
|
312
338
|
*/
|
|
313
339
|
protected async compactSingleBucketRetried(bucket: string, definitionId: BucketDefinitionId | null = null) {
|
|
314
340
|
let retryCount = 0;
|
|
315
|
-
// Retry with exponential backoff up to 3 times on MongoDB errors.
|
|
316
341
|
while (true) {
|
|
342
|
+
this.signal?.throwIfAborted();
|
|
343
|
+
// Do not carry queued bucket state writes from a failed attempt into the rescan.
|
|
344
|
+
this.bucketStateUpdates = [];
|
|
317
345
|
try {
|
|
318
346
|
await this.compactSingleBucket(bucket, definitionId);
|
|
319
|
-
|
|
347
|
+
return;
|
|
320
348
|
} catch (e) {
|
|
321
|
-
if (
|
|
322
|
-
this.logger.warn(`Error compacting bucket ${bucket}, retrying...`, e);
|
|
323
|
-
retryCount++;
|
|
324
|
-
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount));
|
|
325
|
-
} else {
|
|
349
|
+
if (this.signal?.aborted) {
|
|
326
350
|
throw e;
|
|
327
351
|
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
352
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (bucketContext == null) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
const currentState: CurrentBucketState = {
|
|
339
|
-
bucket,
|
|
340
|
-
definitionId: bucketContext.key.definitionId,
|
|
341
|
-
seen: new Map(),
|
|
342
|
-
trackingSize: 0,
|
|
343
|
-
lastNotPut: null,
|
|
344
|
-
opsSincePut: 0,
|
|
345
|
-
checksum: 0,
|
|
346
|
-
opCount: 0,
|
|
347
|
-
opBytes: 0
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
// Constant lower bound.
|
|
351
|
-
const lowerBound = bucketContext.minId;
|
|
352
|
-
// Upper bound is adjusted for each batch.
|
|
353
|
-
let upperBound = bucketContext.maxId;
|
|
354
|
-
|
|
355
|
-
// Paginate through bucket data in batches to avoid cursor timeouts.
|
|
356
|
-
while (true) {
|
|
357
|
-
this.signal?.throwIfAborted();
|
|
358
|
-
|
|
359
|
-
// Query one batch at a time, to avoid cursor timeouts.
|
|
360
|
-
const pipeline = [
|
|
361
|
-
{
|
|
362
|
-
$match: {
|
|
363
|
-
_id: {
|
|
364
|
-
$gte: lowerBound,
|
|
365
|
-
$lt: upperBound
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
{ $sort: { _id: -1 } },
|
|
370
|
-
{ $limit: this.moveBatchQueryLimit },
|
|
371
|
-
{
|
|
372
|
-
$project: {
|
|
373
|
-
_id: 1,
|
|
374
|
-
op: 1,
|
|
375
|
-
table: 1,
|
|
376
|
-
row_id: 1,
|
|
377
|
-
source_table: 1,
|
|
378
|
-
source_key: 1,
|
|
379
|
-
checksum: 1,
|
|
380
|
-
size: { $bsonSize: '$$ROOT' }
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
];
|
|
384
|
-
|
|
385
|
-
const cursor = bucketContext.collection.aggregate<BucketDataDocumentGeneric & { size: number | bigint }>(
|
|
386
|
-
pipeline,
|
|
387
|
-
{
|
|
388
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
389
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
390
|
-
batchSize: this.moveBatchQueryLimit + 1
|
|
391
|
-
}
|
|
392
|
-
);
|
|
393
|
-
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
394
|
-
// Instead, we load up to the limit.
|
|
395
|
-
const rawBatch = await cursor.toArray();
|
|
396
|
-
const batch = rawBatch.map((document) => {
|
|
397
|
-
const { size, ...rest } = document;
|
|
398
|
-
return {
|
|
399
|
-
doc: bucketContext.fromPersistedDocument(rest),
|
|
400
|
-
size
|
|
401
|
-
};
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (batch.length == 0) {
|
|
405
|
-
// We've reached the end.
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Reuse the exact collection _id value from Mongo for the next bound.
|
|
410
|
-
upperBound = rawBatch[rawBatch.length - 1]._id;
|
|
411
|
-
|
|
412
|
-
for (const { doc, size } of batch) {
|
|
413
|
-
if (doc.o > this.maxOpId) {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
currentState.checksum = addChecksums(currentState.checksum, Number(doc.checksum));
|
|
418
|
-
currentState.opCount += 1;
|
|
419
|
-
|
|
420
|
-
let isPersistentPut = doc.op == 'PUT';
|
|
421
|
-
|
|
422
|
-
currentState.opBytes += Number(size);
|
|
423
|
-
if (doc.op == 'REMOVE' || doc.op == 'PUT') {
|
|
424
|
-
const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table!, doc.source_key!)}`;
|
|
425
|
-
const targetOp = currentState.seen.get(key);
|
|
426
|
-
if (targetOp) {
|
|
427
|
-
// Will convert to MOVE, so don't count as PUT.
|
|
428
|
-
isPersistentPut = false;
|
|
429
|
-
|
|
430
|
-
this.updates.push({
|
|
431
|
-
updateOne: {
|
|
432
|
-
filter: { _id: bucketContext.docId(doc.o) },
|
|
433
|
-
update: {
|
|
434
|
-
$set: {
|
|
435
|
-
op: 'MOVE',
|
|
436
|
-
target_op: targetOp
|
|
437
|
-
},
|
|
438
|
-
$unset: {
|
|
439
|
-
source_table: 1,
|
|
440
|
-
source_key: 1,
|
|
441
|
-
table: 1,
|
|
442
|
-
row_id: 1,
|
|
443
|
-
data: 1
|
|
444
|
-
}
|
|
445
|
-
} satisfies mongo.UpdateFilter<BucketDataDocumentGeneric>
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// TODO: better estimate for this.
|
|
450
|
-
currentState.opBytes += 200 - Number(size);
|
|
451
|
-
} else if (currentState.trackingSize < idLimitBytes) {
|
|
452
|
-
// flatstr reduces the memory usage by flattening the string.
|
|
453
|
-
currentState.seen.set(utils.flatstr(key), doc.o);
|
|
454
|
-
// length + 16 for the string
|
|
455
|
-
// 24 for the bigint
|
|
456
|
-
// 50 for map overhead
|
|
457
|
-
// 50 for additional overhead
|
|
458
|
-
currentState.trackingSize += key.length + 140;
|
|
459
|
-
}
|
|
353
|
+
const retryReason = compactionRetryReason(e);
|
|
354
|
+
if (retryReason == null) {
|
|
355
|
+
throw e;
|
|
460
356
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (currentState.lastNotPut == null) {
|
|
467
|
-
currentState.lastNotPut = doc.o;
|
|
468
|
-
}
|
|
469
|
-
currentState.opsSincePut += 1;
|
|
357
|
+
if (retryCount >= COMPACTION_RETRY_LIMIT) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Failed to compact bucket ${bucket} after ${retryCount + 1} attempts (${retryReason}): ${errorMessage(e)}`,
|
|
360
|
+
{ cause: e }
|
|
361
|
+
);
|
|
470
362
|
}
|
|
471
363
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
364
|
+
retryCount++;
|
|
365
|
+
const delay = COMPACTION_RETRY_DELAY_MS * 2 ** (retryCount - 1);
|
|
366
|
+
this.logger.warn(
|
|
367
|
+
`Error compacting bucket ${bucket} (${retryReason}); retrying in ${delay}ms ` +
|
|
368
|
+
`(attempt ${retryCount + 1}/${COMPACTION_RETRY_LIMIT + 1})`,
|
|
369
|
+
e
|
|
370
|
+
);
|
|
371
|
+
await timers.setTimeout(delay, undefined, this.signal ? { signal: this.signal } : undefined);
|
|
475
372
|
}
|
|
476
|
-
|
|
477
|
-
this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
478
373
|
}
|
|
479
|
-
|
|
480
|
-
// Free memory before clearing the bucket.
|
|
481
|
-
currentState.seen.clear();
|
|
482
|
-
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
483
|
-
this.logger.info(
|
|
484
|
-
`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
485
|
-
);
|
|
486
|
-
// Need flush() before clear().
|
|
487
|
-
await this.flush(bucketContext);
|
|
488
|
-
await this.clearBucket(currentState, bucketContext);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Do this after clearBucket so we have accurate counts.
|
|
492
|
-
this.updateBucketChecksums(currentState);
|
|
493
|
-
// Need another flush after updateBucketChecksums().
|
|
494
|
-
await this.flush(bucketContext);
|
|
495
374
|
}
|
|
496
375
|
|
|
497
|
-
protected
|
|
376
|
+
protected abstract compactSingleBucket(bucket: string, definitionId?: BucketDefinitionId | null): Promise<void>;
|
|
377
|
+
|
|
378
|
+
protected collectBucketStateUpdates(
|
|
379
|
+
state: CurrentBucketState,
|
|
380
|
+
compactedOpId: InternalOpId
|
|
381
|
+
): mongo.AnyBulkWriteOperation<BucketStateDocumentBase> {
|
|
498
382
|
if (state.opCount < 0) {
|
|
499
383
|
throw new ServiceAssertionError(
|
|
500
384
|
`Invalid opCount: ${state.opCount} checksum ${state.checksum} opsSincePut: ${state.opsSincePut} maxOpId: ${this.maxOpId}`
|
|
@@ -506,7 +390,7 @@ export abstract class MongoCompactor {
|
|
|
506
390
|
update: {
|
|
507
391
|
$set: {
|
|
508
392
|
compacted_state: {
|
|
509
|
-
op_id:
|
|
393
|
+
op_id: compactedOpId,
|
|
510
394
|
count: state.opCount,
|
|
511
395
|
checksum: BigInt(state.checksum),
|
|
512
396
|
bytes: state.opBytes
|
|
@@ -526,25 +410,11 @@ export abstract class MongoCompactor {
|
|
|
526
410
|
};
|
|
527
411
|
}
|
|
528
412
|
|
|
529
|
-
protected updateBucketChecksums(state: CurrentBucketState) {
|
|
530
|
-
this.bucketStateUpdates.push(this.collectBucketStateUpdates(state));
|
|
413
|
+
protected updateBucketChecksums(state: CurrentBucketState, compactedOpId: InternalOpId) {
|
|
414
|
+
this.bucketStateUpdates.push(this.collectBucketStateUpdates(state, compactedOpId));
|
|
531
415
|
}
|
|
532
416
|
|
|
533
|
-
protected async
|
|
534
|
-
if (this.updates.length > 0) {
|
|
535
|
-
this.logger.info(`Compacting ${this.updates.length} ops`);
|
|
536
|
-
await col.collection.bulkWrite(this.updates, {
|
|
537
|
-
// Order is not important. Since checksums are not affected, these operations can happen in any order,
|
|
538
|
-
// and it's fine if the operations are partially applied. Each individual operation is atomic.
|
|
539
|
-
ordered: false
|
|
540
|
-
});
|
|
541
|
-
this.updates = [];
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
await this.flushBucketStateUpdates();
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private async flushBucketStateUpdates() {
|
|
417
|
+
protected async flushBucketStateUpdates() {
|
|
548
418
|
if (this.bucketStateUpdates.length > 0) {
|
|
549
419
|
this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
550
420
|
await this.writeBucketStateUpdates();
|
|
@@ -552,108 +422,6 @@ export abstract class MongoCompactor {
|
|
|
552
422
|
}
|
|
553
423
|
}
|
|
554
424
|
|
|
555
|
-
/**
|
|
556
|
-
* Perform a CLEAR compact for a bucket.
|
|
557
|
-
*
|
|
558
|
-
* @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
|
|
559
|
-
*/
|
|
560
|
-
protected async clearBucket(currentState: CurrentBucketState, col: SingleBucketStore) {
|
|
561
|
-
const clearOp = currentState.lastNotPut!;
|
|
562
|
-
|
|
563
|
-
const opFilter = {
|
|
564
|
-
_id: {
|
|
565
|
-
$gte: col.minId,
|
|
566
|
-
$lte: col.docId(clearOp)
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
const session = this.db.client.startSession();
|
|
571
|
-
try {
|
|
572
|
-
let done = false;
|
|
573
|
-
while (!done) {
|
|
574
|
-
this.signal?.throwIfAborted();
|
|
575
|
-
let opCountDiff = 0;
|
|
576
|
-
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
577
|
-
// The state after each batch is fully consistent.
|
|
578
|
-
// We need a transaction per batch to make sure checksums stay consistent.
|
|
579
|
-
await session.withTransaction(
|
|
580
|
-
async () => {
|
|
581
|
-
const query = col.collection.find<Pick<BucketDataDocumentGeneric, '_id' | CompactClearProperties>>(
|
|
582
|
-
opFilter,
|
|
583
|
-
{
|
|
584
|
-
session,
|
|
585
|
-
sort: { _id: 1 },
|
|
586
|
-
projection: {
|
|
587
|
-
_id: 1,
|
|
588
|
-
op: 1,
|
|
589
|
-
checksum: 1,
|
|
590
|
-
target_op: 1
|
|
591
|
-
},
|
|
592
|
-
limit: this.clearBatchLimit
|
|
593
|
-
}
|
|
594
|
-
);
|
|
595
|
-
let checksum = 0;
|
|
596
|
-
let lastOp: Pick<BucketDataDoc, 'o' | CompactClearProperties> | null = null;
|
|
597
|
-
let targetOp: bigint | null = null;
|
|
598
|
-
let gotAnOp = false;
|
|
599
|
-
let numberOfOpsToClear = 0;
|
|
600
|
-
for await (const rawOp of query.stream()) {
|
|
601
|
-
const op = col.fromPartialPersistedDocument(rawOp);
|
|
602
|
-
|
|
603
|
-
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
604
|
-
checksum = utils.addChecksums(checksum, Number(op.checksum));
|
|
605
|
-
lastOp = op;
|
|
606
|
-
numberOfOpsToClear += 1;
|
|
607
|
-
if (op.op != 'CLEAR') {
|
|
608
|
-
gotAnOp = true;
|
|
609
|
-
}
|
|
610
|
-
if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
|
|
611
|
-
targetOp = op.target_op;
|
|
612
|
-
}
|
|
613
|
-
} else {
|
|
614
|
-
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (!gotAnOp) {
|
|
618
|
-
done = true;
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
623
|
-
await col.collection.deleteMany(
|
|
624
|
-
{
|
|
625
|
-
_id: {
|
|
626
|
-
$gte: col.minId,
|
|
627
|
-
$lte: col.docId(lastOp!.o)
|
|
628
|
-
}
|
|
629
|
-
},
|
|
630
|
-
{ session }
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
const op = col.toPersistedDocument({
|
|
634
|
-
o: lastOp!.o,
|
|
635
|
-
op: 'CLEAR',
|
|
636
|
-
checksum: BigInt(checksum),
|
|
637
|
-
data: null,
|
|
638
|
-
target_op: targetOp
|
|
639
|
-
});
|
|
640
|
-
await col.collection.insertOne(op, { session });
|
|
641
|
-
|
|
642
|
-
opCountDiff = -numberOfOpsToClear + 1;
|
|
643
|
-
},
|
|
644
|
-
{
|
|
645
|
-
writeConcern: { w: 'majority' },
|
|
646
|
-
readConcern: { level: 'snapshot' }
|
|
647
|
-
}
|
|
648
|
-
);
|
|
649
|
-
// Update outside the transaction, since the transaction can be retried multiple times.
|
|
650
|
-
currentState.opCount += opCountDiff;
|
|
651
|
-
}
|
|
652
|
-
} finally {
|
|
653
|
-
await session.endSession();
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
425
|
protected async updateChecksumsBatch(buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]) {
|
|
658
426
|
const checksums = await this.computeChecksumsForBuckets(buckets);
|
|
659
427
|
const definitionIdByBucket = new Map(buckets.map((bucket) => [bucket.bucket, bucket.definitionId]));
|
|
@@ -694,23 +462,31 @@ export abstract class MongoCompactor {
|
|
|
694
462
|
await this.flushBucketStateUpdates();
|
|
695
463
|
}
|
|
696
464
|
|
|
697
|
-
protected formatBucketDataKey(doc: Pick<BucketDataDoc, 'bucketKey' | 'o'>) {
|
|
698
|
-
return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
465
|
protected abstract writeBucketStateUpdates(): Promise<void>;
|
|
702
466
|
protected abstract computeChecksumsForBuckets(
|
|
703
467
|
buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]
|
|
704
468
|
): Promise<storage.PartialChecksumMap>;
|
|
705
469
|
protected abstract bucketStateFilter(bucket: string, definitionId: BucketDefinitionId | null): mongo.Document;
|
|
706
|
-
|
|
707
|
-
protected abstract getBucketDataContext(
|
|
708
|
-
bucket: string,
|
|
709
|
-
definitionId: BucketDefinitionId | null
|
|
710
|
-
): Promise<SingleBucketStore | null>;
|
|
711
470
|
}
|
|
712
471
|
|
|
713
472
|
export interface BucketDataCollectionContext<TBucketData extends mongo.Document> {
|
|
714
473
|
bucketKey: BucketKey;
|
|
715
474
|
collection: mongo.Collection<TBucketData>;
|
|
716
475
|
}
|
|
476
|
+
|
|
477
|
+
function compactionRetryReason(error: unknown): string | null {
|
|
478
|
+
if (error instanceof ConcurrentCompactionError) {
|
|
479
|
+
return 'concurrent compaction';
|
|
480
|
+
}
|
|
481
|
+
if (isRetryableObjectStorageError(error)) {
|
|
482
|
+
return 'transient object storage failure';
|
|
483
|
+
}
|
|
484
|
+
if (isMongoServerError(error)) {
|
|
485
|
+
return 'MongoDB failure';
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function errorMessage(error: unknown): string {
|
|
491
|
+
return error instanceof Error ? error.message : String(error);
|
|
492
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { POWERSYNC_VERSION, storage } from '@powersync/service-core';
|
|
4
|
-
import { MongoStorageConfig } from '../../types/types.js';
|
|
4
|
+
import { MongoStorageConfig, normalizeClearBatchThrottleRate } from '../../types/types.js';
|
|
5
5
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
6
6
|
import { MongoReportStorage } from '../MongoReportStorage.js';
|
|
7
7
|
import { PowerSyncMongo } from './db.js';
|
|
8
|
+
import type { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
|
|
8
9
|
|
|
9
10
|
export class MongoStorageProvider implements storage.StorageProvider {
|
|
10
11
|
get type() {
|
|
@@ -23,6 +24,23 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const decodedConfig = MongoStorageConfig.decode(storage as any);
|
|
27
|
+
|
|
28
|
+
let objectStorage: ObjectStorage | undefined;
|
|
29
|
+
if (decodedConfig.object_storage?.type === 's3') {
|
|
30
|
+
// Dynamically import S3ObjectStorage to avoid loading AWS SDK unless needed.
|
|
31
|
+
const { S3ObjectStorage } = await import('./v3/object-storage/S3ObjectStorage.js');
|
|
32
|
+
objectStorage = new S3ObjectStorage({
|
|
33
|
+
bucket: decodedConfig.object_storage.bucket,
|
|
34
|
+
region: decodedConfig.object_storage.region,
|
|
35
|
+
prefix: decodedConfig.object_storage.prefix,
|
|
36
|
+
endpoint: decodedConfig.object_storage.endpoint,
|
|
37
|
+
forcePathStyle: decodedConfig.object_storage.force_path_style,
|
|
38
|
+
accessKeyId: decodedConfig.object_storage.access_key_id,
|
|
39
|
+
secretAccessKey: decodedConfig.object_storage.secret_access_key,
|
|
40
|
+
concurrencyLimit: decodedConfig.object_storage.concurrency_limit
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
const client = lib_mongo.db.createMongoClient(decodedConfig, {
|
|
27
45
|
powersyncVersion: POWERSYNC_VERSION,
|
|
28
46
|
maxPoolSize: resolvedConfig.storage.max_pool_size ?? 8
|
|
@@ -49,11 +67,15 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
49
67
|
const syncStorageFactory = new MongoBucketStorage(database, {
|
|
50
68
|
replicationStreamNamePrefix: resolvedConfig.slot_name_prefix,
|
|
51
69
|
readPreference,
|
|
70
|
+
clearBatchThrottleRate: normalizeClearBatchThrottleRate(decodedConfig.clear_batch_throttle_rate),
|
|
52
71
|
checksumCacheTtlMs: resolvedConfig.api_parameters.bucket_count_cache_ttl_minutes * 60_000,
|
|
53
72
|
// Right now, only MongoDB source databases supports incremental reprocessing.
|
|
54
73
|
// Remove this filter when we support it for other source databases.
|
|
55
74
|
// This assumes a single source connection - revisit if we ever support multiple connections.
|
|
56
|
-
supportsMultipleSyncConfigs: resolvedConfig.connections?.[0]?.type == lib_mongo.MONGO_CONNECTION_TYPE
|
|
75
|
+
supportsMultipleSyncConfigs: resolvedConfig.connections?.[0]?.type == lib_mongo.MONGO_CONNECTION_TYPE,
|
|
76
|
+
|
|
77
|
+
objectStorage,
|
|
78
|
+
inlineThresholdBytes: decodedConfig.object_storage?.inline_threshold_bytes
|
|
57
79
|
});
|
|
58
80
|
|
|
59
81
|
// Storage factory for reports
|
|
@@ -66,8 +88,15 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
66
88
|
await syncStorageFactory[Symbol.asyncDispose]();
|
|
67
89
|
await client.close();
|
|
68
90
|
},
|
|
69
|
-
tearDown: () => {
|
|
91
|
+
tearDown: async () => {
|
|
70
92
|
logger.info(`Tearing down storage: ${database.db.namespace}...`);
|
|
93
|
+
if (objectStorage != null) {
|
|
94
|
+
logger.info(
|
|
95
|
+
`Clearing object storage: ${decodedConfig.object_storage!.bucket}/${decodedConfig.object_storage!.prefix ?? ''}.`
|
|
96
|
+
);
|
|
97
|
+
const { objectCount } = await objectStorage.deletePrefix('bucket-data/');
|
|
98
|
+
logger.info(`Deleted ${objectCount} objects from object storage.`);
|
|
99
|
+
}
|
|
71
100
|
return database.db.dropDatabase();
|
|
72
101
|
},
|
|
73
102
|
onFatalError: (callback) => {
|