Skip to content

Commit

Permalink
.Net: Extract Retry Implementation (microsoft#2656)
Browse files Browse the repository at this point in the history
### 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 microsoft#2271
Closes microsoft#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 😄
  • Loading branch information
RogerBarreto authored Sep 4, 2023
1 parent c4ef6ab commit 5de1dba
Show file tree
Hide file tree
Showing 39 changed files with 1,181 additions and 355 deletions.
1 change: 1 addition & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 18 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
187 changes: 82 additions & 105 deletions dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,107 @@

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
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<HttpResponseMessage> 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<HttpResponseMessage>(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)
Expand All @@ -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<MyCustomHandler>
{
}

// Basic custom empty retry handler
public sealed class MyCustomHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> 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");
Expand All @@ -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 ==
*/
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 5de1dba

Please sign in to comment.