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