Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 23.1 #1977

Merged
merged 10 commits into from
Mar 4, 2024
157 changes: 114 additions & 43 deletions src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,115 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration;
using Ocelot.DependencyInjection;
using Ocelot.Errors;
using Ocelot.Errors.QoS;
using Ocelot.Logging;
using Ocelot.Provider.Polly.Interfaces;
using Ocelot.Requester;
using Polly.CircuitBreaker;
using Polly.Timeout;

namespace Ocelot.Provider.Polly;

public static class OcelotBuilderExtensions
{
public static IOcelotBuilder AddPolly<T>(this IOcelotBuilder builder,
QosDelegatingHandlerDelegate delegatingHandler,
Dictionary<Type, Func<Exception, Error>> errorMapping)
where T : class, IPollyQoSProvider<HttpResponseMessage>
{
builder.Services
.AddSingleton(errorMapping)
.AddSingleton<IPollyQoSProvider<HttpResponseMessage>, T>()
.AddSingleton(delegatingHandler);

return builder;
}

public static IOcelotBuilder AddPolly(this IOcelotBuilder builder)
{
var errorMapping = new Dictionary<Type, Func<Exception, Error>>
{
{ typeof(TaskCanceledException), e => new RequestTimedOutError(e) },
{ typeof(TimeoutRejectedException), e => new RequestTimedOutError(e) },
{ typeof(BrokenCircuitException), e => new RequestTimedOutError(e) },
{ typeof(BrokenCircuitException<HttpResponseMessage>), e => new RequestTimedOutError(e) },
};
return AddPolly<PollyQoSProvider>(builder, GetDelegatingHandler, errorMapping);
}

private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory)
=> new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory);
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration;
using Ocelot.DependencyInjection;
using Ocelot.Errors;
using Ocelot.Errors.QoS;
using Ocelot.Logging;
using Ocelot.Provider.Polly.Interfaces;
using Ocelot.Requester;
using Polly.CircuitBreaker;
using Polly.Timeout;

namespace Ocelot.Provider.Polly;

public static class OcelotBuilderExtensions
{
/// <summary>
/// Default mapping of Polly <see cref="Exception"/>s to <see cref="Error"/> objects.
/// </summary>
public static readonly Dictionary<Type, Func<Exception, Error>> DefaultErrorMapping = new Dictionary<Type, Func<Exception, Error>>
{
{typeof(TaskCanceledException), CreateRequestTimedOutError},
{typeof(TimeoutRejectedException), CreateRequestTimedOutError},
{typeof(BrokenCircuitException), CreateRequestTimedOutError},
{typeof(BrokenCircuitException<HttpResponseMessage>), CreateRequestTimedOutError},
};

private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e);

/// <summary>
/// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping.
/// </summary>
/// <typeparam name="T">QoS provider to use (by default use <see cref="PollyQoSProvider"/>).</typeparam>
/// <param name="builder">Ocelot builder to extend.</param>
/// <param name="delegatingHandler">Your customized delegating handler (to manage QoS behavior by yourself).</param>
/// <param name="errorMapping">Your customized error mapping.</param>
/// <returns>The reference to the same extended <see cref="IOcelotBuilder"/> object.</returns>
public static IOcelotBuilder AddPolly<T>(this IOcelotBuilder builder,
QosDelegatingHandlerDelegate delegatingHandler,
Dictionary<Type, Func<Exception, Error>> errorMapping)
where T : class, IPollyQoSProvider<HttpResponseMessage>
{
builder.Services
.AddSingleton(errorMapping)
.AddSingleton<IPollyQoSProvider<HttpResponseMessage>, T>()
.AddSingleton(delegatingHandler);

return builder;
}

/// <summary>
/// Adds Polly QoS provider to Ocelot with custom error mapping, but default <see cref="DelegatingHandler"/> is used.
/// </summary>
/// <typeparam name="T">QoS provider to use (by default use <see cref="PollyQoSProvider"/>).</typeparam>
/// <param name="builder">Ocelot builder to extend.</param>
/// <param name="errorMapping">Your customized error mapping.</param>
/// <returns>The reference to the same extended <see cref="IOcelotBuilder"/> object.</returns>
public static IOcelotBuilder AddPolly<T>(this IOcelotBuilder builder, Dictionary<Type, Func<Exception, Error>> errorMapping)
where T : class, IPollyQoSProvider<HttpResponseMessage> =>
AddPolly<T>(builder, DefaultDelegatingHandler, errorMapping);

