From e2e930b2d7ff3082f0ebcf7278334e450b67200e Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Wed, 19 Feb 2025 15:55:59 +0100 Subject: [PATCH] Reworked BackendManager.Handler to handle multiple downloads in parallel --- .../Main/Backend/BackendManager.Handler.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Duplicati/Library/Main/Backend/BackendManager.Handler.cs b/Duplicati/Library/Main/Backend/BackendManager.Handler.cs index aa1185405..1115d6de7 100644 --- a/Duplicati/Library/Main/Backend/BackendManager.Handler.cs +++ b/Duplicati/Library/Main/Backend/BackendManager.Handler.cs @@ -88,6 +88,10 @@ partial class BackendManager /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(); + /// + /// The list of active downloads + /// + private readonly List activeDownloads = []; /// /// The list of active uploads /// @@ -105,6 +109,10 @@ partial class BackendManager /// private readonly ExecuteContext context; /// + /// The maximum number of parallel downloads + /// + private readonly int maxParallelDownloads; + /// /// The maximum number of parallel uploads /// private readonly int maxParallelUploads; @@ -155,6 +163,8 @@ partial class BackendManager this.backendUrl = backendUrl; this.context = context; + // TODO Currently, only the restore process uses parallel downloads. If others need it as well, maybe use another option. + maxParallelDownloads = Math.Max(1, context.Options.RestoreVolumeDownloaders); maxParallelUploads = Math.Max(1, context.Options.AsynchronousConcurrentUploadLimit); maxRetries = context.Options.NumberOfRetries; retryDelay = context.Options.RetryDelay; @@ -180,6 +190,23 @@ partial class BackendManager ); } + /// + /// Reclaims completed downloads + /// + /// An awaitable task + private async Task ReclaimCompletedDownloads() + { + for (int i = activeDownloads.Count - 1; i >= 0; i--) + { + if (activeDownloads[i].IsCompleted) + { + // Make sure the task is awaited so we capture any exceptions + await activeDownloads[i].ConfigureAwait(false); + activeDownloads.RemoveAt(i); + } + } + } + /// /// Reclaims completed uploads /// @@ -197,6 +224,20 @@ partial class BackendManager } } + /// + /// Ensures that there are at most N - 1 active downloads + /// + /// + /// + private async Task EnsureAtMostNActiveDownloads(int n) + { + while (activeDownloads.Count >= n) + { + await Task.WhenAny(activeDownloads).ConfigureAwait(false); + await ReclaimCompletedDownloads().ConfigureAwait(false); + } + } + /// /// Ensures that there are at most N - 1 active uploads /// @@ -240,6 +281,13 @@ partial class BackendManager putOp.SetComplete(true); activeUploads.Add(ExecuteWithRetry(putOp, tcs.Token)); } + else if (op is GetOperation getOp && !getOp.WaitForComplete) + { + await EnsureAtMostNActiveDownloads(maxParallelDownloads).ConfigureAwait(false); + + // Operation is accepted into queue, so we can signal completion + activeDownloads.Add(ExecuteWithRetry(getOp, tcs.Token)); + } else { // Wait for all of the active uploads to complete