Skip to content

Commit 96c04d4

Browse files
authored
Merge pull request #61 from rameel/fix-s3-upload-stream
Avoid masking original exception
2 parents 3a1206f + 7dfe5fa commit 96c04d4

1 file changed

Lines changed: 63 additions & 64 deletions

File tree

src/Ramstack.FileSystem.Amazon/S3UploadStream.cs

Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Diagnostics.CodeAnalysis;
2-
using System.Runtime.ExceptionServices;
32

43
using Amazon.S3;
54
using Amazon.S3.Model;
@@ -23,7 +22,7 @@ internal sealed class S3UploadStream : Stream
2322
private readonly FileStream _stream;
2423
private readonly List<PartETag> _partETags;
2524

26-
private bool _disposed;
25+
private volatile int _disposed;
2726

2827
/// <inheritdoc />
2928
public override bool CanRead => false;
@@ -163,13 +162,11 @@ protected override void Dispose(bool disposing)
163162
/// <inheritdoc />
164163
public override async ValueTask DisposeAsync()
165164
{
166-
if (_disposed)
165+
if (Interlocked.Exchange(ref _disposed, 1) != 0)
167166
return;
168167

169168
try
170169
{
171-
_disposed = true;
172-
173170
await UploadPartAsync(CancellationToken.None).ConfigureAwait(false);
174171

175172
var request = new CompleteMultipartUploadRequest
@@ -184,16 +181,24 @@ await _client
184181
.CompleteMultipartUploadAsync(request)
185182
.ConfigureAwait(false);
186183
}
187-
catch (Exception exception)
184+
catch
188185
{
189186
await AbortAsync(CancellationToken.None).ConfigureAwait(false);
190-
ExceptionDispatchInfo.Throw(exception);
187+
throw;
191188
}
192189
finally
193190
{
194-
await _stream
195-
.DisposeAsync()
196-
.ConfigureAwait(false);
191+
try
192+
{
193+
await _stream.DisposeAsync().ConfigureAwait(false);
194+
}
195+
catch
196+
{
197+
// Ignore:
198+
// Errors when disposing the temporary buffer are not significant here, because:
199+
// 1) If the upload succeeded, the job is done; a cleanup error can be ignored.
200+
// 2) If the upload failed, preserving the original exception is more important for us.
201+
}
197202
}
198203
}
199204

@@ -216,49 +221,35 @@ private void UploadPart()
216221
/// </returns>
217222
private async ValueTask UploadPartAsync(CancellationToken cancellationToken)
218223
{
219-
// Upload an empty part if nothing has been uploaded yet,
220-
// since we must specify at least one part.
221-
222-
if (_stream.Length != 0 || _partETags.Count == 0)
224+
_stream.Position = 0;
225+
226+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
227+
// The maximum allowed part size is 5 GiB.
228+
// -----------------------------------------------------------------------------------
229+
// We don't need to worry about S3's 5 GiB part limit because:
230+
// 1. All Write/WriteAsync methods are inherently limited by Array.MaxLength (~2 GiB).
231+
// 2. The upload starts as soon as the buffer reaches MinPartSize (5 MiB).
232+
// Even if a single write matches Array.MaxLength, the data is
233+
// uploaded immediately, staying within AWS limits.
234+
235+
var request = new UploadPartRequest
223236
{
224-
try
225-
{
226-
_stream.Position = 0;
227-
228-
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
229-
// The maximum allowed part size is 5 GiB.
230-
// -----------------------------------------------------------------------------------
231-
// We don't need to worry about S3's 5 GiB part limit because:
232-
// 1. All Write/WriteAsync methods are inherently limited by Array.MaxLength (~2 GiB).
233-
// 2. The upload starts as soon as the buffer reaches MinPartSize (5 MiB).
234-
// Even if a single write matches Array.MaxLength, the data is
235-
// uploaded immediately, staying within AWS limits.
236-
237-
var request = new UploadPartRequest
238-
{
239-
BucketName = _bucketName,
240-
Key = _key,
241-
UploadId = _uploadId,
242-
PartNumber = _partETags.Count + 1,
243-
InputStream = _stream,
244-
PartSize = _stream.Length
245-
};
237+
BucketName = _bucketName,
238+
Key = _key,
239+
UploadId = _uploadId,
240+
PartNumber = _partETags.Count + 1,
241+
InputStream = _stream,
242+
PartSize = _stream.Length
243+
};
246244

247-
var response = await _client
248-
.UploadPartAsync(request, cancellationToken)
249-
.ConfigureAwait(false);
245+
var response = await _client
246+
.UploadPartAsync(request, cancellationToken)
247+
.ConfigureAwait(false);
250248

251-
_partETags.Add(new PartETag(response));
249+
_partETags.Add(new PartETag(response));
252250

253-
_stream.Position = 0;
254-
_stream.SetLength(0);
255-
}
256-
catch
257-
{
258-
await AbortAsync(cancellationToken).ConfigureAwait(false);
259-
throw;
260-
}
261-
}
251+
_stream.Position = 0;
252+
_stream.SetLength(0);
262253
}
263254

264255
/// <summary>
@@ -289,23 +280,31 @@ private void Abort()
289280
/// </remarks>
290281
private async ValueTask AbortAsync(CancellationToken cancellationToken)
291282
{
292-
var request = new AbortMultipartUploadRequest
293-
{
294-
BucketName = _bucketName,
295-
Key = _key,
296-
UploadId = _uploadId
297-
};
298-
299-
await _client
300-
.AbortMultipartUploadAsync(request, cancellationToken)
301-
.ConfigureAwait(false);
283+
if (Interlocked.Exchange(ref _disposed, 1) != 0)
284+
return;
302285

303-
_disposed = true;
286+
try
287+
{
288+
await using (_stream.ConfigureAwait(false))
289+
{
290+
var request = new AbortMultipartUploadRequest
291+
{
292+
BucketName = _bucketName,
293+
Key = _key,
294+
UploadId = _uploadId
295+
};
304296

305-
// Prevent subsequent writes to the stream.
306-
await _stream
307-
.DisposeAsync()
308-
.ConfigureAwait(false);
297+
await _client
298+
.AbortMultipartUploadAsync(request, cancellationToken)
299+
.ConfigureAwait(false);
300+
}
301+
}
302+
catch
303+
{
304+
// IGNORE:
305+
// Suppressing the exception during abort to preserve
306+
// the original exception that triggered the abort.
307+
}
309308
}
310309

311310
/// <summary>

0 commit comments

Comments
 (0)