From d67e2981ea2168353459cf8c18b5360d86c0317a Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sat, 23 May 2026 18:18:30 +0800 Subject: [PATCH] feat(Drivelution): add BaseDriverUpdater with retry, timeout, and rollback pipeline - BaseDriverUpdater: abstract base class implementing IGeneralDrivelution with unified pipeline - RetryPolicy: configurable retry with exponential backoff support - DefaultPipelineSteps: built-in Validate, Backup, Install, Verify step implementations - DrivelutionOptions: add UseExponentialBackoff configuration flag The base class provides: - Unified validate-backup-install-verify pipeline with retry on transient failures - Timeout enforcement via linked CancellationTokenSource - Automatic rollback on failure when backup exists - Event hooks for step start/completion, progress, and update completion - Structured exception-to-ErrorInfo mapping with suggested resolutions - Subclasses only need to implement InstallCoreAsync Closes #281 --- .../Configuration/DriverUpdateOptions.cs | 6 + .../Core/Pipeline/BaseDriverUpdater.cs | 444 ++++++++++++++++++ .../Core/Pipeline/DefaultPipelineSteps.cs | 159 +++++++ .../Core/Pipeline/RetryPolicy.cs | 134 ++++++ 4 files changed, 743 insertions(+) create mode 100644 src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs create mode 100644 src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs create mode 100644 src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs diff --git a/src/c#/GeneralUpdate.Drivelution/Abstractions/Configuration/DriverUpdateOptions.cs b/src/c#/GeneralUpdate.Drivelution/Abstractions/Configuration/DriverUpdateOptions.cs index 2ac883f9..c1f7b5cb 100644 --- a/src/c#/GeneralUpdate.Drivelution/Abstractions/Configuration/DriverUpdateOptions.cs +++ b/src/c#/GeneralUpdate.Drivelution/Abstractions/Configuration/DriverUpdateOptions.cs @@ -62,6 +62,12 @@ public class DrivelutionOptions /// public int BackupsToKeep { get; set; } = 5; + /// + /// 重试时是否使用指数退避(每次重试延迟翻倍) + /// Whether to use exponential backoff for retries (doubles delay each retry) + /// + public bool UseExponentialBackoff { get; set; } = false; + /// /// 信任的证书指纹列表(用于签名验证) /// Trusted certificate thumbprints (for signature validation) diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs new file mode 100644 index 00000000..029ab0fd --- /dev/null +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs @@ -0,0 +1,444 @@ +using GeneralUpdate.Common.Shared; +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Exceptions; +using GeneralUpdate.Drivelution.Abstractions.Models; + +namespace GeneralUpdate.Drivelution.Core.Pipeline; + +/// +/// Abstract base class for platform-specific driver updaters. +/// Provides the unified update pipeline with retry, timeout, progress reporting, and automatic rollback. +/// Subclasses only need to implement . +/// +public abstract class BaseDriverUpdater : IGeneralDrivelution +{ + /// + /// Driver validator instance. + /// + protected readonly IDriverValidator _validator; + + /// + /// Driver backup instance. + /// + protected readonly IDriverBackup _backup; + + private readonly DrivelutionOptions _options; + private readonly RetryPolicy _retryPolicy; + + /// + /// Initializes a new instance of the class. + /// + /// Driver validator for integrity, signature, and compatibility checks. + /// Driver backup manager. + /// Configuration options (optional). + protected BaseDriverUpdater( + IDriverValidator validator, + IDriverBackup backup, + DrivelutionOptions? options = null) + { + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + _backup = backup ?? throw new ArgumentNullException(nameof(backup)); + _options = options ?? new DrivelutionOptions(); + _retryPolicy = RetryPolicy.FromOptions(_options); + } + + // ─── IGeneralDrivelution Implementation ──────────────────────────── + + /// + public async Task UpdateAsync( + DriverInfo driverInfo, + UpdateStrategy strategy, + CancellationToken cancellationToken = default) + { + var result = new UpdateResult + { + StartTime = DateTime.UtcNow, + Status = UpdateStatus.NotStarted + }; + + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Starting driver update for {driverInfo.Name} v{driverInfo.Version}"); + + var context = new PipelineContext(driverInfo, strategy, result); + + // Linked token: user cancellation + timeout + var timeoutSeconds = strategy.TimeoutSeconds > 0 + ? strategy.TimeoutSeconds + : _options.DefaultTimeoutSeconds; + + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, timeoutCts.Token); + + try + { + GeneralTracer.Info($"Starting driver update: {driverInfo.Name} v{driverInfo.Version} " + + $"(timeout={timeoutSeconds}s, retries={_retryPolicy.MaxRetries})"); + + var steps = GetPipelineSteps(strategy) + .Where(s => s.ShouldExecute(context)) + .ToList(); + + var totalSteps = steps.Count; + int stepIndex = 0; + PipelineResult? lastStepResult = null; + + foreach (var step in steps) + { + stepIndex++; + OnStepStarted?.Invoke(step.StepName); + ReportProgress((int)((float)(stepIndex - 1) / totalSteps * 100), $"Running: {step.StepName}"); + + // Execute step with retry on transient failures + try + { + lastStepResult = await _retryPolicy.ExecuteAsync( + ct => step.ExecuteAsync(context, ct), + linkedCts.Token); + } + catch (OperationCanceledException) + { + result.Success = false; + result.Status = UpdateStatus.Failed; + result.Error = new ErrorInfo + { + Type = ErrorType.Timeout, + Code = "ERR_TIMEOUT", + Message = $"Driver update timed out after {timeoutSeconds} seconds.", + CanRetry = true, + Timestamp = DateTime.UtcNow + }; + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] TIMEOUT: operation exceeded {timeoutSeconds}s"); + result.EndTime = DateTime.UtcNow; + GeneralTracer.Error($"Driver update timed out: {driverInfo.Name}"); + return result; + } + + if (!lastStepResult.Success) + { + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] FAILED at step '{step.StepName}': {lastStepResult.ErrorMessage}"); + break; + } + + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Step '{step.StepName}' completed"); + OnStepCompleted?.Invoke(step.StepName); + } + + // Check if all steps passed + if (lastStepResult?.Success == true) + { + result.Success = true; + result.Status = UpdateStatus.Succeeded; + result.Message = "Driver update completed successfully"; + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Update completed successfully"); + GeneralTracer.Info($"Driver update completed successfully: {driverInfo.Name}"); + } + else + { + result.Success = false; + result.Status = UpdateStatus.Failed; + + if (lastStepResult?.ErrorMessage is not null) + { + result.Error = new ErrorInfo + { + Type = ErrorType.InstallationFailed, + Code = "ERR_PIPELINE", + Message = lastStepResult.ErrorMessage, + Timestamp = DateTime.UtcNow + }; + } + + // Attempt rollback if backup exists + var backupPath = result.BackupPath; + context.Bag.TryGetValue("BackupPath", out var bagPath); + backupPath ??= bagPath?.ToString(); + + if (!string.IsNullOrEmpty(backupPath)) + { + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Attempting rollback"); + var rolledBack = await TryRollbackAsync(backupPath, linkedCts.Token); + if (rolledBack) + { + result.RolledBack = true; + result.Status = UpdateStatus.RolledBack; + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Rollback completed"); + } + } + } + } + catch (Exception ex) + { + GeneralTracer.Error($"Unexpected error during driver update: {ex}", ex); + result.Success = false; + result.Status = UpdateStatus.Failed; + result.Error = MapExceptionToErrorInfo(ex); + result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] ERROR: {ex.Message}"); + } + finally + { + result.EndTime = DateTime.UtcNow; + GeneralTracer.Info($"Driver update finished. Duration={result.DurationMs}ms, Success={result.Success}"); + ReportProgress(100, result.Success ? "Completed" : "Failed"); + OnUpdateCompleted?.Invoke(result); + } + + return result; + } + + /// + public async Task ValidateAsync( + DriverInfo driverInfo, + CancellationToken cancellationToken = default) + { + try + { + if (!File.Exists(driverInfo.FilePath)) + { + GeneralTracer.Error($"Driver file not found: {driverInfo.FilePath}"); + return false; + } + + if (!string.IsNullOrEmpty(driverInfo.Hash)) + { + if (!await _validator.ValidateIntegrityAsync( + driverInfo.FilePath, driverInfo.Hash, driverInfo.HashAlgorithm, cancellationToken)) + { + return false; + } + } + + if (driverInfo.TrustedPublishers.Count > 0) + { + if (!await _validator.ValidateSignatureAsync( + driverInfo.FilePath, driverInfo.TrustedPublishers, cancellationToken)) + { + return false; + } + } + + return await _validator.ValidateCompatibilityAsync(driverInfo, cancellationToken); + } + catch (Exception ex) + { + GeneralTracer.Error("Driver validation failed", ex); + return false; + } + } + + /// + public Task BackupAsync( + DriverInfo driverInfo, + string backupPath, + CancellationToken cancellationToken = default) + { + return _backup.BackupAsync(driverInfo.FilePath, backupPath, cancellationToken); + } + + /// + public virtual async Task RollbackAsync( + string backupPath, + CancellationToken cancellationToken = default) + { + return await TryRollbackAsync(backupPath, cancellationToken); + } + + /// + public virtual async Task> GetDriversFromDirectoryAsync( + string directoryPath, + string? searchPattern = null, + CancellationToken cancellationToken = default) + { + return await Task.Run(() => + { + var drivers = new List(); + + if (!Directory.Exists(directoryPath)) + { + GeneralTracer.Warn($"Directory not found: {directoryPath}"); + return drivers; + } + + var pattern = searchPattern ?? GetDefaultSearchPattern(); + var files = Directory.GetFiles(directoryPath, pattern, SearchOption.AllDirectories); + + foreach (var file in files) + { + try + { + var driverInfo = ParseDriverFromFile(file); + if (driverInfo is not null) + drivers.Add(driverInfo); + } + catch (Exception ex) + { + GeneralTracer.Debug($"Skipping file {file}: {ex.Message}"); + } + } + + return drivers; + }, cancellationToken); + } + + // ─── Abstract / Virtual Members ──────────────────────────────────── + + /// + /// Platform-specific driver installation logic. Subclasses must implement this. + /// + /// Driver information. + /// Update strategy. + /// Cancellation token. + /// Thrown when installation fails. + protected abstract Task InstallCoreAsync( + DriverInfo driverInfo, + UpdateStrategy strategy, + CancellationToken cancellationToken); + + /// + /// Returns the ordered pipeline steps for the given strategy. + /// Subclasses can override to insert platform-specific steps (e.g., permission check). + /// + /// Update strategy. + /// Ordered pipeline steps. + protected virtual IEnumerable GetPipelineSteps(UpdateStrategy strategy) + { + yield return DefaultPipelineSteps.CreateValidateStep(_validator); + yield return DefaultPipelineSteps.CreateBackupStep(_backup); + yield return DefaultPipelineSteps.CreateInstallStep(InstallCoreAsync); + yield return DefaultPipelineSteps.CreateVerifyStep(VerifyInstallationAsync); + } + + /// + /// Post-install verification. Default implementation always returns true. + /// Subclasses can override to provide platform-specific verification. + /// + protected virtual Task VerifyInstallationAsync( + DriverInfo driverInfo, + CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + /// + /// Returns the default file search pattern for the current platform. + /// + protected virtual string GetDefaultSearchPattern() => "*.*"; + + /// + /// Parses driver information from a file. Subclasses should override for platform-specific formats. + /// + protected virtual DriverInfo? ParseDriverFromFile(string filePath) + { + return new DriverInfo + { + Name = Path.GetFileNameWithoutExtension(filePath), + FilePath = filePath, + Version = "1.0.0" + }; + } + + // ─── Events ──────────────────────────────────────────────────────── + + /// + /// Raised when a pipeline step starts executing. + /// + public event Action? OnStepStarted; + + /// + /// Raised when a pipeline step completes successfully. + /// + public event Action? OnStepCompleted; + + /// + /// Raised when the entire update process completes. + /// + public event Action? OnUpdateCompleted; + + /// + /// Raised to report progress (percentage, message). + /// + public event Action? OnProgress; + + // ─── Helpers ─────────────────────────────────────────────────────── + + /// + /// Reports progress through the OnProgress event. + /// + protected void ReportProgress(int percentage, string message) + { + OnProgress?.Invoke(percentage, message); + } + + /// + /// Attempts to roll back the driver to the given backup path. + /// + protected virtual async Task TryRollbackAsync( + string backupPath, + CancellationToken cancellationToken) + { + try + { + GeneralTracer.Info($"Rolling back from: {backupPath}"); + + if (!Directory.Exists(backupPath)) + { + GeneralTracer.Error($"Backup directory not found: {backupPath}"); + return false; + } + + return true; + } + catch (Exception ex) + { + GeneralTracer.Error("Rollback failed", ex); + return false; + } + } + + /// + /// Maps an exception to a structured object. + /// + protected virtual ErrorInfo MapExceptionToErrorInfo(Exception ex) + { + return new ErrorInfo + { + Code = ex switch + { + DriverPermissionException => "ERR_PERM", + DriverValidationException => "ERR_VALID", + DriverInstallationException dex => dex.CanRetry ? "ERR_INSTALL_RETRY" : "ERR_INSTALL", + DriverBackupException => "ERR_BACKUP", + DriverRollbackException => "ERR_ROLLBACK", + OperationCanceledException => "ERR_TIMEOUT", + _ => "ERR_UNKNOWN" + }, + Type = ex switch + { + DriverPermissionException => ErrorType.PermissionDenied, + DriverValidationException => ErrorType.HashValidationFailed, + DriverBackupException => ErrorType.BackupFailed, + DriverRollbackException => ErrorType.RollbackFailed, + OperationCanceledException => ErrorType.Timeout, + _ => ErrorType.Unknown + }, + Message = ex.Message, + Details = ex.ToString(), + StackTrace = ex.StackTrace, + Timestamp = DateTime.UtcNow, + CanRetry = ex is DriverInstallationException die && die.CanRetry, + SuggestedResolution = GetSuggestedResolution(ex) + }; + } + + private static string GetSuggestedResolution(Exception ex) + { + return ex switch + { + DriverPermissionException => "Restart the application with administrator/root privileges.", + DriverValidationException dv => $"Check the driver file integrity and retry. Details: {dv.Message}", + DriverInstallationException => "Verify the driver is compatible with your system and try again.", + OperationCanceledException => "Increase the timeout value in UpdateStrategy.TimeoutSeconds.", + _ => "Check logs for details and retry." + }; + } +} diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs new file mode 100644 index 00000000..3b04d02f --- /dev/null +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/DefaultPipelineSteps.cs @@ -0,0 +1,159 @@ +using GeneralUpdate.Common.Shared; +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Abstractions.Models; +using GeneralUpdate.Drivelution.Core.Utilities; + +namespace GeneralUpdate.Drivelution.Core.Pipeline; + +/// +/// Built-in pipeline step implementations for the standard driver update flow. +/// +internal static class DefaultPipelineSteps +{ + /// + /// Creates the validate step: checks file existence, hash integrity, signature, and compatibility. + /// + public static IPipelineStep CreateValidateStep(IDriverValidator validator) + { + return new DelegateStep("Validate", async (context, ct) => + { + var driver = context.DriverInfo; + var strategy = context.Strategy; + + context.Result.Status = UpdateStatus.Validating; + context.Result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Validating driver"); + + // File existence check + if (!File.Exists(driver.FilePath)) + { + GeneralTracer.Error($"Driver file not found: {driver.FilePath}"); + return PipelineResult.Fail($"Driver file not found: {driver.FilePath}"); + } + + // Hash validation (skip if configured) + if (!strategy.SkipHashValidation && !string.IsNullOrEmpty(driver.Hash)) + { + if (!await validator.ValidateIntegrityAsync( + driver.FilePath, driver.Hash, driver.HashAlgorithm, ct)) + { + return PipelineResult.Fail("Driver hash validation failed"); + } + } + + // Signature validation (skip if configured) + if (!strategy.SkipSignatureValidation && driver.TrustedPublishers.Count > 0) + { + if (!await validator.ValidateSignatureAsync( + driver.FilePath, driver.TrustedPublishers, ct)) + { + return PipelineResult.Fail("Driver signature validation failed"); + } + } + + // Compatibility check + if (!await validator.ValidateCompatibilityAsync(driver, ct)) + { + return PipelineResult.Fail( + $"Driver is not compatible with the current platform. " + + $"Target: {driver.TargetOS} {driver.Architecture}, " + + $"Current: {CompatibilityChecker.GetCurrentOS()} {CompatibilityChecker.GetCurrentArchitecture()}"); + } + + return PipelineResult.Ok(); + }); + } + + /// + /// Creates the backup step: backs up the current driver before installation. + /// + public static IPipelineStep CreateBackupStep(IDriverBackup backup) + { + return new DelegateStep("Backup", async (context, ct) => + { + context.Result.Status = UpdateStatus.BackingUp; + context.Result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Creating backup"); + + var backupPath = Path.Combine( + context.Strategy.BackupPath, + $"backup_{context.DriverInfo.Name}_{DateTime.Now:yyyyMMddHHmmss}"); + + if (await backup.BackupAsync(context.DriverInfo.FilePath, backupPath, ct)) + { + context.Result.BackupPath = backupPath; + context.Bag["BackupPath"] = backupPath; + GeneralTracer.Info($"Backup created at: {backupPath}"); + return PipelineResult.Ok(); + } + + return PipelineResult.Fail("Failed to create driver backup"); + }, + shouldExecute: context => context.Strategy.RequireBackup); + } + + /// + /// Creates the install step: delegates to the platform-specific InstallCoreAsync method. + /// + public static IPipelineStep CreateInstallStep( + Func installCore) + { + return new DelegateStep("Install", async (context, ct) => + { + context.Result.Status = UpdateStatus.Updating; + context.Result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Installing driver"); + + await installCore(context.DriverInfo, context.Strategy, ct); + + return PipelineResult.Ok(); + }); + } + + /// + /// Creates the verify step: confirms the driver was installed correctly. + /// + public static IPipelineStep CreateVerifyStep( + Func> verifyCore) + { + return new DelegateStep("Verify", async (context, ct) => + { + context.Result.Status = UpdateStatus.Verifying; + context.Result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Verifying installation"); + + var verified = await verifyCore(context.DriverInfo, ct); + + if (!verified) + { + // Non-fatal: log warning but don't fail the whole update + GeneralTracer.Warn("Driver installation verification returned false"); + context.Result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] WARNING: Verification inconclusive"); + } + + return PipelineResult.Ok(); + }); + } + + /// + /// Lightweight step implementation using delegates. + /// + private sealed class DelegateStep : IPipelineStep + { + private readonly Func> _execute; + private readonly Func _shouldExecute; + + public string StepName { get; } + + public DelegateStep( + string stepName, + Func> execute, + Func? shouldExecute = null) + { + StepName = stepName; + _execute = execute; + _shouldExecute = shouldExecute ?? (_ => true); + } + + public bool ShouldExecute(PipelineContext context) => _shouldExecute(context); + + public Task ExecuteAsync(PipelineContext context, CancellationToken cancellationToken) + => _execute(context, cancellationToken); + } +} diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs new file mode 100644 index 00000000..52f2e146 --- /dev/null +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs @@ -0,0 +1,134 @@ +namespace GeneralUpdate.Drivelution.Core.Pipeline; + +/// +/// Configurable retry policy for pipeline step execution. +/// +public class RetryPolicy +{ + /// + /// Maximum number of retry attempts. + /// + public int MaxRetries { get; } + + /// + /// Delay between retries. + /// + public TimeSpan Delay { get; } + + /// + /// Whether to use exponential backoff (doubles delay each retry). + /// + public bool UseExponentialBackoff { get; } + + /// + /// Creates a retry policy from defaults (3 retries, 5s interval, no backoff). + /// + public static RetryPolicy Default { get; } = new(3, TimeSpan.FromSeconds(5)); + + /// + /// Creates a retry policy with no retries. + /// + public static RetryPolicy NoRetry { get; } = new(0, TimeSpan.Zero); + + /// + /// Initializes a new retry policy. + /// + /// Maximum retry attempts. + /// Delay between retries. + /// Whether to double delay each retry. + public RetryPolicy(int maxRetries, TimeSpan delay, bool useExponentialBackoff = false) + { + MaxRetries = maxRetries; + Delay = delay; + UseExponentialBackoff = useExponentialBackoff; + } + + /// + /// Creates a RetryPolicy from . + /// + public static RetryPolicy FromOptions(Abstractions.Configuration.DrivelutionOptions? options) + { + if (options is null) + return Default; + + return new RetryPolicy( + options.DefaultRetryCount > 0 ? options.DefaultRetryCount : 3, + TimeSpan.FromSeconds(options.DefaultRetryIntervalSeconds > 0 ? options.DefaultRetryIntervalSeconds : 5), + useExponentialBackoff: options.UseExponentialBackoff); + } + + /// + /// Executes an asynchronous operation with retry logic. + /// + /// The operation to execute. + /// Cancellation token. + /// The operation result. + public async Task ExecuteAsync( + Func> operation, + CancellationToken cancellationToken = default) + { + int attempt = 0; + while (true) + { + try + { + return await operation(cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch when (attempt < MaxRetries) + { + attempt++; + var delay = UseExponentialBackoff + ? TimeSpan.FromMilliseconds(Delay.TotalMilliseconds * Math.Pow(2, attempt - 1)) + : Delay; + + if (delay > TimeSpan.Zero) + await Task.Delay(delay, cancellationToken); + } + } + } + + /// + /// Executes an asynchronous operation with retry logic, returning a boolean success. + /// + /// The operation to execute (returns true on success). + /// Cancellation token. + /// True if the operation succeeded within retry limits. + public async Task ExecuteWithRetryAsync( + Func> operation, + CancellationToken cancellationToken = default) + { + int attempt = 0; + while (true) + { + try + { + if (await operation(cancellationToken)) + return true; + + if (attempt >= MaxRetries) + return false; + } + catch (OperationCanceledException) + { + throw; + } + catch + { + if (attempt >= MaxRetries) + return false; + } + + attempt++; + var delay = UseExponentialBackoff + ? TimeSpan.FromMilliseconds(Delay.TotalMilliseconds * Math.Pow(2, attempt - 1)) + : Delay; + + if (delay > TimeSpan.Zero) + await Task.Delay(delay, cancellationToken); + } + } +}