fix(s3): retry failed multipart uploads with decreased concurrency

Signed-off-by: Kent Delante <kent.delante@proton.me>
pull/53580/head
Kent Delante 2025-06-10 17:17:36 +07:00
parent c5c6dcb027
commit e9aa004b9b
1 changed files with 41 additions and 16 deletions

@ -107,27 +107,52 @@ trait S3ObjectTrait {
* @throws \Exception when something goes wrong, message will be logged
*/
protected function writeMultiPart(string $urn, StreamInterface $stream, ?string $mimetype = null): void {
$uploader = new MultipartUploader($this->getConnection(), $stream, [
'bucket' => $this->bucket,
'concurrency' => $this->concurrency,
'key' => $urn,
'part_size' => $this->uploadPartSize,
'params' => [
'ContentType' => $mimetype,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters(),
]);
$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,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters(),
]);
try {
$uploader->upload();
$uploaded = true;
} catch (S3MultipartUploadException $e) {
$exception = $e;
$attempts++;
if ($concurrency > 1) {
$concurrency = round($concurrency / 2);
}
try {
$uploader->upload();
} catch (S3MultipartUploadException $e) {
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);
}
}