@powersync/service-module-mongodb-storage 0.20.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 +62 -0
- package/dist/storage/MongoBucketStorage.d.ts +36 -3
- package/dist/storage/MongoBucketStorage.js +372 -207
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +17 -5
- package/dist/storage/implementation/MongoBucketBatch.js +44 -17
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +7 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
- package/dist/storage/implementation/common/PersistedBatch.js +49 -6
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +9 -1
- package/dist/storage/implementation/db.js +27 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +31 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +33 -4
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
- package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +10 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +100 -26
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +82 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +698 -159
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +76 -42
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +84 -4
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +6 -6
- package/dist/storage/implementation/v3/source-table-utils.js +30 -37
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +28 -1
- package/dist/types/types.js +28 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +1 -0
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +507 -269
- package/src/storage/implementation/MongoBucketBatch.ts +67 -24
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +15 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +66 -10
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +35 -2
- package/src/storage/implementation/models.ts +34 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +46 -6
- package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +138 -34
- package/src/storage/implementation/v3/MongoCompactorV3.ts +958 -225
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +93 -48
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +91 -3
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
- package/src/storage/implementation/v3/source-table-utils.ts +39 -41
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +1 -0
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +267 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +273 -22
- package/test/src/storage_compacting.test.ts +968 -246
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +290 -2
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +336 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectStorageLifecycle.js","sourceRoot":"","sources":["../../../../../src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ObjectStorageLifecycle.js","sourceRoot":"","sources":["../../../../../src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
3
|
+
import { MongoWriteBatch } from '../../MongoWriteBatch.js';
|
|
4
|
+
import { BucketDataDocumentV3 } from '../models.js';
|
|
5
|
+
import { VersionedPowerSyncMongoV3 } from '../VersionedPowerSyncMongoV3.js';
|
|
6
|
+
export declare const OBJECT_STORAGE_USAGE_BASE_WRITER_ID = "__base__";
|
|
7
|
+
export declare const DEFAULT_OBJECT_STORAGE_USAGE_STALE_WRITER_MS: number;
|
|
8
|
+
export declare const DEFAULT_OBJECT_STORAGE_USAGE_FOLD_LIMIT = 100;
|
|
9
|
+
export interface ReplicationStreamObjectStorageDefinitionUsageResult {
|
|
10
|
+
replication_stream_id: number;
|
|
11
|
+
definition_id: BucketDefinitionId;
|
|
12
|
+
active_bytes: bigint;
|
|
13
|
+
}
|
|
14
|
+
export declare function createObjectStorageUsageWriterId(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Tracks active S3 references. Values are signed writer-local deltas, so reference changes and
|
|
17
|
+
* accounting updates can be committed together without making replication and compaction share a
|
|
18
|
+
* hot counter document.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ObjectStorageUsage {
|
|
21
|
+
private readonly db;
|
|
22
|
+
private readonly replicationStreamId;
|
|
23
|
+
readonly writerId?: string | undefined;
|
|
24
|
+
constructor(db: VersionedPowerSyncMongoV3, replicationStreamId: number, writerId?: string | undefined);
|
|
25
|
+
static bytes(document: Pick<BucketDataDocumentV3, 'storage_ref'>): bigint;
|
|
26
|
+
static readAllDefinitionUsage(db: VersionedPowerSyncMongoV3): Promise<ReplicationStreamObjectStorageDefinitionUsageResult[]>;
|
|
27
|
+
applyDelta(definitionId: BucketDefinitionId, delta: bigint, writes: MongoWriteBatch): void;
|
|
28
|
+
applyDeltas(deltas: ReadonlyMap<BucketDefinitionId, bigint>, writes: MongoWriteBatch): void;
|
|
29
|
+
removeDefinition(definitionId: BucketDefinitionId, session: mongo.ClientSession): Promise<void>;
|
|
30
|
+
removeStream(session: mongo.ClientSession): Promise<void>;
|
|
31
|
+
foldStaleWriterDeltas(options?: {
|
|
32
|
+
staleWriterMs?: number;
|
|
33
|
+
limit?: number;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
private definitionPath;
|
|
36
|
+
private documentId;
|
|
37
|
+
private validateDefinitionId;
|
|
38
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
export const OBJECT_STORAGE_USAGE_BASE_WRITER_ID = '__base__';
|
|
5
|
+
export const DEFAULT_OBJECT_STORAGE_USAGE_STALE_WRITER_MS = 30 * 60 * 1000;
|
|
6
|
+
export const DEFAULT_OBJECT_STORAGE_USAGE_FOLD_LIMIT = 100;
|
|
7
|
+
export function createObjectStorageUsageWriterId() {
|
|
8
|
+
return randomUUID();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Tracks active S3 references. Values are signed writer-local deltas, so reference changes and
|
|
12
|
+
* accounting updates can be committed together without making replication and compaction share a
|
|
13
|
+
* hot counter document.
|
|
14
|
+
*/
|
|
15
|
+
export class ObjectStorageUsage {
|
|
16
|
+
db;
|
|
17
|
+
replicationStreamId;
|
|
18
|
+
writerId;
|
|
19
|
+
constructor(db, replicationStreamId, writerId) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
this.replicationStreamId = replicationStreamId;
|
|
22
|
+
this.writerId = writerId;
|
|
23
|
+
if (writerId === OBJECT_STORAGE_USAGE_BASE_WRITER_ID) {
|
|
24
|
+
throw new ReplicationAssertionError('The reserved object-storage usage writer id cannot be used by a writer');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
static bytes(document) {
|
|
28
|
+
return BigInt(document.storage_ref?.file_size ?? 0);
|
|
29
|
+
}
|
|
30
|
+
static async readAllDefinitionUsage(db) {
|
|
31
|
+
return db.client.withSession({ snapshot: true }, async (session) => {
|
|
32
|
+
// Deliberately read the whole usage collection. The number of replication streams is
|
|
33
|
+
// expected to stay low, so a collection scan is cheaper than issuing one _id range query
|
|
34
|
+
// per stream (and the usage collection is bounded by streams and writers, not buckets).
|
|
35
|
+
const entries = await db.objectStorageUsage
|
|
36
|
+
.aggregate([
|
|
37
|
+
{ $project: { replication_stream_id: '$_id.g', definitions: { $objectToArray: '$definitions' } } },
|
|
38
|
+
{ $unwind: '$definitions' },
|
|
39
|
+
{
|
|
40
|
+
$group: {
|
|
41
|
+
_id: {
|
|
42
|
+
replication_stream_id: '$replication_stream_id',
|
|
43
|
+
definition_id: '$definitions.k'
|
|
44
|
+
},
|
|
45
|
+
active_bytes: { $sum: '$definitions.v' }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
], { session, readConcern: 'snapshot' })
|
|
49
|
+
.toArray()
|
|
50
|
+
.catch((error) => {
|
|
51
|
+
if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
});
|
|
56
|
+
return entries.map((entry) => {
|
|
57
|
+
const activeBytes = BigInt(entry.active_bytes ?? 0);
|
|
58
|
+
if (activeBytes < 0n) {
|
|
59
|
+
return {
|
|
60
|
+
replication_stream_id: entry._id.replication_stream_id,
|
|
61
|
+
definition_id: entry._id.definition_id,
|
|
62
|
+
active_bytes: 0n
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
replication_stream_id: entry._id.replication_stream_id,
|
|
67
|
+
definition_id: entry._id.definition_id,
|
|
68
|
+
active_bytes: activeBytes
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
applyDelta(definitionId, delta, writes) {
|
|
74
|
+
this.applyDeltas(new Map([[definitionId, delta]]), writes);
|
|
75
|
+
}
|
|
76
|
+
applyDeltas(deltas, writes) {
|
|
77
|
+
if (this.writerId == null) {
|
|
78
|
+
throw new ReplicationAssertionError('A writer id is required to apply object-storage usage');
|
|
79
|
+
}
|
|
80
|
+
const increments = {};
|
|
81
|
+
for (const [definitionId, delta] of deltas) {
|
|
82
|
+
if (delta === 0n) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
increments[this.definitionPath(definitionId)] = delta;
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(increments).length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
writes.updateOne(this.db.objectStorageUsage, { _id: this.documentId() }, { $inc: increments, $currentDate: { updated_at: true } }, { upsert: true });
|
|
91
|
+
}
|
|
92
|
+
async removeDefinition(definitionId, session) {
|
|
93
|
+
await this.db.objectStorageUsage.updateMany({ '_id.g': this.replicationStreamId }, { $unset: { [this.definitionPath(definitionId)]: 1 } }, { session });
|
|
94
|
+
}
|
|
95
|
+
async removeStream(session) {
|
|
96
|
+
await this.db.objectStorageUsage.deleteMany({ '_id.g': this.replicationStreamId }, { session });
|
|
97
|
+
}
|
|
98
|
+
async foldStaleWriterDeltas(options = {}) {
|
|
99
|
+
const staleWriterMs = options.staleWriterMs ?? DEFAULT_OBJECT_STORAGE_USAGE_STALE_WRITER_MS;
|
|
100
|
+
const limit = options.limit ?? DEFAULT_OBJECT_STORAGE_USAGE_FOLD_LIMIT;
|
|
101
|
+
if (limit <= 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await this.db.client.withSession((session) => session.withTransaction(async () => {
|
|
105
|
+
const staleDocuments = await this.db.objectStorageUsage
|
|
106
|
+
.find({
|
|
107
|
+
'_id.g': this.replicationStreamId,
|
|
108
|
+
'_id.w': { $ne: OBJECT_STORAGE_USAGE_BASE_WRITER_ID },
|
|
109
|
+
$expr: {
|
|
110
|
+
$lt: [
|
|
111
|
+
'$updated_at',
|
|
112
|
+
{ $dateSubtract: { startDate: '$$NOW', unit: 'millisecond', amount: staleWriterMs } }
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
}, { session, sort: { updated_at: 1 }, limit })
|
|
116
|
+
.toArray();
|
|
117
|
+
if (staleDocuments.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const deltas = new Map();
|
|
121
|
+
for (const document of staleDocuments) {
|
|
122
|
+
for (const [definitionId, delta] of Object.entries(document.definitions ?? {})) {
|
|
123
|
+
this.validateDefinitionId(definitionId);
|
|
124
|
+
deltas.set(definitionId, (deltas.get(definitionId) ?? 0n) + BigInt(delta));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const increments = {};
|
|
128
|
+
for (const [definitionId, delta] of deltas) {
|
|
129
|
+
if (delta !== 0n) {
|
|
130
|
+
increments[this.definitionPath(definitionId)] = delta;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(increments).length > 0) {
|
|
134
|
+
await this.db.objectStorageUsage.updateOne({ _id: this.documentId(OBJECT_STORAGE_USAGE_BASE_WRITER_ID) }, {
|
|
135
|
+
$inc: increments,
|
|
136
|
+
$currentDate: { updated_at: true }
|
|
137
|
+
}, { upsert: true, session });
|
|
138
|
+
}
|
|
139
|
+
await this.db.objectStorageUsage.deleteMany({
|
|
140
|
+
'_id.g': this.replicationStreamId,
|
|
141
|
+
_id: { $in: staleDocuments.map((document) => document._id) }
|
|
142
|
+
}, { session });
|
|
143
|
+
}, { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } }));
|
|
144
|
+
}
|
|
145
|
+
definitionPath(definitionId) {
|
|
146
|
+
this.validateDefinitionId(definitionId);
|
|
147
|
+
return `definitions.${definitionId}`;
|
|
148
|
+
}
|
|
149
|
+
documentId(writerId = this.writerId) {
|
|
150
|
+
if (writerId == null) {
|
|
151
|
+
throw new ReplicationAssertionError('A writer id is required to identify object-storage usage');
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
g: this.replicationStreamId,
|
|
155
|
+
w: writerId
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
validateDefinitionId(definitionId) {
|
|
159
|
+
if (definitionId.length === 0 || definitionId.includes('.') || definitionId.includes('$')) {
|
|
160
|
+
throw new ReplicationAssertionError(`Invalid bucket definition id for object-storage usage: ${definitionId}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=ObjectStorageUsage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ObjectStorageUsage.js","sourceRoot":"","sources":["../../../../../src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
-
import { type
|
|
2
|
+
import { type DefaultsMode, type ResolvedDefaultsMode } from '@smithy/core/client';
|
|
3
|
+
import { type ObjectStorage, type ObjectStorageOperationOptions, type ObjectStoragePutMetadata } from './ObjectStorage.js';
|
|
4
|
+
export interface S3TimeoutProfile {
|
|
5
|
+
/**
|
|
6
|
+
* Per attempt: establishing the connection, including waiting for a socket from the pool.
|
|
7
|
+
*/
|
|
8
|
+
connectionTimeoutMs: number;
|
|
9
|
+
/**
|
|
10
|
+
* Per attempt: from starting the request until the response headers are received. For uploads
|
|
11
|
+
* this includes sending the body.
|
|
12
|
+
*/
|
|
13
|
+
requestTimeoutMs: number;
|
|
14
|
+
/**
|
|
15
|
+
* The complete operation, including SDK retries and streaming the response body.
|
|
16
|
+
*
|
|
17
|
+
* The SDK timeouts above stop applying once the response headers are received, so this is what
|
|
18
|
+
* bounds how long a single operation can occupy a concurrency slot.
|
|
19
|
+
*/
|
|
20
|
+
operationTimeoutMs: number;
|
|
21
|
+
/**
|
|
22
|
+
* Waiting for a concurrency slot, before the operation itself starts.
|
|
23
|
+
*/
|
|
24
|
+
queueTimeoutMs: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function resolveTimeoutProfile(mode: ResolvedDefaultsMode): S3TimeoutProfile;
|
|
3
27
|
export interface S3ObjectStorageOptions {
|
|
4
28
|
bucket: string;
|
|
5
29
|
region?: string;
|
|
@@ -9,6 +33,12 @@ export interface S3ObjectStorageOptions {
|
|
|
9
33
|
accessKeyId?: string;
|
|
10
34
|
secretAccessKey?: string;
|
|
11
35
|
concurrencyLimit?: number;
|
|
36
|
+
/**
|
|
37
|
+
* AWS defaults mode, used as the baseline for the request timeouts.
|
|
38
|
+
*
|
|
39
|
+
* Defaults to the AWS_DEFAULTS_MODE environment variable. `auto` is treated as `standard`.
|
|
40
|
+
*/
|
|
41
|
+
defaultsMode?: DefaultsMode;
|
|
12
42
|
}
|
|
13
43
|
export declare class S3ObjectStorage implements ObjectStorage {
|
|
14
44
|
/**
|
|
@@ -18,21 +48,16 @@ export declare class S3ObjectStorage implements ObjectStorage {
|
|
|
18
48
|
private bucket;
|
|
19
49
|
private prefix;
|
|
20
50
|
private readonly operationSemaphore;
|
|
51
|
+
private readonly timeouts;
|
|
21
52
|
constructor(options: S3ObjectStorageOptions);
|
|
22
|
-
put(path: string, data: Uint8Array, metadata: ObjectStoragePutMetadata): Promise<void>;
|
|
23
|
-
get(path: string, options?: {
|
|
24
|
-
signal?: AbortSignal;
|
|
25
|
-
}): Promise<{
|
|
53
|
+
put(path: string, data: Uint8Array, metadata: ObjectStoragePutMetadata, options?: ObjectStorageOperationOptions): Promise<void>;
|
|
54
|
+
get(path: string, options?: ObjectStorageOperationOptions): Promise<{
|
|
26
55
|
data: Uint8Array;
|
|
27
56
|
metadata: ObjectStoragePutMetadata;
|
|
28
57
|
}>;
|
|
29
|
-
list(prefix: string, options?:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
delete(paths: string[]): Promise<void>;
|
|
33
|
-
deletePrefix(prefix: string, options?: {
|
|
34
|
-
signal?: AbortSignal;
|
|
35
|
-
}): Promise<{
|
|
58
|
+
list(prefix: string, options?: ObjectStorageOperationOptions): AsyncIterable<string>;
|
|
59
|
+
delete(paths: string[], options?: ObjectStorageOperationOptions): Promise<void>;
|
|
60
|
+
deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{
|
|
36
61
|
objectCount: number;
|
|
37
62
|
}>;
|
|
38
63
|
private listPage;
|
|
@@ -1,16 +1,115 @@
|
|
|
1
1
|
import { DeleteObjectsCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
2
3
|
import { acquireSemaphoreAbortable, isAbortError } from '@powersync/service-core';
|
|
4
|
+
import { loadConfigsForDefaultMode } from '@smithy/core/client';
|
|
3
5
|
import { isThrottlingError, isTransientError } from '@smithy/core/retry';
|
|
6
|
+
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
|
4
7
|
import { Semaphore } from 'async-mutex';
|
|
5
8
|
import { ObjectStorageError } from './ObjectStorage.js';
|
|
6
|
-
|
|
9
|
+
// Never statically import (or re-export) this module!
|
|
10
|
+
// We want to avoid loading the AWS SDK unless the user has configured S3 object storage, since it's a large dependency.
|
|
11
|
+
const DEFAULT_S3_OPERATION_CONCURRENCY = 64;
|
|
7
12
|
const S3_DELETE_PREFIX_BATCH_SIZE = 1000;
|
|
8
13
|
/**
|
|
9
|
-
*
|
|
14
|
+
* Keep prefix deletion below the default shared limit to leave capacity for other operations.
|
|
10
15
|
*/
|
|
11
16
|
const S3_DELETE_PREFIX_CONCURRENCY = 12;
|
|
12
17
|
const MAX_S3_PREFIX_BYTES = 256;
|
|
13
18
|
const SAFE_S3_KEY_BYTES = 896;
|
|
19
|
+
/**
|
|
20
|
+
* Timeouts are derived from the AWS defaults mode, rather than being individually configurable.
|
|
21
|
+
*
|
|
22
|
+
* The mode - AWS_DEFAULTS_MODE or the defaults_mode storage option - describes the expected latency
|
|
23
|
+
* between this service and the object storage endpoint. We use the connection timeout it defines
|
|
24
|
+
* as the baseline for the other timeouts:
|
|
25
|
+
*
|
|
26
|
+
* | mode | connection | request | operation | queue |
|
|
27
|
+
* | ---------------------- | ---------- | ------- | --------- | ----- |
|
|
28
|
+
* | in-region | 1.1s | 5.5s | 11s | 44s |
|
|
29
|
+
* | standard, cross-region | 3.1s | 15.5s | 31s | 124s |
|
|
30
|
+
* | mobile | 30s | 150s | 300s | 1200s |
|
|
31
|
+
*
|
|
32
|
+
* The multipliers are sized for the objects we store: bucket data chunks target 1MB, and are
|
|
33
|
+
* bounded by the 16MB BSON document limit. Even at the upper bound, the request timeout only has to
|
|
34
|
+
* cover a transfer of a few MB/s, and the operation timeout leaves room for a second attempt.
|
|
35
|
+
*
|
|
36
|
+
* `legacy`, the mode used when nothing is configured, does not define any timeouts, and is treated
|
|
37
|
+
* as `standard` here. Running without timeouts is not an option: a stalled request holds one of the
|
|
38
|
+
* limited operation slots until the process restarts, and enough of those block all reads.
|
|
39
|
+
*
|
|
40
|
+
* `auto` is also treated as `standard` - see resolveConfiguredDefaultsMode() below.
|
|
41
|
+
*/
|
|
42
|
+
const BASELINE_CONNECTION_TIMEOUT_MS = 3_100;
|
|
43
|
+
const REQUEST_TIMEOUT_FACTOR = 5;
|
|
44
|
+
const OPERATION_TIMEOUT_FACTOR = 2;
|
|
45
|
+
/**
|
|
46
|
+
* Don't keep idle http sockets around for longer than this timeout.
|
|
47
|
+
*
|
|
48
|
+
* This covers the case where we have large bursts of activity during replication, followed by
|
|
49
|
+
* long periods of inactivity.
|
|
50
|
+
*/
|
|
51
|
+
const SOCKET_IDLE_TIMEOUT_MS = 60_000;
|
|
52
|
+
/**
|
|
53
|
+
* How long an operation may wait for a concurrency slot, as a multiple of the operation timeout.
|
|
54
|
+
*
|
|
55
|
+
* Every slot holder now releases within the operation timeout, so the queue always drains: this is
|
|
56
|
+
* a backpressure limit rather than a deadlock guard. Waiting this long means at least
|
|
57
|
+
* `factor * concurrencyLimit` operations ahead of us each took their full deadline - 256 at the
|
|
58
|
+
* default concurrency. Real operations complete in a fraction of the deadline, so the number of
|
|
59
|
+
* queued operations this actually tolerates is far higher, well above the ~2000 uploads that the
|
|
60
|
+
* largest possible replication flush (MAX_TRANSACTION_DOC_COUNT) can enqueue at once.
|
|
61
|
+
*/
|
|
62
|
+
const QUEUE_TIMEOUT_FACTOR = 4;
|
|
63
|
+
export function resolveTimeoutProfile(mode) {
|
|
64
|
+
// Only connectionTimeout is currently vended by the SDK, but requestTimeout is part of the same
|
|
65
|
+
// contract - use it as the hint if a future version starts defining it.
|
|
66
|
+
const { connectionTimeout, requestTimeout } = loadConfigsForDefaultMode(mode);
|
|
67
|
+
const connectionTimeoutMs = connectionTimeout ?? BASELINE_CONNECTION_TIMEOUT_MS;
|
|
68
|
+
const requestTimeoutMs = requestTimeout ?? connectionTimeoutMs * REQUEST_TIMEOUT_FACTOR;
|
|
69
|
+
const operationTimeoutMs = requestTimeoutMs * OPERATION_TIMEOUT_FACTOR;
|
|
70
|
+
return {
|
|
71
|
+
connectionTimeoutMs,
|
|
72
|
+
requestTimeoutMs,
|
|
73
|
+
operationTimeoutMs,
|
|
74
|
+
queueTimeoutMs: operationTimeoutMs * QUEUE_TIMEOUT_FACTOR
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const AWS_DEFAULTS_MODE_ENV = 'AWS_DEFAULTS_MODE';
|
|
78
|
+
const RESOLVED_DEFAULTS_MODES = ['standard', 'in-region', 'cross-region', 'mobile', 'legacy'];
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the configured AWS defaults mode: the explicit option, then the AWS_DEFAULTS_MODE
|
|
81
|
+
* environment variable, then `legacy`, which is also the AWS default.
|
|
82
|
+
*
|
|
83
|
+
* Unlike the SDK's own lookup, this does not read defaults_mode from the AWS shared config file.
|
|
84
|
+
* That lookup is asynchronous, and the mode is only used to pick timeouts, which we want available
|
|
85
|
+
* synchronously so that operations never have to wait on configuration.
|
|
86
|
+
*
|
|
87
|
+
* `auto` is substituted with `standard`. It asks the SDK to detect whether it is running in the
|
|
88
|
+
* same region as the endpoint, which means querying the EC2 instance metadata service on startup:
|
|
89
|
+
*
|
|
90
|
+
* - The detection only succeeds on EC2-family compute with instance metadata reachable. Anywhere
|
|
91
|
+
* else - including EC2 with IMDS disabled or behind a low hop limit - it silently falls back to
|
|
92
|
+
* `standard`, so the timeouts differ by deployment with no indication of why.
|
|
93
|
+
* - It requires a resolvable region, and fails the client outright when there is none.
|
|
94
|
+
* - Timeouts that depend on where the process happens to run are hard to reason about when
|
|
95
|
+
* diagnosing a stall.
|
|
96
|
+
*
|
|
97
|
+
* Deployments that want the tighter in-region timeouts should ask for them explicitly.
|
|
98
|
+
*/
|
|
99
|
+
function resolveConfiguredDefaultsMode(configured) {
|
|
100
|
+
// An empty environment variable is treated as unset, matching the SDK.
|
|
101
|
+
const mode = (configured || process.env[AWS_DEFAULTS_MODE_ENV] || 'legacy').toLowerCase();
|
|
102
|
+
if (mode === 'auto') {
|
|
103
|
+
logger.warn(`Ignoring AWS defaults mode "auto" for object storage, using "standard" instead. ` +
|
|
104
|
+
`Configure storage.object_storage.defaults_mode to select a mode explicitly.`);
|
|
105
|
+
return 'standard';
|
|
106
|
+
}
|
|
107
|
+
const resolved = RESOLVED_DEFAULTS_MODES.find((candidate) => candidate === mode);
|
|
108
|
+
if (resolved == null) {
|
|
109
|
+
throw new Error(`Invalid AWS defaults mode ${JSON.stringify(mode)}, expected one of ${RESOLVED_DEFAULTS_MODES.join(', ')}, auto`);
|
|
110
|
+
}
|
|
111
|
+
return resolved;
|
|
112
|
+
}
|
|
14
113
|
export class S3ObjectStorage {
|
|
15
114
|
/**
|
|
16
115
|
* Public for tests only.
|
|
@@ -19,6 +118,7 @@ export class S3ObjectStorage {
|
|
|
19
118
|
bucket;
|
|
20
119
|
prefix;
|
|
21
120
|
operationSemaphore;
|
|
121
|
+
timeouts;
|
|
22
122
|
constructor(options) {
|
|
23
123
|
const concurrencyLimit = options.concurrencyLimit ?? DEFAULT_S3_OPERATION_CONCURRENCY;
|
|
24
124
|
if (!Number.isInteger(concurrencyLimit) || concurrencyLimit <= 0) {
|
|
@@ -37,18 +137,37 @@ export class S3ObjectStorage {
|
|
|
37
137
|
this.bucket = options.bucket;
|
|
38
138
|
this.prefix = prefix;
|
|
39
139
|
this.operationSemaphore = new Semaphore(concurrencyLimit);
|
|
140
|
+
// Resolve the mode once, and share it with the client so that its own defaults agree with ours.
|
|
141
|
+
const defaultsMode = resolveConfiguredDefaultsMode(options.defaultsMode);
|
|
142
|
+
this.timeouts = resolveTimeoutProfile(defaultsMode);
|
|
40
143
|
this.client = new S3Client({
|
|
41
144
|
region: options.region,
|
|
42
145
|
endpoint: options.endpoint,
|
|
43
146
|
forcePathStyle: options.forcePathStyle,
|
|
147
|
+
defaultsMode,
|
|
148
|
+
requestHandler: new NodeHttpHandler({
|
|
149
|
+
// Match the shared request limit instead of queuing again at the HTTP pool.
|
|
150
|
+
httpAgent: {
|
|
151
|
+
keepAlive: true,
|
|
152
|
+
maxSockets: concurrencyLimit,
|
|
153
|
+
// The timeout here is a little more general than an "idle timeout", but it does cover the
|
|
154
|
+
// case of closing idle sockets after a period of inactivity.
|
|
155
|
+
timeout: SOCKET_IDLE_TIMEOUT_MS
|
|
156
|
+
},
|
|
157
|
+
httpsAgent: { keepAlive: true, maxSockets: concurrencyLimit, timeout: SOCKET_IDLE_TIMEOUT_MS },
|
|
158
|
+
connectionTimeout: this.timeouts.connectionTimeoutMs,
|
|
159
|
+
requestTimeout: this.timeouts.requestTimeoutMs,
|
|
160
|
+
// Without this, exceeding requestTimeout only logs a warning.
|
|
161
|
+
throwOnRequestTimeout: true
|
|
162
|
+
}),
|
|
44
163
|
credentials: options.accessKeyId && options.secretAccessKey
|
|
45
164
|
? { accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey }
|
|
46
165
|
: undefined
|
|
47
166
|
});
|
|
48
167
|
}
|
|
49
|
-
async put(path, data, metadata) {
|
|
168
|
+
async put(path, data, metadata, options) {
|
|
50
169
|
const fullPath = this.fullPath(path);
|
|
51
|
-
await using
|
|
170
|
+
await using operation = await this.withOperation('upload', fullPath, options?.signal);
|
|
52
171
|
try {
|
|
53
172
|
await this.client.send(new PutObjectCommand({
|
|
54
173
|
Bucket: this.bucket,
|
|
@@ -56,16 +175,17 @@ export class S3ObjectStorage {
|
|
|
56
175
|
Body: data,
|
|
57
176
|
ContentType: metadata.contentType,
|
|
58
177
|
ContentEncoding: metadata.contentEncoding ?? undefined
|
|
59
|
-
}));
|
|
178
|
+
}), { abortSignal: operation.signal });
|
|
60
179
|
}
|
|
61
180
|
catch (error) {
|
|
62
|
-
throw
|
|
181
|
+
throw operation.error(error);
|
|
63
182
|
}
|
|
64
183
|
}
|
|
65
184
|
async get(path, options) {
|
|
66
185
|
const fullPath = this.fullPath(path);
|
|
67
|
-
|
|
68
|
-
|
|
186
|
+
await using operation = await this.withOperation('download', fullPath, options?.signal);
|
|
187
|
+
// Includes the operation deadline, which also covers streaming the response body below.
|
|
188
|
+
const signal = operation.signal;
|
|
69
189
|
try {
|
|
70
190
|
const response = await this.client.send(new GetObjectCommand({
|
|
71
191
|
Bucket: this.bucket,
|
|
@@ -82,7 +202,7 @@ export class S3ObjectStorage {
|
|
|
82
202
|
let offset = 0;
|
|
83
203
|
const stream = response.Body;
|
|
84
204
|
for await (const chunk of stream) {
|
|
85
|
-
signal
|
|
205
|
+
signal.throwIfAborted();
|
|
86
206
|
const nextOffset = offset + chunk.byteLength;
|
|
87
207
|
if (nextOffset > contentLength) {
|
|
88
208
|
throw new Error(`S3 download ContentLength mismatch for ${fullPath}: expected ${contentLength} bytes, received at least ${nextOffset}`);
|
|
@@ -105,7 +225,7 @@ export class S3ObjectStorage {
|
|
|
105
225
|
if (err.name === 'NoSuchKey' || err.Code === 'NoSuchKey') {
|
|
106
226
|
throw new ObjectStorageError(`S3 object not found: ${fullPath}`, { cause: err, retryable: false });
|
|
107
227
|
}
|
|
108
|
-
throw
|
|
228
|
+
throw operation.error(err);
|
|
109
229
|
}
|
|
110
230
|
}
|
|
111
231
|
async *list(prefix, options) {
|
|
@@ -127,9 +247,9 @@ export class S3ObjectStorage {
|
|
|
127
247
|
}
|
|
128
248
|
} while (continuationToken != null);
|
|
129
249
|
}
|
|
130
|
-
async delete(paths) {
|
|
250
|
+
async delete(paths, options) {
|
|
131
251
|
const fullPaths = paths.map((path) => ({ Key: this.fullPath(path) }));
|
|
132
|
-
await this.deleteFullPaths(fullPaths);
|
|
252
|
+
await this.deleteFullPaths(fullPaths, options?.signal);
|
|
133
253
|
}
|
|
134
254
|
async deletePrefix(prefix, options) {
|
|
135
255
|
const fullPrefix = this.fullPath(prefix);
|
|
@@ -189,32 +309,34 @@ export class S3ObjectStorage {
|
|
|
189
309
|
return { objectCount };
|
|
190
310
|
}
|
|
191
311
|
async listPage(fullPrefix, continuationToken, signal) {
|
|
312
|
+
// Acquired outside the try, matching the other operations: withOperation() already reports its
|
|
313
|
+
// own failures as ObjectStorageError.
|
|
314
|
+
await using operation = await this.withOperation('list', fullPrefix, signal);
|
|
192
315
|
try {
|
|
193
|
-
await using _ = await this.withOperation(signal);
|
|
194
316
|
return await this.client.send(new ListObjectsV2Command({
|
|
195
317
|
Bucket: this.bucket,
|
|
196
318
|
Prefix: fullPrefix,
|
|
197
319
|
ContinuationToken: continuationToken,
|
|
198
320
|
MaxKeys: S3_DELETE_PREFIX_BATCH_SIZE
|
|
199
|
-
}), { abortSignal: signal });
|
|
321
|
+
}), { abortSignal: operation.signal });
|
|
200
322
|
}
|
|
201
323
|
catch (error) {
|
|
202
|
-
throw
|
|
324
|
+
throw operation.error(error);
|
|
203
325
|
}
|
|
204
326
|
}
|
|
205
327
|
async deleteFullPaths(fullPaths, signal) {
|
|
206
328
|
if (fullPaths.length === 0)
|
|
207
329
|
return;
|
|
208
|
-
await using
|
|
330
|
+
await using operation = await this.withOperation('delete', `${fullPaths.length} objects`, signal);
|
|
209
331
|
let response;
|
|
210
332
|
try {
|
|
211
333
|
response = await this.client.send(new DeleteObjectsCommand({
|
|
212
334
|
Bucket: this.bucket,
|
|
213
335
|
Delete: { Objects: fullPaths, Quiet: true }
|
|
214
|
-
}), { abortSignal: signal });
|
|
336
|
+
}), { abortSignal: operation.signal });
|
|
215
337
|
}
|
|
216
338
|
catch (error) {
|
|
217
|
-
throw
|
|
339
|
+
throw operation.error(error);
|
|
218
340
|
}
|
|
219
341
|
if (response.Errors?.length) {
|
|
220
342
|
const errors = response.Errors.map((error) => `${error.Key}: ${error.Code}`).join(', ');
|
|
@@ -232,25 +354,75 @@ export class S3ObjectStorage {
|
|
|
232
354
|
}
|
|
233
355
|
return fullPath;
|
|
234
356
|
}
|
|
235
|
-
async withOperation(signal) {
|
|
357
|
+
async withOperation(operation, target, signal) {
|
|
236
358
|
signal?.throwIfAborted();
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
359
|
+
const { operationTimeoutMs, queueTimeoutMs } = this.timeouts;
|
|
360
|
+
// Waiting for a slot is bounded even when the caller has no signal of its own, which is the
|
|
361
|
+
// case for uploads and deletes.
|
|
362
|
+
const queueDeadline = timeoutSignal(queueTimeoutMs, () => s3TimeoutError(operation, target, `timed out after ${queueTimeoutMs} ms waiting for a concurrency slot`));
|
|
363
|
+
let acquired;
|
|
364
|
+
try {
|
|
365
|
+
acquired = await acquireSemaphoreAbortable(this.operationSemaphore, signal ? AbortSignal.any([signal, queueDeadline.signal]) : queueDeadline.signal);
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
queueDeadline.cancel();
|
|
369
|
+
}
|
|
240
370
|
if (acquired === 'aborted') {
|
|
241
|
-
|
|
242
|
-
|
|
371
|
+
// A caller abort takes precedence: it is a cancellation, not an S3 failure.
|
|
372
|
+
signal?.throwIfAborted();
|
|
373
|
+
throw queueDeadline.signal.reason;
|
|
243
374
|
}
|
|
244
375
|
const [, release] = acquired;
|
|
245
376
|
if (signal?.aborted) {
|
|
246
377
|
release();
|
|
247
378
|
signal.throwIfAborted();
|
|
248
379
|
}
|
|
380
|
+
// This deadline only starts once the operation holds a slot - queueing is covered above.
|
|
381
|
+
const deadline = timeoutSignal(operationTimeoutMs, () => s3TimeoutError(operation, target, `did not complete within ${operationTimeoutMs} ms`));
|
|
382
|
+
const operationSignal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
|
|
249
383
|
return {
|
|
250
|
-
|
|
384
|
+
signal: operationSignal,
|
|
385
|
+
error: (cause) => {
|
|
386
|
+
// Aborting a request that is already streaming destroys the socket, and the failure
|
|
387
|
+
// surfaces as a generic "aborted" error that says nothing about the cause. The abort
|
|
388
|
+
// reason is always the better answer: the caller's reason when the caller stopped us -
|
|
389
|
+
// callers abort with the error that stopped them, and upstream handling matches on that
|
|
390
|
+
// type - and an already-classified timeout when the deadline did.
|
|
391
|
+
if (operationSignal.aborted) {
|
|
392
|
+
return operationSignal.reason;
|
|
393
|
+
}
|
|
394
|
+
return s3OperationError(operation, target, cause);
|
|
395
|
+
},
|
|
396
|
+
[Symbol.asyncDispose]: async () => {
|
|
397
|
+
deadline.cancel();
|
|
398
|
+
release();
|
|
399
|
+
}
|
|
251
400
|
};
|
|
252
401
|
}
|
|
253
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* An abort signal that fires after the given timeout, using a timer that does not hold the process
|
|
405
|
+
* open.
|
|
406
|
+
*
|
|
407
|
+
* The reason is what callers see when the timeout fires, so it is the error to report - not a
|
|
408
|
+
* marker to translate later.
|
|
409
|
+
*/
|
|
410
|
+
function timeoutSignal(timeoutMs, createReason) {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const timer = setTimeout(() => controller.abort(createReason()), timeoutMs);
|
|
413
|
+
timer.unref();
|
|
414
|
+
return { signal: controller.signal, cancel: () => clearTimeout(timer) };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* A timeout that the compactor may retry, matching how the AWS SDK classifies its own request
|
|
418
|
+
* timeouts.
|
|
419
|
+
*/
|
|
420
|
+
function s3TimeoutError(operation, target, message) {
|
|
421
|
+
return new ObjectStorageError(`S3 ${operation} for ${target} ${message}`, {
|
|
422
|
+
cause: new DOMException(message, 'TimeoutError'),
|
|
423
|
+
retryable: true
|
|
424
|
+
});
|
|
425
|
+
}
|
|
254
426
|
function s3OperationError(operation, target, error) {
|
|
255
427
|
if (error instanceof ObjectStorageError) {
|
|
256
428
|
return error;
|