Files
duplicati/Duplicati/Library/Main/Backend/BackendManager.cs
T

662 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using CoCoL;
using Duplicati.Library.Interface;
using Duplicati.Library.Main.Database;
using Duplicati.Library.Main.Operation.Common;
using Duplicati.Library.Main.Volumes;
using Duplicati.Library.Utility;
using Duplicati.StreamUtil;
namespace Duplicati.Library.Main.Backend;
#nullable enable
/// <summary>
/// The backend manager
/// </summary>
internal partial class BackendManager : IBackendManager
{
/// <summary>
/// The log tag for the class
/// </summary>
private static readonly string LOGTAG = Logging.Log.LogTagFromType<BackendManager>();
/// <summary>
/// The channel for issuing and handling requests.
/// </summary>
private readonly IChannel<PendingOperationBase> requestChannel = ChannelManager.CreateChannel<PendingOperationBase>(name: "BackendManager-" + Guid.NewGuid().ToString("N"));
/// <summary>
/// The queue runner task
/// </summary>
private readonly Task queueRunner;
/// <summary>
/// The execution context
/// </summary>
private readonly ExecuteContext context;
/// <summary>
/// The backend URL this manager is bound to. Used as the base URL when
/// computing per-operation URL overrides for backends that do not support
/// folder operations (see <see cref="ApplyPathTranslation"/>).
/// </summary>
private readonly string backendUrl;
/// <summary>
/// Indicates whether the backend supports object locking
/// </summary>
private readonly Lazy<bool> supportsObjectLocking;
/// <summary>
/// Indicates whether the backend supports folder operations. Kept internal:
/// the <see cref="IBackendManager"/> interface no longer exposes it, but the
/// backend manager still needs it to decide how to translate relative paths
/// for backends that only understand a single flat namespace per URL.
/// </summary>
private readonly Lazy<bool> supportsFolderOperations;
/// <summary>
/// Flag keeping track of whether the object has been disposed
/// </summary>
private bool isDisposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="BackendManager"/> class.
/// </summary>
/// <param name="backendUrl">The backend URL</param>
/// <param name="options">The options</param>
/// <param name="backendWriter">The backend writer</param>
/// <param name="taskReader">The task reader</param>
public BackendManager(string backendUrl, Options options, IBackendWriter backendWriter, ITaskReader taskReader)
{
if (string.IsNullOrWhiteSpace(backendUrl))
throw new ArgumentNullException(nameof(backendUrl));
this.backendUrl = backendUrl;
var isThrottleDisabled = options.DisableThrottle || options.ThrottleDisabledBackends.Contains(Library.Utility.Utility.GuessScheme(backendUrl) ?? string.Empty);
// To avoid excessive parameter passing, the context is captured here
context = new ExecuteContext(
new ProgressHandler(backendWriter, taskReader),
backendWriter ?? throw new ArgumentNullException(nameof(backendWriter)),
new DatabaseCollector(),
new ThrottleManager() { Limit = isThrottleDisabled ? 0 : options.MaxUploadPrSecond },
new ThrottleManager() { Limit = isThrottleDisabled ? 0 : options.MaxDownloadPrSecond },
taskReader ?? throw new ArgumentNullException(nameof(taskReader)),
isThrottleDisabled,
options ?? throw new ArgumentNullException(nameof(options))
);
supportsObjectLocking = new Lazy<bool>(() =>
{
var backend = DynamicLoader.BackendLoader.GetBackend(backendUrl, context.Options.RawOptions);
if (backend == null)
return false;
try
{
return backend is ILockingBackend;
}
finally
{
(backend as IDisposable)?.Dispose();
}
}, isThreadSafe: true);
supportsFolderOperations = new Lazy<bool>(() =>
{
var backend = DynamicLoader.BackendLoader.GetBackend(backendUrl, context.Options.RawOptions);
if (backend == null)
return false;
try
{
return backend is IFolderEnabledBackend;
}
finally
{
(backend as IDisposable)?.Dispose();
}
}, isThreadSafe: true);
// The BackendManager class is a wrapper that essentially sends
// requests into a queue and processes them in order.
// The Handler class is the one that actually processes the requests.
queueRunner = Handler.RunHandlerAsync(
requestChannel,
backendUrl,
context);
}
/// <summary>
/// Gets a value indicating whether the configured backend supports object locking
/// </summary>
public bool SupportsObjectLocking => supportsObjectLocking.Value;
/// <summary>
/// Enters a task into the queue for processing.
/// </summary>
/// <param name="op">The operation to queue</param>
/// <returns>An awaitable task</returns>
private async Task QueueTaskAsync(PendingOperationBase op)
{
if (queueRunner.IsCompleted)
{
if (queueRunner.IsFaulted)
await queueRunner.ConfigureAwait(false);
if (queueRunner.IsCanceled)
throw new OperationCanceledException("Backend manager is stopped", queueRunner.Exception);
throw new InvalidOperationException("Backend manager is stopped");
}
try
{
await requestChannel.WriteAsync(op).ConfigureAwait(false);
}
catch (RetiredException ex)
{
// Try to get a better error message
if (queueRunner.IsFaulted)
await queueRunner.ConfigureAwait(false);
throw new InvalidOperationException("Backend manager is stopped", ex);
}
}
/// <summary>
/// Calculates the hash of a file
/// </summary>
/// <param name="filename">The filename</param>
/// <param name="options">The options</param>
/// <returns>The hash</returns>
protected static string CalculateFileHash(string filename, Options options)
{
using (var fs = System.IO.File.OpenRead(filename))
using (var hasher = HashFactory.CreateHasher(options.FileHashAlgorithm))
return Convert.ToBase64String(hasher.ComputeHash(fs));
}
/// <summary>
/// Merges a relative sub-path onto the backend URL, returning a new backend URL
/// that points at the sub-path. The merge is done on the path component of the
/// URL so it works across schemes (file://, s3://, ssh://, ...). An empty or null
/// sub-path returns the original URL unchanged.
/// </summary>
/// <param name="subPath">The relative sub-path to append (using '/' as separator), or null/empty for the root.</param>
/// <returns>A backend URL that points at <paramref name="subPath"/> under the backend root.</returns>
private string MergeBackendPath(string? subPath)
{
if (string.IsNullOrWhiteSpace(subPath))
return backendUrl;
var uri = new Library.Utility.Uri(backendUrl);
// Trim trailing path separators from the existing path AND the sub-path before
// joining with a single '/'. The URL path can end with a backslash on Windows
// (e.g. a file:// URL built from a TempFolder path, which always carries a
// trailing directory separator); only trimming '/' would leave that backslash in
// place and produce a doubled separator (e.g. "D:\...\temp\Stage1" -> "...\temp\\Stage1"
// after the OS normalizes the '/'), which on Windows yields an invalid path
// ("\\?\D:\...\temp\\Stage1"). Trimming both separators and joining with a single
// '/' keeps the merged path well-formed regardless of the host OS or how the
// backend URL was constructed.
var basePath = uri.Path?.TrimEnd('/', '\\') ?? "";
var sub = subPath.Trim('/', '\\');
var mergedPath = string.IsNullOrEmpty(basePath)
? sub
: basePath + "/" + sub;
return uri.SetPath(mergedPath).ToString();
}
/// <summary>
/// Applies path translation to a file-oriented operation (put/get/delete) for
/// backends that do not support folder operations.
///
/// Backends that implement <see cref="IFolderEnabledBackend"/> accept a relative
/// path (including sub-folders) directly in their put/get/delete calls, so no
/// translation is applied and the operation runs against the base backend URL with
/// the full relative path as the remote name.
///
/// Backends that do NOT support folder operations only understand a flat namespace
/// of filenames relative to a single backend URL. For those, the relative path is
/// split into a directory part and a filename part: the backend is pointed at the
/// directory (via <see cref="PendingOperationBase.BackendUrlOverride"/>) and the
/// operation passes only the filename to the backend (via
/// <see cref="PendingOperationBase.EffectiveRemoteName"/>). The operation keeps
/// using the full relative path for all bookkeeping (database, progress, logging).
/// </summary>
/// <param name="op">The operation to translate. Its <see cref="PendingOperationBase.RemoteFilename"/> must be set.</param>
private void ApplyPathTranslation(PendingOperationBase op)
{
if (supportsFolderOperations.Value)
return; // Folder-enabled backends take the full relative path directly.
// Non-folder backend: split the relative path into (subPath, filename).
var relPath = op.RemoteFilename;
var lastSlash = relPath.LastIndexOf('/');
if (lastSlash < 0)
return; // Flat filename, no sub-path: use the base backend URL as-is.
var subPath = relPath.Substring(0, lastSlash);
var filename = relPath.Substring(lastSlash + 1);
op.BackendUrlOverride = MergeBackendPath(subPath);
op.EffectiveRemoteName = filename;
}
/// <summary>
/// Decrypts a file using the specified options
/// </summary>
/// <param name="tmpfile">The file to decrypt</param>
/// <param name="filename">The name of the file. Used for detecting encryption algorithm if not specified in options or if it differs from the options</param>
/// <param name="options">The Duplicati options</param>
/// <param name="dispose">True if the input file should be disposed after decryption</param>
/// <returns>The decrypted file</returns>
public TempFile DecryptFile(TempFile volume, string volume_name, Options options, bool dispose)
=> GetOperation.DecryptFile(volume, volume_name, options, dispose);
/// <summary>
/// Deletes a remote file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="size">The size of the remote file, for statistics</param>
/// <param name="waitForComplete">True if the operation should wait for the file to actually be deleted. If this argument is false, the task will complete once the operation is queued</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task DeleteAsync(string remotename, long size, bool waitForComplete, CancellationToken cancelToken)
{
var op = new DeleteOperation(remotename, size, context, waitForComplete, cancelToken);
ApplyPathTranslation(op);
await QueueTaskAsync(op).ConfigureAwait(false);
await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Applies or updates an object lock on a remote file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="lockUntilUtc">The UTC time until which the lock should remain in effect</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task SetObjectLockUntilAsync(string remotename, DateTime lockUntilUtc, CancellationToken cancelToken)
{
var op = new SetObjectLockOperation(remotename, lockUntilUtc, context, cancelToken);
await QueueTaskAsync(op).ConfigureAwait(false);
await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Gets the object lock expiration time for a remote file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The UTC time the lock expires, or null if no lock is set</returns>
public async Task<DateTime?> GetObjectLockUntilAsync(string remotename, CancellationToken cancelToken)
{
var op = new GetObjectLockOperation(remotename, context, cancelToken);
await QueueTaskAsync(op).ConfigureAwait(false);
return await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Gets a file from the remote location
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="hash">The hash of the remote file, for verification</param>
/// <param name="size">The size of the remote file, for verification</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>A temporary file with the contents of the remote file</returns>
public async Task<TempFile> GetAsync(string remotename, string hash, long size, CancellationToken cancelToken)
{
var op = new GetOperation(remotename, size, context, cancelToken)
{
Hash = hash,
Decrypt = true
};
ApplyPathTranslation(op);
await QueueTaskAsync(op).ConfigureAwait(false);
(var file, var _, var _) = await op.GetResultAsync().ConfigureAwait(false);
return file;
}
/// <summary>
/// Gets a file from the remote location without decrypting it
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="hash">The hash of the remote file, for verification</param>
/// <param name="size">The size of the remote file, for verification</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>A temporary file with the contents of the remote file</returns>
public async Task<TempFile> GetDirectAsync(string remotename, string hash, long size, CancellationToken cancelToken)
{
var op = new GetOperation(remotename, size, context, cancelToken)
{
Hash = hash,
Decrypt = false
};
ApplyPathTranslation(op);
await QueueTaskAsync(op).ConfigureAwait(false);
(var file, var _, var _) = await op.GetResultAsync().ConfigureAwait(false);
return file;
}
/// <summary>
/// Gets quota information from the backend
/// </summary>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The quota information</returns>
public async Task<IQuotaInfo?> GetQuotaInfoAsync(CancellationToken cancelToken)
{
var op = new QuotaInfoOperation(context, cancelToken);
await QueueTaskAsync(op).ConfigureAwait(false);
return await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Gets a file from the remote location, along with the hash and size of the file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="hash">The hash of the remote file, or null if not known</param>
/// <param name="size">The size of the remote file, or -1 if not known</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>A tuple containing the temporary file, the hash of the file, and the size of the file</returns>
public async Task<(TempFile File, string Hash, long Size)> GetWithInfoAsync(string remotename, string hash, long size, CancellationToken cancelToken)
{
var op = new GetOperation(remotename, size, context, cancelToken)
{
Hash = hash,
Decrypt = true
};
ApplyPathTranslation(op);
await QueueTaskAsync(op).ConfigureAwait(false);
(var file, var downloadHash, var downloadSize) = await op.GetResultAsync().ConfigureAwait(false);
return (file, downloadHash, downloadSize);
}
/// <summary>
/// Lists files on the remote destination at the specified path
/// </summary>
/// <param name="path">The path to list, or null for the root folder</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The list of files</returns>
public async Task<IEnumerable<Interface.IFileEntry>> ListAsync(string? path, CancellationToken cancelToken)
{
// For folder-enabled backends, ListAsync(path) lists the direct children of
// the sub-folder (the backend supports folder-scoped listing). For backends
// that do not support folder operations, there is no folder-scoped list call,
// so we point the backend at the sub-folder URL (via BackendUrlOverride) and
// ask for a flat listing of that folder instead (UseRootList = true,
// listPath = null). When listing the root (path == null) both kinds behave
// the same: flat listing of the base backend URL.
var useRootList = !supportsFolderOperations.Value;
string? listPath = useRootList ? null : path;
string? urlOverride = useRootList && !string.IsNullOrEmpty(path) ? MergeBackendPath(path) : null;
var op = new ListOperation(listPath, context, cancelToken)
{
BackendUrlOverride = urlOverride,
UseRootList = useRootList,
};
await QueueTaskAsync(op).ConfigureAwait(false);
return await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Ensures the folder at the specified relative path exists on the backend.
///
/// The backend is created bound to the sub-folder URL (the path merged onto the
/// backend URL, the same resolution used for put/get/delete on non-folder backends)
/// and <see cref="IBackend.CreateFolderAsync"/> is invoked. A
/// <see cref="FolderAreadyExistedException"/> is treated as success. This works
/// for both folder-enabled and non-folder backends: pointing the backend at the
/// sub-folder URL means <c>CreateFolderAsync</c> creates that sub-folder
/// (backends create the folder at their bound URL).
/// </summary>
/// <param name="path">The relative path of the folder to ensure, or null/empty for the backend root.</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task EnsureFolderAsync(string? path, CancellationToken cancelToken)
{
var urlOverride = string.IsNullOrEmpty(path) ? null : MergeBackendPath(path);
var op = new CreateFolderOperation(context, cancelToken)
{
BackendUrlOverride = urlOverride,
};
await QueueTaskAsync(op).ConfigureAwait(false);
await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Uploads a volume to the remote location
/// </summary>
/// <param name="volume">The volume to upload</param>
/// <param name="indexVolume">The index volume to upload, if any</param>
/// <param name="indexVolumeFinished">The callback to call when the index volume is finished</param>
/// <param name="waitForComplete">True if the operation should wait for the file to actually be uploaded. If this argument is false, the task will complete once the operation is queued</param>
/// <param name="onDbUpdate">The callback to call when the database is updated</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task PutAsync(VolumeWriterBase volume, IndexVolumeWriter? indexVolume, Func<Task>? indexVolumeFinished, bool waitForComplete, Func<Task>? onDbUpdate, CancellationToken cancelToken)
{
volume.Close();
var op = new PutOperation(volume.RemoteFilename, context, waitForComplete, cancelToken)
{
LocalTempfile = volume.TempFile,
OriginalIndexFile = indexVolume,
Unencrypted = false,
TrackedInDb = true,
IndexVolumeFinishedCallback = indexVolumeFinished,
OnDbUpdate = onDbUpdate ?? PutOperation.OnDbUpdateDefault,
};
// Prepare encryption
op.StartEncryptionAndHashing();
ApplyPathTranslation(op);
await QueueTaskAsync(op).ConfigureAwait(false);
await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Uploads a file to the remote location without encryption
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="tempFile">The temporary file to upload</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task PutFileUnencryptedAsync(string remotename, TempFile tempFile, CancellationToken cancelToken)
{
var op = new PutOperation(remotename, context, true, cancelToken)
{
LocalTempfile = tempFile,
Unencrypted = true, // Avoid encrypting
TrackedInDb = false, // Not tracked
OriginalIndexFile = null,
IndexVolumeFinishedCallback = null,
OnDbUpdate = PutOperation.OnDbUpdateDefault,
};
// Sets the task as already completed
op.StartEncryptionAndHashing();
ApplyPathTranslation(op);
await QueueTaskAsync(op).ConfigureAwait(false);
await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Flushes the database messages to the database.
/// </summary>
/// <param name="database">The database to write to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that completes when the messages are flushed.</returns>
public async Task FlushPendingMessagesAsync(IBackendManagerDatabase database, CancellationToken cancellationToken)
{
await context.Database.FlushPendingMessagesAsync(database, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Waits for the backend queue to be empty.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>An awaitable task.</returns>
public async Task WaitForEmptyAsync(CancellationToken cancellationToken)
{
var op = new WaitForEmptyOperation(context, cancellationToken);
await QueueTaskAsync(op).ConfigureAwait(false);
await op.GetResultAsync().ConfigureAwait(false);
}
/// <summary>
/// Waits for the backend queue to be empty and flushes the database messages
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task WaitForEmptyAsync(IBackendManagerDatabase database, CancellationToken cancellationToken)
{
await FlushPendingMessagesAsync(database, cancellationToken).ConfigureAwait(false);
await WaitForEmptyAsync(cancellationToken).ConfigureAwait(false);
await FlushPendingMessagesAsync(database, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Stops the backend manager and flushes any pending messages to the database.
/// </summary>
/// <param name="database">The database to write pending messages to.</param>
/// <returns>A task that completes when the runner is stopped and messages are flushed.</returns>
public async Task StopRunnerAndFlushMessagesAsync(IBackendManagerDatabase database)
{
await requestChannel.RetireAsync().ConfigureAwait(false);
await FlushPendingMessagesAsync(database, CancellationToken.None).ConfigureAwait(false);
if (queueRunner.IsFaulted)
Logging.Log.WriteWarningMessage(LOGTAG, "BackendManagerShutdown", queueRunner.Exception, "Backend manager queue runner crashed");
}
/// <summary>
/// Stops the backend manager and discards any pending messages
/// </summary>
public void StopRunnerAndDiscardMessages()
{
requestChannel.RetireAsync().Await();
if (queueRunner.IsFaulted)
Logging.Log.WriteWarningMessage(LOGTAG, "BackendManagerShutdown", queueRunner.Exception, "Backend manager queue runner crashed");
context.Database.ClearPendingMessages();
}
/// <summary>
/// Performs a download of the files specified, with pre-fetch to overlap the download and processing
/// </summary>
/// <param name="volumes">The volumes to download</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The downloaded files and the volume they came from</returns>
public async IAsyncEnumerable<(TempFile File, string Hash, long Size, string Name)> GetFilesOverlappedAsync(IEnumerable<IRemoteVolume> volumes, [EnumeratorCancellation] CancellationToken cancelToken)
{
var prevVolume = volumes.FirstOrDefault();
if (prevVolume == null)
yield break;
// Get the first volume, so we do not have pending parallel transfers
var prevResult = await GetWithInfoAsync(prevVolume.Name, prevVolume.Hash, prevVolume.Size, cancelToken)
.ConfigureAwait(false);
foreach (var volume in volumes.Skip(1))
{
// Prepare the next volume, while processing the previous one
var nextTask = GetWithInfoAsync(volume.Name, volume.Hash, volume.Size, cancelToken);
// Assuming we do not throw while yielding, otherwise we would need to dispose nextTask
yield return (prevResult.File, prevResult.Hash, prevResult.Size, prevVolume.Name);
prevResult.File.Dispose();
// Set up for next iteration
prevVolume = volume;
prevResult = await nextTask.ConfigureAwait(false);
}
// Return the last result
yield return (prevResult.File, prevResult.Hash, prevResult.Size, prevVolume.Name);
prevResult.File.Dispose();
}
/// <summary>
/// Performs a direct download of the files specified, with pre-fetch to overlap the download and processing
/// </summary>
/// <param name="volumes">The volumes to download</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The downloaded files and the volume they came from</returns>
public async IAsyncEnumerable<(TempFile File, string Name)> GetFilesOverlappedDirectAsync(IEnumerable<IRemoteVolume> volumes, [EnumeratorCancellation] CancellationToken cancelToken)
{
var prevVolume = volumes.FirstOrDefault();
if (prevVolume == null)
yield break;
// Get the first volume, so we do not have pending parallel transfers
var prevResult = await GetDirectAsync(prevVolume.Name, prevVolume.Hash, prevVolume.Size, cancelToken)
.ConfigureAwait(false);
foreach (var volume in volumes.Skip(1))
{
// Prepare the next volume, while processing the previous one
var nextTask = GetDirectAsync(volume.Name, volume.Hash, volume.Size, cancelToken);
// Assuming we do not throw while yielding, otherwise we would need to dispose nextTask
yield return (prevResult, prevVolume.Name);
prevResult.Dispose();
// Set up for next iteration
prevVolume = volume;
prevResult = await nextTask.ConfigureAwait(false);
}
// Return the last result
yield return (prevResult, prevVolume.Name);
prevResult.Dispose();
}
/// <summary>
/// Updates the throttle values for upload and download
/// </summary>
/// <param name="maxUploadPrSecond">The maximum upload speed in bytes per second</param>
/// <param name="maxDownloadPrSecond">The maximum download speed in bytes per second</param>
public void UpdateThrottleValues(long maxUploadPrSecond, long maxDownloadPrSecond)
{
if (context.IsThrottleDisabled)
return;
context.UploadThrottleManager.Limit = maxUploadPrSecond;
context.DownloadThrottleManager.Limit = maxDownloadPrSecond;
}
/// <summary>
/// Disposes the backend manager
/// </summary>
public void Dispose()
{
if (isDisposed)
return;
isDisposed = true;
requestChannel.RetireAsync().Await();
context.Database.FlushMessagesToLog();
if (!queueRunner.IsCompleted)
{
Task.WhenAny(queueRunner, Task.Delay(1000)).Await();
if (!queueRunner.IsCompleted)
Logging.Log.WriteWarningMessage(LOGTAG, "BackendManagerShutdown", null, "Backend manager queue runner did not stop");
if (queueRunner.IsFaulted)
Logging.Log.WriteWarningMessage(LOGTAG, "BackendManagerShutdown", queueRunner.Exception, "Backend manager queue runner crashed");
}
if (queueRunner.IsCompleted)
queueRunner.Dispose();
}
}