diff --git a/docs/resilience-pipeline-registry.md b/docs/resilience-pipeline-registry.md index 06e476142f4..c6be479e9c6 100644 --- a/docs/resilience-pipeline-registry.md +++ b/docs/resilience-pipeline-registry.md @@ -1,18 +1,204 @@ -# Resilience Pipeline Registry +# Resilience pipeline registry > [!NOTE] -> This is documentation for the upcoming Polly v8 release. +> This documentation supports the upcoming Polly v8 release. -The `ResiliencePipelineRegistry` is a generic class that provides the following functionalities: +The `ResiliencePipelineRegistry` is designed to create and cache resilience pipeline instances. The registry also implements the `ResiliencePipelineProvider`, allowing read-only access to pipelines. -- Thread-safe retrieval and dynamic creation of both generic and non-generic resilience pipelines. +The registry offers these features: + +- Thread-safe retrieval and dynamic creation for both generic and non-generic resilience pipelines. - Dynamic reloading of resilience pipelines when configurations change. -- Support for registering both generic and non-generic resilience pipeline builders, enabling dynamic pipeline instance creation. -- Automatic resource management, including disposal of resources tied to resilience pipelines. +- Capability to register both generic and non-generic resilience pipeline builders, enabling dynamic pipeline instance creation. +- Automated resource management, which includes disposing of resources linked to resilience pipelines. > [!NOTE] -> The generic `TKey` parameter specifies the key type used for caching individual resilience pipelines within the registry. In most use-cases, you will be working with `ResiliencePipelineRegistry`. +> The generic `TKey` parameter sets the key type for caching individual resilience pipelines within the registry. Typically, you would use the string-based `ResiliencePipelineRegistry`. ## Usage -🚧 This documentation is being written as part of the Polly v8 release. +To register pipeline builders, use the `TryAddBuilder(...)` method. This method accepts a callback argument that configures an instance of `ResiliencePipelineBuilder` for the pipeline being defined. The registry supports both generic and non-generic resilience pipelines. + +Here's an example demonstrating these features: + + +```cs +var registry = new ResiliencePipelineRegistry(); + +// Register builder for pipeline "A" +registry.TryAddBuilder("A", (builder, context) => +{ + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); +}); + +// Register generic builder for pipeline "A"; you can use the same key +// because generic and non-generic pipelines are stored separately +registry.TryAddBuilder("A", (builder, context) => +{ + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); +}); + +// Fetch pipeline "A" +ResiliencePipeline pipelineA = registry.GetPipeline("A"); + +// Fetch generic pipeline "A" +ResiliencePipeline genericPipelineA = registry.GetPipeline("A"); + +// Returns false since pipeline "unknown" isn't registered +registry.TryGetPipeline("unknown", out var pipeline); + +// Throws KeyNotFoundException because pipeline "unknown" isn't registered +try +{ + registry.GetPipeline("unknown"); +} +catch (KeyNotFoundException) +{ + // Handle the exception +} +``` + + +Additionally, the registry allows you to add pipelines with the `GetOrAddPipeline(...)` method. In this method, there's no need to register builders. Instead, the caller provides a factory method called when the pipeline isn't cached: + + +```cs +var registry = new ResiliencePipelineRegistry(); + +// Dynamically retrieve or create pipeline "A" +ResiliencePipeline pipeline = registry.GetOrAddPipeline("A", (builder, context) => +{ + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); +}); + +// Dynamically retrieve or create generic pipeline "A" +ResiliencePipeline genericPipeline = registry.GetOrAddPipeline("A", (builder, context) => +{ + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); +}); +``` + + +## Registry options + +The constructor for `ResiliencePipelineRegistry` accepts a parameter of type `ResiliencePipelineRegistryOptions`. This parameter lets you configure the behavior of the registry. Here's a breakdown of the available properties: + +| Property | Default Value | Description | +| ----------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- | +| `BuilderFactory` | Function returning a new `ResiliencePipelineBuilder` each time. | Allows consumers to customize builder creation. | +| `PipelineComparer` | `EqualityComparer.Default` | Comparer the registry uses to fetch resilience pipelines. | +| `BuilderComparer` | `EqualityComparer.Default` | Comparer the registry uses to fetch registered pipeline builders. | +| `InstanceNameFormatter` | `null` | Delegate formatting `TKey` to instance name. | +| `BuilderNameFormatter` | Function returning the `key.ToString()` value. | Delegate formatting `TKey` to builder name. | + +> [>NOTE] +> The `BuilderName` and `InstanceName` are used in [telemetry](telemetry.md#metrics). + +Usage example: + + +```cs +var options = new ResiliencePipelineRegistryOptions +{ + BuilderComparer = StringComparer.OrdinalIgnoreCase, + PipelineComparer = StringComparer.OrdinalIgnoreCase, + BuilderFactory = () => new ResiliencePipelineBuilder() + { + InstanceName = "lets change the defaults", + Name = "lets change the defaults", + }, + BuilderNameFormatter = key => $"key:{key}", + InstanceNameFormatter = key => $"instance-key:{key}", +}; + +var registry = new ResiliencePipelineRegistry(); +``` + + +Even though the example might seem unnecessary, given that the defaults for a registry using the `string` type are suitable, it showcases the various properties of the registry and how to set them up. This is particularly helpful when you use [complex registry keys](#complex-registry-keys). + +## Dynamic reloads + +Dynamic reloading lets you refresh cached pipelines when the reload token, represented as a `CancellationToken`, is triggered. To enable dynamic reloads: + + +```cs +var registry = new ResiliencePipelineRegistry(); + +registry.TryAddBuilder("A", (builder, context) => +{ + // Add the reload token. Tokens that are already canceled are ignored. + context.AddReloadToken(cancellationToken); + + // Define the pipeline. + builder.AddRetry(new RetryStrategyOptions()); +}); + +// This instance remains valid even after a reload. +ResiliencePipeline pipeline = registry.GetPipeline("A"); +``` + + +- If an error occurs during reloading, the cached pipeline remains, and dynamic reloading stops. +- You should not reuse the cancellation token when the pipeline is reloaded. +- Pipelines enabled for reloads remain valid and current post-reload. The registry manages this transparently. + +## Resource disposal + +The registry caches and manages all pipelines and resources linked to them. When you dispose of the registry, all pipelines created by it are also disposed of and can't be used anymore. The following example illustrates this: + + +```cs +var registry = new ResiliencePipelineRegistry(); + +// This instance is valid even after reload. +ResiliencePipeline pipeline = registry + .GetOrAddPipeline("A", (builder, context) => builder.AddTimeout(TimeSpan.FromSeconds(10))); + +// Dispose the registry +registry.Dispose(); + +try +{ + pipeline.Execute(() => { }); +} +catch (ObjectDisposedException) +{ + // Using a pipeline that was disposed by the registry +} +``` + + +The registry also allows for the registration of dispose callbacks. These are called when a pipeline is discarded, either because of the registry's disposal or after the pipeline has reloaded. The example below works well with dynamic reloads, letting you dispose of the `CancellationTokenSource` when it's not needed anymore. + + +```cs +var registry = new ResiliencePipelineRegistry(); + +registry.TryAddBuilder("A", (builder, context) => +{ + var cancellation = new CancellationTokenSource(); + + // Register the source for potential external triggering + RegisterCancellationSource(cancellation); + + // Add the reload token; note that an already cancelled token is disregarded + context.AddReloadToken(cancellation.Token); + + // Configure your pipeline + builder.AddRetry(new RetryStrategyOptions()); + + context.OnPipelineDisposed(() => cancellation.Dispose()); +}); +``` + + +Both `AddReloadToken(...)` and `OnPipelineDisposed(...)` are used to implement the `EnableReloads(...)` extension method that is used by the [Dependency Injection layer](dependency-injection.md#dynamic-reloads). + +## Complex registry keys + +Though the pipeline registry supports complex keys, we suggest you use them when defining pipelines with the [Dependency Injection](dependency-injection.md) (DI) containers. For further information, see the [section on complex pipeline keys](dependency-injection.md#complex-pipeline-keys). diff --git a/docs/strategies/hedging.md b/docs/strategies/hedging.md index e91eccf747a..32f348e9c73 100644 --- a/docs/strategies/hedging.md +++ b/docs/strategies/hedging.md @@ -171,7 +171,7 @@ new ResiliencePipelineBuilder() { try { - // A dedicated ActionContext is provided for each hedged action + // A dedicated ActionContext is provided for each hedged action. // It comes with a separate CancellationToken created specifically for this hedged attempt, // which can be cancelled later if needed. // @@ -248,7 +248,7 @@ internal static class ResilienceKeys ``` -In your `ActionGenerator`, you can easily provide your own `HttpRequestMessage` to ActionContext, and the original callback will use it: +In your `ActionGenerator`, you can easily provide your own `HttpRequestMessage` to `ActionContext`, and the original callback will use it: ```cs @@ -268,6 +268,9 @@ new ResiliencePipelineBuilder() // - Providing alternate endpoint URLs request = PrepareRequest(request); + // Override the request message in the action context + args.ActionContext.Properties.Set(ResilienceKeys.RequestMessage, request); + // Then, execute the original callback return () => args.Callback(args.ActionContext); } diff --git a/src/Polly.Core/Registry/ConfigureBuilderContext.cs b/src/Polly.Core/Registry/ConfigureBuilderContext.cs index 431483258ca..1cf19ca6f56 100644 --- a/src/Polly.Core/Registry/ConfigureBuilderContext.cs +++ b/src/Polly.Core/Registry/ConfigureBuilderContext.cs @@ -1,5 +1,3 @@ -using System.ComponentModel; - namespace Polly.Registry; /// @@ -42,7 +40,6 @@ internal ConfigureBuilderContext(TKey strategyKey, string builderName, string? b /// /// You can add multiple reload tokens to the context. Non-cancelable or already canceled tokens are ignored. /// - [EditorBrowsable(EditorBrowsableState.Never)] public void AddReloadToken(CancellationToken cancellationToken) { if (!cancellationToken.CanBeCanceled || cancellationToken.IsCancellationRequested) diff --git a/src/Snippets/Docs/Hedging.cs b/src/Snippets/Docs/Hedging.cs index b57bd303c9c..41f5d885a26 100644 --- a/src/Snippets/Docs/Hedging.cs +++ b/src/Snippets/Docs/Hedging.cs @@ -191,6 +191,9 @@ public static void ParametrizedCallback() // - Providing alternate endpoint URLs request = PrepareRequest(request); + // Override the request message in the action context + args.ActionContext.Properties.Set(ResilienceKeys.RequestMessage, request); + // Then, execute the original callback return () => args.Callback(args.ActionContext); } diff --git a/src/Snippets/Docs/ResiliencePipelineRegistry.cs b/src/Snippets/Docs/ResiliencePipelineRegistry.cs new file mode 100644 index 00000000000..67fc882d73b --- /dev/null +++ b/src/Snippets/Docs/ResiliencePipelineRegistry.cs @@ -0,0 +1,177 @@ +using System.Net.Http; +using System.Threading; +using Polly; +using Polly.Registry; +using Polly.Retry; + +namespace Snippets.Docs; + +internal static class ResiliencePipelineRegistry +{ + public static async Task Usage() + { + #region registry-usage + + var registry = new ResiliencePipelineRegistry(); + + // Register builder for pipeline "A" + registry.TryAddBuilder("A", (builder, context) => + { + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); + }); + + // Register generic builder for pipeline "A"; you can use the same key + // because generic and non-generic pipelines are stored separately + registry.TryAddBuilder("A", (builder, context) => + { + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); + }); + + // Fetch pipeline "A" + ResiliencePipeline pipelineA = registry.GetPipeline("A"); + + // Fetch generic pipeline "A" + ResiliencePipeline genericPipelineA = registry.GetPipeline("A"); + + // Returns false since pipeline "unknown" isn't registered + registry.TryGetPipeline("unknown", out var pipeline); + + // Throws KeyNotFoundException because pipeline "unknown" isn't registered + try + { + registry.GetPipeline("unknown"); + } + catch (KeyNotFoundException) + { + // Handle the exception + } + + #endregion + } + + public static async Task UsageWithoutBuilders() + { + #region registry-usage-no-builder + + var registry = new ResiliencePipelineRegistry(); + + // Dynamically retrieve or create pipeline "A" + ResiliencePipeline pipeline = registry.GetOrAddPipeline("A", (builder, context) => + { + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); + }); + + // Dynamically retrieve or create generic pipeline "A" + ResiliencePipeline genericPipeline = registry.GetOrAddPipeline("A", (builder, context) => + { + // Define your pipeline + builder.AddRetry(new RetryStrategyOptions()); + }); + + #endregion + } + + public static async Task RegistryOptions() + { + #region registry-options + + var options = new ResiliencePipelineRegistryOptions + { + BuilderComparer = StringComparer.OrdinalIgnoreCase, + PipelineComparer = StringComparer.OrdinalIgnoreCase, + BuilderFactory = () => new ResiliencePipelineBuilder + { + InstanceName = "lets change the defaults", + Name = "lets change the defaults", + }, + BuilderNameFormatter = key => $"key:{key}", + InstanceNameFormatter = key => $"instance-key:{key}", + }; + + var registry = new ResiliencePipelineRegistry(); + + #endregion + } + + public static async Task DynamicReloads() + { + var cancellationToken = CancellationToken.None; + + #region registry-reloads + + var registry = new ResiliencePipelineRegistry(); + + registry.TryAddBuilder("A", (builder, context) => + { + // Add the reload token. Tokens that are already canceled are ignored. + context.AddReloadToken(cancellationToken); + + // Define the pipeline. + builder.AddRetry(new RetryStrategyOptions()); + }); + + // This instance remains valid even after a reload. + ResiliencePipeline pipeline = registry.GetPipeline("A"); + + #endregion + } + + public static void RegistryDisposed() + { + #region registry-disposed + + var registry = new ResiliencePipelineRegistry(); + + // This instance is valid even after reload. + ResiliencePipeline pipeline = registry + .GetOrAddPipeline("A", (builder, context) => builder.AddTimeout(TimeSpan.FromSeconds(10))); + + // Dispose the registry + registry.Dispose(); + + try + { + pipeline.Execute(() => { }); + } + catch (ObjectDisposedException) + { + // Using a pipeline that was disposed by the registry + } + + #endregion + } + + public static async Task DynamicReloadsWithDispose() + { + #region registry-reloads-and-dispose + + var registry = new ResiliencePipelineRegistry(); + + registry.TryAddBuilder("A", (builder, context) => + { + var cancellation = new CancellationTokenSource(); + + // Register the source for potential external triggering + RegisterCancellationSource(cancellation); + + // Add the reload token; note that an already cancelled token is disregarded + context.AddReloadToken(cancellation.Token); + + // Configure your pipeline + builder.AddRetry(new RetryStrategyOptions()); + + context.OnPipelineDisposed(() => cancellation.Dispose()); + }); + + #endregion + } + + private static void RegisterCancellationSource(CancellationTokenSource cancellation) + { + // Register the source + } +} + diff --git a/src/Snippets/Snippets.csproj b/src/Snippets/Snippets.csproj index 3ad377471e6..faaa521637c 100644 --- a/src/Snippets/Snippets.csproj +++ b/src/Snippets/Snippets.csproj @@ -8,7 +8,7 @@ Library false false - $(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021;IDE0017;IDE0060;CS1998;CA1064 + $(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021;IDE0017;IDE0060;CS1998;CA1064;S3257 Snippets