Merge pull request #53419 from nextcloud/leftybournes/fix/object_storage_large_uploads

fix(s3): retry failed multipart uploads with decreased concurrency
This commit is contained in:
Kent Delante 2025-06-18 21:07:06 +08:00 committed by GitHub
commit ea98e4b3f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -119,28 +119,54 @@ trait S3ObjectTrait {
protected function writeMultiPart(string $urn, StreamInterface $stream, array $metaData): void {
$mimetype = $metaData['mimetype'] ?? null;
unset($metaData['mimetype']);
$uploader = new MultipartUploader($this->getConnection(), $stream, [
'bucket' => $this->bucket,
'concurrency' => $this->concurrency,
'key' => $urn,
'part_size' => $this->uploadPartSize,
'params' => [
'ContentType' => $mimetype,
'Metadata' => $this->buildS3Metadata($metaData),
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters(),
]);
try {
$uploader->upload();
} catch (S3MultipartUploadException $e) {
$attempts = 0;
$uploaded = false;
$concurrency = $this->concurrency;
$exception = null;
$state = null;
// retry multipart upload once with concurrency at half on failure
while (!$uploaded && $attempts <= 1) {
$uploader = new MultipartUploader($this->getConnection(), $stream, [
'bucket' => $this->bucket,
'concurrency' => $concurrency,
'key' => $urn,
'part_size' => $this->uploadPartSize,
'state' => $state,
'params' => [
'ContentType' => $mimetype,
'Metadata' => $this->buildS3Metadata($metaData),
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters(),
]);
try {
$uploader->upload();
$uploaded = true;
} catch (S3MultipartUploadException $e) {
$exception = $e;
$attempts++;
if ($concurrency > 1) {
$concurrency = round($concurrency / 2);
}
if ($stream->isSeekable()) {
$stream->rewind();
}
}
}
if (!$uploaded) {
// if anything goes wrong with multipart, make sure that you don´t poison and
// slow down s3 bucket with orphaned fragments
$uploadInfo = $e->getState()->getId();
if ($e->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) {
$uploadInfo = $exception->getState()->getId();
if ($exception->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) {
$this->getConnection()->abortMultipartUpload($uploadInfo);
}
throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway('Error while uploading to S3 bucket', 0, $e);
throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway('Error while uploading to S3 bucket', 0, $exception);
}
}