@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
|
@@ -3,7 +3,7 @@ import { Logger, ReplicationAbortedError } from '@powersync/lib-services-framewo
|
|
|
3
3
|
import { SingleSyncConfigBucketDefinitionMapping, storage } from '@powersync/service-core';
|
|
4
4
|
import { BucketDefinitionId, ParameterIndexId, SyncConfig } from '@powersync/service-sync-rules';
|
|
5
5
|
import * as bson from 'bson';
|
|
6
|
-
import {
|
|
6
|
+
import { clearCollectionInIdRanges, idPrefixFilter } from '../../../utils/util.js';
|
|
7
7
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
8
8
|
import {
|
|
9
9
|
BucketStateDocumentV3,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
SourceTableDocumentV3,
|
|
12
12
|
SyncConfigDefinition
|
|
13
13
|
} from './models.js';
|
|
14
|
+
import { ObjectStorage } from './object-storage/ObjectStorage.js';
|
|
15
|
+
import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
|
|
14
16
|
|
|
15
17
|
type SyncConfigState = ReplicationStreamDocumentV3['sync_configs'][number];
|
|
16
18
|
|
|
@@ -34,6 +36,8 @@ export interface MongoStoppedSyncConfigCleanupOptions extends storage.CleanupSto
|
|
|
34
36
|
replicationStreamId: number;
|
|
35
37
|
db: VersionedPowerSyncMongoV3;
|
|
36
38
|
logger: Logger;
|
|
39
|
+
clearBatchThrottleRate: number;
|
|
40
|
+
objectStorage?: ObjectStorage;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export class MongoStoppedSyncConfigCleanup {
|
|
@@ -41,16 +45,20 @@ export class MongoStoppedSyncConfigCleanup {
|
|
|
41
45
|
private readonly replicationStreamId: number;
|
|
42
46
|
private readonly signal: AbortSignal | undefined;
|
|
43
47
|
private readonly logger: Logger;
|
|
48
|
+
private readonly objectStorage: ObjectStorage | undefined;
|
|
44
49
|
private readonly defaultSchema: string;
|
|
45
50
|
private readonly sourceConnectionTag: string;
|
|
51
|
+
private readonly clearBatchThrottleRate: number;
|
|
46
52
|
|
|
47
53
|
constructor(options: MongoStoppedSyncConfigCleanupOptions) {
|
|
48
54
|
this.db = options.db;
|
|
49
55
|
this.replicationStreamId = options.replicationStreamId;
|
|
50
56
|
this.signal = options.signal;
|
|
51
57
|
this.logger = options.logger;
|
|
58
|
+
this.objectStorage = options.objectStorage;
|
|
52
59
|
this.defaultSchema = options.defaultSchema;
|
|
53
60
|
this.sourceConnectionTag = options.sourceConnectionTag;
|
|
61
|
+
this.clearBatchThrottleRate = options.clearBatchThrottleRate;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
async run(): Promise<storage.CleanupStoppedSyncConfigsResult> {
|
|
@@ -371,9 +379,13 @@ export class MongoStoppedSyncConfigCleanup {
|
|
|
371
379
|
}
|
|
372
380
|
|
|
373
381
|
private async dropBucketDataCollections(definitionIds: BucketDefinitionId[]): Promise<number> {
|
|
382
|
+
const lifecycle = this.objectStorage
|
|
383
|
+
? new ObjectStorageLifecycle(this.db, this.replicationStreamId, this.objectStorage)
|
|
384
|
+
: null;
|
|
374
385
|
for (const definitionId of definitionIds) {
|
|
375
386
|
this.throwIfAborted();
|
|
376
387
|
await this.dropCollection(this.db.bucketData(this.replicationStreamId, definitionId));
|
|
388
|
+
await lifecycle?.deletePrefix(lifecycle.definitionPrefix(definitionId), { signal: this.signal });
|
|
377
389
|
}
|
|
378
390
|
return definitionIds.length;
|
|
379
391
|
}
|
|
@@ -393,26 +405,16 @@ export class MongoStoppedSyncConfigCleanup {
|
|
|
393
405
|
let deletedCount = 0;
|
|
394
406
|
for (const definitionId of definitionIds) {
|
|
395
407
|
this.throwIfAborted();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
_id: idPrefixFilter<BucketStateDocumentV3['_id']>({ d: definitionId }, ['b'])
|
|
401
|
-
},
|
|
402
|
-
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
403
|
-
),
|
|
408
|
+
deletedCount += await clearCollectionInIdRanges(
|
|
409
|
+
this.logger,
|
|
410
|
+
'bucket state',
|
|
411
|
+
collection,
|
|
404
412
|
{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
this.logger.info(
|
|
410
|
-
`Cleared batch of bucket state in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
413
|
+
_id: idPrefixFilter<BucketStateDocumentV3['_id']>({ d: definitionId }, ['b'])
|
|
414
|
+
},
|
|
415
|
+
this.signal,
|
|
416
|
+
this.clearBatchThrottleRate
|
|
414
417
|
);
|
|
415
|
-
deletedCount += result.deletedCount;
|
|
416
418
|
}
|
|
417
419
|
return deletedCount;
|
|
418
420
|
}
|
|
@@ -24,7 +24,7 @@ import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
|
|
|
24
24
|
import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
|
|
25
25
|
import { MongoPersistedReplicationStream } from '../MongoPersistedReplicationStream.js';
|
|
26
26
|
import { MongoSyncBucketStorage, MongoSyncBucketStorageOptions } from '../MongoSyncBucketStorage.js';
|
|
27
|
-
import { loadBucketDataDocument } from './bucket-format.js';
|
|
27
|
+
import { loadBucketDataDocument, maxOpId } from './bucket-format.js';
|
|
28
28
|
import {
|
|
29
29
|
BucketDataDocumentV3,
|
|
30
30
|
BucketParameterDocumentV3,
|
|
@@ -37,12 +37,16 @@ import { MongoBucketBatchV3 } from './MongoBucketBatchV3.js';
|
|
|
37
37
|
import { MongoChecksumsV3 } from './MongoChecksumsV3.js';
|
|
38
38
|
import { MongoCompactorV3 } from './MongoCompactorV3.js';
|
|
39
39
|
import { MongoStoppedSyncConfigCleanup } from './MongoStoppedSyncConfigCleanup.js';
|
|
40
|
+
import { hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
|
|
41
|
+
import { ObjectStorage } from './object-storage/ObjectStorage.js';
|
|
42
|
+
import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
|
|
40
43
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
41
44
|
|
|
42
45
|
export interface MongoSyncBucketStorageContextV3 {
|
|
43
46
|
db: VersionedPowerSyncMongoV3;
|
|
44
47
|
replicationStreamId: number;
|
|
45
48
|
readPreference?: mongo.ReadPreference;
|
|
49
|
+
objectStorage: ObjectStorage | undefined;
|
|
46
50
|
/**
|
|
47
51
|
* Persisted mapping of the single sync config that read operations are served from.
|
|
48
52
|
*
|
|
@@ -52,47 +56,53 @@ export interface MongoSyncBucketStorageContextV3 {
|
|
|
52
56
|
readonly mapping: SingleSyncConfigBucketDefinitionMapping;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const BUCKET_DATA_FETCH_BATCH_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Keep the documents hydrated for one sync response within a bounded payload or slightly higher.
|
|
63
|
+
*
|
|
64
|
+
* This deserializes on-demand, so no deserialization is performed for discarded data.
|
|
65
|
+
*/
|
|
66
|
+
function cutBucketDataBatch(rawDocuments: Buffer[]): {
|
|
67
|
+
documents: BucketDataDocumentV3[];
|
|
68
|
+
wasCut: boolean;
|
|
69
|
+
} {
|
|
70
|
+
let cumulativeBytes = 0;
|
|
71
|
+
let documents: BucketDataDocumentV3[] = [];
|
|
72
|
+
for (const raw of rawDocuments) {
|
|
73
|
+
const doc = bson.deserialize(raw, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS) as BucketDataDocumentV3;
|
|
74
|
+
documents.push(doc);
|
|
75
|
+
cumulativeBytes += doc.size;
|
|
76
|
+
if (cumulativeBytes > BUCKET_DATA_FETCH_BATCH_LIMIT_BYTES) {
|
|
77
|
+
return {
|
|
78
|
+
documents,
|
|
79
|
+
wasCut: documents.length < rawDocuments.length
|
|
80
|
+
};
|
|
64
81
|
}
|
|
65
82
|
}
|
|
83
|
+
return { documents, wasCut: false };
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
function extractRowsFromDocument(
|
|
69
87
|
doc: BucketDataDocumentV3,
|
|
70
88
|
context: { replicationStreamId: number; definitionId: string },
|
|
71
|
-
|
|
72
|
-
endOpId: InternalOpId
|
|
73
|
-
|
|
74
|
-
): { rows: BucketDataDoc[]; remainingLimit: number; limitReached: boolean } {
|
|
89
|
+
bucketStart: InternalOpId,
|
|
90
|
+
endOpId: InternalOpId
|
|
91
|
+
): BucketDataDoc[] {
|
|
75
92
|
const rows: BucketDataDoc[] = [];
|
|
76
93
|
for (const row of loadBucketDataDocument(context, doc)) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (bucketStart == null) {
|
|
80
|
-
throw new Error(`data for unexpected bucket: ${bucket}`);
|
|
81
|
-
}
|
|
94
|
+
// In theory a binary search could be faster than a linear scan to find the start.
|
|
95
|
+
// In practice, most cases should not filter out anything here.
|
|
82
96
|
if (row.o <= bucketStart) {
|
|
83
97
|
continue;
|
|
84
98
|
}
|
|
85
99
|
if (row.o > endOpId) {
|
|
86
|
-
|
|
100
|
+
break;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
rows.push(row);
|
|
90
|
-
remainingLimit--;
|
|
91
|
-
if (remainingLimit <= 0) {
|
|
92
|
-
return { rows, remainingLimit, limitReached: true };
|
|
93
|
-
}
|
|
94
104
|
}
|
|
95
|
-
return
|
|
105
|
+
return rows;
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
@@ -337,6 +347,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
|
337
347
|
const self = this;
|
|
338
348
|
return {
|
|
339
349
|
db: this.db,
|
|
350
|
+
objectStorage: this.objectStorage,
|
|
340
351
|
replicationStreamId: this.replicationStreamId,
|
|
341
352
|
readPreference: this.readPreference,
|
|
342
353
|
get mapping() {
|
|
@@ -357,14 +368,27 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
|
357
368
|
checkpoint: MongoSyncBucketStorageCheckpoint,
|
|
358
369
|
dataBuckets: storage.BucketDataRequest[],
|
|
359
370
|
options?: storage.BucketDataBatchOptions
|
|
360
|
-
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
371
|
+
): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd> {
|
|
361
372
|
return getBucketDataBatchV3(this.versionContext, checkpoint, dataBuckets, options);
|
|
362
373
|
}
|
|
363
374
|
|
|
364
|
-
protected async clearBucketData(
|
|
375
|
+
protected async clearBucketData(signal?: AbortSignal): Promise<void> {
|
|
365
376
|
for (const collection of await this.db.listBucketDataCollections(this.replicationStreamId)) {
|
|
366
377
|
await collection.drop();
|
|
367
378
|
}
|
|
379
|
+
if (this.objectStorage) {
|
|
380
|
+
const lifecycle = new ObjectStorageLifecycle(this.db, this.replicationStreamId, this.objectStorage);
|
|
381
|
+
await lifecycle.deletePrefix(lifecycle.streamPrefix(), { signal });
|
|
382
|
+
}
|
|
383
|
+
await this.db
|
|
384
|
+
.pendingObjectStorageDeletes(this.replicationStreamId)
|
|
385
|
+
.drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
|
|
386
|
+
.catch((error) => {
|
|
387
|
+
if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
throw error;
|
|
391
|
+
});
|
|
368
392
|
}
|
|
369
393
|
|
|
370
394
|
protected async clearParameterIndexes(_signal?: AbortSignal): Promise<void> {
|
|
@@ -412,7 +436,9 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
|
|
|
412
436
|
signal: options.signal,
|
|
413
437
|
logger: options.logger ?? this.logger,
|
|
414
438
|
defaultSchema: options.defaultSchema,
|
|
415
|
-
sourceConnectionTag: options.sourceConnectionTag
|
|
439
|
+
sourceConnectionTag: options.sourceConnectionTag,
|
|
440
|
+
objectStorage: this.objectStorage,
|
|
441
|
+
clearBatchThrottleRate: this.clearBatchThrottleRate
|
|
416
442
|
}).run();
|
|
417
443
|
}
|
|
418
444
|
|
|
@@ -534,7 +560,7 @@ export async function* getBucketDataBatchV3(
|
|
|
534
560
|
checkpoint: MongoSyncBucketStorageCheckpoint,
|
|
535
561
|
dataBuckets: storage.BucketDataRequest[],
|
|
536
562
|
options?: storage.BucketDataBatchOptions
|
|
537
|
-
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
563
|
+
): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd> {
|
|
538
564
|
if (dataBuckets.length == 0) {
|
|
539
565
|
return;
|
|
540
566
|
}
|
|
@@ -553,13 +579,15 @@ export async function* getBucketDataBatchV3(
|
|
|
553
579
|
|
|
554
580
|
if (session != null) {
|
|
555
581
|
session.advanceOperationTime(checkpoint.snapshotTime);
|
|
582
|
+
session.advanceClusterTime(checkpoint.clusterTime);
|
|
556
583
|
}
|
|
557
584
|
|
|
558
585
|
const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
|
|
559
586
|
const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
|
|
560
587
|
const end = checkpoint.checkpoint;
|
|
561
|
-
let remainingLimit = batchLimit;
|
|
562
588
|
|
|
589
|
+
// Group requests by definition, so that we can query each definition's bucket data in a single query.
|
|
590
|
+
// We only return the results of a single query per batch.
|
|
563
591
|
const requestsByDefinition = new Map<string, storage.BucketDataRequest[]>();
|
|
564
592
|
for (const request of dataBuckets) {
|
|
565
593
|
const definitionId = ctx.mapping.bucketSourceId(request.source);
|
|
@@ -570,9 +598,6 @@ export async function* getBucketDataBatchV3(
|
|
|
570
598
|
|
|
571
599
|
const definitionGroups = Array.from(requestsByDefinition.entries());
|
|
572
600
|
for (const [groupIndex, [definitionId, requests]] of definitionGroups.entries()) {
|
|
573
|
-
if (remainingLimit <= 0) {
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
601
|
const hasLaterDefinitionGroups = groupIndex < definitionGroups.length - 1;
|
|
577
602
|
const bucketMap = new Map(requests.map((request) => [request.bucket, request.start]));
|
|
578
603
|
const filters = Array.from(bucketMap.entries()).map(([bucket, start]) => ({
|
|
@@ -588,9 +613,8 @@ export async function* getBucketDataBatchV3(
|
|
|
588
613
|
// MongoDB Filter<T> doesn't accept the $or operator in its type.
|
|
589
614
|
const filter = { $or: filters } as unknown as mongo.Filter<BucketDataDocumentV3>;
|
|
590
615
|
const context = { replicationStreamId: ctx.replicationStreamId, definitionId };
|
|
591
|
-
const limit = remainingLimit;
|
|
592
616
|
|
|
593
|
-
const cursorOptions = { limit:
|
|
617
|
+
const cursorOptions = { limit: batchLimit, batchSize: batchLimit + 1 };
|
|
594
618
|
|
|
595
619
|
// raw: true returns Buffers, but the driver typing doesn't reflect that
|
|
596
620
|
// without an explicit cast to FindCursor<Buffer>.
|
|
@@ -608,88 +632,44 @@ export async function* getBucketDataBatchV3(
|
|
|
608
632
|
throw lib_mongo.mapQueryError(e, 'while reading bucket data');
|
|
609
633
|
});
|
|
610
634
|
|
|
611
|
-
if (
|
|
635
|
+
if (rawData.length >= cursorOptions.limit) {
|
|
612
636
|
hasMore = true;
|
|
613
637
|
}
|
|
614
638
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const completeEmptyBuckets = new Set<string>();
|
|
622
|
-
|
|
623
|
-
for (const raw of rawData) {
|
|
624
|
-
const doc = bson.deserialize(raw, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS) as BucketDataDocumentV3;
|
|
625
|
-
const {
|
|
626
|
-
rows,
|
|
627
|
-
remainingLimit,
|
|
628
|
-
limitReached: docLimitReached
|
|
629
|
-
} = extractRowsFromDocument(doc, context, bucketMap, end, sharedRemainingLimit);
|
|
630
|
-
if (rows.length == 0) {
|
|
631
|
-
// The document straddles the requested (start, end] window: it matched the
|
|
632
|
-
// query, but none of its ops are in range. Since its _id.o (max op) must be
|
|
633
|
-
// > end (any op <= end would have been > start, and thus in range), and
|
|
634
|
-
// document ranges per bucket are disjoint, no later document for this bucket
|
|
635
|
-
// can match either. The bucket is complete through the checkpoint.
|
|
636
|
-
completeEmptyBuckets.add(doc._id.b);
|
|
637
|
-
}
|
|
638
|
-
data.push(...rows);
|
|
639
|
-
documentOpCounts.push(rows.length);
|
|
640
|
-
documentSizes.push(raw.byteLength);
|
|
641
|
-
sharedRemainingLimit = remainingLimit;
|
|
642
|
-
if (docLimitReached) {
|
|
643
|
-
limitReached = true;
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const batchHasMore = hasMore || limitReached;
|
|
649
|
-
|
|
650
|
-
// Empty chunks are not forwarded to clients, but report progress to the caller:
|
|
651
|
-
// the bucket's position advances to the checkpoint, so it is not re-requested.
|
|
652
|
-
// If the batch produced no data at all, the last empty chunk also carries the
|
|
653
|
-
// has_more signal, so the caller re-requests the remaining buckets instead of
|
|
654
|
-
// treating an all-filtered batch as the end of the stream.
|
|
655
|
-
const emptyBuckets = Array.from(completeEmptyBuckets);
|
|
656
|
-
for (const [index, bucket] of emptyBuckets.entries()) {
|
|
657
|
-
const startOpId = bucketMap.get(bucket);
|
|
658
|
-
if (startOpId == null) {
|
|
659
|
-
throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
|
|
660
|
-
}
|
|
661
|
-
const isLastChunkOfBatch = data.length == 0 && index == emptyBuckets.length - 1;
|
|
662
|
-
yield {
|
|
663
|
-
chunkData: {
|
|
664
|
-
bucket,
|
|
665
|
-
after: internalToExternalOpId(startOpId),
|
|
666
|
-
has_more: isLastChunkOfBatch && batchHasMore,
|
|
667
|
-
data: [],
|
|
668
|
-
next_after: internalToExternalOpId(end)
|
|
669
|
-
},
|
|
670
|
-
targetOp: null
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (data.length == 0) {
|
|
675
|
-
if (batchHasMore) {
|
|
676
|
-
// The remaining documents are read in the next round, after the caller has
|
|
677
|
-
// advanced the positions of the empty buckets above.
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
continue;
|
|
639
|
+
// Deserialize the raw documents and cut the batch to a bounded size. Any data not
|
|
640
|
+
// making the cut will be read in the next round, using a fresh query.
|
|
641
|
+
const cutBatch = cutBucketDataBatch(rawData);
|
|
642
|
+
const docs = cutBatch.documents;
|
|
643
|
+
if (cutBatch.wasCut) {
|
|
644
|
+
hasMore = true;
|
|
681
645
|
}
|
|
682
646
|
|
|
683
|
-
|
|
647
|
+
// Hydrate any operations from object storage.
|
|
648
|
+
// In the future we can do this in a more pipelined fashion, but for now we hydrate
|
|
649
|
+
// the entire batch at once.
|
|
650
|
+
await hydrateBucketDataDocuments(docs, ctx.objectStorage, { signal: options?.signal });
|
|
684
651
|
|
|
685
652
|
let currentChunkSizeBytes = 0;
|
|
686
653
|
let currentChunk: utils.SyncBucketData | null = null;
|
|
687
654
|
let targetOp: InternalOpId | null = null;
|
|
655
|
+
let seenBuckets = new Set<string>();
|
|
656
|
+
const batchHasMore = hasMore;
|
|
657
|
+
|
|
658
|
+
for (const doc of docs) {
|
|
659
|
+
const bucket = doc._id.b;
|
|
660
|
+
seenBuckets.add(bucket);
|
|
661
|
+
const bucketStart = bucketMap.get(bucket);
|
|
662
|
+
if (bucketStart == null) {
|
|
663
|
+
throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
|
|
664
|
+
}
|
|
688
665
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
666
|
+
// Reached a new bucket or size limit: yield the current chunk and start a new one.
|
|
667
|
+
if (
|
|
668
|
+
currentChunk == null ||
|
|
669
|
+
currentChunk.bucket != bucket ||
|
|
670
|
+
currentChunkSizeBytes >= chunkSizeLimitBytes ||
|
|
671
|
+
currentChunk.data.length >= batchLimit
|
|
672
|
+
) {
|
|
693
673
|
let start: ProtocolOpId | undefined = undefined;
|
|
694
674
|
if (currentChunk != null) {
|
|
695
675
|
if (currentChunk.bucket == bucket) {
|
|
@@ -705,12 +685,9 @@ export async function* getBucketDataBatchV3(
|
|
|
705
685
|
}
|
|
706
686
|
|
|
707
687
|
if (start == null) {
|
|
708
|
-
|
|
709
|
-
if (startOpId == null) {
|
|
710
|
-
throw new Error(`data for unexpected bucket: ${bucket}`);
|
|
711
|
-
}
|
|
712
|
-
start = internalToExternalOpId(startOpId);
|
|
688
|
+
start = internalToExternalOpId(bucketStart);
|
|
713
689
|
}
|
|
690
|
+
|
|
714
691
|
currentChunk = {
|
|
715
692
|
bucket,
|
|
716
693
|
after: start,
|
|
@@ -720,27 +697,47 @@ export async function* getBucketDataBatchV3(
|
|
|
720
697
|
};
|
|
721
698
|
}
|
|
722
699
|
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
currentChunk.data.push(entry);
|
|
729
|
-
currentChunk.next_after = entry.op_id;
|
|
730
|
-
|
|
731
|
-
if (isLastOpInDocument) {
|
|
732
|
-
currentChunkSizeBytes += documentSizes[docIndex];
|
|
733
|
-
}
|
|
700
|
+
const rows = extractRowsFromDocument(doc, context, bucketStart, end);
|
|
701
|
+
currentChunk.data.push(...rows.map(mapOpEntry));
|
|
702
|
+
currentChunk.next_after = currentChunk.data.at(-1)?.op_id ?? internalToExternalOpId(end);
|
|
703
|
+
targetOp = maxOpId(targetOp, doc.target_op);
|
|
704
|
+
currentChunkSizeBytes += doc.size;
|
|
734
705
|
}
|
|
735
706
|
|
|
736
707
|
if (currentChunk != null) {
|
|
737
708
|
const yieldChunk = currentChunk;
|
|
738
|
-
|
|
709
|
+
// The last chunk may contain more data that was cut in this batch.
|
|
710
|
+
yieldChunk.has_more = batchHasMore;
|
|
739
711
|
yield { chunkData: yieldChunk, targetOp };
|
|
740
712
|
}
|
|
741
713
|
|
|
742
|
-
if (batchHasMore
|
|
743
|
-
|
|
714
|
+
if (!batchHasMore) {
|
|
715
|
+
for (const bucket of bucketMap.keys()) {
|
|
716
|
+
if (!seenBuckets.has(bucket)) {
|
|
717
|
+
// We processed everything for this definition group, but this bucket had no data in the batch.
|
|
718
|
+
// Yield an empty chunk to indicate that it is complete.
|
|
719
|
+
// This prevents re-querying the same bucket in the next batch.
|
|
720
|
+
yield {
|
|
721
|
+
chunkData: {
|
|
722
|
+
bucket,
|
|
723
|
+
after: internalToExternalOpId(bucketMap.get(bucket)!),
|
|
724
|
+
has_more: false,
|
|
725
|
+
data: [],
|
|
726
|
+
next_after: internalToExternalOpId(end)
|
|
727
|
+
},
|
|
728
|
+
targetOp
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (currentChunk != null) {
|
|
735
|
+
// We yielded data in this group (aside from empty buckets).
|
|
736
|
+
// Return to the caller to allow them to process it before continuing to the next group.
|
|
737
|
+
yield { hasMore: batchHasMore || hasLaterDefinitionGroups };
|
|
738
|
+
break;
|
|
739
|
+
} else {
|
|
740
|
+
// No data in this definition group - continue in the next group.
|
|
744
741
|
}
|
|
745
742
|
}
|
|
746
743
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { InternalOpId, storage } from '@powersync/service-core';
|
|
3
|
+
import { BucketDefinitionMapping, InternalOpId, storage } from '@powersync/service-core';
|
|
4
4
|
import { BucketDataSource, BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
5
|
import * as bson from 'bson';
|
|
6
6
|
import { mongoTableId } from '../../../utils/util.js';
|
|
@@ -8,6 +8,7 @@ import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
|
8
8
|
import {
|
|
9
9
|
BucketStateUpdate,
|
|
10
10
|
PersistedBatch,
|
|
11
|
+
PersistedBatchOptions,
|
|
11
12
|
SaveParameterDataOptions,
|
|
12
13
|
UpsertCurrentDataOptions
|
|
13
14
|
} from '../common/PersistedBatch.js';
|
|
@@ -22,14 +23,29 @@ import {
|
|
|
22
23
|
SourceTableDocumentV3,
|
|
23
24
|
taggedBucketParameterDocumentToTagged
|
|
24
25
|
} from './models.js';
|
|
26
|
+
import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
|
|
25
27
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
26
28
|
|
|
27
29
|
export class PersistedBatchV3 extends PersistedBatch {
|
|
28
30
|
currentData: { sourceTableId: bson.ObjectId; operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3> }[] = [];
|
|
29
31
|
sourceTablePendingDeletes = new Map<string, InternalOpId>();
|
|
32
|
+
protected readonly objectStorageLifecycle?: ObjectStorageLifecycle;
|
|
30
33
|
|
|
31
34
|
declare protected readonly db: VersionedPowerSyncMongoV3;
|
|
32
35
|
|
|
36
|
+
constructor(
|
|
37
|
+
db: VersionedPowerSyncMongoV3,
|
|
38
|
+
group_id: number,
|
|
39
|
+
mapping: BucketDefinitionMapping,
|
|
40
|
+
writtenSize: number,
|
|
41
|
+
options?: PersistedBatchOptions
|
|
42
|
+
) {
|
|
43
|
+
super(db, group_id, mapping, writtenSize, options);
|
|
44
|
+
if (this.objectStorage) {
|
|
45
|
+
this.objectStorageLifecycle = new ObjectStorageLifecycle(this.db, this.group_id, this.objectStorage);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
// Abstract override from PersistedBatch (V3-specific error message)
|
|
34
50
|
|
|
35
51
|
protected override checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId {
|
|
@@ -209,26 +225,63 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
209
225
|
operationsByDefinition.set(document.bucketKey.definitionId, existing);
|
|
210
226
|
}
|
|
211
227
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
228
|
+
let uploadCount = 0;
|
|
229
|
+
const plans = Array.from(operationsByDefinition, ([definitionId, documents]) => {
|
|
230
|
+
const operationsByBucket = Map.groupBy(documents, (document) => document.bucketKey.bucket);
|
|
231
|
+
const lifecycle = this.objectStorageLifecycle;
|
|
232
|
+
const createInserts: (() => Promise<mongo.AnyBulkWriteOperation<BucketDataDocumentV3>>)[] = [];
|
|
233
|
+
|
|
234
|
+
for (const [bucket, ops] of operationsByBucket) {
|
|
235
|
+
for (const chunk of chunkBucketData(ops)) {
|
|
236
|
+
const serialized = serializeBucketData(bucket, chunk);
|
|
237
|
+
if (lifecycle == null || serialized.size <= this.inlineThresholdBytes) {
|
|
238
|
+
createInserts.push(async () => ({
|
|
239
|
+
insertOne: {
|
|
240
|
+
document: serialized
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
219
245
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
uploadCount += 1;
|
|
247
|
+
createInserts.push(async () => {
|
|
248
|
+
const minOp = chunk[0].o;
|
|
249
|
+
const maxOp = chunk[chunk.length - 1].o;
|
|
250
|
+
const { ops: bucketOps, ...metadata } = serialized;
|
|
251
|
+
const path = lifecycle.allocatePath(definitionId, bucket, minOp, maxOp);
|
|
252
|
+
const { fileSize } = await lifecycle.bucketData.store(path, bucketOps!);
|
|
253
|
+
return {
|
|
254
|
+
insertOne: {
|
|
255
|
+
document: {
|
|
256
|
+
...metadata,
|
|
257
|
+
storage_ref: {
|
|
258
|
+
path,
|
|
259
|
+
file_size: fileSize
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
228
264
|
});
|
|
229
265
|
}
|
|
230
266
|
}
|
|
231
267
|
|
|
268
|
+
return { definitionId, createInserts };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const createAllInserts = () =>
|
|
272
|
+
Promise.all(
|
|
273
|
+
plans.map(async ({ definitionId, createInserts }) => ({
|
|
274
|
+
definitionId,
|
|
275
|
+
inserts: await Promise.all(createInserts.map((createInsert) => createInsert()))
|
|
276
|
+
}))
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// S3ObjectStorage applies one shared concurrency limit across all callers,
|
|
280
|
+
// so replication can schedule its uploads together without creating a
|
|
281
|
+
// separate limiter here.
|
|
282
|
+
const writes = await createAllInserts();
|
|
283
|
+
|
|
284
|
+
for (const { definitionId, inserts } of writes) {
|
|
232
285
|
if (inserts.length > 0) {
|
|
233
286
|
await this.db.bucketData(this.group_id, definitionId).bulkWrite(inserts, {
|
|
234
287
|
session,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BucketParameterDocumentV3,
|
|
8
8
|
BucketStateDocumentV3,
|
|
9
9
|
CurrentDataDocumentV3,
|
|
10
|
+
ObjectStorageDeletionMarker,
|
|
10
11
|
SourceTableDocumentV3,
|
|
11
12
|
SyncConfigDefinition
|
|
12
13
|
} from './models.js';
|
|
@@ -69,6 +70,10 @@ export class VersionedPowerSyncMongoV3 extends BaseVersionedPowerSyncMongo {
|
|
|
69
70
|
return this.db.collection<BucketDataDocumentV3>(`bucket_data_${replicationStreamId}_${definitionId}`);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
pendingObjectStorageDeletes(replicationStreamId: number): mongo.Collection<ObjectStorageDeletionMarker> {
|
|
74
|
+
return this.db.collection<ObjectStorageDeletionMarker>(`pending_object_storage_deletes_${replicationStreamId}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
listBucketDataCollections(replicationStreamId: number) {
|
|
73
78
|
return this.listCollectionsByPrefix(`bucket_data_${replicationStreamId}_`);
|
|
74
79
|
}
|