|
24 | 24 | using System.Collections.Generic; |
25 | 25 | using System.Diagnostics.CodeAnalysis; |
26 | 26 | using System.IO; |
| 27 | +using System.Linq; |
27 | 28 | using System.Threading; |
28 | 29 | using System.Threading.Tasks; |
29 | 30 | using Amazon.Runtime; |
@@ -377,37 +378,75 @@ public async Task StartDownloadsAsync(DownloadDiscoveryResult discoveryResult, E |
377 | 378 | // Pre-acquire capacity in sequential order to prevent race condition deadlock |
378 | 379 | // This ensures Part 2 gets capacity before Part 3, etc., preventing out-of-order |
379 | 380 | // parts from consuming all buffer slots and blocking the next expected part |
380 | | - for (int partNum = 2; partNum <= discoveryResult.TotalParts; partNum++) |
| 381 | + try |
381 | 382 | { |
382 | | - _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Waiting for buffer space", partNum); |
383 | | - |
384 | | - // Acquire capacity sequentially - guarantees Part 2 before Part 3, etc. |
385 | | - await _dataHandler.WaitForCapacityAsync(internalCts.Token).ConfigureAwait(false); |
386 | | - |
387 | | - _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Buffer space acquired", partNum); |
388 | | - |
389 | | - _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Waiting for HTTP concurrency slot (Available: {1}/{2})", |
390 | | - partNum, _httpConcurrencySlots.CurrentCount, _config.ConcurrentServiceRequests); |
391 | | - |
392 | | - // Acquire HTTP slot in the loop before creating task |
393 | | - // Loop will block here if all slots are in use |
394 | | - await _httpConcurrencySlots.WaitAsync(internalCts.Token).ConfigureAwait(false); |
395 | | - |
396 | | - _logger.DebugFormat("MultipartDownloadManager: [Part {0}] HTTP concurrency slot acquired", partNum); |
397 | | - |
398 | | - try |
399 | | - { |
400 | | - var task = CreateDownloadTaskAsync(partNum, discoveryResult.ObjectSize, wrappedCallback, internalCts.Token); |
401 | | - downloadTasks.Add(task); |
402 | | - } |
403 | | - catch (Exception ex) |
| 383 | + for (int partNum = 2; partNum <= discoveryResult.TotalParts; partNum++) |
404 | 384 | { |
405 | | - // If task creation fails, release the HTTP slot we just acquired |
406 | | - _httpConcurrencySlots.Release(); |
407 | | - _logger.DebugFormat("MultipartDownloadManager: [Part {0}] HTTP concurrency slot released due to task creation failure: {1}", partNum, ex); |
408 | | - throw; |
| 385 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Waiting for buffer space", partNum); |
| 386 | + |
| 387 | + // Acquire capacity sequentially - guarantees Part 2 before Part 3, etc. |
| 388 | + await _dataHandler.WaitForCapacityAsync(internalCts.Token).ConfigureAwait(false); |
| 389 | + |
| 390 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Buffer space acquired", partNum); |
| 391 | + |
| 392 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Waiting for HTTP concurrency slot (Available: {1}/{2})", |
| 393 | + partNum, _httpConcurrencySlots.CurrentCount, _config.ConcurrentServiceRequests); |
| 394 | + |
| 395 | + // Acquire HTTP slot in the loop before creating task |
| 396 | + // Loop will block here if all slots are in use |
| 397 | + await _httpConcurrencySlots.WaitAsync(internalCts.Token).ConfigureAwait(false); |
| 398 | + |
| 399 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] HTTP concurrency slot acquired", partNum); |
| 400 | + |
| 401 | + try |
| 402 | + { |
| 403 | + var task = CreateDownloadTaskAsync(partNum, discoveryResult.ObjectSize, wrappedCallback, internalCts.Token); |
| 404 | + |
| 405 | + // Add failure detection to immediately cancel internal token on first error |
| 406 | + // This prevents the for loop from queuing additional parts after a failure |
| 407 | + _ = task.ContinueWith(t => |
| 408 | + { |
| 409 | + if (t.IsFaulted && !internalCts.IsCancellationRequested) |
| 410 | + { |
| 411 | + // Capture and log the ORIGINAL exception immediately |
| 412 | + var originalException = t.Exception?.GetBaseException(); |
| 413 | + if (_downloadException == null && originalException != null) |
| 414 | + { |
| 415 | + _downloadException = originalException; |
| 416 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Download failed with exception: {1}", |
| 417 | + partNum, originalException.Message); |
| 418 | + } |
| 419 | + |
| 420 | + // Then cancel to stop queuing more parts |
| 421 | + try |
| 422 | + { |
| 423 | + internalCts.Cancel(); |
| 424 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] Cancelled internal token to stop queuing", partNum); |
| 425 | + } |
| 426 | + catch (ObjectDisposedException) |
| 427 | + { |
| 428 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] CancellationTokenSource already disposed during cancellation", partNum); |
| 429 | + } |
| 430 | + } |
| 431 | + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); |
| 432 | + |
| 433 | + downloadTasks.Add(task); |
| 434 | + } |
| 435 | + catch (Exception ex) |
| 436 | + { |
| 437 | + // If task creation fails, release the HTTP slot we just acquired |
| 438 | + _httpConcurrencySlots.Release(); |
| 439 | + _logger.DebugFormat("MultipartDownloadManager: [Part {0}] HTTP concurrency slot released due to task creation failure: {1}", partNum, ex); |
| 440 | + throw; |
| 441 | + } |
409 | 442 | } |
410 | 443 | } |
| 444 | + catch (OperationCanceledException e) |
| 445 | + { |
| 446 | + // Expected when a task fails and ContinueWith cancels internalCts |
| 447 | + // Original exception already captured and logged in ContinueWith callback |
| 448 | + _logger.InfoFormat("MultipartDownloadManager: Stopped queuing early at {0} parts due to failure: {1}", downloadTasks.Count, e); |
| 449 | + } |
411 | 450 |
|
412 | 451 | var expectedTaskCount = downloadTasks.Count; |
413 | 452 | _logger.DebugFormat("MultipartDownloadManager: Background task waiting for {0} download tasks", expectedTaskCount); |
@@ -439,9 +478,11 @@ public async Task StartDownloadsAsync(DownloadDiscoveryResult discoveryResult, E |
439 | 478 |
|
440 | 479 | catch (Exception ex) |
441 | 480 | { |
442 | | - _downloadException = ex; |
443 | | - |
444 | | - |
| 481 | + // Store the exception - this is the real failure from WhenAll, not a cancellation side-effect |
| 482 | + if (_downloadException == null) |
| 483 | + { |
| 484 | + _downloadException = ex; |
| 485 | + } |
445 | 486 |
|
446 | 487 | // Cancel all remaining downloads immediately to prevent cascading timeout errors |
447 | 488 | // This ensures that when one part fails, other tasks stop gracefully instead of |
@@ -688,8 +729,9 @@ private async Task<DownloadDiscoveryResult> DiscoverUsingPartStrategyAsync(Cance |
688 | 729 | }; |
689 | 730 | } |
690 | 731 | } |
691 | | - catch |
| 732 | + catch (Exception ex) |
692 | 733 | { |
| 734 | + _logger.Error(ex, "MultipartDownloadManager: Discovery using PART strategy failed"); |
693 | 735 | // On error, release semaphore and dispose response before rethrowing |
694 | 736 | _httpConcurrencySlots.Release(); |
695 | 737 | firstPartResponse?.Dispose(); |
@@ -796,8 +838,9 @@ private async Task<DownloadDiscoveryResult> DiscoverUsingRangeStrategyAsync(Canc |
796 | 838 | InitialResponse = firstRangeResponse // Keep response with stream |
797 | 839 | }; |
798 | 840 | } |
799 | | - catch |
| 841 | + catch (Exception ex) |
800 | 842 | { |
| 843 | + _logger.Error(ex, "MultipartDownloadManager: Discovery using RANGE strategy failed"); |
801 | 844 | // On error, release semaphore and dispose response before rethrowing |
802 | 845 | _httpConcurrencySlots.Release(); |
803 | 846 | firstRangeResponse?.Dispose(); |
|
0 commit comments