From 5de1dbac6619b579ad4c6075c89200c907c72a4e Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 4 Sep 2023 17:51:06 +0100 Subject: [PATCH] .Net: Extract Retry Implementation (#2656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation and Context Reduce the responsibility surface of Semantic Kernel regarding Resiliency implementations, keeping only the abstractions that can be delegated and handling by well know resiliency libraries and custom implementations. Extract existing logic from Kernel Core to an extension project that can handle retry logic similar to the prior using Polly Policies. Resolves #2271 Closes #2271 ⚠️ Small breaking changes. ### Description This change removes and deprecates the usage of Retry implementations in our Resiliency namespace with `NullHttpHandlerFactory` implementations. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- dotnet/README.md | 1 + dotnet/SK-dotnet.sln | 18 ++ .../Example08_RetryHandler.cs | 187 +++++++------- .../Example22_OpenApiSkill_AzureKeyVault.cs | 9 +- .../Example42_KernelBuilder.cs | 100 ++++---- .../Example51_StepwisePlanner.cs | 5 +- .../KernelSyntaxExamples.csproj | 2 + .../Reliability/RetryThreeTimesWithBackoff.cs | 11 +- .../RetryThreeTimesWithRetryAfterBackoff.cs | 11 +- .../OpenAIKernelBuilderExtensions.cs | 7 - .../Extensions.UnitTests.csproj | 2 + .../Basic/BasicHttpRetryHandlerTests.cs} | 90 +++---- .../Basic/BasicRetryConfigTests.cs | 67 +++++ .../Polly/PollyHttpRetryHandlerTests.cs | 186 ++++++++++++++ .../BasicHttpRetryHandler.cs | 232 ++++++++++++++++++ .../BasicHttpRetryHandlerFactory.cs | 51 ++++ .../Reliability.Basic/BasicRetryConfig.cs | 75 ++++++ .../Reliability.Basic.csproj | 32 +++ ...ReliabilityBasicKernelBuilderExtensions.cs | 28 +++ .../PollyHttpRetryHandler.cs | 61 +++++ .../PollyHttpRetryHandlerFactory.cs | 55 +++++ .../Reliability.Polly.csproj | 36 +++ ...ReliabilityPollyKernelBuilderExtensions.cs | 40 +++ .../OpenAI/AzureOpenAICompletionTests.cs | 8 +- .../OpenAI/OpenAICompletionTests.cs | 11 +- .../IntegrationTests/IntegrationTests.csproj | 2 + .../Http/HttpHandlerFactory{THandler}.cs | 15 ++ .../IDelegatingHandlerFactory.cs | 2 +- .../Http/NullHttpHandler.cs | 12 + .../Http/NullHttpHandlerFactory.cs | 14 ++ .../KernelConfig.cs | 38 +-- .../Reliability/DefaultHttpRetryHandler.cs | 1 + .../DefaultHttpRetryHandlerFactory.cs | 3 + .../Reliability/HttpRetryConfig.cs | 1 + .../SemanticKernel.MetaPackage.csproj | 1 + .../KernelConfigTests.cs | 23 +- .../Reliability/HttpRetryConfigTests.cs | 77 ------ dotnet/src/SemanticKernel/KernelBuilder.cs | 21 +- .../Reliability/NullHttpRetryHandler.cs | 1 + 39 files changed, 1181 insertions(+), 355 deletions(-) rename dotnet/src/{SemanticKernel.UnitTests/Reliability/DefaultHttpRetryHandlerTests.cs => Extensions/Extensions.UnitTests/Reliability/Basic/BasicHttpRetryHandlerTests.cs} (87%) create mode 100644 dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicRetryConfigTests.cs create mode 100644 dotnet/src/Extensions/Extensions.UnitTests/Reliability/Polly/PollyHttpRetryHandlerTests.cs create mode 100644 dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandler.cs create mode 100644 dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandlerFactory.cs create mode 100644 dotnet/src/Extensions/Reliability.Basic/BasicRetryConfig.cs create mode 100644 dotnet/src/Extensions/Reliability.Basic/Reliability.Basic.csproj create mode 100644 dotnet/src/Extensions/Reliability.Basic/ReliabilityBasicKernelBuilderExtensions.cs create mode 100644 dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandler.cs create mode 100644 dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandlerFactory.cs create mode 100644 dotnet/src/Extensions/Reliability.Polly/Reliability.Polly.csproj create mode 100644 dotnet/src/Extensions/Reliability.Polly/ReliabilityPollyKernelBuilderExtensions.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Http/HttpHandlerFactory{THandler}.cs rename dotnet/src/SemanticKernel.Abstractions/{Reliability => Http}/IDelegatingHandlerFactory.cs (86%) create mode 100644 dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandler.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandlerFactory.cs delete mode 100644 dotnet/src/SemanticKernel.UnitTests/Reliability/HttpRetryConfigTests.cs diff --git a/dotnet/README.md b/dotnet/README.md index 22cbe95bf8f3..57671a65e4c0 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -149,3 +149,4 @@ Other SK packages available at nuget.org: 5. **Microsoft.SemanticKernel.Skills.OpenAPI**: OpenAPI skill. 6. **Microsoft.SemanticKernel.Skills.Web**: Web Skill: search the web, download files, etc. +7. **Microsoft.SemanticKernel.Reliability.Polly**: Extension for http resiliency. diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 67a3ba7a12d1..84cccdc1fba3 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -154,6 +154,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Kusto", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateEngine.PromptTemplateEngine", "src\Extensions\TemplateEngine.PromptTemplateEngine\TemplateEngine.PromptTemplateEngine.csproj", "{10E4B697-D4E8-468D-872D-49670FD150FB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reliability.Polly", "src\Extensions\Reliability.Polly\Reliability.Polly.csproj", "{D4540A0F-98E3-4E70-9093-1948AE5B2AAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reliability.Basic", "src\Extensions\Reliability.Basic\Reliability.Basic.csproj", "{3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -377,6 +381,18 @@ Global {10E4B697-D4E8-468D-872D-49670FD150FB}.Publish|Any CPU.Build.0 = Publish|Any CPU {10E4B697-D4E8-468D-872D-49670FD150FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {10E4B697-D4E8-468D-872D-49670FD150FB}.Release|Any CPU.Build.0 = Release|Any CPU + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD}.Publish|Any CPU.Build.0 = Publish|Any CPU + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD}.Release|Any CPU.Build.0 = Release|Any CPU + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Publish|Any CPU.Build.0 = Publish|Any CPU + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -431,6 +447,8 @@ Global {C754950A-E16C-4F96-9CC7-9328E361B5AF} = {FA3720F1-C99A-49B2-9577-A940257098BF} {E07608CC-D710-4655-BB9E-D22CF3CDD193} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {10E4B697-D4E8-468D-872D-49670FD150FB} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633} + {D4540A0F-98E3-4E70-9093-1948AE5B2AAD} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633} + {3DC4DBD8-20A5-4937-B4F5-BB5E24E7A567} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs b/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs index aa55bddcb667..2e86f64fa862 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs @@ -2,12 +2,15 @@ using System; using System.Net; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Reliability.Basic; using Microsoft.SemanticKernel.Skills.Core; -using Reliability; +using Polly; using RepoUtils; // ReSharper disable once InconsistentNaming @@ -15,71 +18,91 @@ public static class Example08_RetryHandler { public static async Task RunAsync() { - var kernel = InitializeKernel(); - var retryHandlerFactory = new RetryThreeTimesWithBackoffFactory(); - InfoLogger.Logger.LogInformation("============================== RetryThreeTimesWithBackoff =============================="); - await RunRetryPolicyAsync(kernel, retryHandlerFactory); + await DefaultNoRetry(); - InfoLogger.Logger.LogInformation("========================= RetryThreeTimesWithRetryAfterBackoff ========================="); - await RunRetryPolicyBuilderAsync(typeof(RetryThreeTimesWithRetryAfterBackoffFactory)); + await ReliabilityBasicExtension(); - InfoLogger.Logger.LogInformation("==================================== NoRetryPolicy ====================================="); - await RunRetryPolicyBuilderAsync(typeof(NullHttpRetryHandlerFactory)); + await ReliabilityPollyExtension(); - InfoLogger.Logger.LogInformation("=============================== DefaultHttpRetryHandler ================================"); - await RunRetryHandlerConfigAsync(new HttpRetryConfig() { MaxRetryCount = 3, UseExponentialBackoff = true }); - - InfoLogger.Logger.LogInformation("======= DefaultHttpRetryConfig [MaxRetryCount = 3, UseExponentialBackoff = true] ======="); - await RunRetryHandlerConfigAsync(new HttpRetryConfig() { MaxRetryCount = 3, UseExponentialBackoff = true }); + await CustomHandler(); } - private static async Task RunRetryHandlerConfigAsync(HttpRetryConfig? httpConfig = null) + private static async Task DefaultNoRetry() { - var kernelBuilder = Kernel.Builder.WithLoggerFactory(InfoLogger.LoggerFactory); - if (httpConfig != null) - { - kernelBuilder = kernelBuilder.Configure(c => c.SetDefaultHttpRetryConfig(httpConfig)); - } + InfoLogger.Logger.LogInformation("============================== Kernel default behavior: No Retry =============================="); + var kernel = InitializeKernelBuilder() + .Build(); - // Add 401 to the list of retryable status codes - // Typically 401 would not be something we retry but for demonstration - // purposes we are doing so as it's easy to trigger when using an invalid key. - kernelBuilder = kernelBuilder.Configure(c => c.DefaultHttpRetryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized)); + await ImportAndExecuteSkillAsync(kernel); + } - // OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play - kernelBuilder = kernelBuilder.WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY"); + private static async Task ReliabilityBasicExtension() + { + InfoLogger.Logger.LogInformation("============================== Using Reliability.Basic extension =============================="); + var retryConfig = new BasicRetryConfig + { + MaxRetryCount = 3, + UseExponentialBackoff = true, + }; + retryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized); - var kernel = kernelBuilder.Build(); + var kernel = InitializeKernelBuilder() + .WithRetryBasic(retryConfig) + .Build(); await ImportAndExecuteSkillAsync(kernel); } - private static IKernel InitializeKernel() + private static async Task ReliabilityPollyExtension() { - var kernel = Kernel.Builder - .WithLoggerFactory(InfoLogger.LoggerFactory) - // OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play - .WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY") + InfoLogger.Logger.LogInformation("============================== Using Reliability.Polly extension =============================="); + var kernel = InitializeKernelBuilder() + .WithRetryPolly(GetPollyPolicy(InfoLogger.LoggerFactory)) .Build(); - return kernel; + await ImportAndExecuteSkillAsync(kernel); } - private static async Task RunRetryPolicyAsync(IKernel kernel, IDelegatingHandlerFactory retryHandlerFactory) + private static async Task CustomHandler() { - kernel.Config.SetHttpRetryHandlerFactory(retryHandlerFactory); + InfoLogger.Logger.LogInformation("============================== Using a Custom Http Handler =============================="); + var kernel = InitializeKernelBuilder() + .WithHttpHandlerFactory(new MyCustomHandlerFactory()) + .Build(); + await ImportAndExecuteSkillAsync(kernel); } - private static async Task RunRetryPolicyBuilderAsync(Type retryHandlerFactoryType) + private static KernelBuilder InitializeKernelBuilder() { - var kernel = Kernel.Builder.WithLoggerFactory(InfoLogger.LoggerFactory) - .WithRetryHandlerFactory((Activator.CreateInstance(retryHandlerFactoryType) as IDelegatingHandlerFactory)!) - // OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play - .WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY") - .Build(); + return Kernel.Builder + .WithLoggerFactory(InfoLogger.LoggerFactory) + // OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play + .WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY"); + } - await ImportAndExecuteSkillAsync(kernel); + private static AsyncPolicy GetPollyPolicy(ILoggerFactory? logger) + { + // Handle 429 and 401 errors + // Typically 401 would not be something we retry but for demonstration + // purposes we are doing so as it's easy to trigger when using an invalid key. + const int tooManyRequests = 429; + const int unauthorized = 401; + + return Policy + .HandleResult(response => + (int)response.StatusCode is tooManyRequests or unauthorized) + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4), + TimeSpan.FromSeconds(8) + }, + (outcome, timespan, retryCount, _) + => InfoLogger.Logger.LogWarning("Error executing action [attempt {RetryCount} of 3], pausing {PausingMilliseconds}ms. Outcome: {StatusCode}", + retryCount, + timespan.TotalMilliseconds, + outcome.Result.StatusCode)); } private static async Task ImportAndExecuteSkillAsync(IKernel kernel) @@ -101,6 +124,22 @@ private static async Task ImportAndExecuteSkillAsync(IKernel kernel) InfoLogger.Logger.LogInformation("Answer: {0}", answer); } + // Basic custom retry handler factory + public sealed class MyCustomHandlerFactory : HttpHandlerFactory + { + } + + // Basic custom empty retry handler + public sealed class MyCustomHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Your custom http handling implementation + + throw new NotImplementedException(); + } + } + private static class InfoLogger { internal static ILogger Logger => LoggerFactory.CreateLogger("Example08_RetryHandler"); @@ -122,65 +161,3 @@ private static ILoggerFactory LogBuilder() } } } - -/* Output: -info: Example08_RetryHandler[0] - ============================== RetryThreeTimesWithBackoff ============================== -info: Example08_RetryHandler[0] - Question: How popular is Polly library? -warn: Reliability.RetryThreeTimesWithBackoff[0] - Error executing action [attempt 1 of 3], pausing 2000ms. Outcome: Unauthorized -warn: Reliability.RetryThreeTimesWithBackoff[0] - Error executing action [attempt 2 of 3], pausing 4000ms. Outcome: Unauthorized -warn: Reliability.RetryThreeTimesWithBackoff[0] - Error executing action [attempt 3 of 3], pausing 8000ms. Outcome: Unauthorized -info: Example08_RetryHandler[0] - Answer: Error: Access denied: The request is not authorized, HTTP status: 401 -info: Example08_RetryHandler[0] - ========================= RetryThreeTimesWithRetryAfterBackoff ========================= -info: Example08_RetryHandler[0] - Question: How popular is Polly library? -warn: Reliability.RetryThreeTimesWithRetryAfterBackoff[0] - Error executing action [attempt 1 of 3], pausing 2000ms. Outcome: Unauthorized -warn: Reliability.RetryThreeTimesWithRetryAfterBackoff[0] - Error executing action [attempt 2 of 3], pausing 2000ms. Outcome: Unauthorized -warn: Reliability.RetryThreeTimesWithRetryAfterBackoff[0] - Error executing action [attempt 3 of 3], pausing 2000ms. Outcome: Unauthorized -info: Example08_RetryHandler[0] - Answer: Error: Access denied: The request is not authorized, HTTP status: 401 -info: Example08_RetryHandler[0] - ==================================== NoRetryPolicy ===================================== -info: Example08_RetryHandler[0] - Question: How popular is Polly library? -info: Example08_RetryHandler[0] - Answer: Error: Access denied: The request is not authorized, HTTP status: 401 -info: Example08_RetryHandler[0] - =============================== DefaultHttpRetryHandler ================================ -info: Example08_RetryHandler[0] - Question: How popular is Polly library? -warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing action [attempt 1 of 3]. Reason: Unauthorized. Will retry after 2000ms -warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing action [attempt 2 of 3]. Reason: Unauthorized. Will retry after 4000ms -warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing action [attempt 3 of 3]. Reason: Unauthorized. Will retry after 8000ms -fail: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing request, max retry count reached. Reason: Unauthorized -info: Example08_RetryHandler[0] - Answer: Error: Access denied: The request is not authorized, HTTP status: 401 -info: Example08_RetryHandler[0] - ======= DefaultHttpRetryConfig [MaxRetryCount = 3, UseExponentialBackoff = true] ======= -info: Example08_RetryHandler[0] - Question: How popular is Polly library? -warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing action [attempt 1 of 3]. Reason: Unauthorized. Will retry after 2000ms -warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing action [attempt 2 of 3]. Reason: Unauthorized. Will retry after 4000ms -warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing action [attempt 3 of 3]. Reason: Unauthorized. Will retry after 8000ms -fail: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0] - Error executing request, max retry count reached. Reason: Unauthorized -info: Example08_RetryHandler[0] - Answer: Error: Access denied: The request is not authorized, HTTP status: 401 -== DONE == -*/ diff --git a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiSkill_AzureKeyVault.cs b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiSkill_AzureKeyVault.cs index 7094d19fa24d..ddcba7f0c53b 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiSkill_AzureKeyVault.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiSkill_AzureKeyVault.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.Reliability; using Microsoft.SemanticKernel.Skills.OpenAPI.Authentication; using Microsoft.SemanticKernel.Skills.OpenAPI.Extensions; using Microsoft.SemanticKernel.Skills.OpenAPI.Skills; @@ -32,11 +31,13 @@ public static async Task RunAsync() public static async Task GetSecretFromAzureKeyVaultWithRetryAsync(InteractiveMsalAuthenticationProvider authenticationProvider) { - var retryConfig = new HttpRetryConfig() { MaxRetryCount = 3, UseExponentialBackoff = true }; - var kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) - .Configure(c => c.SetDefaultHttpRetryConfig(retryConfig)) + .WithRetryBasic(new() + { + MaxRetryCount = 3, + UseExponentialBackoff = true + }) .Build(); var type = typeof(SkillResourceNames); diff --git a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs index c81e2b71e9c0..f70d2ff521db 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs @@ -17,8 +17,10 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Reliability.Basic; +using Microsoft.SemanticKernel.Reliability.Polly; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.SkillDefinition; using Microsoft.SemanticKernel.TemplateEngine.Prompt; @@ -79,8 +81,7 @@ public static Task RunAsync() var skills = new SkillCollection(); var templateEngine = new PromptTemplateEngine(loggerFactory); var kernelConfig = new KernelConfig(); - - using var httpHandler = new DefaultHttpRetryHandler(new HttpRetryConfig(), loggerFactory); + using var httpHandler = kernelConfig.HttpHandlerFactory.Create(loggerFactory); using var httpClient = new HttpClient(httpHandler); var aiServices = new AIServiceCollection(); ITextCompletion Factory() => new AzureTextCompletion( @@ -139,8 +140,8 @@ public static Task RunAsync() // The default behavior can be configured or a custom retry handler can be injected that will apply to all // AI requests (when using the kernel). - var kernel8 = Kernel.Builder - .Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig + var kernel8 = Kernel.Builder.WithRetryBasic( + new BasicRetryConfig { MaxRetryCount = 3, UseExponentialBackoff = true, @@ -149,60 +150,73 @@ public static Task RunAsync() // MaxTotalRetryTime = TimeSpan.FromSeconds(30), // RetryableStatusCodes = new[] { HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout }, // RetryableExceptions = new[] { typeof(HttpRequestException) } - })) + }) .Build(); - var kernel9 = Kernel.Builder - .Configure(c => c.SetHttpRetryHandlerFactory(new NullHttpRetryHandlerFactory())) - .Build(); + var logger = loggerFactory.CreateLogger(); + var retryThreeTimesPolicy = Policy + .Handle(ex + => ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests) + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4), + TimeSpan.FromSeconds(8) + }, + (ex, timespan, retryCount, _) + => logger?.LogWarning(ex, "Error executing action [attempt {RetryCount} of 3], pausing {PausingMilliseconds}ms", retryCount, timespan.TotalMilliseconds)); + + var kernel9 = Kernel.Builder.WithHttpHandlerFactory(new PollyHttpRetryHandlerFactory(retryThreeTimesPolicy)).Build(); - var kernel10 = Kernel.Builder.WithRetryHandlerFactory(new RetryThreeTimesFactory()).Build(); + var kernel10 = Kernel.Builder.WithHttpHandlerFactory(new PollyRetryThreeTimesFactory()).Build(); + + var kernel11 = Kernel.Builder.WithHttpHandlerFactory(new MyCustomHandlerFactory()).Build(); return Task.CompletedTask; } - // Example of a basic custom retry handler - public class RetryThreeTimesFactory : IDelegatingHandlerFactory + // Example using the PollyHttpRetryHandler from Reliability.Polly extension + public class PollyRetryThreeTimesFactory : HttpHandlerFactory { - public DelegatingHandler Create(ILoggerFactory? loggerFactory) + public override DelegatingHandler Create(ILoggerFactory? loggerFactory = null) { - return new RetryThreeTimes(loggerFactory); - } - } - - public class RetryThreeTimes : DelegatingHandler - { - private readonly AsyncRetryPolicy _policy; + var logger = loggerFactory?.CreateLogger(); - public RetryThreeTimes(ILoggerFactory? loggerFactory = null) - { - this._policy = GetPolicy(loggerFactory is not null ? - loggerFactory.CreateLogger(this.GetType()) : - NullLogger.Instance); + Activator.CreateInstance(typeof(PollyHttpRetryHandler), GetPolicy(logger), logger); + return base.Create(loggerFactory); } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + private static AsyncRetryPolicy GetPolicy(ILogger? logger) { - return await this._policy.ExecuteAsync(async () => - { - var response = await base.SendAsync(request, cancellationToken); - return response; - }); + return Policy + .Handle(ex + => ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests) + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4), + TimeSpan.FromSeconds(8) + }, + (ex, timespan, retryCount, _) + => logger?.LogWarning(ex, "Error executing action [attempt {RetryCount} of 3], pausing {PausingMilliseconds}ms", + retryCount, + timespan.TotalMilliseconds)); } + } - private static AsyncRetryPolicy GetPolicy(ILogger logger) + // Basic custom retry handler factory + public class MyCustomHandlerFactory : HttpHandlerFactory + { + } + + // Basic custom empty retry handler + public class MyCustomHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - return Policy - .Handle(ex => ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests) - .WaitAndRetryAsync(new[] - { - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(4), - TimeSpan.FromSeconds(8) - }, - (ex, timespan, retryCount, _) => logger.LogWarning(ex, - "Error executing action [attempt {0} of 3], pausing {1}ms", - retryCount, timespan.TotalMilliseconds)); + // Your custom handler implementation + + throw new NotImplementedException(); } } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs index f531620026e1..d29a872cd151 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Reliability; using Microsoft.SemanticKernel.Skills.Core; using Microsoft.SemanticKernel.Skills.Web; using Microsoft.SemanticKernel.Skills.Web.Bing; @@ -87,12 +86,12 @@ private static IKernel GetKernel() var kernel = builder .WithLoggerFactory(ConsoleLogger.LoggerFactory) - .Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig + .WithRetryBasic(new() { MaxRetryCount = 3, UseExponentialBackoff = true, MinRetryDelay = TimeSpan.FromSeconds(3), - })) + }) .Build(); return kernel; diff --git a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj index 9dded4df0495..34ced17e8f12 100644 --- a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj +++ b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj @@ -41,7 +41,9 @@ + + diff --git a/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithBackoff.cs b/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithBackoff.cs index c061e74b3af5..56969fa4df8a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithBackoff.cs +++ b/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithBackoff.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Http; using Polly; using Polly.Retry; @@ -38,9 +38,9 @@ protected override async Task SendAsync(HttpRequestMessage { return await this._policy.ExecuteAsync(async () => { - var response = await base.SendAsync(request, cancellationToken); + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); return response; - }); + }).ConfigureAwait(false); } private static AsyncRetryPolicy GetPolicy(ILoggerFactory? logger) @@ -48,9 +48,12 @@ private static AsyncRetryPolicy GetPolicy(ILoggerFactory? l // Handle 429 and 401 errors // Typically 401 would not be something we retry but for demonstration // purposes we are doing so as it's easy to trigger when using an invalid key. + const int tooManyRequests = 429; + const int unauthorized = 401; + return Policy .HandleResult(response => - response.StatusCode is System.Net.HttpStatusCode.TooManyRequests or System.Net.HttpStatusCode.Unauthorized) + (int)response.StatusCode is tooManyRequests or unauthorized) .WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(2), diff --git a/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithRetryAfterBackoff.cs b/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithRetryAfterBackoff.cs index 00244f3aaed6..6baa6d213e1d 100644 --- a/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithRetryAfterBackoff.cs +++ b/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithRetryAfterBackoff.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Http; using Polly; using Polly.Retry; @@ -38,9 +38,9 @@ protected override async Task SendAsync(HttpRequestMessage { return await this._policy.ExecuteAsync(async () => { - var response = await base.SendAsync(request, cancellationToken); + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); return response; - }); + }).ConfigureAwait(false); } private static AsyncRetryPolicy GetPolicy(ILoggerFactory? loggerFactory) @@ -48,9 +48,12 @@ private static AsyncRetryPolicy GetPolicy(ILoggerFactory? l // Handle 429 and 401 errors // Typically 401 would not be something we retry but for demonstration // purposes we are doing so as it's easy to trigger when using an invalid key. + const int tooManyRequests = 429; + const int unauthorized = 401; + return Policy .HandleResult(response => - response.StatusCode is System.Net.HttpStatusCode.TooManyRequests or System.Net.HttpStatusCode.Unauthorized) + (int)response.StatusCode is unauthorized or tooManyRequests) .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: (_, r, _) => diff --git a/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs index 828669796c3d..692726d2a2c2 100644 --- a/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AI.OpenAI/OpenAIKernelBuilderExtensions.cs @@ -16,7 +16,6 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ImageGeneration; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; -using Microsoft.SemanticKernel.Reliability; #pragma warning disable IDE0130 // ReSharper disable once CheckNamespace - Using NS of KernelConfig @@ -553,12 +552,6 @@ private static OpenAIClientOptions CreateOpenAIClientOptions(ILoggerFactory logg options.Transport = new HttpClientTransport(HttpClientProvider.GetHttpClient(config, httpClient, loggerFactory)); #pragma warning restore CA2000 // Dispose objects before losing scope - if (config.HttpHandlerFactory is DefaultHttpRetryHandlerFactory factory && factory.Config is not null) - { - options.Retry.MaxRetries = factory.Config.MaxRetryCount; - options.Retry.MaxDelay = factory.Config.MaxRetryDelay; - } - return options; } } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj b/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj index 174e37f623b6..9bd2508a67b3 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj +++ b/dotnet/src/Extensions/Extensions.UnitTests/Extensions.UnitTests.csproj @@ -31,7 +31,9 @@ + + diff --git a/dotnet/src/SemanticKernel.UnitTests/Reliability/DefaultHttpRetryHandlerTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicHttpRetryHandlerTests.cs similarity index 87% rename from dotnet/src/SemanticKernel.UnitTests/Reliability/DefaultHttpRetryHandlerTests.cs rename to dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicHttpRetryHandlerTests.cs index 8bf395d8ac6b..94751de1b100 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Reliability/DefaultHttpRetryHandlerTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicHttpRetryHandlerTests.cs @@ -8,14 +8,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Reliability.Basic; using Moq; using Moq.Protected; using Xunit; -namespace SemanticKernel.UnitTests.Reliability; +namespace SemanticKernel.Extensions.UnitTests.Reliability.Basic; -public class DefaultHttpRetryHandlerTests +public class BasicHttpRetryHandlerTests { [Theory] [InlineData(HttpStatusCode.RequestTimeout)] @@ -25,7 +25,7 @@ public class DefaultHttpRetryHandlerTests public async Task NoMaxRetryCountCallsOnceForStatusAsync(HttpStatusCode statusCode) { // Arrange - using var retry = new DefaultHttpRetryHandler(new HttpRetryConfig() { MaxRetryCount = 0 }, NullLoggerFactory.Instance); + using var retry = new BasicHttpRetryHandler(new BasicRetryConfig() { MaxRetryCount = 0 }, NullLoggerFactory.Instance); using var mockResponse = new HttpResponseMessage(statusCode); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -93,7 +93,7 @@ public async Task ItRetriesOnceOnRetryableExceptionAsync(Type exceptionType) public async Task NoMaxRetryCountCallsOnceForExceptionAsync(Type exceptionType) { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 0 }); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 0 }); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(exceptionType); @@ -117,7 +117,7 @@ public async Task NoMaxRetryCountCallsOnceForExceptionAsync(Type exceptionType) public async Task ItRetriesOnceOnTransientStatusWithExponentialBackoffAsync(HttpStatusCode statusCode) { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { UseExponentialBackoff = true }); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { UseExponentialBackoff = true }); using var mockResponse = new HttpResponseMessage(statusCode); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -139,7 +139,7 @@ public async Task ItRetriesOnceOnTransientStatusWithExponentialBackoffAsync(Http public async Task ItRetriesOnceOnRetryableExceptionWithExponentialBackoffAsync(Type exceptionType) { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { UseExponentialBackoff = true }); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { UseExponentialBackoff = true }); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(exceptionType); @@ -163,15 +163,15 @@ public async Task ItRetriesExponentiallyWithExponentialBackoffAsync(HttpStatusCo { // Arrange var currentTime = DateTimeOffset.UtcNow; - var mockTimeProvider = new Mock(); - var mockDelayProvider = new Mock(); + var mockTimeProvider = new Mock(); + var mockDelayProvider = new Mock(); mockTimeProvider.SetupSequence(x => x.GetCurrentTime()) .Returns(() => currentTime) .Returns(() => currentTime + TimeSpan.FromMilliseconds(5)) .Returns(() => currentTime + TimeSpan.FromMilliseconds(510)) .Returns(() => currentTime + TimeSpan.FromMilliseconds(1015)) .Returns(() => currentTime + TimeSpan.FromMilliseconds(1520)); - using var retry = ConfigureRetryHandler(new HttpRetryConfig() + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { UseExponentialBackoff = true, MaxRetryCount = 3, MinRetryDelay = TimeSpan.FromMilliseconds(500) @@ -203,7 +203,7 @@ public async Task ItRetriesExponentiallyWithExponentialBackoffAsync(HttpStatusCo public async Task ItRetriesOnceOnTransientStatusCodeWithRetryValueAsync(HttpStatusCode statusCode) { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig(), null); + using var retry = ConfigureRetryHandler(new BasicRetryConfig(), null); using var mockResponse = new HttpResponseMessage() { StatusCode = statusCode, @@ -232,7 +232,7 @@ public async Task ItRetriesOnceOnTransientStatusCodeWithRetryValueAsync(HttpStat public async Task ItRetriesStatusCustomCountAsync(HttpStatusCode expectedStatus) { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null); using var mockResponse = new HttpResponseMessage(expectedStatus); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -254,7 +254,7 @@ public async Task ItRetriesStatusCustomCountAsync(HttpStatusCode expectedStatus) public async Task ItRetriesExceptionsCustomCountAsync(Type expectedException) { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(expectedException); @@ -274,7 +274,7 @@ public async Task ItRetriesExceptionsCustomCountAsync(Type expectedException) public async Task NoExceptionNoRetryAsync() { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null); using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -295,7 +295,7 @@ public async Task NoExceptionNoRetryAsync() public async Task ItDoesNotExecuteOnCancellationTokenAsync() { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null); using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -317,7 +317,7 @@ public async Task ItDoesNotExecuteOnCancellationTokenAsync() public async Task ItDoestExecuteOnFalseCancellationTokenAsync() { // Arrange - using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null); + using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null); using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -338,13 +338,13 @@ public async Task ItDoestExecuteOnFalseCancellationTokenAsync() [Fact] public async Task ItRetriesWithMinRetryDelayAsync() { - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MinRetryDelay = TimeSpan.FromMilliseconds(500) }; - var mockDelayProvider = new Mock(); - var mockTimeProvider = new Mock(); + var mockDelayProvider = new Mock(); + var mockTimeProvider = new Mock(); var currentTime = DateTimeOffset.UtcNow; @@ -356,7 +356,7 @@ public async Task ItRetriesWithMinRetryDelayAsync() mockDelayProvider.Setup(x => x.DelayAsync(It.IsAny(), It.IsAny())) .Returns(() => Task.CompletedTask); - using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider); + using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider); using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests); using var testContent = new StringContent("test"); var mockHandler = GetHttpMessageHandlerMock(mockResponse); @@ -378,14 +378,14 @@ public async Task ItRetriesWithMinRetryDelayAsync() [Fact] public async Task ItRetriesWithMaxRetryDelayAsync() { - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MinRetryDelay = TimeSpan.FromMilliseconds(1), MaxRetryDelay = TimeSpan.FromMilliseconds(500) }; - var mockDelayProvider = new Mock(); - var mockTimeProvider = new Mock(); + var mockDelayProvider = new Mock(); + var mockTimeProvider = new Mock(); var currentTime = DateTimeOffset.UtcNow; @@ -397,7 +397,7 @@ public async Task ItRetriesWithMaxRetryDelayAsync() mockDelayProvider.Setup(x => x.DelayAsync(It.IsAny(), It.IsAny())) .Returns(() => Task.CompletedTask); - using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider); + using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider); using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests) { Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(2000)) } @@ -428,7 +428,7 @@ public async Task ItRetriesWithMaxRetryDelayAsync() public async Task ItRetriesWithMaxTotalDelayAsync(HttpStatusCode statusCode) { // Arrange - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MaxRetryCount = 5, MinRetryDelay = TimeSpan.FromMilliseconds(50), @@ -436,8 +436,8 @@ public async Task ItRetriesWithMaxTotalDelayAsync(HttpStatusCode statusCode) MaxTotalRetryTime = TimeSpan.FromMilliseconds(350) }; - var mockDelayProvider = new Mock(); - var mockTimeProvider = new Mock(); + var mockDelayProvider = new Mock(); + var mockTimeProvider = new Mock(); var currentTime = DateTimeOffset.UtcNow; mockTimeProvider.SetupSequence(x => x.GetCurrentTime()) @@ -450,7 +450,7 @@ public async Task ItRetriesWithMaxTotalDelayAsync(HttpStatusCode statusCode) .Returns(() => currentTime + TimeSpan.FromMilliseconds(275)) .Returns(() => currentTime + TimeSpan.FromMilliseconds(330)); - using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider); + using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider); using var mockResponse = new HttpResponseMessage(statusCode); using var testContent = new StringContent("test"); @@ -474,7 +474,7 @@ public async Task ItRetriesWithMaxTotalDelayAsync(HttpStatusCode statusCode) public async Task ItRetriesFewerWithMaxTotalDelayAsync() { // Arrange - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MaxRetryCount = 5, MinRetryDelay = TimeSpan.FromMilliseconds(50), @@ -482,8 +482,8 @@ public async Task ItRetriesFewerWithMaxTotalDelayAsync() MaxTotalRetryTime = TimeSpan.FromMilliseconds(100) }; - var mockDelayProvider = new Mock(); - var mockTimeProvider = new Mock(); + var mockDelayProvider = new Mock(); + var mockTimeProvider = new Mock(); var currentTime = DateTimeOffset.UtcNow; mockTimeProvider.SetupSequence(x => x.GetCurrentTime()) @@ -496,7 +496,7 @@ public async Task ItRetriesFewerWithMaxTotalDelayAsync() .Returns(() => currentTime + TimeSpan.FromMilliseconds(275)) .Returns(() => currentTime + TimeSpan.FromMilliseconds(330)); - using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider); + using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider); using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests); using var testContent = new StringContent("test"); @@ -520,7 +520,7 @@ public async Task ItRetriesFewerWithMaxTotalDelayAsync() public async Task ItRetriesFewerWithMaxTotalDelayOnExceptionAsync() { // Arrange - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MaxRetryCount = 5, MinRetryDelay = TimeSpan.FromMilliseconds(50), @@ -528,8 +528,8 @@ public async Task ItRetriesFewerWithMaxTotalDelayOnExceptionAsync() MaxTotalRetryTime = TimeSpan.FromMilliseconds(100) }; - var mockDelayProvider = new Mock(); - var mockTimeProvider = new Mock(); + var mockDelayProvider = new Mock(); + var mockTimeProvider = new Mock(); var currentTime = DateTimeOffset.UtcNow; mockTimeProvider.SetupSequence(x => x.GetCurrentTime()) @@ -542,7 +542,7 @@ public async Task ItRetriesFewerWithMaxTotalDelayOnExceptionAsync() .Returns(() => currentTime + TimeSpan.FromMilliseconds(275)) .Returns(() => currentTime + TimeSpan.FromMilliseconds(330)); - using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider); + using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider); var mockHandler = GetHttpMessageHandlerMock(typeof(HttpRequestException)); retry.InnerHandler = mockHandler.Object; @@ -562,7 +562,7 @@ public async Task ItRetriesFewerWithMaxTotalDelayOnExceptionAsync() public async Task ItRetriesOnRetryableStatusCodesAsync() { // Arrange - var config = new HttpRetryConfig() { RetryableStatusCodes = new List { HttpStatusCode.Unauthorized } }; + var config = new BasicRetryConfig() { RetryableStatusCodes = new List { HttpStatusCode.Unauthorized } }; using var retry = ConfigureRetryHandler(config); using var mockResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); @@ -585,7 +585,7 @@ public async Task ItRetriesOnRetryableStatusCodesAsync() public async Task ItDoesNotRetryOnNonRetryableStatusCodesAsync() { // Arrange - var config = new HttpRetryConfig() { RetryableStatusCodes = new List { HttpStatusCode.Unauthorized } }; + var config = new BasicRetryConfig() { RetryableStatusCodes = new List { HttpStatusCode.Unauthorized } }; using var retry = ConfigureRetryHandler(config); using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests); @@ -608,7 +608,7 @@ public async Task ItDoesNotRetryOnNonRetryableStatusCodesAsync() public async Task ItRetriesOnRetryableExceptionsAsync() { // Arrange - var config = new HttpRetryConfig() { RetryableExceptionTypes = new List { typeof(InvalidOperationException) } }; + var config = new BasicRetryConfig() { RetryableExceptionTypes = new List { typeof(InvalidOperationException) } }; using var retry = ConfigureRetryHandler(config); using var testContent = new StringContent("test"); @@ -630,7 +630,7 @@ await Assert.ThrowsAsync(async () => public async Task ItDoesNotRetryOnNonRetryableExceptionsAsync() { // Arrange - var config = new HttpRetryConfig() { RetryableExceptionTypes = new List { typeof(InvalidOperationException) } }; + var config = new BasicRetryConfig() { RetryableExceptionTypes = new List { typeof(InvalidOperationException) } }; using var retry = ConfigureRetryHandler(config); using var testContent = new StringContent("test"); @@ -648,13 +648,13 @@ await Assert.ThrowsAsync(async () => .Verify>("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); } - private static DefaultHttpRetryHandler ConfigureRetryHandler(HttpRetryConfig? config = null, - Mock? timeProvider = null, Mock? delayProvider = null) + private static BasicHttpRetryHandler ConfigureRetryHandler(BasicRetryConfig? config = null, + Mock? timeProvider = null, Mock? delayProvider = null) { - delayProvider ??= new Mock(); - timeProvider ??= new Mock(); + delayProvider ??= new Mock(); + timeProvider ??= new Mock(); - var retry = new DefaultHttpRetryHandler(config ?? new HttpRetryConfig(), null, delayProvider.Object, timeProvider.Object); + var retry = new BasicHttpRetryHandler(config ?? new BasicRetryConfig(), null, delayProvider.Object, timeProvider.Object); return retry; } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicRetryConfigTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicRetryConfigTests.cs new file mode 100644 index 000000000000..8e33970784a5 --- /dev/null +++ b/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Basic/BasicRetryConfigTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Reliability.Basic; +using Xunit; + +namespace SemanticKernel.Extensions.UnitTests.Reliability.Basic; + +/// +/// Unit tests of . +/// +public class BasicRetryConfigTests +{ + [Fact] + public async Task NegativeMaxRetryCountThrowsAsync() + { + // Act + await Assert.ThrowsAsync(() => + { + var BasicRetryConfig = new BasicRetryConfig() { MaxRetryCount = -1 }; + return Task.CompletedTask; + }); + } + + [Fact] + public void SetDefaultBasicRetryConfig() + { + // Arrange + var builder = new KernelBuilder(); + var basicRetryConfig = new BasicRetryConfig() { MaxRetryCount = 1 }; + builder.WithRetryBasic(basicRetryConfig); + + // Act + var config = builder.Build().Config; + + // Assert + Assert.IsType(config.HttpHandlerFactory); + var httpHandlerFactory = config.HttpHandlerFactory as BasicHttpRetryHandlerFactory; + Assert.NotNull(httpHandlerFactory); + Assert.Equal(basicRetryConfig, httpHandlerFactory.Config); + } + + [Fact] + public void SetDefaultBasicRetryConfigToDefaultIfNotSet() + { + // Arrange + var retryConfig = new BasicRetryConfig(); + var builder = new KernelBuilder(); + builder.WithRetryBasic(retryConfig); + + // Act + var config = builder.Build().Config; + + // Assert + Assert.IsType(config.HttpHandlerFactory); + var httpHandlerFactory = config.HttpHandlerFactory as BasicHttpRetryHandlerFactory; + Assert.NotNull(httpHandlerFactory); + Assert.Equal(retryConfig.MaxRetryCount, httpHandlerFactory.Config.MaxRetryCount); + Assert.Equal(retryConfig.MaxRetryDelay, httpHandlerFactory.Config.MaxRetryDelay); + Assert.Equal(retryConfig.MinRetryDelay, httpHandlerFactory.Config.MinRetryDelay); + Assert.Equal(retryConfig.MaxTotalRetryTime, httpHandlerFactory.Config.MaxTotalRetryTime); + Assert.Equal(retryConfig.UseExponentialBackoff, httpHandlerFactory.Config.UseExponentialBackoff); + Assert.Equal(retryConfig.RetryableStatusCodes, httpHandlerFactory.Config.RetryableStatusCodes); + Assert.Equal(retryConfig.RetryableExceptionTypes, httpHandlerFactory.Config.RetryableExceptionTypes); + } +} diff --git a/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Polly/PollyHttpRetryHandlerTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Polly/PollyHttpRetryHandlerTests.cs new file mode 100644 index 000000000000..43c3504e1411 --- /dev/null +++ b/dotnet/src/Extensions/Extensions.UnitTests/Reliability/Polly/PollyHttpRetryHandlerTests.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Reliability.Polly; +using Moq; +using Moq.Protected; +using Polly; +using Polly.Utilities; +using Xunit; + +namespace SemanticKernel.Extensions.UnitTests.Reliability.Polly; + +public sealed class PollyHttpRetryHandlerTests : IDisposable +{ + public PollyHttpRetryHandlerTests() + { + SystemClock.SleepAsync = (_, _) => Task.CompletedTask; + SystemClock.Sleep = (_, _) => { }; + } + + public void Dispose() + { + SystemClock.Reset(); + } + + [Theory] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.TooManyRequests)] + public async Task CustomPolicyNoOpShouldNotAvoidSendRequests(HttpStatusCode statusCode) + { + // Arrange + var asyncPolicy = Policy.NoOpAsync(); + var (mockLoggerFactory, mockLogger) = GetLoggerMocks(); + using var retry = new PollyHttpRetryHandler(asyncPolicy); + using var mockResponse = new HttpResponseMessage(statusCode); + using var testContent = new StringContent("test"); + var mockHandler = GetHttpMessageHandlerMock(mockResponse); + + retry.InnerHandler = mockHandler.Object; + using var httpClient = new HttpClient(retry); + + // Act + var response = await httpClient.PostAsync(new Uri("https://www.microsoft.com"), testContent, CancellationToken.None); + + // Assert + mockHandler.Protected() + .Verify>("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); + Assert.Equal(statusCode, response.StatusCode); + } + + [Theory] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.TooManyRequests)] + public async Task CustomPolicyStatusDontMatchNeverTriggers(HttpStatusCode statusCode) + { + // Arrange + var asyncPolicy = Policy + .HandleResult(result => result.StatusCode != statusCode) + .WaitAndRetryAsync( + retryCount: 1, + sleepDurationProvider: (retryTimes) => TimeSpan.FromMilliseconds(10)); + + var (mockLoggerFactory, mockLogger) = GetLoggerMocks(); + using var retry = new PollyHttpRetryHandler(asyncPolicy); + using var mockResponse = new HttpResponseMessage(statusCode); + using var testContent = new StringContent("test"); + var mockHandler = GetHttpMessageHandlerMock(mockResponse); + + retry.InnerHandler = mockHandler.Object; + using var httpClient = new HttpClient(retry); + + // Act + var response = await httpClient.PostAsync(new Uri("https://www.microsoft.com"), testContent, CancellationToken.None); + + // Assert + mockHandler.Protected() + .Verify>("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); + Assert.Equal(statusCode, response.StatusCode); + } + + [Theory] + [InlineData(HttpStatusCode.RequestTimeout, HttpStatusCode.TooManyRequests)] + [InlineData(HttpStatusCode.ServiceUnavailable, HttpStatusCode.TooManyRequests)] + [InlineData(HttpStatusCode.GatewayTimeout, HttpStatusCode.TooManyRequests)] + [InlineData(HttpStatusCode.TooManyRequests, HttpStatusCode.TooManyRequests)] + public async Task CustomPolicyRetryStatusShouldTriggerRetrials(HttpStatusCode statusCode, HttpStatusCode retryStatusCode) + { + // Arrange + var retryCount = 3; + var asyncPolicy = Policy + .HandleResult(result => result.StatusCode == retryStatusCode) + .WaitAndRetryAsync( + retryCount, + (retryNumber) => TimeSpan.FromMilliseconds(10)); + + var (mockLoggerFactory, mockLogger) = GetLoggerMocks(); + using var retry = new PollyHttpRetryHandler(asyncPolicy); + using var mockResponse = new HttpResponseMessage(statusCode); + using var testContent = new StringContent("test"); + var mockHandler = GetHttpMessageHandlerMock(mockResponse); + + retry.InnerHandler = mockHandler.Object; + using var httpClient = new HttpClient(retry); + + // Act + var response = await httpClient.PostAsync(new Uri("https://www.microsoft.com"), testContent, CancellationToken.None); + + // Assert + var expectedSendAsyncTimes = (statusCode == retryStatusCode) + ? retryCount + 1 + : 1; + + mockHandler.Protected() + .Verify>("SendAsync", Times.Exactly(expectedSendAsyncTimes), ItExpr.IsAny(), ItExpr.IsAny()); + Assert.Equal(statusCode, response.StatusCode); + } + + [Theory] + [InlineData(typeof(ApplicationException), typeof(HttpRequestException))] + [InlineData(typeof(HttpRequestException), typeof(HttpRequestException))] + public async Task CustomPolicyRetryExceptionsShouldTriggerRetrials(Type exceptionType, Type retryExceptionType) + { + // Arrange + var retryCount = 1; + var asyncPolicy = Policy.Handle(exception => exception.GetType() == retryExceptionType) + .WaitAndRetryAsync( + retryCount, + (retryNumber) => TimeSpan.FromMilliseconds(10)); + + var (mockLoggerFactory, mockLogger) = GetLoggerMocks(); + using var testContent = new StringContent("test"); + var mockHandler = GetHttpMessageHandlerMock(exceptionType); + using var retry = new PollyHttpRetryHandler(asyncPolicy); + + retry.InnerHandler = mockHandler.Object; + using var httpClient = new HttpClient(retry); + + // Act + var response = await Assert.ThrowsAsync(exceptionType, + async () => await httpClient.PostAsync(new Uri("https://www.microsoft.com"), testContent, CancellationToken.None)); + + // Assert + var expectedSendAsyncTimes = (exceptionType == retryExceptionType) + ? retryCount + 1 + : 1; + + mockHandler.Protected() + .Verify>("SendAsync", Times.Exactly(expectedSendAsyncTimes), ItExpr.IsAny(), ItExpr.IsAny()); + } + + private static (Mock, Mock) GetLoggerMocks() + { + var mockLoggerFactory = new Mock(); + var mockLogger = new Mock(); + mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(mockLogger.Object); + + return (mockLoggerFactory, mockLogger); + } + + private static Mock GetHttpMessageHandlerMock(HttpResponseMessage mockResponse) + { + var mockHandler = new Mock(); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(mockResponse); + return mockHandler; + } + + private static Mock GetHttpMessageHandlerMock(Type exceptionType) + { + var mockHandler = new Mock(); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(Activator.CreateInstance(exceptionType) as Exception); + return mockHandler; + } +} diff --git a/dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandler.cs b/dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandler.cs new file mode 100644 index 000000000000..23d67e02c748 --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandler.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.SemanticKernel.Reliability.Basic; + +/// +/// Handler that retries HTTP requests based on a . +/// +internal sealed class BasicHttpRetryHandler : DelegatingHandler +{ + /// + /// Initializes a new instance of the class. + /// + /// The retry configuration. + /// The to use for logging. If null, no logging will be performed. + internal BasicHttpRetryHandler(BasicRetryConfig? config = null, ILoggerFactory? loggerFactory = null) + : this(config ?? new(), loggerFactory, null, null) + { + } + + internal BasicHttpRetryHandler( + BasicRetryConfig config, + ILoggerFactory? loggerFactory = null, + IDelayProvider? delayProvider = null, + ITimeProvider? timeProvider = null) + { + this._config = config; + this._logger = loggerFactory is not null ? loggerFactory.CreateLogger() : NullLogger.Instance; + this._delayProvider = delayProvider ?? new TaskDelayProvider(); + this._timeProvider = timeProvider ?? new DefaultTimeProvider(); + } + + /// + /// Executes the action with retry logic + /// + /// + /// The request is retried if it throws an exception that is a retryable exception. + /// If the request throws an exception that is not a retryable exception, it is not retried. + /// If the request returns a response with a retryable error code, it is retried. + /// If the request returns a response with a non-retryable error code, it is not retried. + /// If the exception contains a RetryAfter header, the request is retried after the specified delay. + /// If configured to use exponential backoff, the delay is doubled for each retry. + /// + /// The request. + /// The cancellation token. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + int retryCount = 0; + + var start = this._timeProvider.GetCurrentTime(); + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + TimeSpan waitFor; + string reason; + HttpResponseMessage? response = null; + try + { + response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + + // If the request does not require a retry then we're done + if (!this.ShouldRetry(response.StatusCode)) + { + return response; + } + + reason = response.StatusCode.ToString(); + + // If the retry count is greater than the max retry count then we'll + // just return + if (retryCount >= this._config.MaxRetryCount) + { + this._logger.LogError( + "Error executing request, max retry count reached. Reason: {0}", reason); + return response; + } + + // If the retry delay is longer than the total timeout, then we'll + // just return + if (!this.HasTimeForRetry(start, retryCount, response, out waitFor)) + { + var timeTaken = this._timeProvider.GetCurrentTime() - start; + this._logger.LogError( + "Error executing request, max total retry time reached. Reason: {0}. Time spent: {1}ms", reason, + timeTaken.TotalMilliseconds); + return response; + } + } + catch (Exception e) when (this.ShouldRetry(e) || this.ShouldRetry(e.InnerException)) + { + reason = e.GetType().ToString(); + if (retryCount >= this._config.MaxRetryCount) + { + this._logger.LogError(e, + "Error executing request, max retry count reached. Reason: {0}", reason); + throw; + } + else if (!this.HasTimeForRetry(start, retryCount, response, out waitFor)) + { + var timeTaken = this._timeProvider.GetCurrentTime() - start; + this._logger.LogError( + "Error executing request, max total retry time reached. Reason: {0}. Time spent: {1}ms", reason, + timeTaken.TotalMilliseconds); + throw; + } + } + + // If the request requires a retry then we'll retry + this._logger.LogWarning( + "Error executing action [attempt {0} of {1}]. Reason: {2}. Will retry after {3}ms", + retryCount + 1, + this._config.MaxRetryCount, + reason, + waitFor.TotalMilliseconds); + + // Increase retryCount + retryCount++; + + response?.Dispose(); + + // Delay + await this._delayProvider.DelayAsync(waitFor, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Interface for a delay provider, primarily to enable unit testing. + /// + internal interface IDelayProvider + { + Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken); + } + + internal sealed class TaskDelayProvider : IDelayProvider + { + public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) + { + return Task.Delay(delay, cancellationToken); + } + } + + /// + /// Interface for a time provider, primarily to enable unit testing. + /// + internal interface ITimeProvider + { + DateTimeOffset GetCurrentTime(); + } + + internal sealed class DefaultTimeProvider : ITimeProvider + { + public DateTimeOffset GetCurrentTime() + { + return DateTimeOffset.UtcNow; + } + } + + private readonly BasicRetryConfig _config; + private readonly ILogger _logger; + private readonly IDelayProvider _delayProvider; + private readonly ITimeProvider _timeProvider; + + /// + /// Get the wait time for the next retry. + /// + /// Current retry count + /// The response message that potentially contains RetryAfter header. + private TimeSpan GetWaitTime(int retryCount, HttpResponseMessage? response) + { + // If the response contains a RetryAfter header, use that value + // Otherwise, use the configured min retry delay + var retryAfter = response?.Headers.RetryAfter?.Date.HasValue == true + ? response?.Headers.RetryAfter?.Date - this._timeProvider.GetCurrentTime() + : (response?.Headers.RetryAfter?.Delta) ?? this._config.MinRetryDelay; + retryAfter ??= this._config.MinRetryDelay; + + // If the retry delay is longer than the max retry delay, use the max retry delay + var timeToWait = retryAfter > this._config.MaxRetryDelay + ? this._config.MaxRetryDelay + : retryAfter < this._config.MinRetryDelay + ? this._config.MinRetryDelay + : retryAfter ?? default; + + // If exponential backoff is enabled, and the server didn't provide a RetryAfter header, double the delay for each retry + if (this._config.UseExponentialBackoff + && response?.Headers.RetryAfter?.Date is null + && response?.Headers.RetryAfter?.Delta is null) + { + for (var backoffRetryCount = 1; backoffRetryCount < retryCount + 1; backoffRetryCount++) + { + timeToWait = timeToWait.Add(timeToWait); + } + } + + return timeToWait; + } + + /// + /// Determines if there is time left for a retry. + /// + /// The start time of the original request. + /// The current retry count. + /// The response message that potentially contains RetryAfter header. + /// The wait time for the next retry. + /// True if there is time left for a retry, false otherwise. + private bool HasTimeForRetry(DateTimeOffset start, int retryCount, HttpResponseMessage? response, out TimeSpan waitFor) + { + waitFor = this.GetWaitTime(retryCount, response); + var currentTIme = this._timeProvider.GetCurrentTime(); + var result = currentTIme - start + waitFor; + + return result < this._config.MaxTotalRetryTime; + } + + private bool ShouldRetry(HttpStatusCode statusCode) + { + return this._config.RetryableStatusCodes.Contains(statusCode); + } + + private bool ShouldRetry(Exception? exception) + { + return exception != null && this._config.RetryableExceptionTypes.Contains(exception.GetType()); + } +} diff --git a/dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandlerFactory.cs b/dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandlerFactory.cs new file mode 100644 index 000000000000..fa6615bf70a6 --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Basic/BasicHttpRetryHandlerFactory.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Http; + +namespace Microsoft.SemanticKernel.Reliability.Basic; + +/// +/// Internal factory for creating instances. +/// +internal sealed class BasicHttpRetryHandlerFactory : HttpHandlerFactory +{ + /// + /// Creates a new instance of with the provided configuration. + /// + /// Http retry configuration + internal BasicHttpRetryHandlerFactory(BasicRetryConfig? config = null) + { + this.Config = config ?? new(); + } + + /// + /// Creates a new instance of with the default configuration. + /// + /// Logger factory + /// Returns the created handler + public override DelegatingHandler Create(ILoggerFactory? loggerFactory = null) + { + return new BasicHttpRetryHandler(this.Config, loggerFactory); + } + + /// + /// Creates a new instance of with a specified configuration. + /// + /// Specific configuration + /// Logger factory + /// Returns the created handler + public DelegatingHandler Create(BasicRetryConfig config, ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(config, nameof(config)); + + return new BasicHttpRetryHandler(config, loggerFactory); + } + + /// + /// Default retry configuration used when creating a new instance of . + /// + internal BasicRetryConfig Config { get; } +} diff --git a/dotnet/src/Extensions/Reliability.Basic/BasicRetryConfig.cs b/dotnet/src/Extensions/Reliability.Basic/BasicRetryConfig.cs new file mode 100644 index 000000000000..a20d5a0f9c82 --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Basic/BasicRetryConfig.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Reliability.Basic; + +/// +/// Retry configuration for DefaultKernelRetryHandler that uses RetryAfter header when present. +/// +public sealed record BasicRetryConfig +{ + /// + /// Maximum number of retries. + /// + /// Thrown when value is negative. + public int MaxRetryCount + { + get => this._maxRetryCount; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(this.MaxRetryCount), "Max retry count cannot be negative."); + } + + this._maxRetryCount = value; + } + } + + /// + /// Minimum delay between retries. + /// + public TimeSpan MinRetryDelay { get; set; } = TimeSpan.FromSeconds(2); + + /// + /// Maximum delay between retries. + /// + public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// Maximum total time spent retrying. + /// + public TimeSpan MaxTotalRetryTime { get; set; } = TimeSpan.FromMinutes(2); + + /// + /// Whether to use exponential backoff or not. + /// + public bool UseExponentialBackoff { get; set; } + + /// + /// List of status codes that should be retried. + /// + public List RetryableStatusCodes { get; set; } = new() + { + (HttpStatusCode)HttpStatusCodeType.RequestTimeout, + (HttpStatusCode)HttpStatusCodeType.ServiceUnavailable, + (HttpStatusCode)HttpStatusCodeType.GatewayTimeout, + (HttpStatusCode)HttpStatusCodeType.TooManyRequests, + (HttpStatusCode)HttpStatusCodeType.BadGateway, + }; + + /// + /// List of exception types that should be retried. + /// + public List RetryableExceptionTypes { get; set; } = new() + { + typeof(HttpRequestException) + }; + + private int _maxRetryCount = 1; +} diff --git a/dotnet/src/Extensions/Reliability.Basic/Reliability.Basic.csproj b/dotnet/src/Extensions/Reliability.Basic/Reliability.Basic.csproj new file mode 100644 index 000000000000..be21d040ccd9 --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Basic/Reliability.Basic.csproj @@ -0,0 +1,32 @@ + + + + + Microsoft.SemanticKernel.Reliability.Basic + Microsoft.SemanticKernel.Reliability.Basic + netstandard2.0 + + + + + + + + + + Semantic Kernel - Basic Reliability Extension + Semantic Kernel Basic Reliability Extension + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/Extensions/Reliability.Basic/ReliabilityBasicKernelBuilderExtensions.cs b/dotnet/src/Extensions/Reliability.Basic/ReliabilityBasicKernelBuilderExtensions.cs new file mode 100644 index 000000000000..f5d05ea00b7e --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Basic/ReliabilityBasicKernelBuilderExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Reliability.Basic; + +#pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace - Using NS of KernelConfig +namespace Microsoft.SemanticKernel; +#pragma warning restore IDE0130 + +/// +/// Provides extension methods for the . +/// +public static class ReliabilityBasicKernelBuilderExtensions +{ + /// + /// Sets the default retry configuration for any kernel http request. + /// + /// Target instance + /// Retry configuration + /// Self instance + public static KernelBuilder WithRetryBasic(this KernelBuilder builder, + BasicRetryConfig retryConfig) + { + var httpHandlerFactory = new BasicHttpRetryHandlerFactory(retryConfig); + + return builder.WithHttpHandlerFactory(httpHandlerFactory); + } +} diff --git a/dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandler.cs b/dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandler.cs new file mode 100644 index 000000000000..d36aa22c9533 --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandler.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Diagnostics; +using Polly; + +namespace Microsoft.SemanticKernel.Reliability.Polly; + +/// +/// Customizable PollyHttpHandler that will follow the provided policy. +/// +public class PollyHttpRetryHandler : DelegatingHandler +{ + private readonly AsyncPolicy? _typedAsyncPolicy; + private readonly AsyncPolicy? _asyncPolicy; + + /// + /// Creates a new instance of . + /// + /// HttpResponseMessage typed AsyncPolicy dedicated for typed policies. + public PollyHttpRetryHandler(AsyncPolicy typedAsyncPolicy) + { + Verify.NotNull(typedAsyncPolicy); + + this._typedAsyncPolicy = typedAsyncPolicy; + } + + /// + /// Creates a new instance of dedicated for non-typed policies. + /// + /// A non-typed AsyncPolicy + public PollyHttpRetryHandler(AsyncPolicy asyncPolicy) + { + Verify.NotNull(asyncPolicy); + + this._asyncPolicy = asyncPolicy; + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (this._typedAsyncPolicy is not null) + { + return await this._typedAsyncPolicy.ExecuteAsync(async (cancelToken) => + { + var response = await base.SendAsync(request, cancelToken).ConfigureAwait(false); + return response; + }, cancellationToken).ConfigureAwait(false); + } + + return await this._asyncPolicy!.ExecuteAsync(async (cancelToken) => + { + var response = await base.SendAsync(request, cancelToken).ConfigureAwait(false); + return response; + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandlerFactory.cs b/dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandlerFactory.cs new file mode 100644 index 000000000000..4e77774c164f --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Polly/PollyHttpRetryHandlerFactory.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Http; +using Polly; + +namespace Microsoft.SemanticKernel.Reliability.Polly; + +/// +/// Customizable PollyHttpHandlerFactory that will create handlers with the provided policy. +/// +public class PollyHttpRetryHandlerFactory : HttpHandlerFactory +{ + private readonly AsyncPolicy? _typedAsyncPolicy; + private readonly AsyncPolicy? _asyncPolicy; + + /// + /// Creates a new instance of . + /// + /// HttpResponseMessage typed AsyncPolicy dedicated for typed policies. + public PollyHttpRetryHandlerFactory(AsyncPolicy typedAsyncPolicy) + { + Verify.NotNull(typedAsyncPolicy); + + this._typedAsyncPolicy = typedAsyncPolicy; + } + + /// + /// Creates a new instance of dedicated for non-typed policies. + /// + /// A non-typed AsyncPolicy + public PollyHttpRetryHandlerFactory(AsyncPolicy asyncPolicy) + { + Verify.NotNull(asyncPolicy); + + this._asyncPolicy = asyncPolicy; + } + + /// + /// Creates a new instance of with the default configuration. + /// + /// Logger factory + /// Returns the created handler + public override DelegatingHandler Create(ILoggerFactory? loggerFactory = null) + { + if (this._typedAsyncPolicy is not null) + { + return new PollyHttpRetryHandler(this._typedAsyncPolicy); + } + + return new PollyHttpRetryHandler(this._asyncPolicy!); + } +} diff --git a/dotnet/src/Extensions/Reliability.Polly/Reliability.Polly.csproj b/dotnet/src/Extensions/Reliability.Polly/Reliability.Polly.csproj new file mode 100644 index 000000000000..babd57f37789 --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Polly/Reliability.Polly.csproj @@ -0,0 +1,36 @@ + + + + + Microsoft.SemanticKernel.Reliability.Polly + Microsoft.SemanticKernel.Reliability.Polly + netstandard2.0 + + + + + + + + + + Semantic Kernel - Polly Reliability Extension + Semantic Kernel Polly Reliability Extension + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/Extensions/Reliability.Polly/ReliabilityPollyKernelBuilderExtensions.cs b/dotnet/src/Extensions/Reliability.Polly/ReliabilityPollyKernelBuilderExtensions.cs new file mode 100644 index 000000000000..0236e2e81d8e --- /dev/null +++ b/dotnet/src/Extensions/Reliability.Polly/ReliabilityPollyKernelBuilderExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using Microsoft.SemanticKernel.Reliability.Polly; +using Polly; + +#pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace - Using NS of KernelConfig +namespace Microsoft.SemanticKernel; +#pragma warning restore IDE0130 + +/// +/// Provides extension methods for the . +/// +public static class ReliabilityPollyKernelBuilderExtensions +{ + /// + /// Sets the default retry configuration for any kernel http request. + /// + /// Target instance + /// Provided AsyncPolicy + /// Returns target instance for fluent compatibility + public static KernelBuilder WithRetryPolly(this KernelBuilder kernelConfig, AsyncPolicy retryPolicy) + { + var pollyHandler = new PollyHttpRetryHandlerFactory(retryPolicy); + return kernelConfig.WithHttpHandlerFactory(pollyHandler); + } + + /// + /// Sets the default retry configuration for any kernel http request. + /// + /// Target instance + /// Provided HttpResponseMessage AsyncPolicy + /// Returns target instance for fluent compatibility + public static KernelBuilder WithRetryPolly(this KernelBuilder kernelConfig, AsyncPolicy retryPolicy) + { + var pollyHandler = new PollyHttpRetryHandlerFactory(retryPolicy); + return kernelConfig.WithHttpHandlerFactory(pollyHandler); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/AzureOpenAICompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/AzureOpenAICompletionTests.cs index 13760cfc59eb..c8e10545c7a7 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/AzureOpenAICompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/AzureOpenAICompletionTests.cs @@ -8,7 +8,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Diagnostics; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Reliability.Basic; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; using Xunit.Abstractions; @@ -44,13 +44,13 @@ public async Task AzureOpenAIChatNoHttpRetryPolicyTestShouldThrowAsync(string pr var configuration = this._configuration.GetSection("AzureOpenAI").Get(); Assert.NotNull(configuration); - HttpRetryConfig httpRetryConfig = new() { MaxRetryCount = 0 }; - DefaultHttpRetryHandlerFactory defaultHttpRetryHandlerFactory = new(httpRetryConfig); + var httpRetryConfig = new BasicRetryConfig { MaxRetryCount = 0 }; + BasicHttpRetryHandlerFactory defaultHttpRetryHandlerFactory = new(httpRetryConfig); var target = new KernelBuilder() .WithLoggerFactory(this._logger) .WithAzureChatCompletionService(configuration.ChatDeploymentName!, configuration.Endpoint, configuration.ApiKey) - .WithRetryHandlerFactory(defaultHttpRetryHandlerFactory) + .WithHttpHandlerFactory(defaultHttpRetryHandlerFactory) .Build(); // Act diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs index c1f28a26ea70..f752f7dbaede 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs @@ -9,7 +9,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.Reliability.Basic; using Microsoft.SemanticKernel.SkillDefinition; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; @@ -145,7 +145,7 @@ public async Task AzureOpenAITestAsync(bool useChatModel, string prompt, string public async Task OpenAIHttpRetryPolicyTestAsync(string prompt, string expectedOutput) { // Arrange - var retryConfig = new HttpRetryConfig(); + var retryConfig = new BasicRetryConfig(); retryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized); OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); @@ -153,7 +153,7 @@ public async Task OpenAIHttpRetryPolicyTestAsync(string prompt, string expectedO IKernel target = Kernel.Builder .WithLoggerFactory(this._testOutputHelper) - .Configure(c => c.SetDefaultHttpRetryConfig(retryConfig)) + .WithRetryBasic(retryConfig) .WithOpenAITextCompletionService( serviceId: openAIConfiguration.ServiceId, modelId: openAIConfiguration.ModelId, @@ -176,11 +176,12 @@ public async Task OpenAIHttpRetryPolicyTestAsync(string prompt, string expectedO public async Task AzureOpenAIHttpRetryPolicyTestAsync(string prompt, string expectedOutput) { // Arrange - var retryConfig = new HttpRetryConfig(); + var retryConfig = new BasicRetryConfig(); retryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized); + KernelBuilder builder = Kernel.Builder .WithLoggerFactory(this._testOutputHelper) - .Configure(c => c.SetDefaultHttpRetryConfig(retryConfig)); + .WithRetryBasic(retryConfig); var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); Assert.NotNull(azureOpenAIConfiguration); diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index 0a4fd525da8f..6191702dcebf 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -43,7 +43,9 @@ + + diff --git a/dotnet/src/SemanticKernel.Abstractions/Http/HttpHandlerFactory{THandler}.cs b/dotnet/src/SemanticKernel.Abstractions/Http/HttpHandlerFactory{THandler}.cs new file mode 100644 index 000000000000..b5e17dfba4e4 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Http/HttpHandlerFactory{THandler}.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Http; + +public abstract class HttpHandlerFactory : IDelegatingHandlerFactory where THandler : DelegatingHandler +{ + public virtual DelegatingHandler Create(ILoggerFactory? loggerFactory = null) + { + return (DelegatingHandler)Activator.CreateInstance(typeof(THandler), loggerFactory); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Reliability/IDelegatingHandlerFactory.cs b/dotnet/src/SemanticKernel.Abstractions/Http/IDelegatingHandlerFactory.cs similarity index 86% rename from dotnet/src/SemanticKernel.Abstractions/Reliability/IDelegatingHandlerFactory.cs rename to dotnet/src/SemanticKernel.Abstractions/Http/IDelegatingHandlerFactory.cs index 710b46aaeef8..523251f6b966 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Reliability/IDelegatingHandlerFactory.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Http/IDelegatingHandlerFactory.cs @@ -3,7 +3,7 @@ using System.Net.Http; using Microsoft.Extensions.Logging; -namespace Microsoft.SemanticKernel.Reliability; +namespace Microsoft.SemanticKernel.Http; /// /// Factory for creating instances. diff --git a/dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandler.cs b/dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandler.cs new file mode 100644 index 000000000000..32f71672305e --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; + +namespace Microsoft.SemanticKernel.Http; + +/// +/// A http retry handler that does not retry. +/// +public sealed class NullHttpHandler : DelegatingHandler +{ +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandlerFactory.cs b/dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandlerFactory.cs new file mode 100644 index 000000000000..a86467375f9c --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Http/NullHttpHandlerFactory.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Http; + +internal sealed class NullHttpHandlerFactory : IDelegatingHandlerFactory +{ + public DelegatingHandler Create(ILoggerFactory? loggerFactory) + { + return new NullHttpHandler(); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/KernelConfig.cs b/dotnet/src/SemanticKernel.Abstractions/KernelConfig.cs index 1594fe926113..6cadd2069dfd 100644 --- a/dotnet/src/SemanticKernel.Abstractions/KernelConfig.cs +++ b/dotnet/src/SemanticKernel.Abstractions/KernelConfig.cs @@ -1,48 +1,26 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Reliability; +#pragma warning disable IDE0130 // Namespace does not match folder structure + namespace Microsoft.SemanticKernel; /// /// Semantic kernel configuration. -/// TODO: use .NET ServiceCollection (will require a lot of changes) /// public sealed class KernelConfig { /// - /// Factory for creating HTTP handlers. - /// - public IDelegatingHandlerFactory HttpHandlerFactory { get; private set; } = new DefaultHttpRetryHandlerFactory(new HttpRetryConfig()); - - /// - /// Default HTTP retry configuration for built-in HTTP handler factory. + /// Kernel HTTP handler factory. /// - public HttpRetryConfig DefaultHttpRetryConfig { get; private set; } = new(); - - /// - /// Set the http retry handler factory to use for the kernel. - /// - /// Http retry handler factory to use. - /// The updated kernel configuration. - public KernelConfig SetHttpRetryHandlerFactory(IDelegatingHandlerFactory? httpHandlerFactory = null) - { - if (httpHandlerFactory != null) - { - this.HttpHandlerFactory = httpHandlerFactory; - } - - return this; - } + public IDelegatingHandlerFactory HttpHandlerFactory { get; set; } = new NullHttpHandlerFactory(); + [Obsolete("Usage of Semantic Kernel internal core retry abstractions is deprecated, use a Resiliency extension package")] public KernelConfig SetDefaultHttpRetryConfig(HttpRetryConfig? httpRetryConfig) { - if (httpRetryConfig != null) - { - this.DefaultHttpRetryConfig = httpRetryConfig; - this.SetHttpRetryHandlerFactory(new DefaultHttpRetryHandlerFactory(httpRetryConfig)); - } - - return this; + throw new NotSupportedException("Usage of Semantic Kernel internal core retry abstractions is deprecated, use a Reliability extension package for a similar result"); } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandler.cs b/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandler.cs index cf67d7900510..ab5791d18319 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandler.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandler.cs @@ -10,6 +10,7 @@ namespace Microsoft.SemanticKernel.Reliability; +[Obsolete("Usage of Semantic Kernel internal retry abstractions is deprecated.\nCheck KernelSyntaxExamples.Example42_KernelBuilder.cs for alternatives")] public sealed class DefaultHttpRetryHandler : DelegatingHandler { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandlerFactory.cs b/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandlerFactory.cs index d7358985e74c..4a94b16c5fa6 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandlerFactory.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandlerFactory.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Net.Http; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Http; namespace Microsoft.SemanticKernel.Reliability; +[Obsolete("Usage of Semantic Kernel internal retry abstractions is deprecated.\nCheck KernelSyntaxExamples.Example42_KernelBuilder.cs for alternatives")] public class DefaultHttpRetryHandlerFactory : IDelegatingHandlerFactory { public DefaultHttpRetryHandlerFactory(HttpRetryConfig? config = null) diff --git a/dotnet/src/SemanticKernel.Abstractions/Reliability/HttpRetryConfig.cs b/dotnet/src/SemanticKernel.Abstractions/Reliability/HttpRetryConfig.cs index d2f840d04c1d..e18677abd853 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Reliability/HttpRetryConfig.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Reliability/HttpRetryConfig.cs @@ -11,6 +11,7 @@ namespace Microsoft.SemanticKernel.Reliability; /// /// Retry configuration for IHttpRetryPolicy that uses RetryAfter header when present. /// +[Obsolete("Usage of Semantic Kernel internal retry abstractions is deprecated.\nCheck KernelSyntaxExamples.Example42_KernelBuilder.cs for alternatives")] public sealed class HttpRetryConfig { /// diff --git a/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj b/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj index 9ee4e768c1e0..f115ea1f2bf7 100644 --- a/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj +++ b/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj @@ -16,6 +16,7 @@ Empowers app owners to integrate cutting-edge LLM technology quickly and easily + diff --git a/dotnet/src/SemanticKernel.UnitTests/KernelConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/KernelConfigTests.cs index a0bea300cc2e..d6c1a794e938 100644 --- a/dotnet/src/SemanticKernel.UnitTests/KernelConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/KernelConfigTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Reliability; using Moq; using Xunit; @@ -31,7 +32,7 @@ public void HttpRetryHandlerFactoryIsSet() var config = new KernelConfig(); // Act - config.SetHttpRetryHandlerFactory(retry); + config.HttpHandlerFactory = retry; // Assert Assert.Equal(retry, config.HttpHandlerFactory); @@ -45,33 +46,19 @@ public void HttpRetryHandlerFactoryIsSetWithCustomImplementation() var config = new KernelConfig(); // Act - config.SetHttpRetryHandlerFactory(retry.Object); + config.HttpHandlerFactory = retry.Object; // Assert Assert.Equal(retry.Object, config.HttpHandlerFactory); } [Fact] - public void HttpRetryHandlerFactoryIsSetToDefaultHttpRetryHandlerFactoryIfNull() + public void HttpHandlerFactoryIsSetToNullByDefault() { // Arrange var config = new KernelConfig(); - // Act - config.SetHttpRetryHandlerFactory(null); - - // Assert - Assert.IsType(config.HttpHandlerFactory); - } - - [Fact] - public void HttpRetryHandlerFactoryIsSetToDefaultHttpRetryHandlerFactoryIfNotSet() - { - // Arrange - var config = new KernelConfig(); - - // Act // Assert - Assert.IsType(config.HttpHandlerFactory); + Assert.IsType(config.HttpHandlerFactory); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Reliability/HttpRetryConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/Reliability/HttpRetryConfigTests.cs deleted file mode 100644 index 2501217a46a4..000000000000 --- a/dotnet/src/SemanticKernel.UnitTests/Reliability/HttpRetryConfigTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Reliability; -using Xunit; - -namespace SemanticKernel.UnitTests.Reliability; - -/// -/// Unit tests of . -/// -public class HttpRetryConfigTests -{ - [Fact] - public async Task NegativeMaxRetryCountThrowsAsync() - { - // Act - await Assert.ThrowsAsync(() => - { - var httpRetryConfig = new HttpRetryConfig() { MaxRetryCount = -1 }; - return Task.CompletedTask; - }); - } - - [Fact] - public void SetDefaultHttpRetryConfig() - { - // Arrange - var config = new KernelConfig(); - var httpRetryConfig = new HttpRetryConfig() { MaxRetryCount = 1 }; - - // Act - config.SetDefaultHttpRetryConfig(httpRetryConfig); - - // Assert - Assert.Equal(httpRetryConfig, config.DefaultHttpRetryConfig); - } - - [Fact] - public void SetDefaultHttpRetryConfigToDefaultIfNotSet() - { - // Arrange - var config = new KernelConfig(); - - // Act - // Assert - var defaultConfig = new HttpRetryConfig(); - Assert.Equal(defaultConfig.MaxRetryCount, config.DefaultHttpRetryConfig.MaxRetryCount); - Assert.Equal(defaultConfig.MaxRetryDelay, config.DefaultHttpRetryConfig.MaxRetryDelay); - Assert.Equal(defaultConfig.MinRetryDelay, config.DefaultHttpRetryConfig.MinRetryDelay); - Assert.Equal(defaultConfig.MaxTotalRetryTime, config.DefaultHttpRetryConfig.MaxTotalRetryTime); - Assert.Equal(defaultConfig.UseExponentialBackoff, config.DefaultHttpRetryConfig.UseExponentialBackoff); - Assert.Equal(defaultConfig.RetryableStatusCodes, config.DefaultHttpRetryConfig.RetryableStatusCodes); - Assert.Equal(defaultConfig.RetryableExceptionTypes, config.DefaultHttpRetryConfig.RetryableExceptionTypes); - } - - [Fact] - public void SetDefaultHttpRetryConfigToDefaultIfNull() - { - // Arrange - var config = new KernelConfig(); - - // Act - config.SetDefaultHttpRetryConfig(null); - - // Assert - var defaultConfig = new HttpRetryConfig(); - Assert.Equal(defaultConfig.MaxRetryCount, config.DefaultHttpRetryConfig.MaxRetryCount); - Assert.Equal(defaultConfig.MaxRetryDelay, config.DefaultHttpRetryConfig.MaxRetryDelay); - Assert.Equal(defaultConfig.MinRetryDelay, config.DefaultHttpRetryConfig.MinRetryDelay); - Assert.Equal(defaultConfig.MaxTotalRetryTime, config.DefaultHttpRetryConfig.MaxTotalRetryTime); - Assert.Equal(defaultConfig.UseExponentialBackoff, config.DefaultHttpRetryConfig.UseExponentialBackoff); - Assert.Equal(defaultConfig.RetryableStatusCodes, config.DefaultHttpRetryConfig.RetryableStatusCodes); - Assert.Equal(defaultConfig.RetryableExceptionTypes, config.DefaultHttpRetryConfig.RetryableExceptionTypes); - } -} diff --git a/dotnet/src/SemanticKernel/KernelBuilder.cs b/dotnet/src/SemanticKernel/KernelBuilder.cs index b97e1565de47..10e532bfac24 100644 --- a/dotnet/src/SemanticKernel/KernelBuilder.cs +++ b/dotnet/src/SemanticKernel/KernelBuilder.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.Reliability; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.SkillDefinition; using Microsoft.SemanticKernel.TemplateEngine; @@ -51,7 +51,7 @@ public IKernel Build() { if (this._httpHandlerFactory != null) { - this._config.SetHttpRetryHandlerFactory(this._httpHandlerFactory); + this._config.HttpHandlerFactory = this._httpHandlerFactory; } var instance = new Kernel( @@ -144,16 +144,27 @@ public KernelBuilder WithPromptTemplateEngine(IPromptTemplateEngine promptTempla return this; } + /// + /// Add a http handler factory to the kernel to be built. + /// + /// Http handler factory to add. + /// Updated kernel builder including the http handler factory. + public KernelBuilder WithHttpHandlerFactory(IDelegatingHandlerFactory httpHandlerFactory) + { + Verify.NotNull(httpHandlerFactory); + this._httpHandlerFactory = httpHandlerFactory; + return this; + } + /// /// Add a retry handler factory to the kernel to be built. /// /// Retry handler factory to add. /// Updated kernel builder including the retry handler factory. + [Obsolete("This method is deprecated, use WithHttpHandlerFactory instead")] public KernelBuilder WithRetryHandlerFactory(IDelegatingHandlerFactory httpHandlerFactory) { - Verify.NotNull(httpHandlerFactory); - this._httpHandlerFactory = httpHandlerFactory; - return this; + return this.WithHttpHandlerFactory(httpHandlerFactory); } /// diff --git a/dotnet/src/SemanticKernel/Reliability/NullHttpRetryHandler.cs b/dotnet/src/SemanticKernel/Reliability/NullHttpRetryHandler.cs index 4d855bfa532a..40436e6570f3 100644 --- a/dotnet/src/SemanticKernel/Reliability/NullHttpRetryHandler.cs +++ b/dotnet/src/SemanticKernel/Reliability/NullHttpRetryHandler.cs @@ -2,6 +2,7 @@ using System.Net.Http; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Http; namespace Microsoft.SemanticKernel.Reliability;