Misc API review (#1087)
* Rename InitialDestinationsProbed -> InitialProbeCompleted * Rename First -> FirstAlphabetical * Refactor IAffinityFailurePolicy.Handle parameters * Refactor ILoadBalancingPolicy.PickDestination parameters * IAffinityFailurePolicy (revert order change) * Reorder IPassiveHealthCheckPolicy parameters * Rename GetProxyErrorFeature to GetForwarderErrorFeature * Rename session affinity providers to policies * Refactor FindAffinitizedDestinations * Refactor AffinitizeResponse
This commit is contained in:
Родитель
3001de0a9b
Коммит
51129362a4
|
@ -159,7 +159,7 @@ For additional fields see [ClusterConfig](xref:Yarp.ReverseProxy.Configuration.C
|
|||
"LoadBalancingPolicy" : "PowerOfTwoChoices", // Alternatively "First", "Random", "RoundRobin", "LeastRequests"
|
||||
"SessionAffinity": {
|
||||
"Enabled": true, // Defaults to 'false'
|
||||
"Provider": "Cookie", // Default, alternatively "CustomHeader"
|
||||
"Policy": "Cookie", // Default, alternatively "CustomHeader"
|
||||
"FailurePolicy": "Redistribute", // default, Alternatively "Return503"
|
||||
"Settings" : {
|
||||
"CustomHeaderName": "MySessionHeaderName" // Defaults to 'X-Yarp-Proxy-Affinity`
|
||||
|
|
|
@ -69,7 +69,7 @@ public void Configure(IApplicationBuilder app, IHttpForwarder forwarder)
|
|||
// Check if the operation was successful
|
||||
if (error != ForwarderError.None)
|
||||
{
|
||||
var errorFeature = httpContext.Features.Get<IForwarderErrorFeature>();
|
||||
var errorFeature = httpContext.GetForwarderErrorFeature();
|
||||
var exception = errorFeature.Exception;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -136,7 +136,7 @@ proxyPipeline.Use(async (context, next) =>
|
|||
{
|
||||
await next();
|
||||
|
||||
var errorFeature = context.GetProxyErrorFeature();
|
||||
var errorFeature = context.GetForwarderErrorFeature();
|
||||
if (errorFeature != null)
|
||||
{
|
||||
Report(errorFeature.Error, errorFeature.Exception);
|
||||
|
|
|
@ -8,7 +8,7 @@ Session affinity is a mechanism to bind (affinitize) a causally related request
|
|||
|
||||
## Configuration
|
||||
### Services and middleware registration
|
||||
Session affinity services are registered in the DI container via `AddSessionAffinityProvider()` method which is automatically called by `AddReverseProxy()`. The middleware `UseSessionAffinity()` is included by default in the parameterless MapReverseProxy method. If you are customizing the proxy pipeline, place the first middleware **before** adding `UseLoadBalancing()`.
|
||||
Session affinity services are registered in the DI container automatically by `AddReverseProxy()`. The middleware `UseSessionAffinity()` is included by default in the parameterless MapReverseProxy method. If you are customizing the proxy pipeline, place the first middleware **before** adding `UseLoadBalancing()`.
|
||||
|
||||
Example:
|
||||
```C#
|
||||
|
@ -27,7 +27,7 @@ Session affinity is configured per cluster according to the following configurat
|
|||
"<cluster-name>": {
|
||||
"SessionAffinity": {
|
||||
"Enabled": "(true|false)", // defaults to 'false'
|
||||
"Provider": "(Cookie|CustomHeader)", // defaults to 'Cookie'
|
||||
"Policy": "(Cookie|CustomHeader)", // defaults to 'Cookie'
|
||||
"FailurePolicy": "(Redistribute|Return503)", // defaults to 'Redistribute'
|
||||
"AffinityKeyName": "Key1",
|
||||
"Cookie": {
|
||||
|
@ -45,9 +45,9 @@ Session affinity is configured per cluster according to the following configurat
|
|||
}
|
||||
```
|
||||
|
||||
### Provider-specific configuration
|
||||
There is currently one provider-specific strongly-typed configuration section implemented.
|
||||
- `SessionAffinityCookieConfig` exposes settings to customize cookie properties which will be used by `CookieSessionAffinityProvider` for creating new affinity cookies. The properties can be JSON config as show above or in code as shown below:
|
||||
### Policy-specific configuration
|
||||
There is currently one policy-specific strongly-typed configuration section implemented.
|
||||
- `SessionAffinityCookieConfig` exposes settings to customize cookie properties which will be used by `CookieSessionAffinityPolicy` for creating new affinity cookies. The properties can be JSON config as show above or in code as shown below:
|
||||
```C#
|
||||
new ClusterConfig
|
||||
{
|
||||
|
@ -56,7 +56,7 @@ new ClusterConfig
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
@ -74,7 +74,7 @@ new ClusterConfig
|
|||
```
|
||||
|
||||
## Affinity Key
|
||||
Request to destination affinity is established via the affinity key identifying the target destination. That key can be stored on different request parts depending on the given session affinity implementation, but each request cannot have more than one such key. The exact key semantics is implementation dependent, in example the both of built-in `CookieSessionAffinityProvider` and `CustomHeaderAffinityProvider` are currently use `DestinationId` as the affinity key.
|
||||
Request to destination affinity is established via the affinity key identifying the target destination. That key can be stored on different request parts depending on the given session affinity implementation, but each request cannot have more than one such key. The exact key semantics is implementation dependent, in example the both of built-in `CookieSessionAffinityPolicy` and `CustomHeaderAffinityPolicy` are currently use `DestinationId` as the affinity key.
|
||||
|
||||
The current design doesn't require a key to uniquely identify the single affinitized destination. It's allowed to establish affinity to a destination group. In this case, the exact destination to handle the given request will be determined by the load balancer.
|
||||
|
||||
|
@ -86,12 +86,12 @@ Once a request arrives and gets routed to a cluster with session affinity enable
|
|||
|
||||
3. **Affinity key is invalid or no healthy affinitized destinations found**. It's treated as a failure to be handled by a failure policy explained below
|
||||
|
||||
If a new affinity was established for the request, the affinity key gets attached to a response where exact key representation and location depends on the implementation. Currently, there are two built-in providers storing the key on a cookie or custom header. Once the response gets delivered to the client, it's the client responsibility to attach the key to all following requests in the same session. Further, when the next request carrying the key arrives to the proxy, it resolves the existing affinity, but affinity key does not get again attached to the response. Thus, only the first response carries the affinity key.
|
||||
If a new affinity was established for the request, the affinity key gets attached to a response where exact key representation and location depends on the implementation. Currently, there are two built-in policies storing the key on a cookie or custom header. Once the response gets delivered to the client, it's the client responsibility to attach the key to all following requests in the same session. Further, when the next request carrying the key arrives to the proxy, it resolves the existing affinity, but affinity key does not get again attached to the response. Thus, only the first response carries the affinity key.
|
||||
|
||||
There are two built-in affinity providers differing only in how the affinity key is stored on a request. The default provider is `Cookie`.
|
||||
1. `Cookie` - stores the key as a cookie. It expects the request's key to be delivered as a cookie with configured name and sets the same cookie with `Set-Cookie` header on the first response in an affinitized sequence. This is implemented by `CookieSessionAffinityProvider`. Cookie name must be explicitly set via `SessionAffinityConfig.AffinityKeyName` which is used by `CookieSessionAffinityProvider` for this purpose. Other cookie's properties can be configured via `SessionAffinityCookieConfig`. **Important**: `AffinityKeyName` must be unique across all clusters with enabled session affinity to avoid conflicts.
|
||||
There are two built-in affinity policys differing only in how the affinity key is stored on a request. The default policy is `Cookie`.
|
||||
1. `Cookie` - stores the key as a cookie. It expects the request's key to be delivered as a cookie with configured name and sets the same cookie with `Set-Cookie` header on the first response in an affinitized sequence. This is implemented by `CookieSessionAffinityPolicy`. Cookie name must be explicitly set via `SessionAffinityConfig.AffinityKeyName` which is used by `CookieSessionAffinityPolicy` for this purpose. Other cookie's properties can be configured via `SessionAffinityCookieConfig`. **Important**: `AffinityKeyName` must be unique across all clusters with enabled session affinity to avoid conflicts.
|
||||
|
||||
2. `CustomHeader` - stores the key on a header. It expects the affinity key to be delivered in a custom header with configured name and sets the same header on the first response in an affinitized sequence. This is implemented by `CustomHeaderSessionAffinityProvider`. The header name must be set via `SessionAffinityConfig.AffinityKeyName` which is used by `CustomHeaderSessionAffinityProvider` for this purpose. **Important**: `AffinityKeyName` must be unique across all clusters with enabled session affinity to avoid conflicts.
|
||||
2. `CustomHeader` - stores the key on a header. It expects the affinity key to be delivered in a custom header with configured name and sets the same header on the first response in an affinitized sequence. This is implemented by `CustomHeaderSessionAffinityPolicy`. The header name must be set via `SessionAffinityConfig.AffinityKeyName` which is used by `CustomHeaderSessionAffinityPolicy` for this purpose. **Important**: `AffinityKeyName` must be unique across all clusters with enabled session affinity to avoid conflicts.
|
||||
|
||||
## Affinity failure policy
|
||||
If the affinity key cannot be decoded or no healthy destination found it's considered as a failure and an affinity failure policy is called to handle it. The policy has the full access to `HttpContext` and can send response to the client by itself. It returns a boolean value indicating whether the request processing can proceed down the pipeline or must be terminated.
|
||||
|
@ -103,6 +103,6 @@ There are two built-in failure policies. The default is `Redistribute`.
|
|||
|
||||
## Request pipeline
|
||||
The session affinity mechanisms are implemented by the services (mentioned above) and the two following middleware:
|
||||
1. `SessionAffinityMiddleware` - coordinates the request's affinity resolution process. First, it calls a provider specified for the given cluster on `ClusterConfig.SessionAffinity.Provider` property. Then, it checks the affinity resolution status returned by the provider, and calls a failure handling policy set on `ClusterConfig.SessionAffinity.FailurePolicy` in case of failures. It must be added to the pipeline **before** the load balancer.
|
||||
1. `SessionAffinityMiddleware` - coordinates the request's affinity resolution process. First, it calls a policy specified for the given cluster on `ClusterConfig.SessionAffinity.Policy` property. Then, it checks the affinity resolution status returned by the policy, and calls a failure handling policy set on `ClusterConfig.SessionAffinity.FailurePolicy` in case of failures. It must be added to the pipeline **before** the load balancer.
|
||||
|
||||
2. `AffinitizeTransform` - sets the key on the response if a new affinity has been established for the request. Otherwise, if the request follows an existing affinity, it does nothing. This is automatically added as a response transform.
|
||||
|
|
|
@ -79,7 +79,7 @@ namespace Yarp.Sample
|
|||
new ClusterConfig()
|
||||
{
|
||||
ClusterId = "cluster1",
|
||||
SessionAffinity = new SessionAffinityConfig { Enabled = true, Provider = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" },
|
||||
SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" },
|
||||
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "destination1", new DestinationConfig() { Address = "https://example.com" } },
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
"LoadBalancingPolicy": "PowerOfTwoChoices", // Alternatively "First", "Random", "RoundRobin", "LeastRequests"
|
||||
"SessionAffinity": { // Ensures subsequent requests from a client go to the same destination server
|
||||
"Enabled": true, // Defaults to 'false'
|
||||
"Provider": "Cookie", // Default, alternatively "CustomHeader"
|
||||
"Policy": "Cookie", // Default, alternatively "CustomHeader"
|
||||
"FailurePolicy": "Redistribute", // default, alternatively "Return503"
|
||||
"AffinityKeyName": "MySessionCookieName" // defaults to ".Yarp.ReverseProxy.Affinity"
|
||||
},
|
||||
|
|
|
@ -282,7 +282,7 @@ namespace Yarp.ReverseProxy.ServiceFabric
|
|||
SessionAffinity = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = GetLabel<bool?>(labels, "YARP.Backend.SessionAffinity.Enabled", null),
|
||||
Provider = GetLabel<string>(labels, "YARP.Backend.SessionAffinity.Provider", null),
|
||||
Policy = GetLabel<string>(labels, "YARP.Backend.SessionAffinity.Policy", null),
|
||||
FailurePolicy = GetLabel<string>(labels, "YARP.Backend.SessionAffinity.FailurePolicy", null),
|
||||
AffinityKeyName = GetLabel<string>(labels, "YARP.Backend.SessionAffinity.AffinityKeyName", null),
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
|
|
|
@ -211,7 +211,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider
|
|||
return new SessionAffinityConfig
|
||||
{
|
||||
Enabled = section.ReadBool(nameof(SessionAffinityConfig.Enabled)),
|
||||
Provider = section[nameof(SessionAffinityConfig.Provider)],
|
||||
Policy = section[nameof(SessionAffinityConfig.Policy)],
|
||||
FailurePolicy = section[nameof(SessionAffinityConfig.FailurePolicy)],
|
||||
AffinityKeyName = section[nameof(SessionAffinityConfig.AffinityKeyName)],
|
||||
Cookie = CreateSessionAffinityCookieConfig(section.GetSection(nameof(SessionAffinityConfig.Cookie)))
|
||||
|
|
|
@ -17,9 +17,9 @@ namespace Yarp.ReverseProxy.Configuration
|
|||
public bool? Enabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The session affinity provider to use.
|
||||
/// The session affinity policy to use.
|
||||
/// </summary>
|
||||
public string? Provider { get; init; }
|
||||
public string? Policy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Strategy handling missing destination for an affinitized request.
|
||||
|
@ -28,9 +28,9 @@ namespace Yarp.ReverseProxy.Configuration
|
|||
|
||||
/// <summary>
|
||||
/// Identifies the name of the field where the affinity value is stored.
|
||||
/// For the cookie affinity provider this will be the cookie name.
|
||||
/// For the header affinity provider this will be the header name.
|
||||
/// The provider will give its own default if no value is set.
|
||||
/// For the cookie affinity policy this will be the cookie name.
|
||||
/// For the header affinity policy this will be the header name.
|
||||
/// The policy will give its own default if no value is set.
|
||||
/// This value should be unique across clusters to avoid affinity conflicts.
|
||||
/// https://github.com/microsoft/reverse-proxy/issues/976
|
||||
/// This field is required.
|
||||
|
@ -39,7 +39,7 @@ namespace Yarp.ReverseProxy.Configuration
|
|||
|
||||
/// <summary>
|
||||
/// Configuration of a cookie storing the session affinity key in case
|
||||
/// the <see cref="Provider"/> is set to 'Cookie'.
|
||||
/// the <see cref="Policy"/> is set to 'Cookie'.
|
||||
/// </summary>
|
||||
public SessionAffinityCookieConfig? Cookie { get; init; }
|
||||
|
||||
|
@ -52,7 +52,7 @@ namespace Yarp.ReverseProxy.Configuration
|
|||
}
|
||||
|
||||
return Enabled == other.Enabled
|
||||
&& string.Equals(Provider, other.Provider, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(FailurePolicy, other.FailurePolicy, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(AffinityKeyName, other.AffinityKeyName, StringComparison.Ordinal)
|
||||
&& Cookie == other.Cookie;
|
||||
|
@ -62,7 +62,7 @@ namespace Yarp.ReverseProxy.Configuration
|
|||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Enabled,
|
||||
Provider?.GetHashCode(StringComparison.OrdinalIgnoreCase),
|
||||
Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase),
|
||||
FailurePolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase),
|
||||
AffinityKeyName?.GetHashCode(StringComparison.Ordinal),
|
||||
Cookie);
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace Yarp.ReverseProxy.Health
|
|||
_scheduler = new EntityActionScheduler<ClusterState>(cluster => ProbeCluster(cluster), autoStart: false, runOnce: false, timerFactory);
|
||||
}
|
||||
|
||||
public bool InitialDestinationsProbed { get; private set; }
|
||||
public bool InitialProbeCompleted { get; private set; }
|
||||
|
||||
public Task CheckHealthAsync(IEnumerable<ClusterState> clusters)
|
||||
{
|
||||
|
@ -62,7 +62,7 @@ namespace Yarp.ReverseProxy.Health
|
|||
}
|
||||
finally
|
||||
{
|
||||
InitialDestinationsProbed = true;
|
||||
InitialProbeCompleted = true;
|
||||
}
|
||||
|
||||
_scheduler.Start();
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace Yarp.ReverseProxy.Health
|
|||
/// <c>true</c> when all the initially configured destinations have been queried, regardless their availability or returned status code.
|
||||
/// The property stays <c>true</c> for the rest of the proxy process lifetime.
|
||||
/// </returns>
|
||||
bool InitialDestinationsProbed { get; }
|
||||
bool InitialProbeCompleted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks health of all clusters' destinations.
|
||||
|
|
|
@ -19,9 +19,9 @@ namespace Yarp.ReverseProxy.Health
|
|||
/// <summary>
|
||||
/// Registers a successful or failed request and evaluates a new <see cref="DestinationHealthState.Passive"/> value.
|
||||
/// </summary>
|
||||
/// <param name="context">Context.</param>
|
||||
/// <param name="cluster">Request's cluster.</param>
|
||||
/// <param name="destination">Request's destination.</param>
|
||||
/// <param name="context">Context.</param>
|
||||
void RequestProxied(ClusterState cluster, DestinationState destination, HttpContext context);
|
||||
void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace Yarp.ReverseProxy.Health
|
|||
|
||||
var policy = _policies.GetRequiredServiceById(options.Policy, HealthCheckConstants.PassivePolicy.TransportFailureRate);
|
||||
var cluster = context.GetRouteModel().Cluster!;
|
||||
policy.RequestProxied(cluster, proxyFeature.ProxiedDestination, context);
|
||||
policy.RequestProxied(context, cluster, proxyFeature.ProxiedDestination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ namespace Yarp.ReverseProxy.Health
|
|||
_healthUpdater = healthUpdater ?? throw new ArgumentNullException(nameof(healthUpdater));
|
||||
}
|
||||
|
||||
public void RequestProxied(ClusterState cluster, DestinationState destination, HttpContext context)
|
||||
public void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination)
|
||||
{
|
||||
var error = context.Features.Get<IForwarderErrorFeature>();
|
||||
var newHealth = EvaluateProxiedRequest(cluster, destination, error != null);
|
||||
|
|
|
@ -13,9 +13,9 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
/// </summary>
|
||||
internal sealed class FirstLoadBalancingPolicy : ILoadBalancingPolicy
|
||||
{
|
||||
public string Name => LoadBalancingPolicies.First;
|
||||
public string Name => LoadBalancingPolicies.FirstAlphabetical;
|
||||
|
||||
public DestinationState? PickDestination(HttpContext context, IReadOnlyList<DestinationState> availableDestinations)
|
||||
public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)
|
||||
{
|
||||
if (availableDestinations.Count == 0)
|
||||
{
|
||||
|
|
|
@ -21,8 +21,6 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
/// <summary>
|
||||
/// Picks a destination to send traffic to.
|
||||
/// </summary>
|
||||
DestinationState? PickDestination(
|
||||
HttpContext context,
|
||||
IReadOnlyList<DestinationState> availableDestinations);
|
||||
DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
{
|
||||
public string Name => LoadBalancingPolicies.LeastRequests;
|
||||
|
||||
public DestinationState? PickDestination(HttpContext context, IReadOnlyList<DestinationState> availableDestinations)
|
||||
public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)
|
||||
{
|
||||
if (availableDestinations.Count == 0)
|
||||
{
|
||||
|
|
|
@ -50,7 +50,7 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
else
|
||||
{
|
||||
var currentPolicy = _loadBalancingPolicies.GetRequiredServiceById(proxyFeature.Cluster.Config.LoadBalancingPolicy, LoadBalancingPolicies.PowerOfTwoChoices);
|
||||
destination = currentPolicy.PickDestination(context, destinations);
|
||||
destination = currentPolicy.PickDestination(context, proxyFeature.Route.Cluster!, destinations);
|
||||
}
|
||||
|
||||
if (destination == null)
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
/// <summary>
|
||||
/// Select the alphabetically first available destination without considering load. This is useful for dual destination fail-over systems.
|
||||
/// </summary>
|
||||
public static string First => nameof(First);
|
||||
public static string FirstAlphabetical => nameof(FirstAlphabetical);
|
||||
|
||||
/// <summary>
|
||||
/// Select a destination randomly.
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
|
||||
public string Name => LoadBalancingPolicies.PowerOfTwoChoices;
|
||||
|
||||
public DestinationState? PickDestination(HttpContext context, IReadOnlyList<DestinationState> availableDestinations)
|
||||
public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)
|
||||
{
|
||||
if (availableDestinations.Count == 0)
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
|
||||
public string Name => LoadBalancingPolicies.Random;
|
||||
|
||||
public DestinationState? PickDestination(HttpContext context, IReadOnlyList<DestinationState> availableDestinations)
|
||||
public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)
|
||||
{
|
||||
if (availableDestinations.Count == 0)
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Yarp.ReverseProxy.LoadBalancing
|
|||
|
||||
public string Name => LoadBalancingPolicies.RoundRobin;
|
||||
|
||||
public DestinationState? PickDestination(HttpContext context, IReadOnlyList<DestinationState> availableDestinations)
|
||||
public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)
|
||||
{
|
||||
if (availableDestinations.Count == 0)
|
||||
{
|
||||
|
|
|
@ -75,15 +75,15 @@ namespace Yarp.ReverseProxy.Management
|
|||
return builder;
|
||||
}
|
||||
|
||||
public static IReverseProxyBuilder AddSessionAffinityProvider(this IReverseProxyBuilder builder)
|
||||
public static IReverseProxyBuilder AddSessionAffinityPolicies(this IReverseProxyBuilder builder)
|
||||
{
|
||||
builder.Services.TryAddEnumerable(new[] {
|
||||
ServiceDescriptor.Singleton<IAffinityFailurePolicy, RedistributeAffinityFailurePolicy>(),
|
||||
ServiceDescriptor.Singleton<IAffinityFailurePolicy, Return503ErrorAffinityFailurePolicy>()
|
||||
});
|
||||
builder.Services.TryAddEnumerable(new[] {
|
||||
ServiceDescriptor.Singleton<ISessionAffinityProvider, CookieSessionAffinityProvider>(),
|
||||
ServiceDescriptor.Singleton<ISessionAffinityProvider, CustomHeaderSessionAffinityProvider>()
|
||||
ServiceDescriptor.Singleton<ISessionAffinityPolicy, CookieSessionAffinityPolicy>(),
|
||||
ServiceDescriptor.Singleton<ISessionAffinityPolicy, CustomHeaderSessionAffinityPolicy>()
|
||||
});
|
||||
builder.AddTransforms<AffinitizeTransformProvider>();
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
.AddConfigBuilder()
|
||||
.AddRuntimeStateManagers()
|
||||
.AddConfigManager()
|
||||
.AddSessionAffinityProvider()
|
||||
.AddSessionAffinityPolicies()
|
||||
.AddActiveHealthChecks()
|
||||
.AddPassiveHealthCheck()
|
||||
.AddLoadBalancingPolicies()
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Http
|
|||
/// <summary>
|
||||
/// Retrieves the <see cref="IForwarderErrorFeature"/> instance associated with the current request, if any.
|
||||
/// </summary>
|
||||
public static IForwarderErrorFeature? GetProxyErrorFeature(this HttpContext context)
|
||||
public static IForwarderErrorFeature? GetForwarderErrorFeature(this HttpContext context)
|
||||
{
|
||||
return context.Features.Get<IForwarderErrorFeature>();
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
/// </summary>
|
||||
internal sealed class AffinitizeTransform : ResponseTransform
|
||||
{
|
||||
private readonly ISessionAffinityProvider _sessionAffinityProvider;
|
||||
private readonly ISessionAffinityPolicy _sessionAffinityPolicy;
|
||||
|
||||
public AffinitizeTransform(ISessionAffinityProvider sessionAffinityProvider)
|
||||
public AffinitizeTransform(ISessionAffinityPolicy sessionAffinityPolicy)
|
||||
{
|
||||
_sessionAffinityProvider = sessionAffinityProvider ?? throw new ArgumentNullException(nameof(sessionAffinityProvider));
|
||||
_sessionAffinityPolicy = sessionAffinityPolicy ?? throw new ArgumentNullException(nameof(sessionAffinityPolicy));
|
||||
}
|
||||
|
||||
public override ValueTask ApplyAsync(ResponseTransformContext context)
|
||||
|
@ -29,7 +29,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
// The transform should only be added to routes that have affinity enabled.
|
||||
Debug.Assert(options?.Enabled ?? true, "Session affinity is not enabled");
|
||||
var selectedDestination = proxyFeature.ProxiedDestination!;
|
||||
_sessionAffinityProvider.AffinitizeResponse(context.HttpContext, options!, selectedDestination);
|
||||
_sessionAffinityPolicy.AffinitizeResponse(context.HttpContext, proxyFeature.Route.Cluster!, options!, selectedDestination);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
{
|
||||
internal sealed class AffinitizeTransformProvider : ITransformProvider
|
||||
{
|
||||
private readonly IDictionary<string, ISessionAffinityProvider> _sessionAffinityProviders;
|
||||
private readonly IDictionary<string, ISessionAffinityPolicy> _sessionAffinityPolicies;
|
||||
|
||||
public AffinitizeTransformProvider(IEnumerable<ISessionAffinityProvider> sessionAffinityProviders)
|
||||
public AffinitizeTransformProvider(IEnumerable<ISessionAffinityPolicy> sessionAffinityPolicies)
|
||||
{
|
||||
_sessionAffinityProviders = sessionAffinityProviders?.ToDictionaryByUniqueId(p => p.Name)
|
||||
?? throw new ArgumentNullException(nameof(sessionAffinityProviders));
|
||||
_sessionAffinityPolicies = sessionAffinityPolicies?.ToDictionaryByUniqueId(p => p.Name)
|
||||
?? throw new ArgumentNullException(nameof(sessionAffinityPolicies));
|
||||
}
|
||||
|
||||
public void ValidateRoute(TransformRouteValidationContext context)
|
||||
|
@ -30,16 +30,16 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
return;
|
||||
}
|
||||
|
||||
var provider = context.Cluster.SessionAffinity.Provider;
|
||||
if (string.IsNullOrEmpty(provider))
|
||||
var policy = context.Cluster.SessionAffinity.Policy;
|
||||
if (string.IsNullOrEmpty(policy))
|
||||
{
|
||||
// The default.
|
||||
provider = SessionAffinityConstants.Providers.Cookie;
|
||||
policy = SessionAffinityConstants.Policies.Cookie;
|
||||
}
|
||||
|
||||
if (!_sessionAffinityProviders.ContainsKey(provider))
|
||||
if (!_sessionAffinityPolicies.ContainsKey(policy))
|
||||
{
|
||||
context.Errors.Add(new ArgumentException($"No matching {nameof(ISessionAffinityProvider)} found for the session affinity provider '{provider}' set on the cluster '{context.Cluster.ClusterId}'."));
|
||||
context.Errors.Add(new ArgumentException($"No matching {nameof(ISessionAffinityPolicy)} found for the session affinity policy '{policy}' set on the cluster '{context.Cluster.ClusterId}'."));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,8 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
|
||||
if (options != null && options.Enabled.GetValueOrDefault())
|
||||
{
|
||||
var provider = _sessionAffinityProviders.GetRequiredServiceById(options.Provider, SessionAffinityConstants.Providers.Cookie);
|
||||
context.ResponseTransforms.Add(new AffinitizeTransform(provider));
|
||||
var policy = _sessionAffinityPolicies.GetRequiredServiceById(options.Policy, SessionAffinityConstants.Policies.Cookie);
|
||||
context.ResponseTransforms.Add(new AffinitizeTransform(policy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ using Yarp.ReverseProxy.Model;
|
|||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity
|
||||
{
|
||||
internal abstract class BaseSessionAffinityProvider<T> : ISessionAffinityProvider
|
||||
internal abstract class BaseSessionAffinityPolicy<T> : ISessionAffinityPolicy
|
||||
{
|
||||
private readonly IDataProtector _dataProtector;
|
||||
protected static readonly object AffinityKeyId = new object();
|
||||
protected readonly ILogger Logger;
|
||||
|
||||
protected BaseSessionAffinityProvider(IDataProtectionProvider dataProtectionProvider, ILogger logger)
|
||||
protected BaseSessionAffinityPolicy(IDataProtectionProvider dataProtectionProvider, ILogger logger)
|
||||
{
|
||||
_dataProtector = dataProtectionProvider?.CreateProtector(GetType().FullName!) ?? throw new ArgumentNullException(nameof(dataProtectionProvider));
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
@ -26,7 +26,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
|
||||
public abstract string Name { get; }
|
||||
|
||||
public virtual void AffinitizeResponse(HttpContext context, SessionAffinityConfig config, DestinationState destination)
|
||||
public virtual void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination)
|
||||
{
|
||||
if (!config.Enabled.GetValueOrDefault())
|
||||
{
|
||||
|
@ -37,18 +37,18 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
if (!context.Items.ContainsKey(AffinityKeyId))
|
||||
{
|
||||
var affinityKey = GetDestinationAffinityKey(destination);
|
||||
SetAffinityKey(context, config, affinityKey);
|
||||
SetAffinityKey(context, cluster, config, affinityKey);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual AffinityResult FindAffinitizedDestinations(HttpContext context, IReadOnlyList<DestinationState> destinations, string clusterId, SessionAffinityConfig config)
|
||||
public virtual AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList<DestinationState> destinations)
|
||||
{
|
||||
if (!config.Enabled.GetValueOrDefault())
|
||||
{
|
||||
throw new InvalidOperationException($"Session affinity is disabled for cluster {clusterId}.");
|
||||
throw new InvalidOperationException($"Session affinity is disabled for cluster {cluster.ClusterId}.");
|
||||
}
|
||||
|
||||
var requestAffinityKey = GetRequestAffinityKey(context, config);
|
||||
var requestAffinityKey = GetRequestAffinityKey(context, cluster, config);
|
||||
|
||||
if (requestAffinityKey.Key == null)
|
||||
{
|
||||
|
@ -72,12 +72,12 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
|
||||
if (matchingDestinations == null)
|
||||
{
|
||||
Log.DestinationMatchingToAffinityKeyNotFound(Logger, clusterId);
|
||||
Log.DestinationMatchingToAffinityKeyNotFound(Logger, cluster.ClusterId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(Logger, clusterId);
|
||||
Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(Logger, cluster.ClusterId);
|
||||
}
|
||||
|
||||
// Empty destination list passed to this method is handled the same way as if no matching destinations are found.
|
||||
|
@ -92,9 +92,9 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
|
||||
protected abstract T GetDestinationAffinityKey(DestinationState destination);
|
||||
|
||||
protected abstract (T? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, SessionAffinityConfig config);
|
||||
protected abstract (T? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config);
|
||||
|
||||
protected abstract void SetAffinityKey(HttpContext context, SessionAffinityConfig config, T unencryptedKey);
|
||||
protected abstract void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, T unencryptedKey);
|
||||
|
||||
protected string Protect(string unencryptedKey)
|
||||
{
|
|
@ -11,33 +11,33 @@ using Yarp.ReverseProxy.Utilities;
|
|||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity
|
||||
{
|
||||
internal sealed class CookieSessionAffinityProvider : BaseSessionAffinityProvider<string>
|
||||
internal sealed class CookieSessionAffinityPolicy : BaseSessionAffinityPolicy<string>
|
||||
{
|
||||
private readonly IClock _clock;
|
||||
|
||||
public CookieSessionAffinityProvider(
|
||||
public CookieSessionAffinityPolicy(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IClock clock,
|
||||
ILogger<CookieSessionAffinityProvider> logger)
|
||||
ILogger<CookieSessionAffinityPolicy> logger)
|
||||
: base(dataProtectionProvider, logger)
|
||||
{
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
}
|
||||
|
||||
public override string Name => SessionAffinityConstants.Providers.Cookie;
|
||||
public override string Name => SessionAffinityConstants.Policies.Cookie;
|
||||
|
||||
protected override string GetDestinationAffinityKey(DestinationState destination)
|
||||
{
|
||||
return destination.DestinationId;
|
||||
}
|
||||
|
||||
protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, SessionAffinityConfig config)
|
||||
protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config)
|
||||
{
|
||||
var encryptedRequestKey = context.Request.Cookies.TryGetValue(config.AffinityKeyName, out var keyInCookie) ? keyInCookie : null;
|
||||
return Unprotect(encryptedRequestKey);
|
||||
}
|
||||
|
||||
protected override void SetAffinityKey(HttpContext context, SessionAffinityConfig config, string unencryptedKey)
|
||||
protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey)
|
||||
{
|
||||
var affinityCookieOptions = new CookieOptions
|
||||
{
|
|
@ -11,22 +11,22 @@ using Yarp.ReverseProxy.Model;
|
|||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity
|
||||
{
|
||||
internal sealed class CustomHeaderSessionAffinityProvider : BaseSessionAffinityProvider<string>
|
||||
internal sealed class CustomHeaderSessionAffinityPolicy : BaseSessionAffinityPolicy<string>
|
||||
{
|
||||
public CustomHeaderSessionAffinityProvider(
|
||||
public CustomHeaderSessionAffinityPolicy(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<CustomHeaderSessionAffinityProvider> logger)
|
||||
ILogger<CustomHeaderSessionAffinityPolicy> logger)
|
||||
: base(dataProtectionProvider, logger)
|
||||
{}
|
||||
|
||||
public override string Name => SessionAffinityConstants.Providers.CustomHeader;
|
||||
public override string Name => SessionAffinityConstants.Policies.CustomHeader;
|
||||
|
||||
protected override string GetDestinationAffinityKey(DestinationState destination)
|
||||
{
|
||||
return destination.DestinationId;
|
||||
}
|
||||
|
||||
protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, SessionAffinityConfig config)
|
||||
protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config)
|
||||
{
|
||||
var customHeaderName = config.AffinityKeyName;
|
||||
var keyHeaderValues = context.Request.Headers[customHeaderName];
|
||||
|
@ -47,7 +47,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
return Unprotect(keyHeaderValues[0]);
|
||||
}
|
||||
|
||||
protected override void SetAffinityKey(HttpContext context, SessionAffinityConfig config, string unencryptedKey)
|
||||
protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey)
|
||||
{
|
||||
context.Response.Headers.Append(config.AffinityKeyName, Protect(unencryptedKey));
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
using Yarp.ReverseProxy.Model;
|
||||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity
|
||||
{
|
||||
|
@ -22,12 +22,12 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
/// and can change it in any way.
|
||||
/// </summary>
|
||||
/// <param name="context">Current request's context.</param>
|
||||
/// <param name="config">Session affinity config for the cluster.</param>
|
||||
/// <param name="cluster">The associated cluster for the request.</param>
|
||||
/// <param name="affinityStatus">Affinity resolution status.</param>
|
||||
/// <returns>
|
||||
/// 'true' if the failure is considered recoverable and the request processing can proceed.
|
||||
/// Otherwise, 'false' indicating that an error response has been generated and the request's processing must be terminated.
|
||||
/// </returns>
|
||||
Task<bool> Handle(HttpContext context, SessionAffinityConfig config, AffinityStatus affinityStatus);
|
||||
Task<bool> Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
/// <summary>
|
||||
/// Provides session affinity for load-balanced clusters.
|
||||
/// </summary>
|
||||
public interface ISessionAffinityProvider
|
||||
public interface ISessionAffinityPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique identifier for this session affinity implementation. This will be referenced from config.
|
||||
|
@ -22,18 +22,19 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
/// Finds <see cref="DestinationState"/> to which the current request is affinitized by the affinity key.
|
||||
/// </summary>
|
||||
/// <param name="context">Current request's context.</param>
|
||||
/// <param name="destinations"><see cref="DestinationState"/>s available for the request.</param>
|
||||
/// <param name="clusterId">Target cluster ID.</param>
|
||||
/// <param name="cluster">Current request's cluster.</param>
|
||||
/// <param name="config">Affinity config.</param>
|
||||
/// <param name="destinations"><see cref="DestinationState"/>s available for the request.</param>
|
||||
/// <returns><see cref="AffinityResult"/> carrying the found affinitized destinations if any and the <see cref="AffinityStatus"/>.</returns>
|
||||
AffinityResult FindAffinitizedDestinations(HttpContext context, IReadOnlyList<DestinationState> destinations, string clusterId, SessionAffinityConfig config);
|
||||
AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList<DestinationState> destinations);
|
||||
|
||||
/// <summary>
|
||||
/// Affinitize the current response to the given <see cref="DestinationState"/> by setting the affinity key extracted from <see cref="DestinationState"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">Current request's context.</param>
|
||||
/// <param name="cluster">Current request's cluster.</param>
|
||||
/// <param name="config">Affinity config.</param>
|
||||
/// <param name="destination"><see cref="DestinationState"/> to which request is to be affinitized.</param>
|
||||
void AffinitizeResponse(HttpContext context, SessionAffinityConfig config, DestinationState destination);
|
||||
void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
using Yarp.ReverseProxy.Model;
|
||||
using Yarp.ReverseProxy.Utilities;
|
||||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity
|
||||
|
@ -13,7 +13,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
{
|
||||
public string Name => SessionAffinityConstants.FailurePolicies.Redistribute;
|
||||
|
||||
public Task<bool> Handle(HttpContext context, SessionAffinityConfig config, AffinityStatus affinityStatus)
|
||||
public Task<bool> Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus)
|
||||
{
|
||||
if (affinityStatus == AffinityStatus.OK
|
||||
|| affinityStatus == AffinityStatus.AffinityKeyNotSet)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
using Yarp.ReverseProxy.Model;
|
||||
using Yarp.ReverseProxy.Utilities;
|
||||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity
|
||||
|
@ -13,7 +13,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
{
|
||||
public string Name => SessionAffinityConstants.FailurePolicies.Return503Error;
|
||||
|
||||
public Task<bool> Handle(HttpContext context, SessionAffinityConfig config, AffinityStatus affinityStatus)
|
||||
public Task<bool> Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus)
|
||||
{
|
||||
if (affinityStatus == AffinityStatus.OK
|
||||
|| affinityStatus == AffinityStatus.AffinityKeyNotSet)
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
/// </summary>
|
||||
public static class SessionAffinityConstants
|
||||
{
|
||||
public static class Providers
|
||||
public static class Policies
|
||||
{
|
||||
public static string Cookie => nameof(Cookie);
|
||||
|
||||
|
|
|
@ -18,19 +18,19 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
internal sealed class SessionAffinityMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IDictionary<string, ISessionAffinityProvider> _sessionAffinityProviders;
|
||||
private readonly IDictionary<string, ISessionAffinityPolicy> _sessionAffinityPolicies;
|
||||
private readonly IDictionary<string, IAffinityFailurePolicy> _affinityFailurePolicies;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SessionAffinityMiddleware(
|
||||
RequestDelegate next,
|
||||
IEnumerable<ISessionAffinityProvider> sessionAffinityProviders,
|
||||
IEnumerable<ISessionAffinityPolicy> sessionAffinityPolicies,
|
||||
IEnumerable<IAffinityFailurePolicy> affinityFailurePolicies,
|
||||
ILogger<SessionAffinityMiddleware> logger)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_sessionAffinityProviders = sessionAffinityProviders?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(sessionAffinityProviders));
|
||||
_sessionAffinityPolicies = sessionAffinityPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(sessionAffinityPolicies));
|
||||
_affinityFailurePolicies = affinityFailurePolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(affinityFailurePolicies));
|
||||
}
|
||||
|
||||
|
@ -38,23 +38,23 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
{
|
||||
var proxyFeature = context.GetReverseProxyFeature();
|
||||
|
||||
var cluster = proxyFeature.Cluster.Config;
|
||||
var config = cluster.SessionAffinity;
|
||||
var config = proxyFeature.Cluster.Config.SessionAffinity;
|
||||
|
||||
if (config == null || !config.Enabled.GetValueOrDefault())
|
||||
{
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
return InvokeInternal(context, proxyFeature, config, cluster.ClusterId);
|
||||
return InvokeInternal(context, proxyFeature, config);
|
||||
}
|
||||
|
||||
private async Task InvokeInternal(HttpContext context, IReverseProxyFeature proxyFeature, SessionAffinityConfig config, string clusterId)
|
||||
private async Task InvokeInternal(HttpContext context, IReverseProxyFeature proxyFeature, SessionAffinityConfig config)
|
||||
{
|
||||
var destinations = proxyFeature.AvailableDestinations;
|
||||
var cluster = proxyFeature.Route.Cluster!;
|
||||
|
||||
var currentProvider = _sessionAffinityProviders.GetRequiredServiceById(config.Provider, SessionAffinityConstants.Providers.Cookie);
|
||||
var affinityResult = currentProvider.FindAffinitizedDestinations(context, destinations, clusterId, config);
|
||||
var policy = _sessionAffinityPolicies.GetRequiredServiceById(config.Policy, SessionAffinityConstants.Policies.Cookie);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, config, destinations);
|
||||
|
||||
switch (affinityResult.Status)
|
||||
{
|
||||
|
@ -68,17 +68,17 @@ namespace Yarp.ReverseProxy.SessionAffinity
|
|||
case AffinityStatus.DestinationNotFound:
|
||||
|
||||
var failurePolicy = _affinityFailurePolicies.GetRequiredServiceById(config.FailurePolicy, SessionAffinityConstants.FailurePolicies.Redistribute);
|
||||
var keepProcessing = await failurePolicy.Handle(context, config, affinityResult.Status);
|
||||
var keepProcessing = await failurePolicy.Handle(context, proxyFeature.Route.Cluster!, affinityResult.Status);
|
||||
|
||||
if (!keepProcessing)
|
||||
{
|
||||
// Policy reported the failure is unrecoverable and took the full responsibility for its handling,
|
||||
// so we simply stop processing.
|
||||
Log.AffinityResolutionFailedForCluster(_logger, clusterId);
|
||||
Log.AffinityResolutionFailedForCluster(_logger, cluster.ClusterId);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.AffinityResolutionFailureWasHandledProcessingWillBeContinued(_logger, clusterId, failurePolicy.Name);
|
||||
Log.AffinityResolutionFailureWasHandledProcessingWillBeContinued(_logger, cluster.ClusterId, failurePolicy.Name);
|
||||
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace Yarp.ReverseProxy.ServiceFabric.Tests
|
|||
{ "YARP.Backend.BackendId", "MyCoolClusterId" },
|
||||
{ "YARP.Backend.LoadBalancingPolicy", "LeastRequests" },
|
||||
{ "YARP.Backend.SessionAffinity.Enabled", "true" },
|
||||
{ "YARP.Backend.SessionAffinity.Provider", "Cookie" },
|
||||
{ "YARP.Backend.SessionAffinity.Policy", "Cookie" },
|
||||
{ "YARP.Backend.SessionAffinity.FailurePolicy", "Return503Error" },
|
||||
{ "YARP.Backend.SessionAffinity.AffinityKeyName", "Key1" },
|
||||
{ "YARP.Backend.SessionAffinity.Cookie.Domain", "localhost" },
|
||||
|
@ -75,7 +75,7 @@ namespace Yarp.ReverseProxy.ServiceFabric.Tests
|
|||
SessionAffinity = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = SessionAffinityConstants.Providers.Cookie,
|
||||
Policy = SessionAffinityConstants.Policies.Cookie,
|
||||
FailurePolicy = SessionAffinityConstants.FailurePolicies.Return503Error,
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
|
|
|
@ -66,7 +66,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
@ -147,7 +147,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
@ -238,7 +238,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
@ -280,7 +280,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
@ -330,7 +330,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
|
|
@ -76,7 +76,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "Return503Error",
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
AffinityKeyName = "Key1",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
{
|
||||
|
@ -188,7 +188,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
|
|||
""LoadBalancingPolicy"": ""Random"",
|
||||
""SessionAffinity"": {
|
||||
""Enabled"": true,
|
||||
""Provider"": ""Cookie"",
|
||||
""Policy"": ""Cookie"",
|
||||
""FailurePolicy"": ""Return503Error"",
|
||||
""AffinityKeyName"": ""Key1"",
|
||||
""Cookie"": {
|
||||
|
@ -487,7 +487,7 @@ namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests
|
|||
Assert.Equal(LoadBalancingPolicies.Random, abstractCluster1.LoadBalancingPolicy);
|
||||
Assert.Equal(cluster1.SessionAffinity.Enabled, abstractCluster1.SessionAffinity.Enabled);
|
||||
Assert.Equal(cluster1.SessionAffinity.FailurePolicy, abstractCluster1.SessionAffinity.FailurePolicy);
|
||||
Assert.Equal(cluster1.SessionAffinity.Provider, abstractCluster1.SessionAffinity.Provider);
|
||||
Assert.Equal(cluster1.SessionAffinity.Policy, abstractCluster1.SessionAffinity.Policy);
|
||||
Assert.Equal(cluster1.SessionAffinity.AffinityKeyName, abstractCluster1.SessionAffinity.AffinityKeyName);
|
||||
Assert.Equal(cluster1.SessionAffinity.Cookie.Domain, abstractCluster1.SessionAffinity.Cookie.Domain);
|
||||
Assert.Equal(cluster1.SessionAffinity.Cookie.Expiration, abstractCluster1.SessionAffinity.Cookie.Expiration);
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "policy1",
|
||||
Provider = "provider1",
|
||||
Policy = "policy1",
|
||||
AffinityKeyName = "Key1"
|
||||
};
|
||||
|
||||
|
@ -22,7 +22,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "policy1",
|
||||
Provider = "provider1",
|
||||
Policy = "policy1",
|
||||
AffinityKeyName = "Key1"
|
||||
};
|
||||
|
||||
|
@ -38,7 +38,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "policy1",
|
||||
Provider = "provider1",
|
||||
Policy = "policy1",
|
||||
AffinityKeyName = "Key1"
|
||||
};
|
||||
|
||||
|
@ -46,7 +46,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = false,
|
||||
FailurePolicy = "policy2",
|
||||
Provider = "provider2",
|
||||
Policy = "policy2",
|
||||
AffinityKeyName = "Key1"
|
||||
};
|
||||
|
||||
|
@ -62,7 +62,7 @@ namespace Yarp.ReverseProxy.Configuration.Tests
|
|||
{
|
||||
Enabled = true,
|
||||
FailurePolicy = "policy1",
|
||||
Provider = "provider1",
|
||||
Policy = "policy1",
|
||||
AffinityKeyName = "Key1"
|
||||
};
|
||||
|
||||
|
|
|
@ -46,11 +46,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object);
|
||||
clusters.Add(cluster2);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) });
|
||||
|
||||
|
@ -103,11 +103,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, TimeSpan.FromMilliseconds(Interval1));
|
||||
monitor.OnClusterAdded(cluster2);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(new ClusterState[0]);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
timerFactory.FireAll();
|
||||
|
||||
|
@ -136,11 +136,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: TimeSpan.FromMilliseconds(Interval1));
|
||||
monitor.OnClusterAdded(cluster2);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(new ClusterState[0]);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
timerFactory.FireAll();
|
||||
|
||||
|
@ -175,11 +175,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, interval: TimeSpan.FromMilliseconds(Interval0));
|
||||
monitor.OnClusterAdded(cluster0);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(new ClusterState[0]);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
timerFactory.FireAll();
|
||||
|
||||
|
@ -218,11 +218,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: TimeSpan.FromMilliseconds(Interval1));
|
||||
monitor.OnClusterAdded(cluster2);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(new ClusterState[0]);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
timerFactory.FireAll();
|
||||
|
||||
|
@ -267,11 +267,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: TimeSpan.FromMilliseconds(Interval1));
|
||||
monitor.OnClusterAdded(cluster2);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(new ClusterState[0]);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
timerFactory.FireAll();
|
||||
|
||||
|
@ -316,11 +316,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster = GetClusterInfo("cluster0", "policy0", true, httpClient.Object, destinationCount: 3);
|
||||
clusters.Add(cluster);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(
|
||||
p => p.ProbingCompleted(
|
||||
|
@ -378,11 +378,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster = GetClusterInfo("cluster0", "policy0", true, httpClient.Object, destinationCount: 3);
|
||||
clusters.Add(cluster);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(
|
||||
p => p.ProbingCompleted(
|
||||
|
@ -407,11 +407,11 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster = GetClusterInfo("cluster0", "policy0", true, httpClient.Object);
|
||||
clusters.Add(cluster);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
await monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Once);
|
||||
policy.Verify(p => p.Name);
|
||||
|
@ -446,28 +446,28 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster2 = GetClusterInfo("cluster2", "policy0", true, httpClient2.Object, destinationCount: 1);
|
||||
clusters.Add(cluster2);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
var healthCheckTask = monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs0.SetResult(new HttpResponseMessage(firstResult));
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs1.SetResult(new HttpResponseMessage(secondResult));
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs2.SetResult(new HttpResponseMessage(thirdResult));
|
||||
|
||||
await healthCheckTask;
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(3));
|
||||
policy.Verify(p => p.Name);
|
||||
|
@ -492,23 +492,23 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1);
|
||||
clusters.Add(cluster1);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
var healthCheckTask = monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs0.SetResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
// Never set result to the second destination for it to time out.
|
||||
|
||||
await healthCheckTask;
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));
|
||||
policy.Verify(p => p.Name);
|
||||
|
@ -533,7 +533,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1);
|
||||
clusters.Add(cluster1);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
var healthCheckTask = monitor.CheckHealthAsync(clusters);
|
||||
|
||||
|
@ -541,7 +541,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
|
||||
await healthCheckTask;
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));
|
||||
policy.Verify(p => p.Name);
|
||||
|
@ -566,23 +566,23 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1);
|
||||
clusters.Add(cluster1);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
var healthCheckTask = monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs0.SetException(new Exception());
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs1.SetResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
await healthCheckTask;
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));
|
||||
policy.Verify(p => p.Name);
|
||||
|
@ -607,23 +607,23 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1);
|
||||
clusters.Add(cluster1);
|
||||
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
var healthCheckTask = monitor.CheckHealthAsync(clusters);
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs0.SetException(new Exception());
|
||||
|
||||
Assert.False(healthCheckTask.IsCompleted);
|
||||
Assert.False(monitor.InitialDestinationsProbed);
|
||||
Assert.False(monitor.InitialProbeCompleted);
|
||||
|
||||
tcs1.SetException(new Exception());
|
||||
|
||||
await healthCheckTask;
|
||||
|
||||
Assert.True(monitor.InitialDestinationsProbed);
|
||||
Assert.True(monitor.InitialProbeCompleted);
|
||||
|
||||
policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));
|
||||
policy.Verify(p => p.Name);
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
await middleware.Invoke(context0);
|
||||
|
||||
Assert.True(nextInvoked);
|
||||
policies[0].Verify(p => p.RequestProxied(cluster0, cluster0.DestinationsState.AllDestinations[1], context0), Times.Once);
|
||||
policies[0].Verify(p => p.RequestProxied(context0, cluster0, cluster0.DestinationsState.AllDestinations[1]), Times.Once);
|
||||
policies[0].VerifyGet(p => p.Name, Times.Once);
|
||||
policies[0].VerifyNoOtherCalls();
|
||||
policies[1].VerifyGet(p => p.Name, Times.Once);
|
||||
|
@ -44,7 +44,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
await middleware.Invoke(context1);
|
||||
|
||||
Assert.True(nextInvoked);
|
||||
policies[1].Verify(p => p.RequestProxied(cluster1, cluster1.DestinationsState.AllDestinations[0], context1), Times.Once);
|
||||
policies[1].Verify(p => p.RequestProxied(context1, cluster1, cluster1.DestinationsState.AllDestinations[0]), Times.Once);
|
||||
policies[1].VerifyNoOtherCalls();
|
||||
policies[0].VerifyNoOtherCalls();
|
||||
}
|
||||
|
|
|
@ -41,10 +41,10 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// Successful requests
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster0, cluster0.Destinations.Values.First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(cluster0, cluster0.Destinations.Values.Skip(1).First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(cluster1, cluster1.Destinations.Values.First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(cluster1, cluster1.Destinations.Values.Skip(1).First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster0, cluster0.Destinations.Values.First());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster0, cluster0.Destinations.Values.Skip(1).First());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster1, cluster1.Destinations.Values.First());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster1, cluster1.Destinations.Values.Skip(1).First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(4000));
|
||||
}
|
||||
|
||||
|
@ -57,8 +57,8 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// Failed requests
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster0, cluster0.Destinations.Values.Skip(1).First(), GetFailedRequestContext(ForwarderError.RequestTimedOut));
|
||||
policy.RequestProxied(cluster1, cluster1.Destinations.Values.First(), GetFailedRequestContext(ForwarderError.Request));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster0, cluster0.Destinations.Values.Skip(1).First());
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(4000));
|
||||
}
|
||||
|
||||
|
@ -68,10 +68,10 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
healthUpdater.VerifyNoOtherCalls();
|
||||
|
||||
// Two more failed requests
|
||||
policy.RequestProxied(cluster1, cluster1.Destinations.Values.First(), GetFailedRequestContext(ForwarderError.Request));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First());
|
||||
// End of the detection window
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(6000));
|
||||
policy.RequestProxied(cluster1, cluster1.Destinations.Values.First(), GetFailedRequestContext(ForwarderError.Request));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First());
|
||||
|
||||
healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(7));
|
||||
healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Unhealthy, reactivationPeriod1), Times.Once);
|
||||
|
@ -92,7 +92,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// Initial failed requests
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), GetFailedRequestContext(ForwarderError.RequestTimedOut));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(1000));
|
||||
}
|
||||
|
||||
|
@ -102,8 +102,8 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// Successful requests
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.First());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(5000));
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// Failed requests
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), GetFailedRequestContext(ForwarderError.RequestTimedOut));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(1));
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(10998));
|
||||
|
||||
// New failed request, but 2 oldest failures have moved out of the detection window
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), GetFailedRequestContext(ForwarderError.RequestTimedOut));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
|
||||
healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(4));
|
||||
healthUpdater.VerifyNoOtherCalls();
|
||||
|
@ -151,7 +151,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// Initial sucessful requests
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
}
|
||||
|
||||
healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(2));
|
||||
|
@ -161,7 +161,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// They are 'concurrent' because the clock is not updated.
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), GetFailedRequestContext(ForwarderError.RequestTimedOut));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
}
|
||||
|
||||
healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(3));
|
||||
|
@ -171,7 +171,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// More successful requests
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
|
@ -182,7 +182,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
// More failed requests
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.Skip(1).First(), GetFailedRequestContext(ForwarderError.RequestTimedOut));
|
||||
policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());
|
||||
clock.AdvanceClockBy(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
|
@ -190,7 +190,7 @@ namespace Yarp.ReverseProxy.Health.Tests
|
|||
healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Exactly(2));
|
||||
healthUpdater.VerifyNoOtherCalls();
|
||||
|
||||
policy.RequestProxied(cluster, cluster.Destinations.Values.First(), new DefaultHttpContext());
|
||||
policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.First());
|
||||
|
||||
healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod), Times.Once);
|
||||
healthUpdater.VerifyNoOtherCalls();
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
[Fact]
|
||||
public async Task PickDestination_SingleDestinations_ShortCircuit()
|
||||
{
|
||||
var context = CreateContext(LoadBalancingPolicies.First, new[]
|
||||
var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, new[]
|
||||
{
|
||||
new DestinationState("destination1")
|
||||
});
|
||||
|
@ -77,7 +77,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
public async Task Invoke_Works()
|
||||
{
|
||||
// Selects the alphabetically first available destination.
|
||||
var context = CreateContext(LoadBalancingPolicies.First, new[]
|
||||
var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, new[]
|
||||
{
|
||||
new DestinationState("destination2"),
|
||||
new DestinationState("destination1"),
|
||||
|
@ -99,7 +99,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
[Fact]
|
||||
public async Task Invoke_WithoutDestinations_503()
|
||||
{
|
||||
var context = CreateContext(LoadBalancingPolicies.First, Array.Empty<DestinationState>());
|
||||
var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, Array.Empty<DestinationState>());
|
||||
|
||||
var sut = CreateMiddleware(context =>
|
||||
{
|
||||
|
@ -133,7 +133,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
.Setup(p => p.Name)
|
||||
.Returns(PolicyName);
|
||||
policy
|
||||
.Setup(p => p.PickDestination(It.IsAny<HttpContext>(), It.IsAny<IReadOnlyList<DestinationState>>()))
|
||||
.Setup(p => p.PickDestination(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationState>>()))
|
||||
.Returns((DestinationState)null);
|
||||
|
||||
var sut = CreateMiddleware(context =>
|
||||
|
@ -168,14 +168,14 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
.Setup(p => p.Name)
|
||||
.Returns(LoadBalancingPolicies.PowerOfTwoChoices);
|
||||
policy
|
||||
.Setup(p => p.PickDestination(It.IsAny<HttpContext>(), It.IsAny<IReadOnlyList<DestinationState>>()))
|
||||
.Setup(p => p.PickDestination(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationState>>()))
|
||||
.Returns((DestinationState)destinations[0]);
|
||||
|
||||
var sut = CreateMiddleware(_ => Task.CompletedTask, policy.Object);
|
||||
|
||||
await sut.Invoke(context);
|
||||
|
||||
policy.Verify(p => p.PickDestination(context, destinations), Times.Once);
|
||||
policy.Verify(p => p.PickDestination(context, It.IsAny<ClusterState>(), destinations), Times.Once);
|
||||
}
|
||||
|
||||
private static HttpContext CreateContext(string loadBalancingPolicy, IReadOnlyList<DestinationState> destinations)
|
||||
|
@ -188,16 +188,17 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
var route = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default);
|
||||
context.Features.Set<IReverseProxyFeature>(
|
||||
new ReverseProxyFeature()
|
||||
{
|
||||
AvailableDestinations = destinations,
|
||||
Route = route,
|
||||
Cluster = cluster.Model
|
||||
});
|
||||
context.Features.Set(cluster);
|
||||
|
||||
var routeConfig = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default);
|
||||
var endpoint = new Endpoint(default, new EndpointMetadataCollection(routeConfig), string.Empty);
|
||||
var endpoint = new Endpoint(default, new EndpointMetadataCollection(route), string.Empty);
|
||||
context.SetEndpoint(endpoint);
|
||||
|
||||
return context;
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var result = loadBalancer.PickDestination(context, destinations);
|
||||
var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);
|
||||
Assert.Same(destinations[0], result);
|
||||
result.ConcurrentRequestCount++;
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
|
||||
for (var i = 0; i < Iterations; i++)
|
||||
{
|
||||
var result = loadBalancer.PickDestination(context, destinations);
|
||||
var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);
|
||||
Assert.Same(destinations[RandomInstance.Sequence[i]], result);
|
||||
result.ConcurrentRequestCount++;
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
|
||||
for (var i = 0; i < Iterations; i++)
|
||||
{
|
||||
var result = loadBalancer.PickDestination(context, destinations);
|
||||
var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);
|
||||
var first = destinations[RandomInstance.Sequence[i * 2]];
|
||||
var second = destinations[RandomInstance.Sequence[i * 2 + 1]];
|
||||
var expected = first.ConcurrentRequestCount <= second.ConcurrentRequestCount ? first : second;
|
||||
|
@ -116,7 +116,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var result = loadBalancer.PickDestination(context, destinations);
|
||||
var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);
|
||||
Assert.Same(destinations.OrderBy(d => d.ConcurrentRequestCount).First(), result);
|
||||
result.ConcurrentRequestCount++;
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ namespace Yarp.ReverseProxy.LoadBalancing.Tests
|
|||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var result = loadBalancer.PickDestination(context, destinations);
|
||||
var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);
|
||||
Assert.Same(destinations[i % destinations.Length], result);
|
||||
result.ConcurrentRequestCount++;
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void EnableSessionAffinity_AddsTransform()
|
||||
{
|
||||
var affinityProvider = new Mock<ISessionAffinityProvider>(MockBehavior.Strict);
|
||||
affinityProvider.SetupGet(p => p.Name).Returns("Provider");
|
||||
var affinityPolicy = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);
|
||||
affinityPolicy.SetupGet(p => p.Name).Returns("Policy");
|
||||
|
||||
var transformProvider = new AffinitizeTransformProvider(new[] { affinityProvider.Object });
|
||||
var transformProvider = new AffinitizeTransformProvider(new[] { affinityPolicy.Object });
|
||||
|
||||
var cluster = new ClusterConfig
|
||||
{
|
||||
|
@ -26,7 +26,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
SessionAffinity = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Provider",
|
||||
Policy = "Policy",
|
||||
AffinityKeyName = "Key1"
|
||||
}
|
||||
};
|
||||
|
@ -51,10 +51,10 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void EnableSession_InvalidMode_Fails()
|
||||
{
|
||||
var affinityProvider = new Mock<ISessionAffinityProvider>(MockBehavior.Strict);
|
||||
affinityProvider.SetupGet(p => p.Name).Returns("Provider");
|
||||
var affinityPolicy = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);
|
||||
affinityPolicy.SetupGet(p => p.Name).Returns("Policy");
|
||||
|
||||
var transformProvider = new AffinitizeTransformProvider(new[] { affinityProvider.Object });
|
||||
var transformProvider = new AffinitizeTransformProvider(new[] { affinityPolicy.Object });
|
||||
|
||||
var cluster = new ClusterConfig
|
||||
{
|
||||
|
@ -62,7 +62,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
SessionAffinity = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Invalid",
|
||||
Policy = "Invalid",
|
||||
AffinityKeyName = "Key1"
|
||||
}
|
||||
};
|
||||
|
@ -74,7 +74,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
transformProvider.ValidateCluster(validationContext);
|
||||
|
||||
var ex = Assert.Single(validationContext.Errors);
|
||||
Assert.Equal("No matching ISessionAffinityProvider found for the session affinity provider 'Invalid' set on the cluster 'cluster1'.", ex.Message);
|
||||
Assert.Equal("No matching ISessionAffinityPolicy found for the session affinity policy 'Invalid' set on the cluster 'cluster1'.", ex.Message);
|
||||
|
||||
var builderContext = new TransformBuilderContext()
|
||||
{
|
||||
|
@ -82,7 +82,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
};
|
||||
|
||||
ex = Assert.Throws<ArgumentException>(() => transformProvider.Apply(builderContext));
|
||||
Assert.Equal($"No {typeof(ISessionAffinityProvider).FullName} was found for the id 'Invalid'. (Parameter 'id')", ex.Message);
|
||||
Assert.Equal($"No {typeof(ISessionAffinityPolicy).FullName} was found for the id 'Invalid'. (Parameter 'id')", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
|
|||
using Moq;
|
||||
using Xunit;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
using Yarp.ReverseProxy.Forwarder;
|
||||
using Yarp.ReverseProxy.Model;
|
||||
using Yarp.ReverseProxy.Transforms;
|
||||
|
||||
|
@ -20,8 +21,8 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
{
|
||||
var cluster = GetCluster();
|
||||
var destination = cluster.Destinations.Values.First();
|
||||
var provider = new Mock<ISessionAffinityProvider>(MockBehavior.Strict);
|
||||
provider.Setup(p => p.AffinitizeResponse(It.IsAny<HttpContext>(), It.IsNotNull<SessionAffinityConfig>(), It.IsAny<DestinationState>()));
|
||||
var provider = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);
|
||||
provider.Setup(p => p.AffinitizeResponse(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), It.IsNotNull<SessionAffinityConfig>(), It.IsAny<DestinationState>()));
|
||||
|
||||
var transform = new AffinitizeTransform(provider.Object);
|
||||
|
||||
|
@ -29,6 +30,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
context.Features.Set<IReverseProxyFeature>(new ReverseProxyFeature()
|
||||
{
|
||||
Cluster = cluster.Model,
|
||||
Route = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default),
|
||||
ProxiedDestination = destination,
|
||||
});
|
||||
|
||||
|
@ -50,7 +52,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
SessionAffinity = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Provider-B",
|
||||
Policy = "Policy-B",
|
||||
FailurePolicy = "Policy-1",
|
||||
AffinityKeyName = "Key1"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ using Yarp.ReverseProxy.Model;
|
|||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
||||
{
|
||||
public class BaseSesstionAffinityProviderTests
|
||||
public class BaseSesstionAffinityPolicyTests
|
||||
{
|
||||
private const string InvalidKeyNull = "!invalid key - null!";
|
||||
private const string InvalidKeyThrow = "!invalid key - throw!";
|
||||
|
@ -23,7 +23,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
private readonly SessionAffinityConfig _defaultOptions = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Stub",
|
||||
Policy = "Stub",
|
||||
FailurePolicy = "Return503",
|
||||
AffinityKeyName = "StubAffinityKey"
|
||||
};
|
||||
|
@ -41,9 +41,10 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
EventId expectedEventId)
|
||||
{
|
||||
var dataProtector = GetDataProtector();
|
||||
var logger = AffinityTestHelper.GetLogger<BaseSessionAffinityProvider<string>>();
|
||||
var logger = AffinityTestHelper.GetLogger<BaseSessionAffinityPolicy<string>>();
|
||||
var provider = new ProviderStub(dataProtector.Object, logger.Object);
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, allDestinations, "cluster-1", _defaultOptions);
|
||||
var cluster = new ClusterState("cluster");
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, cluster, _defaultOptions, allDestinations);
|
||||
|
||||
if(unprotectCalled)
|
||||
{
|
||||
|
@ -73,33 +74,34 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void FindAffinitizedDestination_AffinityDisabledOnCluster_ReturnsAffinityDisabled()
|
||||
{
|
||||
var provider = new ProviderStub(GetDataProtector().Object, AffinityTestHelper.GetLogger<BaseSessionAffinityProvider<string>>().Object);
|
||||
var provider = new ProviderStub(GetDataProtector().Object, AffinityTestHelper.GetLogger<BaseSessionAffinityPolicy<string>>().Object);
|
||||
var options = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = false,
|
||||
Provider = _defaultOptions.Provider,
|
||||
Policy = _defaultOptions.Policy,
|
||||
FailurePolicy = _defaultOptions.FailurePolicy,
|
||||
AffinityKeyName = _defaultOptions.AffinityKeyName
|
||||
};
|
||||
Assert.Throws<InvalidOperationException>(() => provider.FindAffinitizedDestinations(new DefaultHttpContext(), new[] { new DestinationState("1") }, "cluster-1", options));
|
||||
var cluster = new ClusterState("cluster");
|
||||
Assert.Throws<InvalidOperationException>(() => provider.FindAffinitizedDestinations(new DefaultHttpContext(), cluster, options, new[] { new DestinationState("1") }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AffinitizeRequest_AffinityDisabled_DoNothing()
|
||||
{
|
||||
var dataProtector = GetDataProtector();
|
||||
var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseSessionAffinityProvider<string>>().Object);
|
||||
Assert.Throws<InvalidOperationException>(() => provider.AffinitizeResponse(new DefaultHttpContext(), new SessionAffinityConfig(), new DestinationState("id")));
|
||||
var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseSessionAffinityPolicy<string>>().Object);
|
||||
Assert.Throws<InvalidOperationException>(() => provider.AffinitizeResponse(new DefaultHttpContext(), new ClusterState("cluster"), new SessionAffinityConfig(), new DestinationState("id")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AffinitizeRequest_RequestIsAffinitized_DoNothing()
|
||||
{
|
||||
var dataProtector = GetDataProtector();
|
||||
var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseSessionAffinityProvider<string>>().Object);
|
||||
var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseSessionAffinityPolicy<string>>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
provider.DirectlySetExtractedKeyOnContext(context, "ExtractedKey");
|
||||
provider.AffinitizeResponse(context, _defaultOptions, new DestinationState("id"));
|
||||
provider.AffinitizeResponse(context, new ClusterState("cluster"), _defaultOptions, new DestinationState("id"));
|
||||
Assert.Null(provider.LastSetEncryptedKey);
|
||||
dataProtector.Verify(p => p.Protect(It.IsAny<byte[]>()), Times.Never);
|
||||
}
|
||||
|
@ -108,9 +110,9 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
public void AffinitizeRequest_RequestIsNotAffinitized_SetAffinityKey()
|
||||
{
|
||||
var dataProtector = GetDataProtector();
|
||||
var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseSessionAffinityProvider<string>>().Object);
|
||||
var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseSessionAffinityPolicy<string>>().Object);
|
||||
var destination = new DestinationState("dest-A");
|
||||
provider.AffinitizeResponse(new DefaultHttpContext(), _defaultOptions, destination);
|
||||
provider.AffinitizeResponse(new DefaultHttpContext(), new ClusterState("cluster"), _defaultOptions, destination);
|
||||
Assert.Equal("ZGVzdC1B", provider.LastSetEncryptedKey);
|
||||
var keyBytes = Encoding.UTF8.GetBytes(destination.DestinationId);
|
||||
dataProtector.Verify(p => p.Protect(It.Is<byte[]>(b => b.SequenceEqual(keyBytes))), Times.Once);
|
||||
|
@ -159,7 +161,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
return result;
|
||||
}
|
||||
|
||||
private class ProviderStub : BaseSessionAffinityProvider<string>
|
||||
private class ProviderStub : BaseSessionAffinityPolicy<string>
|
||||
{
|
||||
public static readonly string KeyNameSetting = "AffinityKeyName";
|
||||
|
||||
|
@ -181,16 +183,16 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
return destination.DestinationId;
|
||||
}
|
||||
|
||||
protected override (string Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, SessionAffinityConfig options)
|
||||
protected override (string Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig options)
|
||||
{
|
||||
Assert.Equal(Name, options.Provider);
|
||||
Assert.Equal(Name, options.Policy);
|
||||
// HttpContext.Items is used here to store the request affinity key for simplicity.
|
||||
// In real world scenario, a provider will extract it from request (e.g. header, cookie, etc.)
|
||||
var encryptedKey = context.Items.TryGetValue(options.AffinityKeyName, out var requestKey) ? requestKey : null;
|
||||
return Unprotect((string)encryptedKey);
|
||||
}
|
||||
|
||||
protected override void SetAffinityKey(HttpContext context, SessionAffinityConfig options, string unencryptedKey)
|
||||
protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig options, string unencryptedKey)
|
||||
{
|
||||
var encryptedKey = Protect(unencryptedKey);
|
||||
context.Items[options.AffinityKeyName] = encryptedKey;
|
|
@ -11,12 +11,12 @@ using Yarp.ReverseProxy.Model;
|
|||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
||||
{
|
||||
public class CookieSessionAffinityProviderTests
|
||||
public class CookieSessionAffinityPolicyTests
|
||||
{
|
||||
private readonly SessionAffinityConfig _config = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
FailurePolicy = "Return503",
|
||||
AffinityKeyName = "My.Affinity",
|
||||
Cookie = new SessionAffinityCookieConfig
|
||||
|
@ -35,17 +35,18 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet()
|
||||
{
|
||||
var provider = new CookieSessionAffinityProvider(
|
||||
var policy = new CookieSessionAffinityPolicy(
|
||||
AffinityTestHelper.GetDataProtector().Object,
|
||||
new ManualClock(),
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityProvider>().Object);
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);
|
||||
|
||||
Assert.Equal(SessionAffinityConstants.Providers.Cookie, provider.Name);
|
||||
Assert.Equal(SessionAffinityConstants.Policies.Cookie, policy.Name);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["Cookie"] = new[] { $"Some-Cookie=ZZZ" };
|
||||
var cluster = new ClusterState("cluster");
|
||||
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, _destinations, "cluster-1", _config);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);
|
||||
|
||||
Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status);
|
||||
Assert.Null(affinityResult.Destinations);
|
||||
|
@ -54,15 +55,16 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success()
|
||||
{
|
||||
var provider = new CookieSessionAffinityProvider(
|
||||
var policy = new CookieSessionAffinityPolicy(
|
||||
AffinityTestHelper.GetDataProtector().Object,
|
||||
new ManualClock(),
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityProvider>().Object);
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
var affinitizedDestination = _destinations[1];
|
||||
context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination);
|
||||
var cluster = new ClusterState("cluster");
|
||||
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, _destinations, "cluster-1", _config);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);
|
||||
|
||||
Assert.Equal(AffinityStatus.OK, affinityResult.Status);
|
||||
Assert.Equal(1, affinityResult.Destinations.Count);
|
||||
|
@ -72,13 +74,13 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse()
|
||||
{
|
||||
var provider = new CookieSessionAffinityProvider(
|
||||
var policy = new CookieSessionAffinityPolicy(
|
||||
AffinityTestHelper.GetDataProtector().Object,
|
||||
new ManualClock(),
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityProvider>().Object);
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
provider.AffinitizeResponse(context, _config, _destinations[1]);
|
||||
policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]);
|
||||
|
||||
var affinityCookieHeader = context.Response.Headers["Set-Cookie"];
|
||||
Assert.Equal("My.Affinity=ZGVzdC1C; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader);
|
||||
|
@ -87,13 +89,13 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void AffinitizeRequest_CookieConfigSpecified_UseIt()
|
||||
{
|
||||
var provider = new CookieSessionAffinityProvider(
|
||||
var policy = new CookieSessionAffinityPolicy(
|
||||
AffinityTestHelper.GetDataProtector().Object,
|
||||
new ManualClock(),
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityProvider>().Object);
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
provider.AffinitizeResponse(context, _config, _destinations[1]);
|
||||
policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]);
|
||||
|
||||
var affinityCookieHeader = context.Response.Headers["Set-Cookie"];
|
||||
Assert.Equal("My.Affinity=ZGVzdC1C; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader);
|
||||
|
@ -102,19 +104,20 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing()
|
||||
{
|
||||
var provider = new CookieSessionAffinityProvider(
|
||||
var policy = new CookieSessionAffinityPolicy(
|
||||
AffinityTestHelper.GetDataProtector().Object,
|
||||
new ManualClock(),
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityProvider>().Object);
|
||||
AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
var affinitizedDestination = _destinations[0];
|
||||
context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination);
|
||||
var cluster = new ClusterState("cluster");
|
||||
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, _destinations, "cluster-1", _config);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);
|
||||
|
||||
Assert.Equal(AffinityStatus.OK, affinityResult.Status);
|
||||
|
||||
provider.AffinitizeResponse(context, _config, affinitizedDestination);
|
||||
policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination);
|
||||
|
||||
Assert.False(context.Response.Headers.ContainsKey("Cookie"));
|
||||
}
|
|
@ -9,13 +9,13 @@ using Yarp.ReverseProxy.Model;
|
|||
|
||||
namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
||||
{
|
||||
public class CustomHeaderSessionAffinityProviderTests
|
||||
public class CustomHeaderSessionAffinityPolicyTests
|
||||
{
|
||||
private const string AffinityHeaderName = "X-MyAffinity";
|
||||
private readonly SessionAffinityConfig _defaultOptions = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Cookie",
|
||||
Policy = "Cookie",
|
||||
FailurePolicy = "Return503",
|
||||
AffinityKeyName = AffinityHeaderName
|
||||
};
|
||||
|
@ -24,14 +24,15 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet()
|
||||
{
|
||||
var provider = new CustomHeaderSessionAffinityProvider(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityProvider>().Object);
|
||||
var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);
|
||||
|
||||
Assert.Equal(SessionAffinityConstants.Providers.CustomHeader, provider.Name);
|
||||
Assert.Equal(SessionAffinityConstants.Policies.CustomHeader, policy.Name);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["SomeHeader"] = new[] { "SomeValue" };
|
||||
var cluster = new ClusterState("cluster");
|
||||
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, _destinations, "cluster-1", _defaultOptions);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations);
|
||||
|
||||
Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status);
|
||||
Assert.Null(affinityResult.Destinations);
|
||||
|
@ -40,13 +41,14 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success()
|
||||
{
|
||||
var provider = new CustomHeaderSessionAffinityProvider(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityProvider>().Object);
|
||||
var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["SomeHeader"] = new[] { "SomeValue" };
|
||||
var affinitizedDestination = _destinations[1];
|
||||
context.Request.Headers[AffinityHeaderName] = new[] { affinitizedDestination.DestinationId.ToUTF8BytesInBase64() };
|
||||
var cluster = new ClusterState("cluster");
|
||||
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, _destinations, "cluster-1", _defaultOptions);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations);
|
||||
|
||||
Assert.Equal(AffinityStatus.OK, affinityResult.Status);
|
||||
Assert.Equal(1, affinityResult.Destinations.Count);
|
||||
|
@ -56,12 +58,12 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void AffinitizedRequest_AffinityKeyIsNotExtracted_SetKeyOnResponse()
|
||||
{
|
||||
var provider = new CustomHeaderSessionAffinityProvider(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityProvider>().Object);
|
||||
var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
var chosenDestination = _destinations[1];
|
||||
var expectedAffinityHeaderValue = chosenDestination.DestinationId.ToUTF8BytesInBase64();
|
||||
|
||||
provider.AffinitizeResponse(context, _defaultOptions, chosenDestination);
|
||||
policy.AffinitizeResponse(context, new ClusterState("cluster"), _defaultOptions, chosenDestination);
|
||||
|
||||
Assert.True(context.Response.Headers.ContainsKey(AffinityHeaderName));
|
||||
Assert.Equal(expectedAffinityHeaderValue, context.Response.Headers[AffinityHeaderName]);
|
||||
|
@ -70,17 +72,18 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
[Fact]
|
||||
public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing()
|
||||
{
|
||||
var provider = new CustomHeaderSessionAffinityProvider(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityProvider>().Object);
|
||||
var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["SomeHeader"] = new[] { "SomeValue" };
|
||||
var affinitizedDestination = _destinations[1];
|
||||
context.Request.Headers[AffinityHeaderName] = new[] { affinitizedDestination.DestinationId.ToUTF8BytesInBase64() };
|
||||
var cluster = new ClusterState("cluster");
|
||||
|
||||
var affinityResult = provider.FindAffinitizedDestinations(context, _destinations, "cluster-1", _defaultOptions);
|
||||
var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations);
|
||||
|
||||
Assert.Equal(AffinityStatus.OK, affinityResult.Status);
|
||||
|
||||
provider.AffinitizeResponse(context, _defaultOptions, affinitizedDestination);
|
||||
policy.AffinitizeResponse(context, cluster, _defaultOptions, affinitizedDestination);
|
||||
|
||||
Assert.False(context.Response.Headers.ContainsKey(AffinityHeaderName));
|
||||
}
|
|
@ -18,7 +18,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
var policy = new RedistributeAffinityFailurePolicy();
|
||||
|
||||
Assert.Equal(SessionAffinityConstants.FailurePolicies.Redistribute, policy.Name);
|
||||
Assert.True(await policy.Handle(new DefaultHttpContext(), default, status));
|
||||
Assert.True(await policy.Handle(new DefaultHttpContext(), cluster: null, affinityStatus: status));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -29,7 +29,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
var policy = new RedistributeAffinityFailurePolicy();
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => policy.Handle(context, default, status));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => policy.Handle(context, cluster: null, affinityStatus: status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
|
||||
Assert.Equal(SessionAffinityConstants.FailurePolicies.Return503Error, policy.Name);
|
||||
|
||||
Assert.False(await policy.Handle(context, default, status));
|
||||
Assert.False(await policy.Handle(context, cluster: null, affinityStatus: status));
|
||||
Assert.Equal(503, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
var policy = new Return503ErrorAffinityFailurePolicy();
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => policy.Handle(context, default, status));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => policy.Handle(context, cluster: null, affinityStatus: status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
SessionAffinity = new SessionAffinityConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Provider = "Provider-B",
|
||||
Policy = "Policy-B",
|
||||
FailurePolicy = "Policy-1",
|
||||
AffinityKeyName = "Key1"
|
||||
}
|
||||
|
@ -45,32 +45,31 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
cluster.Destinations.TryGetValue(foundDestinationId, out foundDestination);
|
||||
}
|
||||
var invokedMode = string.Empty;
|
||||
const string expectedMode = "Provider-B";
|
||||
var providers = RegisterAffinityProviders(
|
||||
const string expectedMode = "Policy-B";
|
||||
var policies = RegisterAffinityPolicies(
|
||||
true,
|
||||
cluster.Destinations.Values.ToList(),
|
||||
cluster.ClusterId,
|
||||
("Provider-A", AffinityStatus.DestinationNotFound, (DestinationState)null, (Action<ISessionAffinityProvider>)(p => throw new InvalidOperationException($"Provider {p.Name} call is not expected."))),
|
||||
("Policy-A", AffinityStatus.DestinationNotFound, (DestinationState)null, (Action<ISessionAffinityPolicy>)(p => throw new InvalidOperationException($"Policy {p.Name} call is not expected."))),
|
||||
(expectedMode, status, foundDestination, p => invokedMode = p.Name));
|
||||
var nextInvoked = false;
|
||||
var middleware = new SessionAffinityMiddleware(c => {
|
||||
nextInvoked = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
providers.Select(p => p.Object), new IAffinityFailurePolicy[0],
|
||||
policies.Select(p => p.Object), new IAffinityFailurePolicy[0],
|
||||
new Mock<ILogger<SessionAffinityMiddleware>>().Object);
|
||||
var context = new DefaultHttpContext();
|
||||
context.SetEndpoint(endpoint);
|
||||
var destinationFeature = GetDestinationsFeature(cluster.Destinations.Values.ToList(), cluster.Model);
|
||||
var destinationFeature = GetReverseProxyFeature(cluster);
|
||||
context.Features.Set(destinationFeature);
|
||||
|
||||
await middleware.Invoke(context);
|
||||
|
||||
Assert.Equal(expectedMode, invokedMode);
|
||||
Assert.True(nextInvoked);
|
||||
providers[0].VerifyGet(p => p.Name, Times.Once);
|
||||
providers[0].VerifyNoOtherCalls();
|
||||
providers[1].VerifyAll();
|
||||
policies[0].VerifyGet(p => p.Name, Times.Once);
|
||||
policies[0].VerifyNoOtherCalls();
|
||||
policies[1].VerifyAll();
|
||||
|
||||
if (foundDestinationId != null)
|
||||
{
|
||||
|
@ -92,7 +91,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
{
|
||||
var cluster = GetCluster();
|
||||
var endpoint = GetEndpoint(cluster);
|
||||
var providers = RegisterAffinityProviders(true, cluster.Destinations.Values.ToList(), cluster.ClusterId, ("Provider-B", affinityStatus, null, _ => { }));
|
||||
var policies = RegisterAffinityPolicies(true, cluster.Destinations.Values.ToList(), ("Policy-B", affinityStatus, null, _ => { }));
|
||||
var invokedPolicy = string.Empty;
|
||||
const string expectedPolicy = "Policy-1";
|
||||
var failurePolicies = RegisterFailurePolicies(
|
||||
|
@ -105,10 +104,10 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
nextInvoked = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
providers.Select(p => p.Object), failurePolicies.Select(p => p.Object),
|
||||
policies.Select(p => p.Object), failurePolicies.Select(p => p.Object),
|
||||
logger.Object);
|
||||
var context = new DefaultHttpContext();
|
||||
var destinationFeature = GetDestinationsFeature(cluster.Destinations.Values.ToList(), cluster.Model);
|
||||
var destinationFeature = GetReverseProxyFeature(cluster);
|
||||
|
||||
context.SetEndpoint(endpoint);
|
||||
context.Features.Set(destinationFeature);
|
||||
|
@ -140,36 +139,36 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
return cluster;
|
||||
}
|
||||
|
||||
internal IReadOnlyList<Mock<ISessionAffinityProvider>> RegisterAffinityProviders(
|
||||
internal IReadOnlyList<Mock<ISessionAffinityPolicy>> RegisterAffinityPolicies(
|
||||
bool lookupMiddlewareTest,
|
||||
IReadOnlyList<DestinationState> expectedDestinations,
|
||||
string expectedCluster,
|
||||
params (string Mode, AffinityStatus? Status, DestinationState Destinations, Action<ISessionAffinityProvider> Callback)[] prototypes)
|
||||
params (string Mode, AffinityStatus? Status, DestinationState Destinations, Action<ISessionAffinityPolicy> Callback)[] prototypes)
|
||||
{
|
||||
var result = new List<Mock<ISessionAffinityProvider>>();
|
||||
var result = new List<Mock<ISessionAffinityPolicy>>();
|
||||
foreach (var (mode, status, destinations, callback) in prototypes)
|
||||
{
|
||||
var provider = new Mock<ISessionAffinityProvider>(MockBehavior.Strict);
|
||||
provider.SetupGet(p => p.Name).Returns(mode);
|
||||
var policy = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);
|
||||
policy.SetupGet(p => p.Name).Returns(mode);
|
||||
if (lookupMiddlewareTest)
|
||||
{
|
||||
provider.Setup(p => p.FindAffinitizedDestinations(
|
||||
policy.Setup(p => p.FindAffinitizedDestinations(
|
||||
It.IsAny<HttpContext>(),
|
||||
expectedDestinations,
|
||||
expectedCluster,
|
||||
ClusterConfig.Config.SessionAffinity))
|
||||
It.IsAny<ClusterState>(),
|
||||
ClusterConfig.Config.SessionAffinity,
|
||||
expectedDestinations))
|
||||
.Returns(new AffinityResult(destinations, status.Value))
|
||||
.Callback(() => callback(provider.Object));
|
||||
.Callback(() => callback(policy.Object));
|
||||
}
|
||||
else
|
||||
{
|
||||
provider.Setup(p => p.AffinitizeResponse(
|
||||
policy.Setup(p => p.AffinitizeResponse(
|
||||
It.IsAny<HttpContext>(),
|
||||
It.IsAny<ClusterState>(),
|
||||
ClusterConfig.Config.SessionAffinity,
|
||||
expectedDestinations[0]))
|
||||
.Callback(() => callback(provider.Object));
|
||||
.Callback(() => callback(policy.Object));
|
||||
}
|
||||
result.Add(provider);
|
||||
result.Add(policy);
|
||||
}
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
|
@ -181,7 +180,7 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
{
|
||||
var policy = new Mock<IAffinityFailurePolicy>(MockBehavior.Strict);
|
||||
policy.SetupGet(p => p.Name).Returns(name);
|
||||
policy.Setup(p => p.Handle(It.IsAny<HttpContext>(), It.Is<SessionAffinityConfig>(o => o.FailurePolicy == name), expectedStatus))
|
||||
policy.Setup(p => p.Handle(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), expectedStatus))
|
||||
.ReturnsAsync(handled)
|
||||
.Callback(() => callback(policy.Object));
|
||||
result.Add(policy);
|
||||
|
@ -189,12 +188,13 @@ namespace Yarp.ReverseProxy.SessionAffinity.Tests
|
|||
return result.AsReadOnly();
|
||||
}
|
||||
|
||||
internal IReverseProxyFeature GetDestinationsFeature(IReadOnlyList<DestinationState> destinations, ClusterModel clusterModel)
|
||||
internal IReverseProxyFeature GetReverseProxyFeature(ClusterState cluster)
|
||||
{
|
||||
return new ReverseProxyFeature()
|
||||
{
|
||||
AvailableDestinations = destinations,
|
||||
Cluster = clusterModel,
|
||||
AvailableDestinations = cluster.Destinations.Values.ToList(),
|
||||
Route = new RouteModel(new RouteConfig(), cluster: cluster, HttpTransformer.Default),
|
||||
Cluster = cluster.Model,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace Yarp.ReverseProxy.Sample.Controllers
|
|||
public IActionResult CheckHealth()
|
||||
{
|
||||
// TODO: Implement health controller, use guid in route.
|
||||
return _healthCheckMonitor.InitialDestinationsProbed ? Ok() : StatusCode(503);
|
||||
return _healthCheckMonitor.InitialProbeCompleted ? Ok() : StatusCode(503);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ namespace Yarp.ReverseProxy.Sample
|
|||
new ClusterConfig()
|
||||
{
|
||||
ClusterId = "cluster1",
|
||||
SessionAffinity = new SessionAffinityConfig { Enabled = true, Provider = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" },
|
||||
SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" },
|
||||
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "destination1", new DestinationConfig() { Address = "https://localhost:10000" } }
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"LoadBalancingPolicy": "Random",
|
||||
"SessionAffinity": {
|
||||
"Enabled": "true",
|
||||
"Provider": "Cookie",
|
||||
"Policy": "Cookie",
|
||||
"AffinityKeyName": ".Yarp.Affinity"
|
||||
},
|
||||
"HealthCheck": {
|
||||
|
|
|
@ -59,7 +59,7 @@ namespace Yarp.ReverseProxy.Sample
|
|||
endpoints.Map("/{**catch-all}", async httpContext =>
|
||||
{
|
||||
await httpProxy.SendAsync(httpContext, "https://example.com", httpClient, requestOptions, transformer);
|
||||
var errorFeature = httpContext.GetProxyErrorFeature();
|
||||
var errorFeature = httpContext.GetForwarderErrorFeature();
|
||||
if (errorFeature != null)
|
||||
{
|
||||
var error = errorFeature.Error;
|
||||
|
|
Загрузка…
Ссылка в новой задаче