@powersync/service-module-mongodb-storage 0.21.0 → 0.22.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 +17 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -0
- package/dist/storage/MongoBucketStorage.js +7 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -1
- package/dist/storage/implementation/MongoBucketBatch.js +9 -8
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +4 -2
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
- package/dist/storage/implementation/common/PersistedBatch.js +6 -4
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +5 -0
- package/dist/storage/implementation/db.js +13 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +13 -6
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +2 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +17 -14
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +6 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.js +164 -69
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/types/types.d.ts +5 -0
- package/dist/types/types.js +12 -0
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +1 -0
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +6 -6
- package/src/storage/MongoBucketStorage.ts +12 -1
- package/src/storage/implementation/MongoBucketBatch.ts +13 -10
- package/src/storage/implementation/MongoStorageProvider.ts +12 -2
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
- package/src/storage/implementation/common/PersistedBatch.ts +12 -8
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +14 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +17 -8
- package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +51 -49
- package/src/storage/implementation/v3/MongoCompactorV3.ts +276 -97
- package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
- package/src/types/types.ts +18 -0
- package/src/utils/test-utils.ts +1 -0
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/object_storage_usage.test.ts +16 -4
- package/test/src/storage_compacting.test.ts +257 -5
- package/test/src/storage_s3_reading.test.ts +47 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
UpsertCurrentDataOptions
|
|
14
14
|
} from '../common/PersistedBatch.js';
|
|
15
15
|
import { SourceRecordLookupState } from '../common/SourceRecordStore.js';
|
|
16
|
+
import { MongoWriteBatch } from '../MongoWriteBatch.js';
|
|
16
17
|
import { serializeBucketData } from './bucket-format.js';
|
|
17
18
|
import { chunkBucketData } from './chunking.js';
|
|
18
19
|
import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
|
|
@@ -28,8 +29,15 @@ import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.
|
|
|
28
29
|
import { ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
|
|
29
30
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
30
31
|
|
|
32
|
+
interface SourceRecordWrite {
|
|
33
|
+
sourceTableId: bson.ObjectId;
|
|
34
|
+
operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3>;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
export class PersistedBatchV3 extends PersistedBatch {
|
|
32
|
-
|
|
38
|
+
// Upserts and soft deletes supply the complete source-record state, including
|
|
39
|
+
// pending_delete. Keep the final state per key, without removing any history.
|
|
40
|
+
currentData = new Map<string, SourceRecordWrite>();
|
|
33
41
|
sourceTablePendingDeletes = new Map<string, InternalOpId>();
|
|
34
42
|
protected readonly objectStorageLifecycle?: ObjectStorageLifecycle;
|
|
35
43
|
protected readonly objectStorageUsage?: ObjectStorageUsage;
|
|
@@ -131,7 +139,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
|
|
134
|
-
this.currentData.
|
|
142
|
+
this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
|
|
135
143
|
sourceTableId,
|
|
136
144
|
operation: {
|
|
137
145
|
deleteOne: {
|
|
@@ -147,7 +155,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
147
155
|
replicaId: storage.ReplicaId,
|
|
148
156
|
checkpointGreaterThan: InternalOpId
|
|
149
157
|
) {
|
|
150
|
-
this.currentData.
|
|
158
|
+
this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
|
|
151
159
|
sourceTableId,
|
|
152
160
|
operation: {
|
|
153
161
|
updateOne: {
|
|
@@ -195,7 +203,7 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
195
203
|
};
|
|
196
204
|
});
|
|
197
205
|
|
|
198
|
-
this.currentData.
|
|
206
|
+
this.currentData.set(this.currentDataKey(values.sourceTableId, values.replicaId), {
|
|
199
207
|
sourceTableId: values.sourceTableId,
|
|
200
208
|
operation: {
|
|
201
209
|
updateOne: {
|
|
@@ -216,12 +224,12 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
protected get currentDataCount() {
|
|
219
|
-
return this.currentData.
|
|
227
|
+
return this.currentData.size;
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
// Flush methods
|
|
223
231
|
|
|
224
|
-
protected async
|
|
232
|
+
protected async queueBucketData(writes: MongoWriteBatch) {
|
|
225
233
|
const operationsByDefinition = new Map<BucketDefinitionId, BucketDataDoc[]>();
|
|
226
234
|
for (const document of this.bucketData) {
|
|
227
235
|
const existing = operationsByDefinition.get(document.bucketKey.definitionId) ?? [];
|
|
@@ -284,10 +292,10 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
284
292
|
// S3ObjectStorage applies one shared concurrency limit across all callers,
|
|
285
293
|
// so replication can schedule its uploads together without creating a
|
|
286
294
|
// separate limiter here.
|
|
287
|
-
const
|
|
295
|
+
const bucketWrites = await createAllInserts();
|
|
288
296
|
|
|
289
297
|
const usageDeltas = this.objectStorageUsage == null ? undefined : new Map<BucketDefinitionId, bigint>();
|
|
290
|
-
for (const { definitionId, inserts } of
|
|
298
|
+
for (const { definitionId, inserts } of bucketWrites) {
|
|
291
299
|
if (usageDeltas != null) {
|
|
292
300
|
const delta = inserts.reduce(
|
|
293
301
|
(sum, operation) => sum + ObjectStorageUsage.bytes(operation.insertOne.document),
|
|
@@ -298,18 +306,15 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
if (inserts.length > 0) {
|
|
301
|
-
|
|
302
|
-
session,
|
|
303
|
-
ordered: false
|
|
304
|
-
});
|
|
309
|
+
writes.bulkWriteUnordered(this.db.bucketData(this.group_id, definitionId), inserts);
|
|
305
310
|
}
|
|
306
311
|
}
|
|
307
312
|
if (usageDeltas != null) {
|
|
308
|
-
|
|
313
|
+
this.objectStorageUsage!.applyDeltas(usageDeltas, writes);
|
|
309
314
|
}
|
|
310
315
|
}
|
|
311
316
|
|
|
312
|
-
protected
|
|
317
|
+
protected queueBucketParameters(writes: MongoWriteBatch): void {
|
|
313
318
|
const operationsByIndex = new Map<string, typeof this.bucketParameters>();
|
|
314
319
|
for (const document of this.bucketParameters) {
|
|
315
320
|
const existing = operationsByIndex.get(document.index) ?? [];
|
|
@@ -318,23 +323,20 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
318
323
|
}
|
|
319
324
|
|
|
320
325
|
for (const [indexId, documents] of operationsByIndex.entries()) {
|
|
321
|
-
|
|
326
|
+
writes.bulkWriteUnordered(
|
|
327
|
+
this.db.parameterIndex(this.group_id, indexId),
|
|
322
328
|
documents.map((document) => ({
|
|
323
329
|
insertOne: {
|
|
324
330
|
document: taggedBucketParameterDocumentToTagged(document)
|
|
325
331
|
}
|
|
326
|
-
}))
|
|
327
|
-
{
|
|
328
|
-
session,
|
|
329
|
-
ordered: false
|
|
330
|
-
}
|
|
332
|
+
}))
|
|
331
333
|
);
|
|
332
334
|
}
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
protected
|
|
336
|
-
const operationsBySourceTable = new Map<string,
|
|
337
|
-
for (const operation of this.currentData) {
|
|
337
|
+
protected queueCurrentData(writes: MongoWriteBatch): void {
|
|
338
|
+
const operationsBySourceTable = new Map<string, SourceRecordWrite[]>();
|
|
339
|
+
for (const operation of this.currentData.values()) {
|
|
338
340
|
const sourceTableId = operation.sourceTableId.toHexString();
|
|
339
341
|
const existing = operationsBySourceTable.get(sourceTableId) ?? [];
|
|
340
342
|
existing.push(operation);
|
|
@@ -357,33 +359,31 @@ export class PersistedBatchV3 extends PersistedBatch {
|
|
|
357
359
|
});
|
|
358
360
|
|
|
359
361
|
if (sourceTableUpdates.length > 0) {
|
|
360
|
-
|
|
362
|
+
writes.bulkWriteUnordered(this.db.sourceTables(this.group_id), sourceTableUpdates);
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
for (const operations of operationsBySourceTable.values()) {
|
|
364
366
|
const sourceTableId = operations[0]!.sourceTableId;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
session,
|
|
369
|
-
ordered: true
|
|
370
|
-
}
|
|
367
|
+
writes.bulkWriteUnordered(
|
|
368
|
+
this.db.sourceRecords(this.group_id, sourceTableId),
|
|
369
|
+
operations.map((entry) => entry.operation)
|
|
371
370
|
);
|
|
372
371
|
}
|
|
373
372
|
}
|
|
374
373
|
|
|
375
|
-
protected
|
|
376
|
-
|
|
377
|
-
session,
|
|
378
|
-
ordered: false
|
|
379
|
-
});
|
|
374
|
+
protected queueBucketStates(writes: MongoWriteBatch): void {
|
|
375
|
+
writes.bulkWriteUnordered(this.db.bucketState(this.group_id), this.getBucketStateUpdates());
|
|
380
376
|
}
|
|
381
377
|
|
|
382
378
|
protected resetCurrentData() {
|
|
383
|
-
this.currentData
|
|
379
|
+
this.currentData.clear();
|
|
384
380
|
this.sourceTablePendingDeletes.clear();
|
|
385
381
|
}
|
|
386
382
|
|
|
383
|
+
private currentDataKey(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): string {
|
|
384
|
+
return Buffer.from(bson.serialize({ t: sourceTableId, k: replicaId })).toString('base64');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
387
|
private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[] {
|
|
388
388
|
return Array.from(this.bucketStates.values()).map((state: BucketStateUpdate) => {
|
|
389
389
|
if (state.definitionId == null) {
|
|
@@ -54,9 +54,16 @@ export async function hydrateBucketDataDocuments(
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
using _ = options?.tracer?.span('s3', 'read');
|
|
57
|
-
await Promise.
|
|
57
|
+
const results = await Promise.allSettled(
|
|
58
58
|
storedDocuments.map(async (document) => {
|
|
59
59
|
document.ops = await store.retrieve(document.storage_ref!.path, { signal: options.signal });
|
|
60
60
|
})
|
|
61
61
|
);
|
|
62
|
+
// Drain every download before propagating failure so callers can release their
|
|
63
|
+
// worker slot and payloads without a sibling download hydrating a document later.
|
|
64
|
+
for (const result of results) {
|
|
65
|
+
if (result.status === 'rejected') {
|
|
66
|
+
throw result.reason;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
62
69
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
1
|
import { Logger } from '@powersync/lib-services-framework';
|
|
3
2
|
import * as bson from 'bson';
|
|
4
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { MongoWriteBatch } from '../../MongoWriteBatch.js';
|
|
5
5
|
import { ObjectStorageDeletionMarker } from '../models.js';
|
|
6
6
|
import { VersionedPowerSyncMongoV3 } from '../VersionedPowerSyncMongoV3.js';
|
|
7
7
|
import { BucketDataObjectStorage } from './BucketDataObjectStorage.js';
|
|
@@ -93,7 +93,7 @@ export class ObjectStorageLifecycle {
|
|
|
93
93
|
*
|
|
94
94
|
* Call this in the same transaction as the one creating the references.
|
|
95
95
|
*/
|
|
96
|
-
|
|
96
|
+
publishUploads(uploads: PreparedObjectStorageUpload[], writes: MongoWriteBatch): void {
|
|
97
97
|
for (const upload of uploads) {
|
|
98
98
|
if (!this.canPublish(upload)) {
|
|
99
99
|
throw new Error(`Object storage publication lease expired for ${upload.path}`);
|
|
@@ -103,12 +103,13 @@ export class ObjectStorageLifecycle {
|
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
.pendingObjectStorageDeletes(this.replicationStreamId)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
writes.deleteMany(
|
|
107
|
+
this.db.pendingObjectStorageDeletes(this.replicationStreamId),
|
|
108
|
+
{ _id: { $in: uploads.map((upload) => upload.markerId) } },
|
|
109
|
+
(deletedCount) => {
|
|
110
|
+
if (deletedCount !== uploads.length) throw new Error('Missing object storage publication markers');
|
|
111
|
+
}
|
|
112
|
+
);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|
|
@@ -122,14 +123,14 @@ export class ObjectStorageLifecycle {
|
|
|
122
123
|
*
|
|
123
124
|
* Call this in the same transaction as the one removing the references to these files.
|
|
124
125
|
*/
|
|
125
|
-
|
|
126
|
+
retire(paths: Iterable<string>, writes: MongoWriteBatch, now = new Date()): void {
|
|
126
127
|
const markers: ObjectStorageDeletionMarker[] = Array.from(paths, (path) => ({
|
|
127
128
|
_id: new bson.ObjectId(),
|
|
128
129
|
path,
|
|
129
130
|
delete_after: new Date(now.getTime() + OBJECT_STORAGE_REFERENCE_GRACE_MS)
|
|
130
131
|
}));
|
|
131
132
|
if (markers.length) {
|
|
132
|
-
|
|
133
|
+
writes.insertMany(this.db.pendingObjectStorageDeletes(this.replicationStreamId), markers);
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -3,6 +3,7 @@ import { mongo } from '@powersync/lib-service-mongodb';
|
|
|
3
3
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
4
4
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { MongoWriteBatch } from '../../MongoWriteBatch.js';
|
|
6
7
|
import { BucketDataDocumentV3, ObjectStorageUsageDocument } from '../models.js';
|
|
7
8
|
import { VersionedPowerSyncMongoV3 } from '../VersionedPowerSyncMongoV3.js';
|
|
8
9
|
|
|
@@ -93,11 +94,11 @@ export class ObjectStorageUsage {
|
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
applyDelta(definitionId: BucketDefinitionId, delta: bigint, writes: MongoWriteBatch): void {
|
|
98
|
+
this.applyDeltas(new Map([[definitionId, delta]]), writes);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
applyDeltas(deltas: ReadonlyMap<BucketDefinitionId, bigint>, writes: MongoWriteBatch): void {
|
|
101
102
|
if (this.writerId == null) {
|
|
102
103
|
throw new ReplicationAssertionError('A writer id is required to apply object-storage usage');
|
|
103
104
|
}
|
|
@@ -113,13 +114,11 @@ export class ObjectStorageUsage {
|
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
writes.updateOne(
|
|
118
|
+
this.db.objectStorageUsage,
|
|
117
119
|
{ _id: this.documentId() },
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
$currentDate: { updated_at: true }
|
|
121
|
-
},
|
|
122
|
-
{ upsert: true, session }
|
|
120
|
+
{ $inc: increments, $currentDate: { updated_at: true } },
|
|
121
|
+
{ upsert: true }
|
|
123
122
|
);
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -19,10 +19,13 @@ import {
|
|
|
19
19
|
type ObjectStoragePutMetadata
|
|
20
20
|
} from './ObjectStorage.js';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Never statically import (or re-export) this module!
|
|
23
|
+
// We want to avoid loading the AWS SDK unless the user has configured S3 object storage, since it's a large dependency.
|
|
24
|
+
|
|
25
|
+
const DEFAULT_S3_OPERATION_CONCURRENCY = 64;
|
|
23
26
|
const S3_DELETE_PREFIX_BATCH_SIZE = 1000;
|
|
24
27
|
/**
|
|
25
|
-
*
|
|
28
|
+
* Keep prefix deletion below the default shared limit to leave capacity for other operations.
|
|
26
29
|
*/
|
|
27
30
|
const S3_DELETE_PREFIX_CONCURRENCY = 12;
|
|
28
31
|
const MAX_S3_PREFIX_BYTES = 256;
|
|
@@ -54,12 +57,20 @@ const SAFE_S3_KEY_BYTES = 896;
|
|
|
54
57
|
const BASELINE_CONNECTION_TIMEOUT_MS = 3_100;
|
|
55
58
|
const REQUEST_TIMEOUT_FACTOR = 5;
|
|
56
59
|
const OPERATION_TIMEOUT_FACTOR = 2;
|
|
60
|
+
/**
|
|
61
|
+
* Don't keep idle http sockets around for longer than this timeout.
|
|
62
|
+
*
|
|
63
|
+
* This covers the case where we have large bursts of activity during replication, followed by
|
|
64
|
+
* long periods of inactivity.
|
|
65
|
+
*/
|
|
66
|
+
const SOCKET_IDLE_TIMEOUT_MS = 60_000;
|
|
67
|
+
|
|
57
68
|
/**
|
|
58
69
|
* How long an operation may wait for a concurrency slot, as a multiple of the operation timeout.
|
|
59
70
|
*
|
|
60
71
|
* Every slot holder now releases within the operation timeout, so the queue always drains: this is
|
|
61
72
|
* a backpressure limit rather than a deadlock guard. Waiting this long means at least
|
|
62
|
-
* `factor * concurrencyLimit` operations ahead of us each took their full deadline -
|
|
73
|
+
* `factor * concurrencyLimit` operations ahead of us each took their full deadline - 256 at the
|
|
63
74
|
* default concurrency. Real operations complete in a fraction of the deadline, so the number of
|
|
64
75
|
* queued operations this actually tolerates is far higher, well above the ~2000 uploads that the
|
|
65
76
|
* largest possible replication flush (MAX_TRANSACTION_DOC_COUNT) can enqueue at once.
|
|
@@ -221,6 +232,15 @@ export class S3ObjectStorage implements ObjectStorage {
|
|
|
221
232
|
forcePathStyle: options.forcePathStyle,
|
|
222
233
|
defaultsMode,
|
|
223
234
|
requestHandler: new NodeHttpHandler({
|
|
235
|
+
// Match the shared request limit instead of queuing again at the HTTP pool.
|
|
236
|
+
httpAgent: {
|
|
237
|
+
keepAlive: true,
|
|
238
|
+
maxSockets: concurrencyLimit,
|
|
239
|
+
// The timeout here is a little more general than an "idle timeout", but it does cover the
|
|
240
|
+
// case of closing idle sockets after a period of inactivity.
|
|
241
|
+
timeout: SOCKET_IDLE_TIMEOUT_MS
|
|
242
|
+
},
|
|
243
|
+
httpsAgent: { keepAlive: true, maxSockets: concurrencyLimit, timeout: SOCKET_IDLE_TIMEOUT_MS },
|
|
224
244
|
connectionTimeout: this.timeouts.connectionTimeoutMs,
|
|
225
245
|
requestTimeout: this.timeouts.requestTimeoutMs,
|
|
226
246
|
// Without this, exceeding requestTimeout only logs a warning.
|
package/src/types/types.ts
CHANGED
|
@@ -65,6 +65,9 @@ export const MongoStorageConfig = service_types.configFile.BaseStorageConfig.and
|
|
|
65
65
|
*/
|
|
66
66
|
clear_batch_throttle_rate: t.number.optional(),
|
|
67
67
|
|
|
68
|
+
/** Maximum concurrent buckets across V3 chunk-compaction jobs. Range: 1–64. Default: 4 with object storage, otherwise 2. */
|
|
69
|
+
chunk_compaction_concurrency: t.number.optional(),
|
|
70
|
+
|
|
68
71
|
object_storage: S3ObjectStorageConfig.optional()
|
|
69
72
|
})
|
|
70
73
|
);
|
|
@@ -73,6 +76,21 @@ export type MongoStorageConfig = t.Encoded<typeof MongoStorageConfig>;
|
|
|
73
76
|
export type MongoStorageConfigDecoded = t.Decoded<typeof MongoStorageConfig>;
|
|
74
77
|
|
|
75
78
|
export const DEFAULT_CLEAR_BATCH_THROTTLE_RATE = 0.2;
|
|
79
|
+
export const DEFAULT_CHUNK_COMPACTION_CONCURRENCY = 2;
|
|
80
|
+
export const DEFAULT_OBJECT_STORAGE_CHUNK_COMPACTION_CONCURRENCY = 4;
|
|
81
|
+
|
|
82
|
+
export function normalizeChunkCompactionConcurrency(value: number | undefined, hasObjectStorage = false): number {
|
|
83
|
+
const concurrency =
|
|
84
|
+
value ??
|
|
85
|
+
(hasObjectStorage ? DEFAULT_OBJECT_STORAGE_CHUNK_COMPACTION_CONCURRENCY : DEFAULT_CHUNK_COMPACTION_CONCURRENCY);
|
|
86
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) {
|
|
87
|
+
throw new ServiceError(
|
|
88
|
+
ErrorCode.PSYNC_S3201,
|
|
89
|
+
'storage.chunk_compaction_concurrency must be an integer between 1 and 64'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return concurrency;
|
|
93
|
+
}
|
|
76
94
|
|
|
77
95
|
export function normalizeClearBatchThrottleRate(value: number | undefined): number {
|
|
78
96
|
const rate = value ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE;
|
package/src/utils/test-utils.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function mongoTestStorageFactoryGenerator(factoryOptions: MongoTestStorag
|
|
|
31
31
|
checksumOptions: factoryOptions.checksumOptions,
|
|
32
32
|
supportsMultipleSyncConfigs: factoryOptions.supportsMultipleSyncConfigs,
|
|
33
33
|
objectStorage: factoryOptions.objectStorage,
|
|
34
|
+
chunkCompactionConcurrency: factoryOptions.chunkCompactionConcurrency,
|
|
34
35
|
inlineThresholdBytes: factoryOptions.inlineThresholdBytes
|
|
35
36
|
});
|
|
36
37
|
},
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MongoStorageConfig, normalizeChunkCompactionConcurrency } from '@module/types/types.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('chunk compaction concurrency configuration', () => {
|
|
5
|
+
test('defaults to two workers', () => {
|
|
6
|
+
expect(normalizeChunkCompactionConcurrency(undefined)).toBe(2);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('defaults to four workers with object storage', () => {
|
|
10
|
+
expect(normalizeChunkCompactionConcurrency(undefined, true)).toBe(4);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test.each([false, true])('preserves an explicit override with object storage %s', (hasObjectStorage) => {
|
|
14
|
+
expect(normalizeChunkCompactionConcurrency(3, hasObjectStorage)).toBe(3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('decodes a configured worker count', () => {
|
|
18
|
+
const config = MongoStorageConfig.decode({
|
|
19
|
+
type: 'mongodb',
|
|
20
|
+
uri: 'mongodb://localhost:27017/powersync',
|
|
21
|
+
chunk_compaction_concurrency: 4
|
|
22
|
+
});
|
|
23
|
+
expect(normalizeChunkCompactionConcurrency(config.chunk_compaction_concurrency)).toBe(4);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.each([1, 64])('accepts boundary worker count %s', (value) => {
|
|
27
|
+
expect(normalizeChunkCompactionConcurrency(value)).toBe(value);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test.each([0, -1, 1.5, 65, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])(
|
|
31
|
+
'rejects invalid worker count %s',
|
|
32
|
+
(value) => {
|
|
33
|
+
expect(() => normalizeChunkCompactionConcurrency(value)).toThrow(
|
|
34
|
+
'storage.chunk_compaction_concurrency must be an integer between 1 and 64'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -123,14 +123,18 @@ describe('ObjectStorageUsage', () => {
|
|
|
123
123
|
await expect(
|
|
124
124
|
withTransaction(db, async (session) => {
|
|
125
125
|
await bucketData.insertOne(document, { session });
|
|
126
|
-
|
|
126
|
+
const writes = db.createWriteBatch(session, { ordered: false });
|
|
127
|
+
usage.applyDelta(definitionId, 123n, writes);
|
|
128
|
+
await writes.execute();
|
|
127
129
|
throw new Error('abort this attempt');
|
|
128
130
|
})
|
|
129
131
|
).rejects.toThrow('abort this attempt');
|
|
130
132
|
|
|
131
133
|
await withTransaction(db, async (session) => {
|
|
132
134
|
await bucketData.insertOne(document, { session });
|
|
133
|
-
|
|
135
|
+
const writes = db.createWriteBatch(session, { ordered: false });
|
|
136
|
+
usage.applyDelta(definitionId, 123n, writes);
|
|
137
|
+
await writes.execute();
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
expect(await bucketData.countDocuments({ storage_ref: { $exists: true } })).toBe(1);
|
|
@@ -182,7 +186,11 @@ describe('ObjectStorageUsage', () => {
|
|
|
182
186
|
const writerId = 'reused-writer';
|
|
183
187
|
const usage = new ObjectStorageUsage(db, replicationStreamId, writerId);
|
|
184
188
|
|
|
185
|
-
await withTransaction(db, (session) =>
|
|
189
|
+
await withTransaction(db, async (session) => {
|
|
190
|
+
const writes = db.createWriteBatch(session, { ordered: false });
|
|
191
|
+
usage.applyDelta(definitionId, 10n, writes);
|
|
192
|
+
await writes.execute();
|
|
193
|
+
});
|
|
186
194
|
await db.objectStorageUsage.updateOne(
|
|
187
195
|
{ _id: { g: replicationStreamId, w: writerId } },
|
|
188
196
|
{ $set: { updated_at: new Date(0) } }
|
|
@@ -191,7 +199,11 @@ describe('ObjectStorageUsage', () => {
|
|
|
191
199
|
|
|
192
200
|
// The folder committed first and deleted the writer shard. The writer's
|
|
193
201
|
// next transaction must recreate it through the upsert.
|
|
194
|
-
await withTransaction(db, (session) =>
|
|
202
|
+
await withTransaction(db, async (session) => {
|
|
203
|
+
const writes = db.createWriteBatch(session, { ordered: false });
|
|
204
|
+
usage.applyDelta(definitionId, 4n, writes);
|
|
205
|
+
await writes.execute();
|
|
206
|
+
});
|
|
195
207
|
expect((await readUsageEntries(db, replicationStreamId))[0].active_bytes).toBe(14n);
|
|
196
208
|
|
|
197
209
|
await db.objectStorageUsage.updateOne(
|