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 | 215 ++++++++-------- .../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 | 23 +- .../Reliability/NullHttpRetryHandler.cs | 1 + 39 files changed, 1196 insertions(+), 370 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 22cbe95bf8..57671a65e4 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 67a3ba7a12..84cccdc1fb 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 aa55bddcb6..2e86f64fa8 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 + await ImportAndExecuteSkillAsync(kernel); + } + + 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 = InitializeKernelBuilder() + .WithRetryBasic(retryConfig) + .Build(); + + await ImportAndExecuteSkillAsync(kernel); + } + + private static async Task ReliabilityPollyExtension() + { + InfoLogger.Logger.LogInformation("============================== Using Reliability.Polly extension =============================="); + var kernel = InitializeKernelBuilder() + .WithRetryPolly(GetPollyPolicy(InfoLogger.LoggerFactory)) + .Build(); + + await ImportAndExecuteSkillAsync(kernel); + } + + private static async Task CustomHandler() + { + InfoLogger.Logger.LogInformation("============================== Using a Custom Http Handler =============================="); + var kernel = InitializeKernelBuilder() + .WithHttpHandlerFactory(new MyCustomHandlerFactory()) + .Build(); + + await ImportAndExecuteSkillAsync(kernel); + } + + private static KernelBuilder InitializeKernelBuilder() + { + 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"); + } + + 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. - kernelBuilder = kernelBuilder.Configure(c => c.DefaultHttpRetryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized)); + const int tooManyRequests = 429; + const int unauthorized = 401; - // 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"); - - var kernel = kernelBuilder.Build(); - - await ImportAndExecuteSkillAsync(kernel); - } - - private static IKernel InitializeKernel() - { - 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") - .Build(); - - return kernel; - } - - private static async Task RunRetryPolicyAsync(IKernel kernel, IDelegatingHandlerFactory retryHandlerFactory) - { - kernel.Config.SetHttpRetryHandlerFactory(retryHandlerFactory); - await ImportAndExecuteSkillAsync(kernel); - } - - private static async Task RunRetryPolicyBuilderAsync(Type retryHandlerFactoryType) - { - 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(); - - await ImportAndExecuteSkillAsync(kernel); + 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 @@ public static class Example08_RetryHandler 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 @@ public static class Example08_RetryHandler } } } - -/* 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 7094d19fa2..ddcba7f0c5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiSkill_AzureKeyVault.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiSkill_AzureKeyVault.cs @@ -5,7 +5,6 @@ using System.Text.Json; 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 class Example22_OpenApiSkill_AzureKeyVault 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 c81e2b71e9..f70d2ff521 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs @@ -17,8 +17,10 @@ using Microsoft.SemanticKernel.AI.TextCompletion; 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 class Example42_KernelBuilder 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 class Example42_KernelBuilder // 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 class Example42_KernelBuilder // 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 kernel10 = Kernel.Builder.WithRetryHandlerFactory(new RetryThreeTimesFactory()).Build(); + var kernel9 = Kernel.Builder.WithHttpHandlerFactory(new PollyHttpRetryHandlerFactory(retryThreeTimesPolicy)).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); + var logger = loggerFactory?.CreateLogger(); + + Activator.CreateInstance(typeof(PollyHttpRetryHandler), GetPolicy(logger), logger); + return base.Create(loggerFactory); + } + + private static AsyncRetryPolicy GetPolicy(ILogger? logger) + { + 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)); } } - public class RetryThreeTimes : DelegatingHandler + // Basic custom retry handler factory + public class MyCustomHandlerFactory : HttpHandlerFactory { - private readonly AsyncRetryPolicy _policy; + } - public RetryThreeTimes(ILoggerFactory? loggerFactory = null) + // Basic custom empty retry handler + public class MyCustomHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - this._policy = GetPolicy(loggerFactory is not null ? - loggerFactory.CreateLogger(this.GetType()) : - NullLogger.Instance); - } + // Your custom handler implementation - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return await this._policy.ExecuteAsync(async () => - { - var response = await base.SendAsync(request, cancellationToken); - return response; - }); - } - - private static AsyncRetryPolicy GetPolicy(ILogger logger) - { - 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)); + throw new NotImplementedException(); } } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs index f531620026..d29a872cd1 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example51_StepwisePlanner.cs @@ -5,7 +5,6 @@ using System.Diagnostics; 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 @@ public static class Example51_StepwisePlanner 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 9dded4df04..34ced17e8f 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 c061e74b3a..56969fa4df 100644 --- a/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithBackoff.cs +++ b/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithBackoff.cs @@ -5,7 +5,7 @@ using System.Net.Http; 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 @@ public class RetryThreeTimesWithBackoff : DelegatingHandler { 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 @@ public class RetryThreeTimesWithBackoff : DelegatingHandler // 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 00244f3aae..6baa6d213e 100644 --- a/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithRetryAfterBackoff.cs +++ b/dotnet/samples/KernelSyntaxExamples/Reliability/RetryThreeTimesWithRetryAfterBackoff.cs @@ -5,7 +5,7 @@ using System.Net.Http; 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 @@ public class RetryThreeTimesWithRetryAfterBackoff : DelegatingHandler { 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 @@ public class RetryThreeTimesWithRetryAfterBackoff : DelegatingHandler // 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 828669796c..692726d2a2 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.ChatCompletionWithData; 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 @@ public static class OpenAIKernelBuilderExtensions 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 174e37f623..9bd2508a67 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 8bf395d8ac..94751de1b1 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.Net.Http.Headers; 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests { // 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests [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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests [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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests .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 class DefaultHttpRetryHandlerTests public async Task ItRetriesFewerWithMaxTotalDelayAsync() { // Arrange - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MaxRetryCount = 5, MinRetryDelay = TimeSpan.FromMilliseconds(50), @@ -482,8 +482,8 @@ public class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests .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 class DefaultHttpRetryHandlerTests public async Task ItRetriesFewerWithMaxTotalDelayOnExceptionAsync() { // Arrange - var httpRetryConfig = new HttpRetryConfig + var BasicRetryConfig = new BasicRetryConfig { MaxRetryCount = 5, MinRetryDelay = TimeSpan.FromMilliseconds(50), @@ -528,8 +528,8 @@ public class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests .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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 class DefaultHttpRetryHandlerTests 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 @@ public class DefaultHttpRetryHandlerTests 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 @@ public class DefaultHttpRetryHandlerTests .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 0000000000..8e33970784 --- /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 0000000000..43c3504e14 --- /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 0000000000..23d67e02c7 --- /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 0000000000..fa6615bf70 --- /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 0000000000..a20d5a0f9c --- /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 0000000000..be21d040cc --- /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 0000000000..f5d05ea00b --- /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 0000000000..d36aa22c95 --- /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 0000000000..4e77774c16 --- /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 0000000000..babd57f377 --- /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 0000000000..0236e2e81d --- /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 13760cfc59..c8e10545c7 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/AzureOpenAICompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/AzureOpenAICompletionTests.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Configuration; 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 sealed class AzureOpenAICompletionTests : IDisposable 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 c1f28a26ea..f752f7dbae 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Configuration; 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 sealed class OpenAICompletionTests : IDisposable 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 sealed class OpenAICompletionTests : IDisposable 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 sealed class OpenAICompletionTests : IDisposable 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 0a4fd525da..6191702dce 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 0000000000..b5e17dfba4 --- /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 710b46aaee..523251f6b9 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 0000000000..32f7167230 --- /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 0000000000..a86467375f --- /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 1594fe9261..6cadd2069d 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. + /// Kernel HTTP handler factory. /// - public IDelegatingHandlerFactory HttpHandlerFactory { get; private set; } = new DefaultHttpRetryHandlerFactory(new HttpRetryConfig()); - - /// - /// Default HTTP retry configuration for built-in 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 cf67d79005..ab5791d183 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandler.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Reliability/DefaultHttpRetryHandler.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions; 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 d7358985e7..4a94b16c5f 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 d2f840d04c..e18677abd8 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 9ee4e768c1..f115ea1f2b 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 a0bea300cc..d6c1a794e9 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 class KernelConfigTests var config = new KernelConfig(); // Act - config.SetHttpRetryHandlerFactory(retry); + config.HttpHandlerFactory = retry; // Assert Assert.Equal(retry, config.HttpHandlerFactory); @@ -45,33 +46,19 @@ public class KernelConfigTests 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 2501217a46..0000000000 --- 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 b97e1565de..10e532bfac 100644 --- a/dotnet/src/SemanticKernel/KernelBuilder.cs +++ b/dotnet/src/SemanticKernel/KernelBuilder.cs @@ -8,9 +8,9 @@ using System.Threading.Tasks; 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 sealed class KernelBuilder { if (this._httpHandlerFactory != null) { - this._config.SetHttpRetryHandlerFactory(this._httpHandlerFactory); + this._config.HttpHandlerFactory = this._httpHandlerFactory; } var instance = new Kernel( @@ -145,17 +145,28 @@ public sealed class KernelBuilder } /// - /// Add a retry handler factory to the kernel to be built. + /// Add a http handler factory to the kernel to be built. /// - /// Retry handler factory to add. - /// Updated kernel builder including the retry handler factory. - public KernelBuilder WithRetryHandlerFactory(IDelegatingHandlerFactory httpHandlerFactory) + /// 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) + { + return this.WithHttpHandlerFactory(httpHandlerFactory); + } + /// /// Use the given configuration with the kernel to be built. /// diff --git a/dotnet/src/SemanticKernel/Reliability/NullHttpRetryHandler.cs b/dotnet/src/SemanticKernel/Reliability/NullHttpRetryHandler.cs index 4d855bfa53..40436e6570 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;