/// <summary>
/// Adds Polly QoS provider to Ocelot with custom <see cref="DelegatingHandler"/> delegate, but default error mapping is used.
/// </summary>
/// <typeparam name="T">QoS provider to use (by default use <see cref="PollyQoSProvider"/>).</typeparam>
/// <param name="builder">Ocelot builder to extend.</param>
/// <param name="delegatingHandler">Your customized delegating handler (to manage QoS behavior by yourself).</param>
/// <returns>The reference to the same extended <see cref="IOcelotBuilder"/> object.</returns>
public static IOcelotBuilder AddPolly<T>(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler)
where T : class, IPollyQoSProvider<HttpResponseMessage> =>
AddPolly<T>(builder, delegatingHandler, DefaultErrorMapping);

/// <summary>
/// Adds Polly QoS provider to Ocelot by defaults.
/// </summary>
/// <remarks>
/// Defaults:
/// <list type="bullet">
/// <item><see cref="DefaultDelegatingHandler"/></item>
/// <item><see cref="DefaultErrorMapping"/></item>
/// </list>
/// </remarks>
/// <typeparam name="T">QoS provider to use (by default use <see cref="PollyQoSProvider"/>).</typeparam>
/// <param name="builder">Ocelot builder to extend.</param>
/// <returns>The reference to the same extended <see cref="IOcelotBuilder"/> object.</returns>
public static IOcelotBuilder AddPolly<T>(this IOcelotBuilder builder)
where T : class, IPollyQoSProvider<HttpResponseMessage> =>
AddPolly<T>(builder, DefaultDelegatingHandler, DefaultErrorMapping);

/// <summary>
/// Adds Polly QoS provider to Ocelot by defaults with default QoS provider.
/// </summary>
/// <remarks>
/// Defaults:
/// <list type="bullet">
/// <item><see cref="PollyQoSProvider"/></item>
/// <item><see cref="DefaultDelegatingHandler"/></item>
/// <item><see cref="DefaultErrorMapping"/></item>
/// </list>
/// </remarks>
/// <param name="builder">Ocelot builder to extend.</param>
/// <returns>The reference to the same extended <see cref="IOcelotBuilder"/> object.</returns>
public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) =>
AddPolly<PollyQoSProvider>(builder, DefaultDelegatingHandler, DefaultErrorMapping);

/// <summary>
/// Creates default delegating handler based on the <see cref="PollyPoliciesDelegatingHandler"/> type.
/// </summary>
/// <param name="route">The downstream route to apply the handler for.</param>
/// <param name="contextAccessor">The context accessor of the route.</param>
/// <param name="loggerFactory">The factory of logger.</param>
/// <returns>A <see cref="DelegatingHandler"/> object, but concreate type is the <see cref="PollyPoliciesDelegatingHandler"/> class.</returns>
public static DelegatingHandler DefaultDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory)
=> new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory);
}
93 changes: 81 additions & 12 deletions src/Ocelot/Configuration/Creator/RouteKeyCreator.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,86 @@
using Ocelot.Configuration.File;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.LoadBalancer.LoadBalancers;

namespace Ocelot.Configuration.Creator
{
public class RouteKeyCreator : IRouteKeyCreator
namespace Ocelot.Configuration.Creator;

public class RouteKeyCreator : IRouteKeyCreator
{
/// <summary>
/// Creates the unique <see langword="string"/> key based on the route properties for load balancing etc.
/// </summary>
/// <remarks>
/// Key template:
/// <list type="bullet">
/// <item>UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey</item>
/// </list>
/// </remarks>
/// <param name="fileRoute">The route object.</param>
/// <returns>A <see langword="string"/> object containing the key.</returns>
public string Create(FileRoute fileRoute)
{
public string Create(FileRoute fileRoute) => IsStickySession(fileRoute)
? $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}"
: $"{fileRoute.UpstreamPathTemplate}|{string.Join(',', fileRoute.UpstreamHttpMethod)}|{string.Join(',', fileRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}";
var isStickySession = fileRoute.LoadBalancerOptions is
{
Type: nameof(CookieStickySessions),
Key.Length: > 0
};

if (isStickySession)
{
return $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}";
}

var upstreamHttpMethods = Csv(fileRoute.UpstreamHttpMethod);
var downstreamHostAndPorts = Csv(fileRoute.DownstreamHostAndPorts.Select(downstream => $"{downstream.Host}:{downstream.Port}"));

var keyBuilder = new StringBuilder()

// UpstreamHttpMethod and UpstreamPathTemplate are required
.AppendNext(upstreamHttpMethods)
.AppendNext(fileRoute.UpstreamPathTemplate)

// Other properties are optional, replace undefined values with defaults to aid debugging
.AppendNext(Coalesce(fileRoute.UpstreamHost, "no-host"))

.AppendNext(Coalesce(downstreamHostAndPorts, "no-host-and-port"))
.AppendNext(Coalesce(fileRoute.ServiceNamespace, "no-svc-ns"))
.AppendNext(Coalesce(fileRoute.ServiceName, "no-svc-name"))
.AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Type, "no-lb-type"))
.AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Key, "no-lb-key"));

