@@ -46,8 +46,6 @@ internal class MultipartDownloadManager : IDownloadManager
4646 private readonly SemaphoreSlim _httpConcurrencySlots ;
4747 private readonly bool _ownsHttpThrottler ;
4848 private readonly RequestEventHandler _requestEventHandler ;
49-
50- private Exception _downloadException ;
5149 private bool _disposed = false ;
5250 private bool _discoveryCompleted = false ;
5351 private Task _downloadCompletionTask ;
@@ -166,15 +164,6 @@ public MultipartDownloadManager(IAmazonS3 s3Client, BaseDownloadRequest request,
166164 }
167165 }
168166
169- /// <inheritdoc/>
170- public Exception DownloadException
171- {
172- get
173- {
174- return _downloadException ;
175- }
176- }
177-
178167 /// <summary>
179168 /// Discovers the download strategy and starts concurrent downloads in a single unified operation.
180169 /// This eliminates resource leakage by managing HTTP slots and buffer capacity internally.
@@ -259,7 +248,6 @@ private async Task<DownloadResult> PerformDiscoveryAsync(CancellationToken cance
259248 }
260249 catch ( Exception ex )
261250 {
262- _downloadException = ex ;
263251 _logger . Error ( ex , "MultipartDownloadManager: Discovery failed" ) ;
264252 throw ;
265253 }
@@ -336,7 +324,6 @@ private async Task PerformDownloadsAsync(DownloadResult downloadResult, EventHan
336324 }
337325 catch ( Exception ex )
338326 {
339- _downloadException = ex ;
340327 _logger . Error ( ex , "MultipartDownloadManager: Download failed" ) ;
341328
342329 HandleDownloadError ( ex , internalCts ) ;
@@ -414,7 +401,7 @@ private async Task StartBackgroundDownloadsAsync(DownloadResult downloadResult,
414401 _logger . DebugFormat ( "MultipartDownloadManager: Background task waiting for {0} download tasks" , expectedTaskCount ) ;
415402
416403 // Wait for all downloads to complete (fails fast on first exception)
417- await TaskHelpers . WhenAllOrFirstExceptionAsync ( downloadTasks , internalCts . Token ) . ConfigureAwait ( false ) ;
404+ await TaskHelpers . WhenAllFailFastAsync ( downloadTasks , internalCts . Token ) . ConfigureAwait ( false ) ;
418405
419406 _logger . DebugFormat ( "MultipartDownloadManager: All download tasks completed successfully" ) ;
420407
@@ -429,7 +416,6 @@ private async Task StartBackgroundDownloadsAsync(DownloadResult downloadResult,
429416 #pragma warning disable CA1031 // Do not catch general exception types
430417 catch ( Exception ex )
431418 {
432- _downloadException = ex ;
433419 HandleDownloadError ( ex , internalCts ) ;
434420 throw ;
435421 }
@@ -451,13 +437,21 @@ private async Task CreateDownloadTasksAsync(DownloadResult downloadResult, Event
451437 // Pre-acquire capacity in sequential order to prevent race condition deadlock
452438 // This ensures Part 2 gets capacity before Part 3, etc., preventing out-of-order
453439 // parts from consuming all buffer slots and blocking the next expected part
454- for ( int partNum = 2 ; partNum <= downloadResult . TotalParts ; partNum ++ )
440+ for ( int partNum = 2 ; partNum <= downloadResult . TotalParts && ! internalCts . IsCancellationRequested ; partNum ++ )
455441 {
456442 _logger . DebugFormat ( "MultipartDownloadManager: [Part {0}] Waiting for buffer space" , partNum ) ;
457443
458444 // Acquire capacity sequentially - guarantees Part 2 before Part 3, etc.
459445 await _dataHandler . WaitForCapacityAsync ( internalCts . Token ) . ConfigureAwait ( false ) ;
460446
447+ // Check cancellation after acquiring capacity - a task may have failed while waiting
448+ if ( internalCts . IsCancellationRequested )
449+ {
450+ _logger . InfoFormat ( "MultipartDownloadManager: [Part {0}] Stopping early - cancellation requested after capacity acquired" , partNum ) ;
451+ _dataHandler . ReleaseCapacity ( ) ;
452+ break ;
453+ }
454+
461455 _logger . DebugFormat ( "MultipartDownloadManager: [Part {0}] Buffer space acquired" , partNum ) ;
462456
463457 _logger . DebugFormat ( "MultipartDownloadManager: [Part {0}] Waiting for HTTP concurrency slot (Available: {1}/{2})" ,
@@ -466,6 +460,15 @@ private async Task CreateDownloadTasksAsync(DownloadResult downloadResult, Event
466460 // Acquire HTTP slot in the loop before creating task
467461 // Loop will block here if all slots are in use
468462 await _httpConcurrencySlots . WaitAsync ( internalCts . Token ) . ConfigureAwait ( false ) ;
463+
464+ // Check cancellation after acquiring HTTP slot - a task may have failed while waiting
465+ if ( internalCts . IsCancellationRequested )
466+ {
467+ _logger . InfoFormat ( "MultipartDownloadManager: [Part {0}] Stopping early - cancellation requested after HTTP slot acquired" , partNum ) ;
468+ _httpConcurrencySlots . Release ( ) ;
469+ _dataHandler . ReleaseCapacity ( ) ;
470+ break ;
471+ }
469472
470473 _logger . DebugFormat ( "MultipartDownloadManager: [Part {0}] HTTP concurrency slot acquired" , partNum ) ;
471474
@@ -478,10 +481,16 @@ private async Task CreateDownloadTasksAsync(DownloadResult downloadResult, Event
478481 {
479482 // If task creation fails, release the HTTP slot we just acquired
480483 _httpConcurrencySlots . Release ( ) ;
484+ _dataHandler . ReleaseCapacity ( ) ;
481485 _logger . DebugFormat ( "MultipartDownloadManager: [Part {0}] HTTP concurrency slot released due to task creation failure: {1}" , partNum , ex ) ;
482486 throw ;
483487 }
484488 }
489+
490+ if ( internalCts . IsCancellationRequested && downloadTasks . Count < downloadResult . TotalParts - 1 )
491+ {
492+ _logger . InfoFormat ( "MultipartDownloadManager: Stopped queuing early at {0} parts due to cancellation" , downloadTasks . Count ) ;
493+ }
485494 }
486495
487496 /// <summary>
@@ -491,7 +500,7 @@ private void ValidateDownloadCompletion(int expectedTaskCount, int totalParts)
491500 {
492501 // SEP Part GET Step 6 / Ranged GET Step 8:
493502 // "validate that the total number of part GET requests sent matches with the expected PartsCount"
494- // Note: This should always be true if we reach this point, since WhenAllOrFirstException
503+ // Note: This should always be true if we reach this point, since WhenAllFailFastAsync
495504 // ensures all tasks completed successfully (or threw on first failure).
496505 // The check serves as a defensive assertion for SEP compliance.
497506 // Note: expectedTaskCount + 1 accounts for Part 1 being buffered during discovery
0 commit comments