.Net: Extract Retry Implementation (#2656)

### Motivation and Context

Reduce the responsibility surface of Semantic Kernel regarding
Resiliency implementations, keeping only the abstractions that can be
delegated and handling by well know resiliency libraries and custom
implementations.

Extract existing logic from Kernel Core to an extension project that can
handle retry logic similar to the prior using Polly Policies.

Resolves #2271
Closes #2271 

⚠️ Small breaking changes.

### Description

This change removes and deprecates the usage of Retry implementations in
our Resiliency namespace with `NullHttpHandlerFactory` implementations.

### Contribution Checklist

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
This commit is contained in:
Roger Barreto
2023-09-04 16:51:06 +00:00
committed by GitHub
parent c4ef6ab227
commit 5de1dbac66
39 changed files with 1196 additions and 370 deletions
+1
View File
@@ -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
View File
@@ -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}
@@ -2,12 +2,15 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Http;
using Microsoft.SemanticKernel.Reliability.Basic;
using Microsoft.SemanticKernel.Skills.Core;
using Reliability;
using Polly;
using RepoUtils;
// ReSharper disable once InconsistentNaming
@@ -15,71 +18,91 @@ public static class Example08_RetryHandler
{
public static async Task RunAsync()
{
var kernel = InitializeKernel();
var retryHandlerFactory = new RetryThreeTimesWithBackoffFactory();
InfoLogger.Logger.LogInformation("============================== RetryThreeTimesWithBackoff ==============================");
await RunRetryPolicyAsync(kernel, retryHandlerFactory);
await DefaultNoRetry();
InfoLogger.Logger.LogInformation("========================= RetryThreeTimesWithRetryAfterBackoff =========================");
await RunRetryPolicyBuilderAsync(typeof(RetryThreeTimesWithRetryAfterBackoffFactory));
await ReliabilityBasicExtension();
InfoLogger.Logger.LogInformation("==================================== NoRetryPolicy =====================================");
await RunRetryPolicyBuilderAsync(typeof(NullHttpRetryHandlerFactory));
await ReliabilityPollyExtension();
InfoLogger.Logger.LogInformation("=============================== DefaultHttpRetryHandler ================================");
await RunRetryHandlerConfigAsync(new HttpRetryConfig() { MaxRetryCount = 3, UseExponentialBackoff = true });
InfoLogger.Logger.LogInformation("======= DefaultHttpRetryConfig [MaxRetryCount = 3, UseExponentialBackoff = true] =======");
await RunRetryHandlerConfigAsync(new HttpRetryConfig() { MaxRetryCount = 3, UseExponentialBackoff = true });
await CustomHandler();
}
private static async Task RunRetryHandlerConfigAsync(HttpRetryConfig? httpConfig = null)
private static async Task DefaultNoRetry()
{
var kernelBuilder = Kernel.Builder.WithLoggerFactory(InfoLogger.LoggerFactory);
if (httpConfig != null)
{
kernelBuilder = kernelBuilder.Configure(c => c.SetDefaultHttpRetryConfig(httpConfig));
}
InfoLogger.Logger.LogInformation("============================== Kernel default behavior: No Retry ==============================");
var kernel = InitializeKernelBuilder()
.Build();
// Add 401 to the list of retryable status codes
await ImportAndExecuteSkillAsync(kernel);
}
private static async Task ReliabilityBasicExtension()
{
InfoLogger.Logger.LogInformation("============================== Using Reliability.Basic extension ==============================");
var retryConfig = new BasicRetryConfig
{
MaxRetryCount = 3,
UseExponentialBackoff = true,
};
retryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized);
var kernel = InitializeKernelBuilder()
.WithRetryBasic(retryConfig)
.Build();
await ImportAndExecuteSkillAsync(kernel);
}
private static async Task ReliabilityPollyExtension()
{
InfoLogger.Logger.LogInformation("============================== Using Reliability.Polly extension ==============================");
var kernel = InitializeKernelBuilder()
.WithRetryPolly(GetPollyPolicy(InfoLogger.LoggerFactory))
.Build();
await ImportAndExecuteSkillAsync(kernel);
}
private static async Task CustomHandler()
{
InfoLogger.Logger.LogInformation("============================== Using a Custom Http Handler ==============================");
var kernel = InitializeKernelBuilder()
.WithHttpHandlerFactory(new MyCustomHandlerFactory())
.Build();
await ImportAndExecuteSkillAsync(kernel);
}
private static KernelBuilder InitializeKernelBuilder()
{
return Kernel.Builder
.WithLoggerFactory(InfoLogger.LoggerFactory)
// OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play
.WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY");
}
private static AsyncPolicy<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.
kernelBuilder = kernelBuilder.Configure(c => c.DefaultHttpRetryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized));
const int tooManyRequests = 429;
const int unauthorized = 401;
// OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play
kernelBuilder = kernelBuilder.WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY");
var kernel = kernelBuilder.Build();
await ImportAndExecuteSkillAsync(kernel);
}
private static IKernel InitializeKernel()
{
var kernel = Kernel.Builder
.WithLoggerFactory(InfoLogger.LoggerFactory)
// OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play
.WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY")
.Build();
return kernel;
}
private static async Task RunRetryPolicyAsync(IKernel kernel, IDelegatingHandlerFactory retryHandlerFactory)
{
kernel.Config.SetHttpRetryHandlerFactory(retryHandlerFactory);
await ImportAndExecuteSkillAsync(kernel);
}
private static async Task RunRetryPolicyBuilderAsync(Type retryHandlerFactoryType)
{
var kernel = Kernel.Builder.WithLoggerFactory(InfoLogger.LoggerFactory)
.WithRetryHandlerFactory((Activator.CreateInstance(retryHandlerFactoryType) as IDelegatingHandlerFactory)!)
// OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play
.WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY")
.Build();
await ImportAndExecuteSkillAsync(kernel);
return Policy
.HandleResult<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)
@@ -101,6 +124,22 @@ public static class Example08_RetryHandler
InfoLogger.Logger.LogInformation("Answer: {0}", answer);
}
// Basic custom retry handler factory
public sealed class MyCustomHandlerFactory : HttpHandlerFactory<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");
@@ -122,65 +161,3 @@ public static class Example08_RetryHandler
}
}
}
/* Output:
info: Example08_RetryHandler[0]
============================== RetryThreeTimesWithBackoff ==============================
info: Example08_RetryHandler[0]
Question: How popular is Polly library?
warn: Reliability.RetryThreeTimesWithBackoff[0]
Error executing action [attempt 1 of 3], pausing 2000ms. Outcome: Unauthorized
warn: Reliability.RetryThreeTimesWithBackoff[0]
Error executing action [attempt 2 of 3], pausing 4000ms. Outcome: Unauthorized
warn: Reliability.RetryThreeTimesWithBackoff[0]
Error executing action [attempt 3 of 3], pausing 8000ms. Outcome: Unauthorized
info: Example08_RetryHandler[0]
Answer: Error: Access denied: The request is not authorized, HTTP status: 401
info: Example08_RetryHandler[0]
========================= RetryThreeTimesWithRetryAfterBackoff =========================
info: Example08_RetryHandler[0]
Question: How popular is Polly library?
warn: Reliability.RetryThreeTimesWithRetryAfterBackoff[0]
Error executing action [attempt 1 of 3], pausing 2000ms. Outcome: Unauthorized
warn: Reliability.RetryThreeTimesWithRetryAfterBackoff[0]
Error executing action [attempt 2 of 3], pausing 2000ms. Outcome: Unauthorized
warn: Reliability.RetryThreeTimesWithRetryAfterBackoff[0]
Error executing action [attempt 3 of 3], pausing 2000ms. Outcome: Unauthorized
info: Example08_RetryHandler[0]
Answer: Error: Access denied: The request is not authorized, HTTP status: 401
info: Example08_RetryHandler[0]
==================================== NoRetryPolicy =====================================
info: Example08_RetryHandler[0]
Question: How popular is Polly library?
info: Example08_RetryHandler[0]
Answer: Error: Access denied: The request is not authorized, HTTP status: 401
info: Example08_RetryHandler[0]
=============================== DefaultHttpRetryHandler ================================
info: Example08_RetryHandler[0]
Question: How popular is Polly library?
warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing action [attempt 1 of 3]. Reason: Unauthorized. Will retry after 2000ms
warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing action [attempt 2 of 3]. Reason: Unauthorized. Will retry after 4000ms
warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing action [attempt 3 of 3]. Reason: Unauthorized. Will retry after 8000ms
fail: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing request, max retry count reached. Reason: Unauthorized
info: Example08_RetryHandler[0]
Answer: Error: Access denied: The request is not authorized, HTTP status: 401
info: Example08_RetryHandler[0]
======= DefaultHttpRetryConfig [MaxRetryCount = 3, UseExponentialBackoff = true] =======
info: Example08_RetryHandler[0]
Question: How popular is Polly library?
warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing action [attempt 1 of 3]. Reason: Unauthorized. Will retry after 2000ms
warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing action [attempt 2 of 3]. Reason: Unauthorized. Will retry after 4000ms
warn: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing action [attempt 3 of 3]. Reason: Unauthorized. Will retry after 8000ms
fail: Microsoft.SemanticKernel.Reliability.DefaultHttpRetryHandler[0]
Error executing request, max retry count reached. Reason: Unauthorized
info: Example08_RetryHandler[0]
Answer: Error: Access denied: The request is not authorized, HTTP status: 401
== DONE ==
*/
@@ -5,7 +5,6 @@ using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Skills.OpenAPI.Authentication;
using Microsoft.SemanticKernel.Skills.OpenAPI.Extensions;
using Microsoft.SemanticKernel.Skills.OpenAPI.Skills;
@@ -32,11 +31,13 @@ public static class Example22_OpenApiSkill_AzureKeyVault
public static async Task GetSecretFromAzureKeyVaultWithRetryAsync(InteractiveMsalAuthenticationProvider authenticationProvider)
{
var retryConfig = new HttpRetryConfig() { MaxRetryCount = 3, UseExponentialBackoff = true };
var kernel = new KernelBuilder()
.WithLoggerFactory(ConsoleLogger.LoggerFactory)
.Configure(c => c.SetDefaultHttpRetryConfig(retryConfig))
.WithRetryBasic(new()
{
MaxRetryCount = 3,
UseExponentialBackoff = true
})
.Build();
var type = typeof(SkillResourceNames);
@@ -17,8 +17,10 @@ using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Http;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Reliability.Basic;
using Microsoft.SemanticKernel.Reliability.Polly;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.TemplateEngine.Prompt;
@@ -79,8 +81,7 @@ public static class Example42_KernelBuilder
var skills = new SkillCollection();
var templateEngine = new PromptTemplateEngine(loggerFactory);
var kernelConfig = new KernelConfig();
using var httpHandler = new DefaultHttpRetryHandler(new HttpRetryConfig(), loggerFactory);
using var httpHandler = kernelConfig.HttpHandlerFactory.Create(loggerFactory);
using var httpClient = new HttpClient(httpHandler);
var aiServices = new AIServiceCollection();
ITextCompletion Factory() => new AzureTextCompletion(
@@ -139,8 +140,8 @@ public static class Example42_KernelBuilder
// The default behavior can be configured or a custom retry handler can be injected that will apply to all
// AI requests (when using the kernel).
var kernel8 = Kernel.Builder
.Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig
var kernel8 = Kernel.Builder.WithRetryBasic(
new BasicRetryConfig
{
MaxRetryCount = 3,
UseExponentialBackoff = true,
@@ -149,60 +150,73 @@ public static class Example42_KernelBuilder
// MaxTotalRetryTime = TimeSpan.FromSeconds(30),
// RetryableStatusCodes = new[] { HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout },
// RetryableExceptions = new[] { typeof(HttpRequestException) }
}))
})
.Build();
var kernel9 = Kernel.Builder
.Configure(c => c.SetHttpRetryHandlerFactory(new NullHttpRetryHandlerFactory()))
.Build();
var logger = loggerFactory.CreateLogger<PollyHttpRetryHandlerFactory>();
var retryThreeTimesPolicy = Policy
.Handle<HttpOperationException>(ex
=> ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(8)
},
(ex, timespan, retryCount, _)
=> logger?.LogWarning(ex, "Error executing action [attempt {RetryCount} of 3], pausing {PausingMilliseconds}ms", retryCount, timespan.TotalMilliseconds));
var kernel10 = Kernel.Builder.WithRetryHandlerFactory(new RetryThreeTimesFactory()).Build();
var kernel9 = Kernel.Builder.WithHttpHandlerFactory(new PollyHttpRetryHandlerFactory(retryThreeTimesPolicy)).Build();
var kernel10 = Kernel.Builder.WithHttpHandlerFactory(new PollyRetryThreeTimesFactory()).Build();
var kernel11 = Kernel.Builder.WithHttpHandlerFactory(new MyCustomHandlerFactory()).Build();
return Task.CompletedTask;
}
// Example of a basic custom retry handler
public class RetryThreeTimesFactory : IDelegatingHandlerFactory
// Example using the PollyHttpRetryHandler from Reliability.Polly extension
public class PollyRetryThreeTimesFactory : HttpHandlerFactory<PollyHttpRetryHandler>
{
public DelegatingHandler Create(ILoggerFactory? loggerFactory)
public override DelegatingHandler Create(ILoggerFactory? loggerFactory = null)
{
return new RetryThreeTimes(loggerFactory);
var logger = loggerFactory?.CreateLogger<PollyRetryThreeTimesFactory>();
Activator.CreateInstance(typeof(PollyHttpRetryHandler), GetPolicy(logger), logger);
return base.Create(loggerFactory);
}
private static AsyncRetryPolicy GetPolicy(ILogger? logger)
{
return Policy
.Handle<HttpOperationException>(ex
=> ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(8)
},
(ex, timespan, retryCount, _)
=> logger?.LogWarning(ex, "Error executing action [attempt {RetryCount} of 3], pausing {PausingMilliseconds}ms",
retryCount,
timespan.TotalMilliseconds));
}
}
public class RetryThreeTimes : DelegatingHandler
// Basic custom retry handler factory
public class MyCustomHandlerFactory : HttpHandlerFactory<MyCustomHandler>
{
private readonly AsyncRetryPolicy _policy;
}
public RetryThreeTimes(ILoggerFactory? loggerFactory = null)
// Basic custom empty retry handler
public class MyCustomHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
this._policy = GetPolicy(loggerFactory is not null ?
loggerFactory.CreateLogger(this.GetType()) :
NullLogger.Instance);
}
// Your custom handler implementation
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await this._policy.ExecuteAsync(async () =>
{
var response = await base.SendAsync(request, cancellationToken);
return response;
});
}
private static AsyncRetryPolicy GetPolicy(ILogger logger)
{
return Policy
.Handle<HttpOperationException>(ex => ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(8)
},
(ex, timespan, retryCount, _) => logger.LogWarning(ex,
"Error executing action [attempt {0} of 3], pausing {1}ms",
retryCount, timespan.TotalMilliseconds));
throw new NotImplementedException();
}
}
}
@@ -5,7 +5,6 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Skills.Core;
using Microsoft.SemanticKernel.Skills.Web;
using Microsoft.SemanticKernel.Skills.Web.Bing;
@@ -87,12 +86,12 @@ public static class Example51_StepwisePlanner
var kernel = builder
.WithLoggerFactory(ConsoleLogger.LoggerFactory)
.Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig
.WithRetryBasic(new()
{
MaxRetryCount = 3,
UseExponentialBackoff = true,
MinRetryDelay = TimeSpan.FromSeconds(3),
}))
})
.Build();
return kernel;
@@ -41,7 +41,9 @@
<ProjectReference Include="..\..\src\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\..\src\Extensions\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Pinecone\Connectors.Memory.Pinecone.csproj" />
<ProjectReference Include="..\..\src\Extensions\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\..\src\Extensions\TemplateEngine.PromptTemplateEngine\TemplateEngine.PromptTemplateEngine.csproj" />
<ProjectReference Include="..\..\src\Extensions\Reliability.Polly\Reliability.Polly.csproj" />
<ProjectReference Include="..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Skills\Skills.Core\Skills.Core.csproj" />
<ProjectReference Include="..\..\src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
@@ -5,7 +5,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Http;
using Polly;
using Polly.Retry;
@@ -38,9 +38,9 @@ public class RetryThreeTimesWithBackoff : DelegatingHandler
{
return await this._policy.ExecuteAsync(async () =>
{
var response = await base.SendAsync(request, cancellationToken);
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
return response;
});
}).ConfigureAwait(false);
}
private static AsyncRetryPolicy<HttpResponseMessage> GetPolicy(ILoggerFactory? logger)
@@ -48,9 +48,12 @@ public class RetryThreeTimesWithBackoff : DelegatingHandler
// Handle 429 and 401 errors
// Typically 401 would not be something we retry but for demonstration
// purposes we are doing so as it's easy to trigger when using an invalid key.
const int tooManyRequests = 429;
const int unauthorized = 401;
return Policy
.HandleResult<HttpResponseMessage>(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),
@@ -5,7 +5,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Http;
using Polly;
using Polly.Retry;
@@ -38,9 +38,9 @@ public class RetryThreeTimesWithRetryAfterBackoff : DelegatingHandler
{
return await this._policy.ExecuteAsync(async () =>
{
var response = await base.SendAsync(request, cancellationToken);
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
return response;
});
}).ConfigureAwait(false);
}
private static AsyncRetryPolicy<HttpResponseMessage> GetPolicy(ILoggerFactory? loggerFactory)
@@ -48,9 +48,12 @@ public class RetryThreeTimesWithRetryAfterBackoff : DelegatingHandler
// Handle 429 and 401 errors
// Typically 401 would not be something we retry but for demonstration
// purposes we are doing so as it's easy to trigger when using an invalid key.
const int tooManyRequests = 429;
const int unauthorized = 401;
return Policy
.HandleResult<HttpResponseMessage>(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, _) =>
@@ -16,7 +16,6 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletionWithData;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ImageGeneration;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Reliability;
#pragma warning disable IDE0130
// ReSharper disable once CheckNamespace - Using NS of KernelConfig
@@ -553,12 +552,6 @@ public static class OpenAIKernelBuilderExtensions
options.Transport = new HttpClientTransport(HttpClientProvider.GetHttpClient(config, httpClient, loggerFactory));
#pragma warning restore CA2000 // Dispose objects before losing scope
if (config.HttpHandlerFactory is DefaultHttpRetryHandlerFactory factory && factory.Config is not null)
{
options.Retry.MaxRetries = factory.Config.MaxRetryCount;
options.Retry.MaxDelay = factory.Config.MaxRetryDelay;
}
return options;
}
}
@@ -31,7 +31,9 @@
<ProjectReference Include="..\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
<ProjectReference Include="..\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
<ProjectReference Include="..\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\TemplateEngine.PromptTemplateEngine\TemplateEngine.PromptTemplateEngine.csproj" />
<ProjectReference Include="..\Reliability.Polly\Reliability.Polly.csproj" />
</ItemGroup>
</Project>
@@ -8,14 +8,14 @@ using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Reliability.Basic;
using Moq;
using Moq.Protected;
using Xunit;
namespace SemanticKernel.UnitTests.Reliability;
namespace SemanticKernel.Extensions.UnitTests.Reliability.Basic;
public class DefaultHttpRetryHandlerTests
public class BasicHttpRetryHandlerTests
{
[Theory]
[InlineData(HttpStatusCode.RequestTimeout)]
@@ -25,7 +25,7 @@ public class DefaultHttpRetryHandlerTests
public async Task NoMaxRetryCountCallsOnceForStatusAsync(HttpStatusCode statusCode)
{
// Arrange
using var retry = new DefaultHttpRetryHandler(new HttpRetryConfig() { MaxRetryCount = 0 }, NullLoggerFactory.Instance);
using var retry = new BasicHttpRetryHandler(new BasicRetryConfig() { MaxRetryCount = 0 }, NullLoggerFactory.Instance);
using var mockResponse = new HttpResponseMessage(statusCode);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -93,7 +93,7 @@ public class DefaultHttpRetryHandlerTests
public async Task NoMaxRetryCountCallsOnceForExceptionAsync(Type exceptionType)
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 0 });
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 0 });
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(exceptionType);
@@ -117,7 +117,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesOnceOnTransientStatusWithExponentialBackoffAsync(HttpStatusCode statusCode)
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { UseExponentialBackoff = true });
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { UseExponentialBackoff = true });
using var mockResponse = new HttpResponseMessage(statusCode);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -139,7 +139,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesOnceOnRetryableExceptionWithExponentialBackoffAsync(Type exceptionType)
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { UseExponentialBackoff = true });
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { UseExponentialBackoff = true });
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(exceptionType);
@@ -163,15 +163,15 @@ public class DefaultHttpRetryHandlerTests
{
// Arrange
var currentTime = DateTimeOffset.UtcNow;
var mockTimeProvider = new Mock<DefaultHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<DefaultHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<BasicHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<BasicHttpRetryHandler.IDelayProvider>();
mockTimeProvider.SetupSequence(x => x.GetCurrentTime())
.Returns(() => currentTime)
.Returns(() => currentTime + TimeSpan.FromMilliseconds(5))
.Returns(() => currentTime + TimeSpan.FromMilliseconds(510))
.Returns(() => currentTime + TimeSpan.FromMilliseconds(1015))
.Returns(() => currentTime + TimeSpan.FromMilliseconds(1520));
using var retry = ConfigureRetryHandler(new HttpRetryConfig()
using var retry = ConfigureRetryHandler(new BasicRetryConfig()
{
UseExponentialBackoff = true, MaxRetryCount = 3,
MinRetryDelay = TimeSpan.FromMilliseconds(500)
@@ -203,7 +203,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesOnceOnTransientStatusCodeWithRetryValueAsync(HttpStatusCode statusCode)
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig(), null);
using var retry = ConfigureRetryHandler(new BasicRetryConfig(), null);
using var mockResponse = new HttpResponseMessage()
{
StatusCode = statusCode,
@@ -232,7 +232,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesStatusCustomCountAsync(HttpStatusCode expectedStatus)
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null);
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null);
using var mockResponse = new HttpResponseMessage(expectedStatus);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -254,7 +254,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesExceptionsCustomCountAsync(Type expectedException)
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null);
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(expectedException);
@@ -274,7 +274,7 @@ public class DefaultHttpRetryHandlerTests
public async Task NoExceptionNoRetryAsync()
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null);
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -295,7 +295,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItDoesNotExecuteOnCancellationTokenAsync()
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null);
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -317,7 +317,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItDoestExecuteOnFalseCancellationTokenAsync()
{
// Arrange
using var retry = ConfigureRetryHandler(new HttpRetryConfig() { MaxRetryCount = 3 }, null);
using var retry = ConfigureRetryHandler(new BasicRetryConfig() { MaxRetryCount = 3 }, null);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -338,13 +338,13 @@ public class DefaultHttpRetryHandlerTests
[Fact]
public async Task ItRetriesWithMinRetryDelayAsync()
{
var httpRetryConfig = new HttpRetryConfig
var BasicRetryConfig = new BasicRetryConfig
{
MinRetryDelay = TimeSpan.FromMilliseconds(500)
};
var mockDelayProvider = new Mock<DefaultHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<DefaultHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<BasicHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<BasicHttpRetryHandler.ITimeProvider>();
var currentTime = DateTimeOffset.UtcNow;
@@ -356,7 +356,7 @@ public class DefaultHttpRetryHandlerTests
mockDelayProvider.Setup(x => x.DelayAsync(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.CompletedTask);
using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider);
using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
using var testContent = new StringContent("test");
var mockHandler = GetHttpMessageHandlerMock(mockResponse);
@@ -378,14 +378,14 @@ public class DefaultHttpRetryHandlerTests
[Fact]
public async Task ItRetriesWithMaxRetryDelayAsync()
{
var httpRetryConfig = new HttpRetryConfig
var BasicRetryConfig = new BasicRetryConfig
{
MinRetryDelay = TimeSpan.FromMilliseconds(1),
MaxRetryDelay = TimeSpan.FromMilliseconds(500)
};
var mockDelayProvider = new Mock<DefaultHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<DefaultHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<BasicHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<BasicHttpRetryHandler.ITimeProvider>();
var currentTime = DateTimeOffset.UtcNow;
@@ -397,7 +397,7 @@ public class DefaultHttpRetryHandlerTests
mockDelayProvider.Setup(x => x.DelayAsync(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.CompletedTask);
using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider);
using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests)
{
Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(2000)) }
@@ -428,7 +428,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesWithMaxTotalDelayAsync(HttpStatusCode statusCode)
{
// Arrange
var httpRetryConfig = new HttpRetryConfig
var BasicRetryConfig = new BasicRetryConfig
{
MaxRetryCount = 5,
MinRetryDelay = TimeSpan.FromMilliseconds(50),
@@ -436,8 +436,8 @@ public class DefaultHttpRetryHandlerTests
MaxTotalRetryTime = TimeSpan.FromMilliseconds(350)
};
var mockDelayProvider = new Mock<DefaultHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<DefaultHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<BasicHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<BasicHttpRetryHandler.ITimeProvider>();
var currentTime = DateTimeOffset.UtcNow;
mockTimeProvider.SetupSequence(x => x.GetCurrentTime())
@@ -450,7 +450,7 @@ public class DefaultHttpRetryHandlerTests
.Returns(() => currentTime + TimeSpan.FromMilliseconds(275))
.Returns(() => currentTime + TimeSpan.FromMilliseconds(330));
using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider);
using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider);
using var mockResponse = new HttpResponseMessage(statusCode);
using var testContent = new StringContent("test");
@@ -474,7 +474,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesFewerWithMaxTotalDelayAsync()
{
// Arrange
var httpRetryConfig = new HttpRetryConfig
var BasicRetryConfig = new BasicRetryConfig
{
MaxRetryCount = 5,
MinRetryDelay = TimeSpan.FromMilliseconds(50),
@@ -482,8 +482,8 @@ public class DefaultHttpRetryHandlerTests
MaxTotalRetryTime = TimeSpan.FromMilliseconds(100)
};
var mockDelayProvider = new Mock<DefaultHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<DefaultHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<BasicHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<BasicHttpRetryHandler.ITimeProvider>();
var currentTime = DateTimeOffset.UtcNow;
mockTimeProvider.SetupSequence(x => x.GetCurrentTime())
@@ -496,7 +496,7 @@ public class DefaultHttpRetryHandlerTests
.Returns(() => currentTime + TimeSpan.FromMilliseconds(275))
.Returns(() => currentTime + TimeSpan.FromMilliseconds(330));
using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider);
using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
using var testContent = new StringContent("test");
@@ -520,7 +520,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesFewerWithMaxTotalDelayOnExceptionAsync()
{
// Arrange
var httpRetryConfig = new HttpRetryConfig
var BasicRetryConfig = new BasicRetryConfig
{
MaxRetryCount = 5,
MinRetryDelay = TimeSpan.FromMilliseconds(50),
@@ -528,8 +528,8 @@ public class DefaultHttpRetryHandlerTests
MaxTotalRetryTime = TimeSpan.FromMilliseconds(100)
};
var mockDelayProvider = new Mock<DefaultHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<DefaultHttpRetryHandler.ITimeProvider>();
var mockDelayProvider = new Mock<BasicHttpRetryHandler.IDelayProvider>();
var mockTimeProvider = new Mock<BasicHttpRetryHandler.ITimeProvider>();
var currentTime = DateTimeOffset.UtcNow;
mockTimeProvider.SetupSequence(x => x.GetCurrentTime())
@@ -542,7 +542,7 @@ public class DefaultHttpRetryHandlerTests
.Returns(() => currentTime + TimeSpan.FromMilliseconds(275))
.Returns(() => currentTime + TimeSpan.FromMilliseconds(330));
using var retry = ConfigureRetryHandler(httpRetryConfig, mockTimeProvider, mockDelayProvider);
using var retry = ConfigureRetryHandler(BasicRetryConfig, mockTimeProvider, mockDelayProvider);
var mockHandler = GetHttpMessageHandlerMock(typeof(HttpRequestException));
retry.InnerHandler = mockHandler.Object;
@@ -562,7 +562,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesOnRetryableStatusCodesAsync()
{
// Arrange
var config = new HttpRetryConfig() { RetryableStatusCodes = new List<HttpStatusCode> { HttpStatusCode.Unauthorized } };
var config = new BasicRetryConfig() { RetryableStatusCodes = new List<HttpStatusCode> { HttpStatusCode.Unauthorized } };
using var retry = ConfigureRetryHandler(config);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);
@@ -585,7 +585,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItDoesNotRetryOnNonRetryableStatusCodesAsync()
{
// Arrange
var config = new HttpRetryConfig() { RetryableStatusCodes = new List<HttpStatusCode> { HttpStatusCode.Unauthorized } };
var config = new BasicRetryConfig() { RetryableStatusCodes = new List<HttpStatusCode> { HttpStatusCode.Unauthorized } };
using var retry = ConfigureRetryHandler(config);
using var mockResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
@@ -608,7 +608,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItRetriesOnRetryableExceptionsAsync()
{
// Arrange
var config = new HttpRetryConfig() { RetryableExceptionTypes = new List<Type> { typeof(InvalidOperationException) } };
var config = new BasicRetryConfig() { RetryableExceptionTypes = new List<Type> { typeof(InvalidOperationException) } };
using var retry = ConfigureRetryHandler(config);
using var testContent = new StringContent("test");
@@ -630,7 +630,7 @@ public class DefaultHttpRetryHandlerTests
public async Task ItDoesNotRetryOnNonRetryableExceptionsAsync()
{
// Arrange
var config = new HttpRetryConfig() { RetryableExceptionTypes = new List<Type> { typeof(InvalidOperationException) } };
var config = new BasicRetryConfig() { RetryableExceptionTypes = new List<Type> { typeof(InvalidOperationException) } };
using var retry = ConfigureRetryHandler(config);
using var testContent = new StringContent("test");
@@ -648,13 +648,13 @@ public class DefaultHttpRetryHandlerTests
.Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
}
private static DefaultHttpRetryHandler ConfigureRetryHandler(HttpRetryConfig? config = null,
Mock<DefaultHttpRetryHandler.ITimeProvider>? timeProvider = null, Mock<DefaultHttpRetryHandler.IDelayProvider>? delayProvider = null)
private static BasicHttpRetryHandler ConfigureRetryHandler(BasicRetryConfig? config = null,
Mock<BasicHttpRetryHandler.ITimeProvider>? timeProvider = null, Mock<BasicHttpRetryHandler.IDelayProvider>? delayProvider = null)
{
delayProvider ??= new Mock<DefaultHttpRetryHandler.IDelayProvider>();
timeProvider ??= new Mock<DefaultHttpRetryHandler.ITimeProvider>();
delayProvider ??= new Mock<BasicHttpRetryHandler.IDelayProvider>();
timeProvider ??= new Mock<BasicHttpRetryHandler.ITimeProvider>();
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;
}
@@ -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;
/// <summary>
/// Unit tests of <see cref="KernelConfig"/>.
/// </summary>
public class BasicRetryConfigTests
{
[Fact]
public async Task NegativeMaxRetryCountThrowsAsync()
{
// Act
await Assert.ThrowsAsync<System.ArgumentOutOfRangeException>(() =>
{
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<BasicHttpRetryHandlerFactory>(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<BasicHttpRetryHandlerFactory>(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);
}
}
@@ -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<HttpResponseMessage>();
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<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
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<HttpResponseMessage>(result => result.StatusCode != statusCode)
.WaitAndRetryAsync<HttpResponseMessage>(
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<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
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<HttpResponseMessage>(result => result.StatusCode == retryStatusCode)
.WaitAndRetryAsync<HttpResponseMessage>(
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<Task<HttpResponseMessage>>("SendAsync", Times.Exactly(expectedSendAsyncTimes), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
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 => 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<Task<HttpResponseMessage>>("SendAsync", Times.Exactly(expectedSendAsyncTimes), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
}
private static (Mock<ILoggerFactory>, Mock<ILogger>) GetLoggerMocks()
{
var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>();
mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
return (mockLoggerFactory, mockLogger);
}
private static Mock<HttpMessageHandler> GetHttpMessageHandlerMock(HttpResponseMessage mockResponse)
{
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
return mockHandler;
}
private static Mock<HttpMessageHandler> GetHttpMessageHandlerMock(Type exceptionType)
{
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(Activator.CreateInstance(exceptionType) as Exception);
return mockHandler;
}
}
@@ -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;
/// <summary>
/// Handler that retries HTTP requests based on a <see cref="BasicRetryConfig"/>.
/// </summary>
internal sealed class BasicHttpRetryHandler : DelegatingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="DefaultHttpRetryHandler"/> class.
/// </summary>
/// <param name="config">The retry configuration.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
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<BasicHttpRetryHandler>() : NullLogger.Instance;
this._delayProvider = delayProvider ?? new TaskDelayProvider();
this._timeProvider = timeProvider ?? new DefaultTimeProvider();
}
/// <summary>
/// Executes the action with retry logic
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected override async Task<HttpResponseMessage> 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);
}
}
/// <summary>
/// Interface for a delay provider, primarily to enable unit testing.
/// </summary>
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);
}
}
/// <summary>
/// Interface for a time provider, primarily to enable unit testing.
/// </summary>
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;
/// <summary>
/// Get the wait time for the next retry.
/// </summary>
/// <param name="retryCount">Current retry count</param>
/// <param name="response">The response message that potentially contains RetryAfter header.</param>
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;
}
/// <summary>
/// Determines if there is time left for a retry.
/// </summary>
/// <param name="start">The start time of the original request.</param>
/// <param name="retryCount">The current retry count.</param>
/// <param name="response">The response message that potentially contains RetryAfter header.</param>
/// <param name="waitFor">The wait time for the next retry.</param>
/// <returns>True if there is time left for a retry, false otherwise.</returns>
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());
}
}
@@ -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;
/// <summary>
/// Internal factory for creating <see cref="DefaultHttpRetryHandler"/> instances.
/// </summary>
internal sealed class BasicHttpRetryHandlerFactory : HttpHandlerFactory<BasicHttpRetryHandler>
{
/// <summary>
/// Creates a new instance of <see cref="DefaultHttpRetryHandlerFactory"/> with the provided configuration.
/// </summary>
/// <param name="config">Http retry configuration</param>
internal BasicHttpRetryHandlerFactory(BasicRetryConfig? config = null)
{
this.Config = config ?? new();
}
/// <summary>
/// Creates a new instance of <see cref="DefaultHttpRetryHandler"/> with the default configuration.
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <returns>Returns the created handler</returns>
public override DelegatingHandler Create(ILoggerFactory? loggerFactory = null)
{
return new BasicHttpRetryHandler(this.Config, loggerFactory);
}
/// <summary>
/// Creates a new instance of <see cref="DefaultHttpRetryHandler"/> with a specified configuration.
/// </summary>
/// <param name="config">Specific configuration</param>
/// <param name="loggerFactory">Logger factory</param>
/// <returns>Returns the created handler</returns>
public DelegatingHandler Create(BasicRetryConfig config, ILoggerFactory? loggerFactory = null)
{
Verify.NotNull(config, nameof(config));
return new BasicHttpRetryHandler(config, loggerFactory);
}
/// <summary>
/// Default retry configuration used when creating a new instance of <see cref="DefaultHttpRetryHandler"/>.
/// </summary>
internal BasicRetryConfig Config { get; }
}
@@ -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;
/// <summary>
/// Retry configuration for DefaultKernelRetryHandler that uses RetryAfter header when present.
/// </summary>
public sealed record BasicRetryConfig
{
/// <summary>
/// Maximum number of retries.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when value is negative.</exception>
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;
}
}
/// <summary>
/// Minimum delay between retries.
/// </summary>
public TimeSpan MinRetryDelay { get; set; } = TimeSpan.FromSeconds(2);
/// <summary>
/// Maximum delay between retries.
/// </summary>
public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Maximum total time spent retrying.
/// </summary>
public TimeSpan MaxTotalRetryTime { get; set; } = TimeSpan.FromMinutes(2);
/// <summary>
/// Whether to use exponential backoff or not.
/// </summary>
public bool UseExponentialBackoff { get; set; }
/// <summary>
/// List of status codes that should be retried.
/// </summary>
public List<HttpStatusCode> RetryableStatusCodes { get; set; } = new()
{
(HttpStatusCode)HttpStatusCodeType.RequestTimeout,
(HttpStatusCode)HttpStatusCodeType.ServiceUnavailable,
(HttpStatusCode)HttpStatusCodeType.GatewayTimeout,
(HttpStatusCode)HttpStatusCodeType.TooManyRequests,
(HttpStatusCode)HttpStatusCodeType.BadGateway,
};
/// <summary>
/// List of exception types that should be retried.
/// </summary>
public List<Type> RetryableExceptionTypes { get; set; } = new()
{
typeof(HttpRequestException)
};
private int _maxRetryCount = 1;
}
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
<AssemblyName>Microsoft.SemanticKernel.Reliability.Basic</AssemblyName>
<RootNamespace>Microsoft.SemanticKernel.Reliability.Basic</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/*.cs" Link="Diagnostics/%(Filename)%(Extension)" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Semantic Kernel - Basic Reliability Extension</Title>
<Description>Semantic Kernel Basic Reliability Extension</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
<InternalsVisibleTo Include="IntegrationTests" />
<InternalsVisibleTo Include="SemanticKernel.Extensions.UnitTests" />
</ItemGroup>
</Project>
@@ -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
/// <summary>
/// Provides extension methods for the <see cref="KernelBuilder"/>.
/// </summary>
public static class ReliabilityBasicKernelBuilderExtensions
{
/// <summary>
/// Sets the default retry configuration for any kernel http request.
/// </summary>
/// <param name="builder">Target instance</param>
/// <param name="retryConfig">Retry configuration</param>
/// <returns>Self instance</returns>
public static KernelBuilder WithRetryBasic(this KernelBuilder builder,
BasicRetryConfig retryConfig)
{
var httpHandlerFactory = new BasicHttpRetryHandlerFactory(retryConfig);
return builder.WithHttpHandlerFactory(httpHandlerFactory);
}
}
@@ -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;
/// <summary>
/// Customizable PollyHttpHandler that will follow the provided policy.
/// </summary>
public class PollyHttpRetryHandler : DelegatingHandler
{
private readonly AsyncPolicy<HttpResponseMessage>? _typedAsyncPolicy;
private readonly AsyncPolicy? _asyncPolicy;
/// <summary>
/// Creates a new instance of <see cref="PollyHttpRetryHandler"/>.
/// </summary>
/// <param name="typedAsyncPolicy">HttpResponseMessage typed AsyncPolicy</param> dedicated for <see cref="HttpResponseMessage"/> typed policies.
public PollyHttpRetryHandler(AsyncPolicy<HttpResponseMessage> typedAsyncPolicy)
{
Verify.NotNull(typedAsyncPolicy);
this._typedAsyncPolicy = typedAsyncPolicy;
}
/// <summary>
/// Creates a new instance of <see cref="PollyHttpRetryHandler"/> dedicated for non-typed policies.
/// </summary>
/// <param name="asyncPolicy">A non-typed AsyncPolicy</param>
public PollyHttpRetryHandler(AsyncPolicy asyncPolicy)
{
Verify.NotNull(asyncPolicy);
this._asyncPolicy = asyncPolicy;
}
/// <inheritdoc/>
protected override async Task<HttpResponseMessage> 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);
}
}
@@ -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;
/// <summary>
/// Customizable PollyHttpHandlerFactory that will create handlers with the provided policy.
/// </summary>
public class PollyHttpRetryHandlerFactory : HttpHandlerFactory<PollyHttpRetryHandler>
{
private readonly AsyncPolicy<HttpResponseMessage>? _typedAsyncPolicy;
private readonly AsyncPolicy? _asyncPolicy;
/// <summary>
/// Creates a new instance of <see cref="PollyHttpRetryHandler"/>.
/// </summary>
/// <param name="typedAsyncPolicy">HttpResponseMessage typed AsyncPolicy</param> dedicated for <see cref="HttpResponseMessage"/> typed policies.
public PollyHttpRetryHandlerFactory(AsyncPolicy<HttpResponseMessage> typedAsyncPolicy)
{
Verify.NotNull(typedAsyncPolicy);
this._typedAsyncPolicy = typedAsyncPolicy;
}
/// <summary>
/// Creates a new instance of <see cref="PollyHttpRetryHandler"/> dedicated for non-typed policies.
/// </summary>
/// <param name="asyncPolicy">A non-typed AsyncPolicy</param>
public PollyHttpRetryHandlerFactory(AsyncPolicy asyncPolicy)
{
Verify.NotNull(asyncPolicy);
this._asyncPolicy = asyncPolicy;
}
/// <summary>
/// Creates a new instance of <see cref="DefaultHttpRetryHandler"/> with the default configuration.
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <returns>Returns the created handler</returns>
public override DelegatingHandler Create(ILoggerFactory? loggerFactory = null)
{
if (this._typedAsyncPolicy is not null)
{
return new PollyHttpRetryHandler(this._typedAsyncPolicy);
}
return new PollyHttpRetryHandler(this._asyncPolicy!);
}
}
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
<AssemblyName>Microsoft.SemanticKernel.Reliability.Polly</AssemblyName>
<RootNamespace>Microsoft.SemanticKernel.Reliability.Polly</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/*.cs" Link="Diagnostics/%(Filename)%(Extension)" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Semantic Kernel - Polly Reliability Extension</Title>
<Description>Semantic Kernel Polly Reliability Extension</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
<InternalsVisibleTo Include="IntegrationTests" />
<InternalsVisibleTo Include="SemanticKernel.Extensions.UnitTests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Polly" />
</ItemGroup>
</Project>
@@ -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
/// <summary>
/// Provides extension methods for the <see cref="KernelBuilder"/>.
/// </summary>
public static class ReliabilityPollyKernelBuilderExtensions
{
/// <summary>
/// Sets the default retry configuration for any kernel http request.
/// </summary>
/// <param name="kernelConfig">Target instance</param>
/// <param name="retryPolicy">Provided AsyncPolicy</param>
/// <returns>Returns target instance for fluent compatibility</returns>
public static KernelBuilder WithRetryPolly(this KernelBuilder kernelConfig, AsyncPolicy retryPolicy)
{
var pollyHandler = new PollyHttpRetryHandlerFactory(retryPolicy);
return kernelConfig.WithHttpHandlerFactory(pollyHandler);
}
/// <summary>
/// Sets the default retry configuration for any kernel http request.
/// </summary>
/// <param name="kernelConfig">Target instance</param>
/// <param name="retryPolicy">Provided HttpResponseMessage AsyncPolicy</param>
/// <returns>Returns target instance for fluent compatibility</returns>
public static KernelBuilder WithRetryPolly(this KernelBuilder kernelConfig, AsyncPolicy<HttpResponseMessage> retryPolicy)
{
var pollyHandler = new PollyHttpRetryHandlerFactory(retryPolicy);
return kernelConfig.WithHttpHandlerFactory(pollyHandler);
}
}
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Reliability.Basic;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
using Xunit.Abstractions;
@@ -44,13 +44,13 @@ public sealed class AzureOpenAICompletionTests : IDisposable
var configuration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
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
@@ -9,7 +9,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Reliability.Basic;
using Microsoft.SemanticKernel.SkillDefinition;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
@@ -145,7 +145,7 @@ public sealed class OpenAICompletionTests : IDisposable
public async Task OpenAIHttpRetryPolicyTestAsync(string prompt, string expectedOutput)
{
// Arrange
var retryConfig = new HttpRetryConfig();
var retryConfig = new BasicRetryConfig();
retryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized);
OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get<OpenAIConfiguration>();
@@ -153,7 +153,7 @@ public sealed class OpenAICompletionTests : IDisposable
IKernel target = Kernel.Builder
.WithLoggerFactory(this._testOutputHelper)
.Configure(c => c.SetDefaultHttpRetryConfig(retryConfig))
.WithRetryBasic(retryConfig)
.WithOpenAITextCompletionService(
serviceId: openAIConfiguration.ServiceId,
modelId: openAIConfiguration.ModelId,
@@ -176,11 +176,12 @@ public sealed class OpenAICompletionTests : IDisposable
public async Task AzureOpenAIHttpRetryPolicyTestAsync(string prompt, string expectedOutput)
{
// Arrange
var retryConfig = new HttpRetryConfig();
var retryConfig = new BasicRetryConfig();
retryConfig.RetryableStatusCodes.Add(HttpStatusCode.Unauthorized);
KernelBuilder builder = Kernel.Builder
.WithLoggerFactory(this._testOutputHelper)
.Configure(c => c.SetDefaultHttpRetryConfig(retryConfig));
.WithRetryBasic(retryConfig);
var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
Assert.NotNull(azureOpenAIConfiguration);
@@ -43,7 +43,9 @@
<ProjectReference Include="..\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
<ProjectReference Include="..\Extensions\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
<ProjectReference Include="..\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\Extensions\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\Extensions\TemplateEngine.PromptTemplateEngine\TemplateEngine.PromptTemplateEngine.csproj" />
<ProjectReference Include="..\Extensions\Reliability.Polly\Reliability.Polly.csproj" />
<ProjectReference Include="..\Skills\Skills.Core\Skills.Core.csproj" />
<ProjectReference Include="..\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
<ProjectReference Include="..\Skills\Skills.Web\Skills.Web.csproj" />
@@ -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<THandler> : IDelegatingHandlerFactory where THandler : DelegatingHandler
{
public virtual DelegatingHandler Create(ILoggerFactory? loggerFactory = null)
{
return (DelegatingHandler)Activator.CreateInstance(typeof(THandler), loggerFactory);
}
}
@@ -3,7 +3,7 @@
using System.Net.Http;
using Microsoft.Extensions.Logging;
namespace Microsoft.SemanticKernel.Reliability;
namespace Microsoft.SemanticKernel.Http;
/// <summary>
/// Factory for creating <see cref="DelegatingHandler"/> instances.
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http;
namespace Microsoft.SemanticKernel.Http;
/// <summary>
/// A http retry handler that does not retry.
/// </summary>
public sealed class NullHttpHandler : DelegatingHandler
{
}
@@ -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();
}
}
@@ -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;
/// <summary>
/// Semantic kernel configuration.
/// TODO: use .NET ServiceCollection (will require a lot of changes)
/// </summary>
public sealed class KernelConfig
{
/// <summary>
/// Factory for creating HTTP handlers.
/// Kernel HTTP handler factory.
/// </summary>
public IDelegatingHandlerFactory HttpHandlerFactory { get; private set; } = new DefaultHttpRetryHandlerFactory(new HttpRetryConfig());
/// <summary>
/// Default HTTP retry configuration for built-in HTTP handler factory.
/// </summary>
public HttpRetryConfig DefaultHttpRetryConfig { get; private set; } = new();
/// <summary>
/// Set the http retry handler factory to use for the kernel.
/// </summary>
/// <param name="httpHandlerFactory">Http retry handler factory to use.</param>
/// <returns>The updated kernel configuration.</returns>
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");
}
}
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.SemanticKernel.Reliability;
[Obsolete("Usage of Semantic Kernel internal retry abstractions is deprecated.\nCheck KernelSyntaxExamples.Example42_KernelBuilder.cs for alternatives")]
public sealed class DefaultHttpRetryHandler : DelegatingHandler
{
/// <summary>
@@ -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)
@@ -11,6 +11,7 @@ namespace Microsoft.SemanticKernel.Reliability;
/// <summary>
/// Retry configuration for IHttpRetryPolicy that uses RetryAfter header when present.
/// </summary>
[Obsolete("Usage of Semantic Kernel internal retry abstractions is deprecated.\nCheck KernelSyntaxExamples.Example42_KernelBuilder.cs for alternatives")]
public sealed class HttpRetryConfig
{
/// <summary>
@@ -16,6 +16,7 @@ Empowers app owners to integrate cutting-edge LLM technology quickly and easily
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Extensions\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\Extensions\TemplateEngine.PromptTemplateEngine\TemplateEngine.PromptTemplateEngine.csproj" />
<ProjectReference Include="..\SemanticKernel\SemanticKernel.csproj" PrivateAssets="none" />
<ProjectReference Include="..\Connectors\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" PrivateAssets="none" />
@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Http;
using Microsoft.SemanticKernel.Reliability;
using Moq;
using Xunit;
@@ -31,7 +32,7 @@ public class KernelConfigTests
var config = new KernelConfig();
// Act
config.SetHttpRetryHandlerFactory(retry);
config.HttpHandlerFactory = retry;
// Assert
Assert.Equal(retry, config.HttpHandlerFactory);
@@ -45,33 +46,19 @@ public class KernelConfigTests
var config = new KernelConfig();
// Act
config.SetHttpRetryHandlerFactory(retry.Object);
config.HttpHandlerFactory = retry.Object;
// Assert
Assert.Equal(retry.Object, config.HttpHandlerFactory);
}
[Fact]
public void HttpRetryHandlerFactoryIsSetToDefaultHttpRetryHandlerFactoryIfNull()
public void HttpHandlerFactoryIsSetToNullByDefault()
{
// Arrange
var config = new KernelConfig();
// Act
config.SetHttpRetryHandlerFactory(null);
// Assert
Assert.IsType<DefaultHttpRetryHandlerFactory>(config.HttpHandlerFactory);
}
[Fact]
public void HttpRetryHandlerFactoryIsSetToDefaultHttpRetryHandlerFactoryIfNotSet()
{
// Arrange
var config = new KernelConfig();
// Act
// Assert
Assert.IsType<DefaultHttpRetryHandlerFactory>(config.HttpHandlerFactory);
Assert.IsType<NullHttpHandlerFactory>(config.HttpHandlerFactory);
}
}
@@ -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;
/// <summary>
/// Unit tests of <see cref="KernelConfig"/>.
/// </summary>
public class HttpRetryConfigTests
{
[Fact]
public async Task NegativeMaxRetryCountThrowsAsync()
{
// Act
await Assert.ThrowsAsync<System.ArgumentOutOfRangeException>(() =>
{
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);
}
}
+17 -6
View File
@@ -8,9 +8,9 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Http;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.Services;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.TemplateEngine;
@@ -51,7 +51,7 @@ public sealed class KernelBuilder
{
if (this._httpHandlerFactory != null)
{
this._config.SetHttpRetryHandlerFactory(this._httpHandlerFactory);
this._config.HttpHandlerFactory = this._httpHandlerFactory;
}
var instance = new Kernel(
@@ -145,17 +145,28 @@ public sealed class KernelBuilder
}
/// <summary>
/// Add a retry handler factory to the kernel to be built.
/// Add a http handler factory to the kernel to be built.
/// </summary>
/// <param name="httpHandlerFactory">Retry handler factory to add.</param>
/// <returns>Updated kernel builder including the retry handler factory.</returns>
public KernelBuilder WithRetryHandlerFactory(IDelegatingHandlerFactory httpHandlerFactory)
/// <param name="httpHandlerFactory">Http handler factory to add.</param>
/// <returns>Updated kernel builder including the http handler factory.</returns>
public KernelBuilder WithHttpHandlerFactory(IDelegatingHandlerFactory httpHandlerFactory)
{
Verify.NotNull(httpHandlerFactory);
this._httpHandlerFactory = httpHandlerFactory;
return this;
}
/// <summary>
/// Add a retry handler factory to the kernel to be built.
/// </summary>
/// <param name="httpHandlerFactory">Retry handler factory to add.</param>
/// <returns>Updated kernel builder including the retry handler factory.</returns>
[Obsolete("This method is deprecated, use WithHttpHandlerFactory instead")]
public KernelBuilder WithRetryHandlerFactory(IDelegatingHandlerFactory httpHandlerFactory)
{
return this.WithHttpHandlerFactory(httpHandlerFactory);
}
/// <summary>
/// Use the given configuration with the kernel to be built.
/// </summary>
@@ -2,6 +2,7 @@
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Http;
namespace Microsoft.SemanticKernel.Reliability;