private static bool IsStickySession(FileRoute fileRoute) =>
!string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Type)
&& !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Key)
&& fileRoute.LoadBalancerOptions.Type == nameof(CookieStickySessions);
}
return keyBuilder.ToString();
}

/// <summary>
/// Helper function to convert multiple strings into a comma-separated string.
/// </summary>
/// <param name="values">The collection of strings to join by comma separator.</param>
/// <returns>A <see langword="string"/> in the comma-separated format.</returns>
private static string Csv(IEnumerable<string> values) => string.Join(',', values);

/// <summary>
/// Helper function to return the first non-null-or-whitespace string.
/// </summary>
/// <param name="first">The 1st string to check.</param>
/// <param name="second">The 2nd string to check.</param>
/// <returns>A <see langword="string"/> which is not empty.</returns>
private static string Coalesce(string first, string second) => string.IsNullOrWhiteSpace(first) ? second : first;
}

internal static class RouteKeyCreatorHelpers
{
/// <summary>
/// Helper function to append a string to the key builder, separated by a pipe.
/// </summary>
/// <param name="builder">The builder of the key.</param>
/// <param name="next">The next word to add.</param>
/// <returns>The reference to the builder.</returns>
public static StringBuilder AppendNext(this StringBuilder builder, string next)
{
if (builder.Length > 0)
{
builder.Append('|');
}

return builder.Append(next);
}
}
1 change: 1 addition & 0 deletions src/Ocelot/Errors/OcelotErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ public enum OcelotErrorCode
ConnectionToDownstreamServiceError = 38,
CouldNotFindLoadBalancerCreator = 39,
ErrorInvokingLoadBalancerCreator = 40,
PayloadTooLargeError = 41,
}
}
53 changes: 24 additions & 29 deletions src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Ocelot.Configuration;
using Ocelot.Responses;
using Ocelot.Responses;

namespace Ocelot.LoadBalancer.LoadBalancers
{
Expand All @@ -18,45 +18,40 @@ public Response<ILoadBalancer> Get(DownstreamRoute route, ServiceProviderConfigu
{
try
{
Response<ILoadBalancer> result;

if (_loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer))
{
loadBalancer = _loadBalancers[route.LoadBalancerKey];

{
// TODO Fix ugly reflection issue of dymanic detection in favor of static type property
if (route.LoadBalancerOptions.Type != loadBalancer.GetType().Name)
{
result = _factory.Get(route, config);
if (result.IsError)
{
return new ErrorResponse<ILoadBalancer>(result.Errors);
}

loadBalancer = result.Data;
AddLoadBalancer(route.LoadBalancerKey, loadBalancer);
{
return GetResponse(route, config);
}

return new OkResponse<ILoadBalancer>(loadBalancer);
}

result = _factory.Get(route, config);

if (result.IsError)
{
return new ErrorResponse<ILoadBalancer>(result.Errors);
}

loadBalancer = result.Data;
AddLoadBalancer(route.LoadBalancerKey, loadBalancer);
return new OkResponse<ILoadBalancer>(loadBalancer);
return GetResponse(route, config);
}
catch (Exception ex)
{
return new ErrorResponse<ILoadBalancer>(new List<Errors.Error>
{
new UnableToFindLoadBalancerError($"unabe to find load balancer for {route.LoadBalancerKey} exception is {ex}"),
});
return new ErrorResponse<ILoadBalancer>(
[
new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};"),
]);
}
}

private Response<ILoadBalancer> GetResponse(DownstreamRoute route, ServiceProviderConfiguration config)
{
var result = _factory.Get(route, config);

if (result.IsError)
{
return new ErrorResponse<ILoadBalancer>(result.Errors);
}

var loadBalancer = result.Data;
AddLoadBalancer(route.LoadBalancerKey, loadBalancer);
return new OkResponse<ILoadBalancer>(loadBalancer);
}

private void AddLoadBalancer(string key, ILoadBalancer loadBalancer)
Expand Down
2 changes: 1 addition & 1 deletion src/Ocelot/Middleware/HttpItemsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static IInternalConfiguration IInternalConfiguration(this IDictionary<obj
public static List<Error> Errors(this IDictionary<object, object> input)
{
var errors = input.Get<List<Error>>("Errors");
return errors ?? new List<Error>();
return errors ?? [];
}

public static DownstreamRouteFinder.DownstreamRouteHolder
Expand Down
Loading