From 2954782d1845fcfc865989941c376154822c90bf Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:29:21 +0100 Subject: [PATCH 1/2] refactor!: replace Polly with Kevlar BREAKING CHANGE: Advanced.WithRetryPolicy is replaced by Advanced.WithRetryShield and accepts Kevlar Shield instances or factories. --- Directory.Packages.props | 2 +- README.md | 2 +- README_Template.md | 2 +- docs/docs/how-to/defining-modules.md | 2 +- docs/docs/how-to/retry-policy.md | 22 ++--- docs/docs/migrating-to-v3.md | 4 +- .../Modules/UnitTests/RunUnitTestModule.cs | 4 +- .../AdvancedModuleConfigurationBuilder.cs | 22 ++--- .../Configuration/ModuleConfiguration.cs | 6 +- .../ModuleConfigurationAttributeAdapter.cs | 2 +- .../ModuleConfigurationBuilder.cs | 12 +-- .../Configuration/ModuleRetryPolicyFactory.cs | 22 +++-- .../Engine/ModuleExecutionPipeline.cs | 20 ++--- .../Helpers/DefaultRetryPolicyProvider.cs | 18 ----- .../Http/ResilienceHttpHandler.cs | 42 +++++----- src/ModularPipelines/ModularPipelines.csproj | 2 +- .../Configuration/ModuleConfigurationTests.cs | 40 +++++----- .../Execution/ComposableModuleTests.cs | 2 - .../Execution/RetryTests.cs | 15 ++-- .../Execution/SubModuleTests.cs | 15 ++-- .../ModularPipelines.OptionsGenerator.csproj | 2 +- .../ResilientCliCommandExecutor.cs | 80 +++++++++---------- 22 files changed, 154 insertions(+), 184 deletions(-) delete mode 100644 src/ModularPipelines/Helpers/DefaultRetryPolicyProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 3b4d223bc03..cf88d7638cc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,7 @@ + @@ -70,7 +71,6 @@ - diff --git a/README.md b/README.md index 822f535bcda..8cc2ec0397f 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ ModularPipelines takes a different approach: each unit of work is a self-contain * **Secret obfuscation** - Automatic masking in logs * **Hooks** - Run code before/after any module * **Skip conditions** - Dynamically skip modules based on custom logic -* **Retry policies** - Configurable retry with Polly integration +* **Retry policies** - Configurable retry with Kevlar integration * **Requirements validation** - Check prerequisites before running * **Progress reporting** - Real-time console output with parallel execution visualization * **Source controlled** - Your pipeline is code, version it like code diff --git a/README_Template.md b/README_Template.md index 5783c24b21f..c7b5dcb634e 100644 --- a/README_Template.md +++ b/README_Template.md @@ -186,7 +186,7 @@ ModularPipelines takes a different approach: each unit of work is a self-contain * **Secret obfuscation** - Automatic masking in logs * **Hooks** - Run code before/after any module * **Skip conditions** - Dynamically skip modules based on custom logic -* **Retry policies** - Configurable retry with Polly integration +* **Retry policies** - Configurable retry with Kevlar integration * **Requirements validation** - Check prerequisites before running * **Progress reporting** - Real-time console output with parallel execution visualization * **Source controlled** - Your pipeline is code, version it like code diff --git a/docs/docs/how-to/defining-modules.md b/docs/docs/how-to/defining-modules.md index ce2d8590672..d50707df0e3 100644 --- a/docs/docs/how-to/defining-modules.md +++ b/docs/docs/how-to/defining-modules.md @@ -92,7 +92,7 @@ public class MyModule : Module |--------|-------------| | `.WithTimeout(TimeSpan)` | Maximum execution time before module is cancelled | | `.WithRetry(int, TimeSpan?, Func?)` | Retry attempts, jittered base delay, and optional exception filter | -| `.Advanced.WithRetryPolicy(IAsyncPolicy)` | Custom Polly policy for advanced scenarios | +| `.Advanced.WithRetryShield(Shield)` | Custom Kevlar shield for advanced scenarios | | `.WithSkipWhen(...)` | Condition to skip the module | | `.WithIgnoreFailures()` | Don't fail the pipeline if this module fails | | `.WithIgnoreFailuresWhen(...)` | Conditionally ignore failures | diff --git a/docs/docs/how-to/retry-policy.md b/docs/docs/how-to/retry-policy.md index 63452901f7c..bb893e3b171 100644 --- a/docs/docs/how-to/retry-policy.md +++ b/docs/docs/how-to/retry-policy.md @@ -42,18 +42,18 @@ protected override ModuleConfiguration Configure() => ModuleConfiguration.Create .Build(); ``` -### Advanced Polly Policy +### Advanced Kevlar Shield -For policy features outside the standard API, use the explicit `.Advanced` surface: +For resilience features outside the standard API, use a Kevlar `Shield` through the explicit `.Advanced` surface: ```csharp public class MyModule : Module { protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryPolicy( - Policy.Handle() - .WaitAndRetryAsync(5, i => TimeSpan.FromSeconds(i * i))) + .WithRetryShield( + Shield.When() + .Retry(5, Backoff.Custom(i => TimeSpan.FromSeconds(i * i)))) .Build(); protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) @@ -63,20 +63,20 @@ public class MyModule : Module } ``` -### Context-Aware Retry Policy +### Context-Aware Retry Shield -If you need access to the pipeline context when building your policy: +If you need access to the pipeline context when building your shield: ```csharp public class MyModule : Module { protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryPolicy(ctx => + .WithRetryShield(ctx => { var retryCount = ctx.Environment.IsCI ? 5 : 2; - return Policy.Handle() - .WaitAndRetryAsync(retryCount, i => TimeSpan.FromSeconds(i)); + return Shield.When() + .Retry(retryCount, Backoff.Custom(i => TimeSpan.FromSeconds(i))); }) .Build(); } @@ -117,4 +117,4 @@ builder.ConfigurePipelineOptions(options => options with await builder.ExecutePipelineAsync(); ``` -This applies to all modules that don't override their retry policy. Modules can override this default by configuring their own retry policy in `Configure()`. +This applies to all modules that don't override their retry configuration. Modules can override this default by configuring retries in `Configure()`. diff --git a/docs/docs/migrating-to-v3.md b/docs/docs/migrating-to-v3.md index 90fcda91763..2b9a06e0f76 100644 --- a/docs/docs/migrating-to-v3.md +++ b/docs/docs/migrating-to-v3.md @@ -240,7 +240,7 @@ public class MyModule : Module | V2 Override | V3 Configure() Method | |-------------|----------------------| | `TimeSpan Timeout` property | `.WithTimeout(TimeSpan)` | -| `AsyncRetryPolicy RetryPolicy` property | `.WithRetry(int, ...)` or `.Advanced.WithRetryPolicy(IAsyncPolicy)` | +| `AsyncRetryPolicy RetryPolicy` property | `.WithRetry(int, ...)` or `.Advanced.WithRetryShield(Shield)` | | `Task ShouldSkip()` method | `.WithSkipWhen(...)` | | `Task ShouldIgnoreFailures()` method | `.WithIgnoreFailures()` or `.WithIgnoreFailuresWhen(...)` | | `ModuleRunType.AlwaysRun` | `.WithAlwaysRun()` | @@ -867,7 +867,7 @@ The following have been removed in V3: | `ShouldIgnoreFailures()` method | `Configure().WithIgnoreFailures()` | | `ModuleRunType` property | `Configure().WithAlwaysRun()` | | `Timeout` property | `Configure().WithTimeout()` | -| `RetryPolicy` property | `Configure().WithRetry()` or `.Advanced.WithRetryPolicy()` | +| `RetryPolicy` property | `Configure().WithRetry()` or `.Advanced.WithRetryShield()` | | `GetModule()` on module | `context.GetModule()` | ## New Features in V3 diff --git a/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs b/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs index ecc9f8eeb85..ef57e2987c3 100644 --- a/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs +++ b/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Kevlar; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ModularPipelines; @@ -14,7 +15,6 @@ using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.Options; -using Polly; using Spectre.Console; using File = ModularPipelines.FileSystem.File; @@ -40,7 +40,7 @@ public abstract partial class RunUnitTestModule(IOptions pipel protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .WithSkipWhen(GetSkipDecision) .Advanced - .WithRetryPolicy(Policy.Handle().RetryAsync(0)) + .WithRetryShield(Shield.Retry(0)) .Build(); protected virtual SkipDecision GetSkipDecision(IModuleContext context) => SkipDecision.DoNotSkip; diff --git a/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs b/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs index efc89ba9361..8f6edc70b10 100644 --- a/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs +++ b/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs @@ -1,10 +1,10 @@ +using Kevlar; using ModularPipelines.Context; -using Polly; namespace ModularPipelines.Configuration; /// -/// Provides advanced module configuration that depends on third-party policy abstractions. +/// Provides advanced module configuration using Kevlar resilience shields. /// public sealed class AdvancedModuleConfigurationBuilder { @@ -16,24 +16,24 @@ internal AdvancedModuleConfigurationBuilder(ModuleConfigurationBuilder builder) } /// - /// Sets a custom Polly async policy for module execution. + /// Sets a custom Kevlar shield for module execution. /// - /// The Polly async policy to execute around the module. + /// The Kevlar shield to execute around the module. /// The parent module configuration builder. - public ModuleConfigurationBuilder WithRetryPolicy(IAsyncPolicy policy) + public ModuleConfigurationBuilder WithRetryShield(Shield shield) { - ArgumentNullException.ThrowIfNull(policy); - return _builder.SetAdvancedRetryPolicy(_ => policy); + ArgumentNullException.ThrowIfNull(shield); + return _builder.SetAdvancedRetryShield(_ => shield); } /// - /// Sets a factory that creates a custom Polly async policy from the module context. + /// Sets a factory that creates a custom Kevlar shield from the module context. /// - /// The policy factory. + /// The shield factory. /// The parent module configuration builder. - public ModuleConfigurationBuilder WithRetryPolicy(Func factory) + public ModuleConfigurationBuilder WithRetryShield(Func factory) { ArgumentNullException.ThrowIfNull(factory); - return _builder.SetAdvancedRetryPolicy(factory); + return _builder.SetAdvancedRetryShield(factory); } } diff --git a/src/ModularPipelines/Configuration/ModuleConfiguration.cs b/src/ModularPipelines/Configuration/ModuleConfiguration.cs index caf74fc73bd..d985c6021a5 100644 --- a/src/ModularPipelines/Configuration/ModuleConfiguration.cs +++ b/src/ModularPipelines/Configuration/ModuleConfiguration.cs @@ -1,8 +1,8 @@ using System.Collections.Frozen; +using Kevlar; using ModularPipelines.Context; using ModularPipelines.Enums; using ModularPipelines.Models; -using Polly; namespace ModularPipelines.Configuration; @@ -158,7 +158,7 @@ public sealed class ModuleConfiguration internal ModuleRetryConfiguration? RetryConfiguration { get; init; } /// - /// Gets the advanced policy factory for module execution. + /// Gets the advanced Kevlar shield factory for module execution. /// - internal Func? AdvancedRetryPolicyFactory { get; init; } + internal Func? AdvancedRetryShieldFactory { get; init; } } diff --git a/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs b/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs index 61d835d8bac..705770cd3c6 100644 --- a/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs +++ b/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs @@ -39,7 +39,7 @@ public static ModuleConfiguration Apply( PlanningSkipCondition = configured.PlanningSkipCondition, Timeout = configured.Timeout, RetryConfiguration = configured.RetryConfiguration, - AdvancedRetryPolicyFactory = configured.AdvancedRetryPolicyFactory, + AdvancedRetryShieldFactory = configured.AdvancedRetryShieldFactory, IgnoreFailuresCondition = configured.IgnoreFailuresCondition, AlwaysRun = configured.AlwaysRun, ParallelConstraintKeys = configured.ParallelConstraintKeys diff --git a/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs b/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs index 7047f8f315f..ab61628c943 100644 --- a/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs +++ b/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs @@ -1,10 +1,10 @@ +using Kevlar; using ModularPipelines.Context; using ModularPipelines.Engine; using ModularPipelines.Enums; using ModularPipelines.Exceptions; using ModularPipelines.Models; using ModularPipelines.Modules; -using Polly; namespace ModularPipelines.Configuration; @@ -45,7 +45,7 @@ public sealed class ModuleConfigurationBuilder private string? _cacheAssemblyVersionKey; private TimeSpan? _timeout; private ModuleRetryConfiguration? _retryConfiguration; - private Func? _advancedRetryPolicyFactory; + private Func? _advancedRetryShieldFactory; private Func>? _ignoreFailuresCondition; private bool _alwaysRun; private string[]? _parallelConstraintKeys; @@ -364,7 +364,7 @@ public ModuleConfigurationBuilder WithRetry( } _retryConfiguration = new ModuleRetryConfiguration(count, retryBaseDelay, shouldRetry); - _advancedRetryPolicyFactory = null; + _advancedRetryShieldFactory = null; return this; } @@ -432,7 +432,7 @@ public ModuleConfiguration Build() PlanningSkipCondition = ComposePlanningSkipConditions(), Timeout = _timeout, RetryConfiguration = _retryConfiguration, - AdvancedRetryPolicyFactory = _advancedRetryPolicyFactory, + AdvancedRetryShieldFactory = _advancedRetryShieldFactory, IgnoreFailuresCondition = _ignoreFailuresCondition, AlwaysRun = _alwaysRun, ParallelConstraintKeys = _parallelConstraintKeys, @@ -447,9 +447,9 @@ public ModuleConfiguration Build() }; } - internal ModuleConfigurationBuilder SetAdvancedRetryPolicy(Func factory) + internal ModuleConfigurationBuilder SetAdvancedRetryShield(Func factory) { - _advancedRetryPolicyFactory = factory; + _advancedRetryShieldFactory = factory; _retryConfiguration = null; return this; } diff --git a/src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs b/src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs index e1a162abbfe..93564d12b35 100644 --- a/src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs +++ b/src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs @@ -1,21 +1,19 @@ -using Polly; +using Kevlar; namespace ModularPipelines.Configuration; internal static class ModuleRetryPolicyFactory { - internal static IAsyncPolicy Create(ModuleRetryConfiguration configuration) + internal static Shield Create(ModuleRetryConfiguration configuration) { - var policyBuilder = configuration.ShouldRetry is null - ? Policy.Handle() - : Policy.Handle(configuration.ShouldRetry); - - return policyBuilder.WaitAndRetryAsync( - configuration.Count, - retryAttempt => CalculateDelay( - retryAttempt, - configuration.BaseDelay, - Random.Shared.NextDouble())); + return Shield + .When(configuration.ShouldRetry ?? (static _ => true)) + .Retry( + configuration.Count, + Backoff.Custom(retryAttempt => CalculateDelay( + retryAttempt, + configuration.BaseDelay, + Random.Shared.NextDouble()))); } internal static TimeSpan CalculateDelay( diff --git a/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs b/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs index ed89eb59f28..c37e31542e3 100644 --- a/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs +++ b/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs @@ -1,3 +1,4 @@ +using Kevlar; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ModularPipelines.Caching; @@ -12,7 +13,6 @@ using ModularPipelines.Modules; using ModularPipelines.Options; using ModularPipelines.Tracing; -using Polly; namespace ModularPipelines.Engine; @@ -486,8 +486,8 @@ private async Task ExecuteWithPolicies( var cancellationToken = executionContext.ModuleCancellationTokenSource.Token; - // Get retry policy if applicable - var retryPolicy = GetRetryPolicy(config, moduleContext); + // Get retry shield if applicable + var retryShield = GetRetryShield(config, moduleContext); var moduleAttemptCount = 0; var moduleAttemptRespondedToCancellation = 0; @@ -508,8 +508,10 @@ async Task ExecuteModuleAttempt(CancellationToken ct) } // Create the execution function that optionally includes retry - Func> executeFunc = retryPolicy != null - ? ct => retryPolicy.ExecuteAsync(ExecuteModuleAttempt, ct) + Func> executeFunc = retryShield != null + ? async ct => await retryShield.ExecuteAsync( + async retryToken => await ExecuteModuleAttempt(retryToken).ConfigureAwait(false), + ct).ConfigureAwait(false) : ExecuteModuleAttempt; // Use TimeoutHelper with detailed results to get information about token cooperation @@ -532,7 +534,7 @@ async Task ExecuteModuleAttempt(CancellationToken ct) if (timeoutResult.TimedOut) { var wasCancellationTokenRespected = timeoutResult.WasCancellationTokenRespected - && (retryPolicy is null + && (retryShield is null || Volatile.Read(ref moduleAttemptRespondedToCancellation) == 1); // Create a detailed timeout exception with information about token cooperation @@ -551,13 +553,13 @@ private TimeSpan GetTimeout(ModuleConfiguration config) return config.Timeout ?? _pipelineOptions.Value.DefaultModuleTimeout; } - private static IAsyncPolicy? GetRetryPolicy( + private static Shield? GetRetryShield( ModuleConfiguration config, IModuleContext moduleContext) { - if (config.AdvancedRetryPolicyFactory != null) + if (config.AdvancedRetryShieldFactory != null) { - return config.AdvancedRetryPolicyFactory(moduleContext); + return config.AdvancedRetryShieldFactory(moduleContext); } if (config.RetryConfiguration != null) diff --git a/src/ModularPipelines/Helpers/DefaultRetryPolicyProvider.cs b/src/ModularPipelines/Helpers/DefaultRetryPolicyProvider.cs deleted file mode 100644 index 679decd921f..00000000000 --- a/src/ModularPipelines/Helpers/DefaultRetryPolicyProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ModularPipelines.Context; -using Polly; -using Polly.Retry; - -namespace ModularPipelines.Helpers; - -internal static class DefaultRetryPolicyProvider -{ - public static AsyncRetryPolicy GetDefaultRetryPolicy(IPipelineContext context) - { - var retryCount = context.Services.Options.DefaultRetryCount; - - return Policy.Handle() - .WaitAndRetryAsync(retryCount, - i => TimeSpan.FromMilliseconds(i * i * 100) - ); - } -} \ No newline at end of file diff --git a/src/ModularPipelines/Http/ResilienceHttpHandler.cs b/src/ModularPipelines/Http/ResilienceHttpHandler.cs index 83f32090c74..2acd651fec6 100644 --- a/src/ModularPipelines/Http/ResilienceHttpHandler.cs +++ b/src/ModularPipelines/Http/ResilienceHttpHandler.cs @@ -1,14 +1,13 @@ +using Kevlar; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ModularPipelines.Logging; using ModularPipelines.Options; -using Polly; -using Polly.Retry; namespace ModularPipelines.Http; /// -/// A delegating handler that adds retry resilience to HTTP requests using Polly. +/// A delegating handler that adds retry resilience to HTTP requests using Kevlar. /// Handles transient failures including network errors and server errors (5xx). /// internal class ResilienceHttpHandler : DelegatingHandler @@ -41,25 +40,27 @@ protected override async Task SendAsync(HttpRequestMessage contentBytes = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); } - var retryPolicy = BuildRetryPolicy(options); + var retryShield = BuildRetryShield(options); - return await retryPolicy.ExecuteAsync( + return await retryShield.ExecuteAsync( async ct => await base.SendAsync(CloneRequest(request, contentBytes), ct).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); } - private AsyncRetryPolicy BuildRetryPolicy(HttpResilienceOptions options) + private Shield BuildRetryShield(HttpResilienceOptions options) { - return Policy - .Handle(ex => options.RetryOnHttpRequestException) - .Or(ex => + return Shield.For() + .When(_ => options.RetryOnHttpRequestException) + .Or(exception => options.RetryOnTimeout && - ex.InnerException is TimeoutException) + exception.InnerException is TimeoutException) .OrResult(response => ShouldRetryStatusCode(response, options)) - .WaitAndRetryAsync( - options.MaxRetryAttempts, - retryAttempt => CalculateDelay(retryAttempt, options), - OnRetry); + .Retry(retryOptions => + { + retryOptions.MaxRetries = options.MaxRetryAttempts; + retryOptions.Backoff = Backoff.Custom(retryAttempt => CalculateDelay(retryAttempt, options)); + retryOptions.OnRetry = OnRetry; + }); } private static bool ShouldRetryStatusCode(HttpResponseMessage response, HttpResilienceOptions options) @@ -93,17 +94,18 @@ private TimeSpan CalculateDelay(int retryAttempt, HttpResilienceOptions options) return baseDelay; } - private Task OnRetry(DelegateResult outcome, TimeSpan delay, int retryAttempt, Polly.Context context) + private void OnRetry(RetryEvent retryEvent) { var logger = _loggerProvider.GetLogger(); + var outcome = retryEvent.Outcome; if (outcome.Exception != null) { logger.LogWarning("HTTP request failed with {ExceptionType}: {Message}. Retry attempt {RetryAttempt} after {Delay}ms", outcome.Exception.GetType().Name, outcome.Exception.Message, - retryAttempt, - (int) delay.TotalMilliseconds); + retryEvent.Attempt, + (int) retryEvent.Delay.TotalMilliseconds); } else if (outcome.Result != null) { @@ -111,16 +113,14 @@ private Task OnRetry(DelegateResult outcome, TimeSpan delay { logger.LogWarning("HTTP request returned {StatusCode}. Retry attempt {RetryAttempt} after {Delay}ms", (int) outcome.Result.StatusCode, - retryAttempt, - (int) delay.TotalMilliseconds); + retryEvent.Attempt, + (int) retryEvent.Delay.TotalMilliseconds); } finally { outcome.Result.Dispose(); } } - - return Task.CompletedTask; } /// diff --git a/src/ModularPipelines/ModularPipelines.csproj b/src/ModularPipelines/ModularPipelines.csproj index 5bd0eeae4b5..d0ef6bb570d 100644 --- a/src/ModularPipelines/ModularPipelines.csproj +++ b/src/ModularPipelines/ModularPipelines.csproj @@ -136,7 +136,7 @@ compile;contentfiles;build;analyzers - + diff --git a/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs b/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs index ba9ec3456fe..6c2ed0bf90d 100644 --- a/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs +++ b/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs @@ -1,10 +1,10 @@ +using Kevlar; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Enums; using ModularPipelines.Models; using ModularPipelines.Modules; using Moq; -using Polly; namespace ModularPipelines.UnitTests.Configuration; @@ -42,7 +42,7 @@ public async Task Default_RetryConfiguration_IsNull() using (Assert.Multiple()) { await Assert.That(config.RetryConfiguration).IsNull(); - await Assert.That(config.AdvancedRetryPolicyFactory).IsNull(); + await Assert.That(config.AdvancedRetryShieldFactory).IsNull(); } } @@ -344,7 +344,7 @@ public async Task WithRetry_UsesDefaultBaseDelay() await Assert.That(config.RetryConfiguration!.Count).IsEqualTo(3); await Assert.That(config.RetryConfiguration.BaseDelay).IsEqualTo(TimeSpan.FromMilliseconds(100)); await Assert.That(config.RetryConfiguration.ShouldRetry).IsNull(); - await Assert.That(config.AdvancedRetryPolicyFactory).IsNull(); + await Assert.That(config.AdvancedRetryShieldFactory).IsNull(); } } @@ -383,43 +383,43 @@ public async Task WithRetry_RejectsNegativeValues() } [Test] - public async Task Advanced_WithRetryPolicy_Direct_SetsAdvancedRetryPolicyFactory() + public async Task Advanced_WithRetryShield_Direct_SetsAdvancedRetryShieldFactory() { - var policy = Policy.NoOpAsync(); + var shield = Shield.Retry(0); var config = ModuleConfiguration.Create() .Advanced - .WithRetryPolicy(policy) + .WithRetryShield(shield) .Build(); - await Assert.That(config.AdvancedRetryPolicyFactory).IsNotNull(); + await Assert.That(config.AdvancedRetryShieldFactory).IsNotNull(); var context = Mock.Of(); - var result = config.AdvancedRetryPolicyFactory!(context); + var result = config.AdvancedRetryShieldFactory!(context); - await Assert.That(result).IsEqualTo(policy); + await Assert.That(result).IsEqualTo(shield); } [Test] - public async Task Advanced_WithRetryPolicy_Factory_SetsAdvancedRetryPolicyFactory() + public async Task Advanced_WithRetryShield_Factory_SetsAdvancedRetryShieldFactory() { - var policy = Policy.NoOpAsync(); + var shield = Shield.Retry(0); var config = ModuleConfiguration.Create() .Advanced - .WithRetryPolicy(_ => policy) + .WithRetryShield(_ => shield) .Build(); - await Assert.That(config.AdvancedRetryPolicyFactory).IsNotNull(); + await Assert.That(config.AdvancedRetryShieldFactory).IsNotNull(); var context = Mock.Of(); - var result = config.AdvancedRetryPolicyFactory!(context); + var result = config.AdvancedRetryShieldFactory!(context); - await Assert.That(result).IsEqualTo(policy); + await Assert.That(result).IsEqualTo(shield); } [Test] - public async Task StandardConfigurationSurface_DoesNotExposePollyTypes() + public async Task StandardConfigurationSurface_DoesNotExposeKevlarTypes() { var publicSurfaceTypes = typeof(ModuleConfigurationBuilder) .GetMethods() @@ -430,7 +430,7 @@ public async Task StandardConfigurationSurface_DoesNotExposePollyTypes() .GetProperties() .Select(property => property.PropertyType)); - await Assert.That(publicSurfaceTypes.Any(ContainsPollyType)).IsFalse(); + await Assert.That(publicSurfaceTypes.Any(ContainsKevlarType)).IsFalse(); } [Test] @@ -580,7 +580,7 @@ public async Task Builder_FluentChaining_AllMethodsChain() #endregion - private static bool ContainsPollyType(Type type) => - type.Namespace?.StartsWith("Polly", StringComparison.Ordinal) == true - || type.GetGenericArguments().Any(ContainsPollyType); + private static bool ContainsKevlarType(Type type) => + type.Namespace?.StartsWith("Kevlar", StringComparison.Ordinal) == true + || type.GetGenericArguments().Any(ContainsKevlarType); } diff --git a/test/ModularPipelines.UnitTests/Execution/ComposableModuleTests.cs b/test/ModularPipelines.UnitTests/Execution/ComposableModuleTests.cs index 2bdc9330a11..4aa370cc6f2 100644 --- a/test/ModularPipelines.UnitTests/Execution/ComposableModuleTests.cs +++ b/test/ModularPipelines.UnitTests/Execution/ComposableModuleTests.cs @@ -6,8 +6,6 @@ using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.TestHelpers; -using Polly; -using Polly.Retry; using TUnit.Assertions.Extensions; namespace ModularPipelines.UnitTests.Execution; diff --git a/test/ModularPipelines.UnitTests/Execution/RetryTests.cs b/test/ModularPipelines.UnitTests/Execution/RetryTests.cs index 00e7a52414f..3e1463ab23b 100644 --- a/test/ModularPipelines.UnitTests/Execution/RetryTests.cs +++ b/test/ModularPipelines.UnitTests/Execution/RetryTests.cs @@ -1,3 +1,4 @@ +using Kevlar; using Microsoft.Extensions.DependencyInjection; using ModularPipelines.Configuration; using ModularPipelines.Context; @@ -7,8 +8,6 @@ using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.TestHelpers; -using Polly; -using Polly.Retry; namespace ModularPipelines.UnitTests.Execution; @@ -118,9 +117,9 @@ private class FailedModuleWithCustomRetryPolicy : Module protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryPolicy(Policy - .Handle() - .WaitAndRetryAsync(DefaultRetryCount, _ => TimeSpan.Zero)) + .WithRetryShield(Shield + .When() + .Retry(DefaultRetryCount, Backoff.None)) .Build(); protected internal override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) @@ -144,9 +143,9 @@ private class FailedModuleWithTimeout : Module protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .WithTimeout(TimeSpan.FromMilliseconds(ModuleTimeoutMs)) .Advanced - .WithRetryPolicy(Policy - .Handle() - .WaitAndRetryAsync(DefaultRetryCount, _ => TimeSpan.FromMilliseconds(RetryDelayMs))) + .WithRetryShield(Shield + .When() + .Retry(DefaultRetryCount, Backoff.Constant(TimeSpan.FromMilliseconds(RetryDelayMs)))) .Build(); protected internal override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) diff --git a/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs b/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs index 5bfe62074c3..d2b6ea141c5 100644 --- a/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs +++ b/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs @@ -1,3 +1,4 @@ +using Kevlar; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ModularPipelines.Configuration; @@ -8,8 +9,6 @@ using EnumerableAsyncProcessor.Extensions; using ModularPipelines.Exceptions; using ModularPipelines.TestHelpers; -using Polly; -using Polly.Retry; namespace ModularPipelines.UnitTests.Execution; @@ -171,7 +170,7 @@ private class SucceedingSubModulesDoNotRetryModule : Module protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryPolicy(Policy.Handle().RetryAsync(3)) + .WithRetryShield(Shield.When().Retry(3, Backoff.None)) .Build(); protected internal override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) @@ -209,7 +208,7 @@ private class SucceedingSubModulesDoNotRetryModule_WithReturnType : Module ModuleConfiguration.Create() .Advanced - .WithRetryPolicy(Policy.Handle().RetryAsync(3)) + .WithRetryShield(Shield.When().Retry(3, Backoff.None)) .Build(); protected internal override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) @@ -401,8 +400,8 @@ public async Task Module_With_Retry_Policy_Retries_Entire_Execution() var module = host.Services.GetServices().OfType().First(); - // Polly retries the entire module execution, so all counters increment on each retry - // With RetryAsync(3), we get 1 original + 3 retries = 4 total executions + // Kevlar retries the entire module execution, so all counters increment on each retry. + // With Retry(3), we get 1 original + 3 retries = 4 total executions. using (Assert.Multiple()) { await Assert.That(module._oneCount).IsEqualTo(4); @@ -423,8 +422,8 @@ public async Task Module_With_Retry_Policy_Retries_Entire_Execution_With_Return_ var module = host.Services.GetServices().OfType().First(); - // Polly retries the entire module execution, so all counters increment on each retry - // With RetryAsync(3), we get 1 original + 3 retries = 4 total executions + // Kevlar retries the entire module execution, so all counters increment on each retry. + // With Retry(3), we get 1 original + 3 retries = 4 total executions. using (Assert.Multiple()) { await Assert.That(module._oneCount).IsEqualTo(4); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/ModularPipelines.OptionsGenerator.csproj b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/ModularPipelines.OptionsGenerator.csproj index 4bfdc24b084..f343dd3935e 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/ModularPipelines.OptionsGenerator.csproj +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/ModularPipelines.OptionsGenerator.csproj @@ -22,7 +22,7 @@ - + diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/TypeDetection/ResilientCliCommandExecutor.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/TypeDetection/ResilientCliCommandExecutor.cs index 46a06918f5c..f270e328417 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/TypeDetection/ResilientCliCommandExecutor.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/TypeDetection/ResilientCliCommandExecutor.cs @@ -1,13 +1,11 @@ +using Kevlar; using Microsoft.Extensions.Logging; -using Polly; -using Polly.CircuitBreaker; -using Polly.Retry; namespace ModularPipelines.OptionsGenerator.TypeDetection; /// /// Decorator that adds resilience patterns (retry and circuit breaker) to CLI command execution. -/// Uses Polly for: +/// Uses Kevlar for: /// - Exponential backoff retry (3 attempts) for transient failures /// - Circuit breaker (5 failures opens circuit for 30 seconds) to prevent cascading failures /// @@ -15,7 +13,7 @@ public sealed class ResilientCliCommandExecutor : ICliCommandExecutor { private readonly ICliCommandExecutor _inner; private readonly ILogger _logger; - private readonly ResiliencePipeline _pipeline; + private readonly Shield _shield; /// /// Default retry count for transient failures. @@ -58,51 +56,45 @@ public ResilientCliCommandExecutor( _inner = inner; _logger = logger; - _pipeline = new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions + _shield = Shield.For() + .WhenResult(IsTransientFailure) + .Retry(options => { - MaxRetryAttempts = maxRetries, - BackoffType = DelayBackoffType.Exponential, - Delay = baseDelay, - ShouldHandle = new PredicateBuilder() - .HandleResult(r => IsTransientFailure(r)), - OnRetry = args => + options.MaxRetries = maxRetries; + options.Backoff = Backoff.Exponential(baseDelay, jitter: false); + options.OnRetry = retryEvent => { _logger.LogWarning( "CLI command failed (attempt {Attempt}/{MaxAttempts}), retrying in {Delay}ms...", - args.AttemptNumber, + retryEvent.Attempt, maxRetries, - args.RetryDelay.TotalMilliseconds); - return default; - } + retryEvent.Delay.TotalMilliseconds); + }; }) - .AddCircuitBreaker(new CircuitBreakerStrategyOptions + .CircuitBreaker(options => { - FailureRatio = 0.5, - MinimumThroughput = circuitBreakerThreshold, - SamplingDuration = TimeSpan.FromSeconds(30), - BreakDuration = circuitBreakerDuration, - ShouldHandle = new PredicateBuilder() - .HandleResult(r => IsTransientFailure(r)), - OnOpened = args => + options.FailureRatio = 0.5; + options.MinimumThroughput = circuitBreakerThreshold; + options.SamplingWindow = TimeSpan.FromSeconds(30); + options.BreakDuration = circuitBreakerDuration; + options.OnStateChanged = stateChangedEvent => { - _logger.LogError( - "Circuit breaker OPENED - CLI commands are failing. Will retry after {Duration}s", - args.BreakDuration.TotalSeconds); - return default; - }, - OnClosed = _ => - { - _logger.LogInformation("Circuit breaker CLOSED - CLI commands are healthy again"); - return default; - }, - OnHalfOpened = _ => - { - _logger.LogInformation("Circuit breaker HALF-OPEN - Testing if CLI commands are healthy..."); - return default; - } - }) - .Build(); + switch (stateChangedEvent.To) + { + case CircuitState.Open: + _logger.LogError( + "Circuit breaker OPENED - CLI commands are failing. Will retry after {Duration}s", + circuitBreakerDuration.TotalSeconds); + break; + case CircuitState.Closed: + _logger.LogInformation("Circuit breaker CLOSED - CLI commands are healthy again"); + break; + case CircuitState.HalfOpen: + _logger.LogInformation("Circuit breaker HALF-OPEN - Testing if CLI commands are healthy..."); + break; + } + }; + }); } public async Task ExecuteAsync( @@ -113,11 +105,11 @@ public async Task ExecuteAsync( { try { - return await _pipeline.ExecuteAsync( + return await _shield.ExecuteAsync( async token => await _inner.ExecuteAsync(command, arguments, token, workingDirectory), cancellationToken); } - catch (BrokenCircuitException ex) + catch (CircuitOpenException ex) { _logger.LogError("Circuit breaker is open - CLI execution rejected: {Command} {Arguments}", command, arguments); return new CliCommandResult From 7cc28eb2a96605b25962f928fa0f10dd78ec658a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:36:14 +0100 Subject: [PATCH 2/2] refactor!: generalize shield configuration BREAKING CHANGE: Advanced.WithRetryShield is renamed to Advanced.WithShield because Kevlar shields can compose resilience strategies beyond retries. --- README.md | 2 +- README_Template.md | 2 +- .../module-execution-lifecycle.md | 2 +- docs/docs/how-to/defining-modules.md | 2 +- docs/docs/how-to/hooks.md | 2 +- docs/docs/how-to/retry-policy.md | 18 +++++++------- docs/docs/migrating-to-v3.md | 4 ++-- .../Modules/UnitTests/RunUnitTestModule.cs | 2 +- .../AdvancedModuleConfigurationBuilder.cs | 8 +++---- .../Configuration/ModuleConfiguration.cs | 4 ++-- .../ModuleConfigurationAttributeAdapter.cs | 2 +- .../ModuleConfigurationBuilder.cs | 10 ++++---- ...Factory.cs => ModuleRetryShieldFactory.cs} | 2 +- .../Engine/ModuleExecutionPipeline.cs | 24 +++++++++---------- .../Options/PipelineOptions.cs | 2 +- .../Configuration/ModuleConfigurationTests.cs | 22 ++++++++--------- .../Execution/RetryTests.cs | 4 ++-- .../Execution/SubModuleTests.cs | 4 ++-- 18 files changed, 58 insertions(+), 58 deletions(-) rename src/ModularPipelines/Configuration/{ModuleRetryPolicyFactory.cs => ModuleRetryShieldFactory.cs} (96%) diff --git a/README.md b/README.md index 8cc2ec0397f..e9826e29144 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ ModularPipelines takes a different approach: each unit of work is a self-contain * **Secret obfuscation** - Automatic masking in logs * **Hooks** - Run code before/after any module * **Skip conditions** - Dynamically skip modules based on custom logic -* **Retry policies** - Configurable retry with Kevlar integration +* **Resilience shields** - Configurable retries and advanced Kevlar integration * **Requirements validation** - Check prerequisites before running * **Progress reporting** - Real-time console output with parallel execution visualization * **Source controlled** - Your pipeline is code, version it like code diff --git a/README_Template.md b/README_Template.md index c7b5dcb634e..16f6662077e 100644 --- a/README_Template.md +++ b/README_Template.md @@ -186,7 +186,7 @@ ModularPipelines takes a different approach: each unit of work is a self-contain * **Secret obfuscation** - Automatic masking in logs * **Hooks** - Run code before/after any module * **Skip conditions** - Dynamically skip modules based on custom logic -* **Retry policies** - Configurable retry with Kevlar integration +* **Resilience shields** - Configurable retries and advanced Kevlar integration * **Requirements validation** - Check prerequisites before running * **Progress reporting** - Real-time console output with parallel execution visualization * **Source controlled** - Your pipeline is code, version it like code diff --git a/docs/docs/architecture/module-execution-lifecycle.md b/docs/docs/architecture/module-execution-lifecycle.md index da4c78a7423..8a1ed09862b 100644 --- a/docs/docs/architecture/module-execution-lifecycle.md +++ b/docs/docs/architecture/module-execution-lifecycle.md @@ -24,7 +24,7 @@ For a module that runs successfully, the phases are: 11. Attribute `IModuleEndHandler` handlers run sequentially by priority. 12. The module result is published and dependants become eligible. -`OnBeforeExecuteAsync` and `OnAfterExecuteAsync` wrap the complete retry policy, not each +`OnBeforeExecuteAsync` and `OnAfterExecuteAsync` wrap the complete resilience shield, not each individual attempt. ## Skipped modules diff --git a/docs/docs/how-to/defining-modules.md b/docs/docs/how-to/defining-modules.md index d50707df0e3..2dad6fbe284 100644 --- a/docs/docs/how-to/defining-modules.md +++ b/docs/docs/how-to/defining-modules.md @@ -92,7 +92,7 @@ public class MyModule : Module |--------|-------------| | `.WithTimeout(TimeSpan)` | Maximum execution time before module is cancelled | | `.WithRetry(int, TimeSpan?, Func?)` | Retry attempts, jittered base delay, and optional exception filter | -| `.Advanced.WithRetryShield(Shield)` | Custom Kevlar shield for advanced scenarios | +| `.Advanced.WithShield(Shield)` | Custom Kevlar resilience shield for advanced scenarios | | `.WithSkipWhen(...)` | Condition to skip the module | | `.WithIgnoreFailures()` | Don't fail the pipeline if this module fails | | `.WithIgnoreFailuresWhen(...)` | Conditionally ignore failures | diff --git a/docs/docs/how-to/hooks.md b/docs/docs/how-to/hooks.md index ef28da992b7..3b89f7c6d4c 100644 --- a/docs/docs/how-to/hooks.md +++ b/docs/docs/how-to/hooks.md @@ -139,7 +139,7 @@ The order for a successful module is: 3. Global `OnModuleStartAsync` 4. Attribute `IModuleStartHandler` 5. Module `OnBeforeExecuteAsync` -6. Module `ExecuteAsync` with its retry policy +6. Module `ExecuteAsync` through its configured resilience shield 7. Module `OnAfterExecuteAsync` 8. Global `OnModuleEndAsync` 9. Attribute `IModuleEndHandler` diff --git a/docs/docs/how-to/retry-policy.md b/docs/docs/how-to/retry-policy.md index bb893e3b171..5785a383edd 100644 --- a/docs/docs/how-to/retry-policy.md +++ b/docs/docs/how-to/retry-policy.md @@ -1,9 +1,9 @@ --- -title: Retry Policies +title: Retries and Resilience Shields sidebar_position: 6 --- -# Retry Policies +# Retries and Resilience Shields When creating modules, you can configure retries per module using the `Configure()` method. The standard API supports exponential backoff, jitter, and exception filtering without exposing @@ -42,7 +42,7 @@ protected override ModuleConfiguration Configure() => ModuleConfiguration.Create .Build(); ``` -### Advanced Kevlar Shield +### Advanced Resilience Shield For resilience features outside the standard API, use a Kevlar `Shield` through the explicit `.Advanced` surface: @@ -51,7 +51,7 @@ public class MyModule : Module { protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryShield( + .WithShield( Shield.When() .Retry(5, Backoff.Custom(i => TimeSpan.FromSeconds(i * i)))) .Build(); @@ -63,7 +63,7 @@ public class MyModule : Module } ``` -### Context-Aware Retry Shield +### Context-Aware Resilience Shield If you need access to the pipeline context when building your shield: @@ -72,7 +72,7 @@ public class MyModule : Module { protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryShield(ctx => + .WithShield(ctx => { var retryCount = ctx.Environment.IsCI ? 5 : 2; return Shield.When() @@ -84,7 +84,7 @@ public class MyModule : Module ## Combining with Other Behaviors -Retry policies can be combined with other module behaviors: +Retry configuration can be combined with other module behaviors: ```csharp public class ResilientModule : Module @@ -97,9 +97,9 @@ public class ResilientModule : Module } ``` -## Default Retry Policy +## Default Retry Configuration -Retry policies are off by default. You can set a default retry count on the `PipelineOptions`: +Retries are off by default. You can set a default retry count on the `PipelineOptions`: ```csharp var builder = Pipeline.CreateBuilder(args); diff --git a/docs/docs/migrating-to-v3.md b/docs/docs/migrating-to-v3.md index 2b9a06e0f76..f73d448bb7c 100644 --- a/docs/docs/migrating-to-v3.md +++ b/docs/docs/migrating-to-v3.md @@ -240,7 +240,7 @@ public class MyModule : Module | V2 Override | V3 Configure() Method | |-------------|----------------------| | `TimeSpan Timeout` property | `.WithTimeout(TimeSpan)` | -| `AsyncRetryPolicy RetryPolicy` property | `.WithRetry(int, ...)` or `.Advanced.WithRetryShield(Shield)` | +| `AsyncRetryPolicy RetryPolicy` property | `.WithRetry(int, ...)` or `.Advanced.WithShield(Shield)` | | `Task ShouldSkip()` method | `.WithSkipWhen(...)` | | `Task ShouldIgnoreFailures()` method | `.WithIgnoreFailures()` or `.WithIgnoreFailuresWhen(...)` | | `ModuleRunType.AlwaysRun` | `.WithAlwaysRun()` | @@ -867,7 +867,7 @@ The following have been removed in V3: | `ShouldIgnoreFailures()` method | `Configure().WithIgnoreFailures()` | | `ModuleRunType` property | `Configure().WithAlwaysRun()` | | `Timeout` property | `Configure().WithTimeout()` | -| `RetryPolicy` property | `Configure().WithRetry()` or `.Advanced.WithRetryShield()` | +| `RetryPolicy` property | `Configure().WithRetry()` or `.Advanced.WithShield()` | | `GetModule()` on module | `context.GetModule()` | ## New Features in V3 diff --git a/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs b/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs index ef57e2987c3..d785325c2bf 100644 --- a/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs +++ b/src/ModularPipelines.Build/Modules/UnitTests/RunUnitTestModule.cs @@ -40,7 +40,7 @@ public abstract partial class RunUnitTestModule(IOptions pipel protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .WithSkipWhen(GetSkipDecision) .Advanced - .WithRetryShield(Shield.Retry(0)) + .WithShield(Shield.Retry(0)) .Build(); protected virtual SkipDecision GetSkipDecision(IModuleContext context) => SkipDecision.DoNotSkip; diff --git a/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs b/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs index 8f6edc70b10..114324205ad 100644 --- a/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs +++ b/src/ModularPipelines/Configuration/AdvancedModuleConfigurationBuilder.cs @@ -20,10 +20,10 @@ internal AdvancedModuleConfigurationBuilder(ModuleConfigurationBuilder builder) /// /// The Kevlar shield to execute around the module. /// The parent module configuration builder. - public ModuleConfigurationBuilder WithRetryShield(Shield shield) + public ModuleConfigurationBuilder WithShield(Shield shield) { ArgumentNullException.ThrowIfNull(shield); - return _builder.SetAdvancedRetryShield(_ => shield); + return _builder.SetResilienceShield(_ => shield); } /// @@ -31,9 +31,9 @@ public ModuleConfigurationBuilder WithRetryShield(Shield shield) /// /// The shield factory. /// The parent module configuration builder. - public ModuleConfigurationBuilder WithRetryShield(Func factory) + public ModuleConfigurationBuilder WithShield(Func factory) { ArgumentNullException.ThrowIfNull(factory); - return _builder.SetAdvancedRetryShield(factory); + return _builder.SetResilienceShield(factory); } } diff --git a/src/ModularPipelines/Configuration/ModuleConfiguration.cs b/src/ModularPipelines/Configuration/ModuleConfiguration.cs index d985c6021a5..f74183aeeb3 100644 --- a/src/ModularPipelines/Configuration/ModuleConfiguration.cs +++ b/src/ModularPipelines/Configuration/ModuleConfiguration.cs @@ -158,7 +158,7 @@ public sealed class ModuleConfiguration internal ModuleRetryConfiguration? RetryConfiguration { get; init; } /// - /// Gets the advanced Kevlar shield factory for module execution. + /// Gets the resilience shield factory for module execution. /// - internal Func? AdvancedRetryShieldFactory { get; init; } + internal Func? ResilienceShieldFactory { get; init; } } diff --git a/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs b/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs index 705770cd3c6..00caa267c51 100644 --- a/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs +++ b/src/ModularPipelines/Configuration/ModuleConfigurationAttributeAdapter.cs @@ -39,7 +39,7 @@ public static ModuleConfiguration Apply( PlanningSkipCondition = configured.PlanningSkipCondition, Timeout = configured.Timeout, RetryConfiguration = configured.RetryConfiguration, - AdvancedRetryShieldFactory = configured.AdvancedRetryShieldFactory, + ResilienceShieldFactory = configured.ResilienceShieldFactory, IgnoreFailuresCondition = configured.IgnoreFailuresCondition, AlwaysRun = configured.AlwaysRun, ParallelConstraintKeys = configured.ParallelConstraintKeys diff --git a/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs b/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs index ab61628c943..7d3b8708bbd 100644 --- a/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs +++ b/src/ModularPipelines/Configuration/ModuleConfigurationBuilder.cs @@ -45,7 +45,7 @@ public sealed class ModuleConfigurationBuilder private string? _cacheAssemblyVersionKey; private TimeSpan? _timeout; private ModuleRetryConfiguration? _retryConfiguration; - private Func? _advancedRetryShieldFactory; + private Func? _resilienceShieldFactory; private Func>? _ignoreFailuresCondition; private bool _alwaysRun; private string[]? _parallelConstraintKeys; @@ -364,7 +364,7 @@ public ModuleConfigurationBuilder WithRetry( } _retryConfiguration = new ModuleRetryConfiguration(count, retryBaseDelay, shouldRetry); - _advancedRetryShieldFactory = null; + _resilienceShieldFactory = null; return this; } @@ -432,7 +432,7 @@ public ModuleConfiguration Build() PlanningSkipCondition = ComposePlanningSkipConditions(), Timeout = _timeout, RetryConfiguration = _retryConfiguration, - AdvancedRetryShieldFactory = _advancedRetryShieldFactory, + ResilienceShieldFactory = _resilienceShieldFactory, IgnoreFailuresCondition = _ignoreFailuresCondition, AlwaysRun = _alwaysRun, ParallelConstraintKeys = _parallelConstraintKeys, @@ -447,9 +447,9 @@ public ModuleConfiguration Build() }; } - internal ModuleConfigurationBuilder SetAdvancedRetryShield(Func factory) + internal ModuleConfigurationBuilder SetResilienceShield(Func factory) { - _advancedRetryShieldFactory = factory; + _resilienceShieldFactory = factory; _retryConfiguration = null; return this; } diff --git a/src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs b/src/ModularPipelines/Configuration/ModuleRetryShieldFactory.cs similarity index 96% rename from src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs rename to src/ModularPipelines/Configuration/ModuleRetryShieldFactory.cs index 93564d12b35..659601b3ed7 100644 --- a/src/ModularPipelines/Configuration/ModuleRetryPolicyFactory.cs +++ b/src/ModularPipelines/Configuration/ModuleRetryShieldFactory.cs @@ -2,7 +2,7 @@ namespace ModularPipelines.Configuration; -internal static class ModuleRetryPolicyFactory +internal static class ModuleRetryShieldFactory { internal static Shield Create(ModuleRetryConfiguration configuration) { diff --git a/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs b/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs index c37e31542e3..8997f5e779a 100644 --- a/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs +++ b/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs @@ -486,8 +486,8 @@ private async Task ExecuteWithPolicies( var cancellationToken = executionContext.ModuleCancellationTokenSource.Token; - // Get retry shield if applicable - var retryShield = GetRetryShield(config, moduleContext); + // Get resilience shield if applicable + var resilienceShield = GetResilienceShield(config, moduleContext); var moduleAttemptCount = 0; var moduleAttemptRespondedToCancellation = 0; @@ -507,10 +507,10 @@ async Task ExecuteModuleAttempt(CancellationToken ct) } } - // Create the execution function that optionally includes retry - Func> executeFunc = retryShield != null - ? async ct => await retryShield.ExecuteAsync( - async retryToken => await ExecuteModuleAttempt(retryToken).ConfigureAwait(false), + // Create the execution function that optionally includes resilience strategies + Func> executeFunc = resilienceShield != null + ? async ct => await resilienceShield.ExecuteAsync( + async shieldToken => await ExecuteModuleAttempt(shieldToken).ConfigureAwait(false), ct).ConfigureAwait(false) : ExecuteModuleAttempt; @@ -534,7 +534,7 @@ async Task ExecuteModuleAttempt(CancellationToken ct) if (timeoutResult.TimedOut) { var wasCancellationTokenRespected = timeoutResult.WasCancellationTokenRespected - && (retryShield is null + && (resilienceShield is null || Volatile.Read(ref moduleAttemptRespondedToCancellation) == 1); // Create a detailed timeout exception with information about token cooperation @@ -553,25 +553,25 @@ private TimeSpan GetTimeout(ModuleConfiguration config) return config.Timeout ?? _pipelineOptions.Value.DefaultModuleTimeout; } - private static Shield? GetRetryShield( + private static Shield? GetResilienceShield( ModuleConfiguration config, IModuleContext moduleContext) { - if (config.AdvancedRetryShieldFactory != null) + if (config.ResilienceShieldFactory != null) { - return config.AdvancedRetryShieldFactory(moduleContext); + return config.ResilienceShieldFactory(moduleContext); } if (config.RetryConfiguration != null) { - return ModuleRetryPolicyFactory.Create(config.RetryConfiguration); + return ModuleRetryShieldFactory.Create(config.RetryConfiguration); } // Check if default retry count is configured var defaultRetryCount = moduleContext.Services.Options.DefaultRetryCount; if (defaultRetryCount > 0) { - return ModuleRetryPolicyFactory.Create(new ModuleRetryConfiguration( + return ModuleRetryShieldFactory.Create(new ModuleRetryConfiguration( defaultRetryCount, ModuleRetryConfiguration.DefaultBaseDelay, ShouldRetry: null)); diff --git a/src/ModularPipelines/Options/PipelineOptions.cs b/src/ModularPipelines/Options/PipelineOptions.cs index 063df831bc8..ac730f6a621 100644 --- a/src/ModularPipelines/Options/PipelineOptions.cs +++ b/src/ModularPipelines/Options/PipelineOptions.cs @@ -40,7 +40,7 @@ namespace ModularPipelines.Options; /// /// Module Behaviors: /// Module-level configuration uses . A module with -/// configured will use its custom retry policy instead of +/// configured will use its custom retry configuration instead of /// . Modules without configuration fall back to global settings. /// /// diff --git a/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs b/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs index 6c2ed0bf90d..046d1a6fc01 100644 --- a/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs +++ b/test/ModularPipelines.UnitTests/Configuration/ModuleConfigurationTests.cs @@ -42,7 +42,7 @@ public async Task Default_RetryConfiguration_IsNull() using (Assert.Multiple()) { await Assert.That(config.RetryConfiguration).IsNull(); - await Assert.That(config.AdvancedRetryShieldFactory).IsNull(); + await Assert.That(config.ResilienceShieldFactory).IsNull(); } } @@ -344,7 +344,7 @@ public async Task WithRetry_UsesDefaultBaseDelay() await Assert.That(config.RetryConfiguration!.Count).IsEqualTo(3); await Assert.That(config.RetryConfiguration.BaseDelay).IsEqualTo(TimeSpan.FromMilliseconds(100)); await Assert.That(config.RetryConfiguration.ShouldRetry).IsNull(); - await Assert.That(config.AdvancedRetryShieldFactory).IsNull(); + await Assert.That(config.ResilienceShieldFactory).IsNull(); } } @@ -383,37 +383,37 @@ public async Task WithRetry_RejectsNegativeValues() } [Test] - public async Task Advanced_WithRetryShield_Direct_SetsAdvancedRetryShieldFactory() + public async Task Advanced_WithShield_Direct_SetsResilienceShieldFactory() { var shield = Shield.Retry(0); var config = ModuleConfiguration.Create() .Advanced - .WithRetryShield(shield) + .WithShield(shield) .Build(); - await Assert.That(config.AdvancedRetryShieldFactory).IsNotNull(); + await Assert.That(config.ResilienceShieldFactory).IsNotNull(); var context = Mock.Of(); - var result = config.AdvancedRetryShieldFactory!(context); + var result = config.ResilienceShieldFactory!(context); await Assert.That(result).IsEqualTo(shield); } [Test] - public async Task Advanced_WithRetryShield_Factory_SetsAdvancedRetryShieldFactory() + public async Task Advanced_WithShield_Factory_SetsResilienceShieldFactory() { var shield = Shield.Retry(0); var config = ModuleConfiguration.Create() .Advanced - .WithRetryShield(_ => shield) + .WithShield(_ => shield) .Build(); - await Assert.That(config.AdvancedRetryShieldFactory).IsNotNull(); + await Assert.That(config.ResilienceShieldFactory).IsNotNull(); var context = Mock.Of(); - var result = config.AdvancedRetryShieldFactory!(context); + var result = config.ResilienceShieldFactory!(context); await Assert.That(result).IsEqualTo(shield); } @@ -438,7 +438,7 @@ public async Task StandardConfigurationSurface_DoesNotExposeKevlarTypes() [Arguments(1, 200)] public async Task RetryDelayCalculator_AddsBoundedJitter(double jitterFactor, int expectedMilliseconds) { - var delay = ModuleRetryPolicyFactory.CalculateDelay( + var delay = ModuleRetryShieldFactory.CalculateDelay( retryAttempt: 2, baseDelay: TimeSpan.FromMilliseconds(100), jitterFactor); diff --git a/test/ModularPipelines.UnitTests/Execution/RetryTests.cs b/test/ModularPipelines.UnitTests/Execution/RetryTests.cs index 3e1463ab23b..327caffa354 100644 --- a/test/ModularPipelines.UnitTests/Execution/RetryTests.cs +++ b/test/ModularPipelines.UnitTests/Execution/RetryTests.cs @@ -117,7 +117,7 @@ private class FailedModuleWithCustomRetryPolicy : Module protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryShield(Shield + .WithShield(Shield .When() .Retry(DefaultRetryCount, Backoff.None)) .Build(); @@ -143,7 +143,7 @@ private class FailedModuleWithTimeout : Module protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .WithTimeout(TimeSpan.FromMilliseconds(ModuleTimeoutMs)) .Advanced - .WithRetryShield(Shield + .WithShield(Shield .When() .Retry(DefaultRetryCount, Backoff.Constant(TimeSpan.FromMilliseconds(RetryDelayMs)))) .Build(); diff --git a/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs b/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs index d2b6ea141c5..dd7b2308dfe 100644 --- a/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs +++ b/test/ModularPipelines.UnitTests/Execution/SubModuleTests.cs @@ -170,7 +170,7 @@ private class SucceedingSubModulesDoNotRetryModule : Module protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() .Advanced - .WithRetryShield(Shield.When().Retry(3, Backoff.None)) + .WithShield(Shield.When().Retry(3, Backoff.None)) .Build(); protected internal override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) @@ -208,7 +208,7 @@ private class SucceedingSubModulesDoNotRetryModule_WithReturnType : Module ModuleConfiguration.Create() .Advanced - .WithRetryShield(Shield.When().Retry(3, Backoff.None)) + .WithShield(Shield.When().Retry(3, Backoff.None)) .Build(); protected internal override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)