Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Request can be affinitized to destinations #174

Merged
merged 31 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
875a391
Request can be affinitized to destinations by an affinity key extract…
alnikola May 18, 2020
aca8d94
Comment on affinity to a destination pool
alnikola May 18, 2020
0ba1554
- Session affinity logic separated from load balancing into 2 new mid…
alnikola May 19, 2020
074572b
- Separate affinity middleware setup methods
alnikola May 20, 2020
55286d1
- Missing affinitized destination failure handling
alnikola May 22, 2020
a812015
- Logging added to BaseAffinityProvider
alnikola May 26, 2020
edf3ae7
AffinityResult.Destinations converted to property
alnikola May 26, 2020
ad428d2
Redundant Dictionary ctor removed
alnikola May 26, 2020
ee67154
- Session affinity default options
alnikola May 26, 2020
db9afae
- ISessionAffinityProvider and BackendConfig are made public
alnikola May 27, 2020
ce858b8
- AffinitizedDestinationLookupMiddleware checks the affinity status t…
alnikola May 28, 2020
b63d8ac
Affinity key is set on response only if it is a new affinity
alnikola May 28, 2020
77edcaa
CustomHeaderSessionAffinityProvider treats multiple key header values…
alnikola May 28, 2020
9668cfb
Failing tests fixed
alnikola Jun 1, 2020
48becbb
BaseSessionAffinityProvider's tests
alnikola Jun 1, 2020
98db698
CookieSessionAffinityProvider's tests
alnikola Jun 1, 2020
1fb3e16
- CustomHeaderSessionAffinityProvider's tests
alnikola Jun 2, 2020
b6a89ac
- AffinitizedDestinationLookupMiddleware tests
alnikola Jun 2, 2020
591837e
Renaming
alnikola Jun 3, 2020
94c5967
Merge branch 'master' of https://github.com/microsoft/reverse-proxy i…
alnikola Jun 3, 2020
9226fdc
- Redundant single-item array allocations removed
alnikola Jun 3, 2020
d375c28
DestinationInfo comparison fixed
alnikola Jun 3, 2020
dbf9fff
- AffinitizeRequestMiddleware tests
alnikola Jun 4, 2020
81de6c1
- BaseSessionAffinityProvider throws exception if affinity is disabled
alnikola Jun 4, 2020
f1f545f
Logging covered by tests
alnikola Jun 4, 2020
0285342
Unnecessary using removed
alnikola Jun 4, 2020
1325732
SessionAffinity sample scenario
alnikola Jun 4, 2020
ed7fc0f
- Defaults for SessionAffinityOptions removed
alnikola Jun 8, 2020
d63c9f4
Fix RedistributeAffinityFailurePolicyTests
alnikola Jun 8, 2020
0e8e382
- Redundant destination assignment removed
alnikola Jun 8, 2020
e4fee15
Get rid of redundant async/await
alnikola Jun 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions samples/ReverseProxy.Sample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ public void Configure(IApplicationBuilder app)

return next();
});
proxyPipeline.UseAffinitizedDestinationLookup();
proxyPipeline.UseProxyLoadBalancing();
proxyPipeline.UseRequestAffinitizer();
});
});
}
Expand Down
4 changes: 4 additions & 0 deletions samples/ReverseProxy.Sample/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"LoadBalancing": {
"Mode": "Random"
},
"SessionAffinity": {
"Enabled": "true",
"Mode": "Cookie"
},
"Metadata": {
"CustomHealth": "false"
},
Expand Down
5 changes: 3 additions & 2 deletions samples/SampleClient/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
Expand Down Expand Up @@ -34,7 +34,8 @@ public static async Task<int> Main(string[] args)
{"Http1", () => new Http1Scenario()},
{"Http2", () => new Http2Scenario()},
{"RawUpgrade", () => new RawUpgradeScenario()},
{"WebSockets", () => new WebSocketsScenario()}
{"WebSockets", () => new WebSocketsScenario()},
{"SessionAffinity", () => new SessionAffinityScenario()}
};

if (string.IsNullOrEmpty(parsedArgs.Scenario))
Expand Down
84 changes: 84 additions & 0 deletions samples/SampleClient/Scenarios/SessionAffinityScenario.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace SampleClient.Scenarios
{
internal class SessionAffinityScenario : IScenario
{
public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)
{
using var handler = new HttpClientHandler
{
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
// Session affinity key will be stored in a cookie
UseCookies = true,
UseProxy = false
};
using var client = new HttpMessageInvoker(handler);
var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), "api/dump");
var stopwatch = Stopwatch.StartNew();

var request0 = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) };
Console.WriteLine($"Sending first request to {targetUri} with HTTP/1.1");
var response0 = await client.SendAsync(request0, cancellation);

PrintDuration(stopwatch, response0);
PrintAffinityCookie(handler, targetUri, response0);
await ReadAndPrintBody(response0, cancellation);

stopwatch.Reset();

var request1 = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) };
Console.WriteLine($"Sending second request to {targetUri} with HTTP/1.1");
var response1 = await client.SendAsync(request1, cancellation);

PrintDuration(stopwatch, response1);
PrintAffinityCookie(handler, targetUri, response1);
await ReadAndPrintBody(response1, cancellation);
}

private static void PrintDuration(Stopwatch stopwatch, HttpResponseMessage response)
{
Console.WriteLine($"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms");
response.EnsureSuccessStatusCode();
}

private static void PrintAffinityCookie(HttpClientHandler handler, Uri targetUri, HttpResponseMessage response)
{
if (response.Headers.TryGetValues("Set-Cookie", out var setCookieValue))
{
Console.WriteLine($"Received header Set-Cookie: {setCookieValue.ToArray()[0]}");
}
else
{
Console.WriteLine($"Response doesn't have Set-Cookie header.");
}

var affinityCookie = handler.CookieContainer.GetCookies(targetUri)[".Microsoft.ReverseProxy.Affinity"];
Console.WriteLine($"Affinity key stored on a cookie {affinityCookie.Value}");
}

private static async Task ReadAndPrintBody(HttpResponseMessage response, CancellationToken cancellation)
{
var body = await response.Content.ReadAsStringAsync(cancellation);
var json = JsonDocument.Parse(body);
Console.WriteLine(
"Received response:" +
$"{Environment.NewLine}" +
$"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}");
response.EnsureSuccessStatusCode();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using Microsoft.ReverseProxy.Abstractions.BackendDiscovery.Contract;

namespace Microsoft.ReverseProxy.Abstractions
{
Expand Down Expand Up @@ -39,6 +40,11 @@ public sealed class Backend : IDeepCloneable<Backend>
/// </summary>
public LoadBalancingOptions LoadBalancing { get; set; }

/// <summary>
/// Session affinity options.
/// </summary>
public SessionAffinityOptions SessionAffinity { get; set; }

/// <summary>
/// Active health checking options.
/// </summary>
Expand All @@ -64,6 +70,7 @@ Backend IDeepCloneable<Backend>.DeepClone()
QuotaOptions = QuotaOptions?.DeepClone(),
PartitioningOptions = PartitioningOptions?.DeepClone(),
LoadBalancing = LoadBalancing?.DeepClone(),
SessionAffinity = SessionAffinity?.DeepClone(),
HealthCheckOptions = HealthCheckOptions?.DeepClone(),
Destinations = Destinations.DeepClone(StringComparer.Ordinal),
Metadata = Metadata?.DeepClone(StringComparer.Ordinal),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.ReverseProxy.Abstractions.BackendDiscovery.Contract
{
/// <summary>
/// Defines cookie-specific affinity provider options.
/// </summary>
public class CookieSessionAffinityProviderOptions
{
private CookieBuilder _cookieBuilder = new AffinityCookieBuilder();

public static readonly string DefaultCookieName = ".Microsoft.ReverseProxy.Affinity";

public CookieBuilder Cookie
{
get => _cookieBuilder;
set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
}

private class AffinityCookieBuilder : CookieBuilder
{
public AffinityCookieBuilder()
{
Name = DefaultCookieName;
SecurePolicy = CookieSecurePolicy.None;
SameSite = SameSiteMode.Unspecified;
HttpOnly = true;
IsEssential = false;
}

public override TimeSpan? Expiration
{
get => null;
set => throw new InvalidOperationException(nameof(Expiration) + " cannot be set for the cookie defined by " + nameof(CookieSessionAffinityProviderOptions));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.ReverseProxy.Abstractions.BackendDiscovery.Contract
{
/// <summary>
/// Names of built-in session affinity services.
/// </summary>
public static class SessionAffinityConstants
{
public static class Modes
{
public static string Cookie => "Cookie";

public static string CustomHeader => "CustomHeader";
}

public static class AffinityFailurePolicies
{
public static string Redistribute => "Redistribute";

public static string Return503Error => "Return503Error";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Microsoft.ReverseProxy.Abstractions.BackendDiscovery.Contract
{
/// <summary>
/// Session affinitity options.
/// </summary>
public sealed class SessionAffinityOptions
alnikola marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Indicates whether session affinity is enabled.
/// </summary>
public bool Enabled { get; set; }

/// <summary>
/// Session affinity mode which is implemented by one of providers.
/// </summary>
public string Mode { get; set; }

/// <summary>
/// Strategy handling missing destination for an affinitized request.
/// </summary>
public string AffinityFailurePolicy { get; set; }

/// <summary>
/// Key-value pair collection holding extra settings specific to different affinity modes.
/// </summary>
public IDictionary<string, string> Settings { get; set; }

internal SessionAffinityOptions DeepClone()
alnikola marked this conversation as resolved.
Show resolved Hide resolved
{
return new SessionAffinityOptions
{
Enabled = Enabled,
Mode = Mode,
AffinityFailurePolicy = AffinityFailurePolicy,
Settings = Settings?.DeepClone(StringComparer.Ordinal)
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.ReverseProxy.Abstractions;
using Microsoft.ReverseProxy.Abstractions.BackendDiscovery.Contract;
using Microsoft.ReverseProxy.Abstractions.Telemetry;
using Microsoft.ReverseProxy.Abstractions.Time;
using Microsoft.ReverseProxy.Service;
using Microsoft.ReverseProxy.Service.Management;
using Microsoft.ReverseProxy.Service.Metrics;
using Microsoft.ReverseProxy.Service.Proxy;
using Microsoft.ReverseProxy.Service.Proxy.Infrastructure;
using Microsoft.ReverseProxy.Service.SessionAffinity;
using Microsoft.ReverseProxy.Telemetry;
using Microsoft.ReverseProxy.Utilities;

Expand Down Expand Up @@ -82,5 +87,19 @@ public static IReverseProxyBuilder AddBackgroundWorkers(this IReverseProxyBuilde

return builder;
}

public static IReverseProxyBuilder AddSessionAffinityProvider(this IReverseProxyBuilder builder)
{
builder.Services.TryAddEnumerable(new[] {
new ServiceDescriptor(typeof(IAffinityFailurePolicy), typeof(RedistributeAffinityFailurePolicy), ServiceLifetime.Singleton),
new ServiceDescriptor(typeof(IAffinityFailurePolicy), typeof(Return503ErrorAffinityFailurePolicy), ServiceLifetime.Singleton)
});
builder.Services.TryAddEnumerable(new[] {
new ServiceDescriptor(typeof(ISessionAffinityProvider), typeof(CookieSessionAffinityProvider), ServiceLifetime.Singleton),
new ServiceDescriptor(typeof(ISessionAffinityProvider), typeof(CustomHeaderSessionAffinityProvider), ServiceLifetime.Singleton)
});

return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ public static IReverseProxyBuilder AddReverseProxy(this IServiceCollection servi
.AddRuntimeStateManagers()
.AddConfigManager()
.AddDynamicEndpointDataSource()
.AddSessionAffinityProvider()
.AddProxy()
.AddBackgroundWorkers();

services.AddDataProtection();

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints)
{
endpoints.MapReverseProxy(app =>
{
app.UseAffinitizedDestinationLookup();
app.UseProxyLoadBalancing();
app.UseRequestAffinitizer();
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/ReverseProxy/EventIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,13 @@ internal static class EventIds
public static readonly EventId OperationStarted = new EventId(31, "OperationStarted");
public static readonly EventId OperationEnded = new EventId(32, "OperationEnded");
public static readonly EventId OperationFailed = new EventId(33, "OperationFailed");
public static readonly EventId AffinityResolutionFailedForBackend = new EventId(34, "AffinityResolutionFailedForBackend");
public static readonly EventId MultipleDestinationsOnBackendToEstablishRequestAffinity = new EventId(35, "MultipleDestinationsOnBackendToEstablishRequestAffinity");
public static readonly EventId AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnBackend = new EventId(36, "AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnBackend");
public static readonly EventId NoDestinationOnBackendToEstablishRequestAffinity = new EventId(37, "NoDestinationOnBackendToEstablishRequestAffinity");
public static readonly EventId RequestAffinityKeyDecryptionFailed = new EventId(38, "RequestAffinityKeyDecryptionFailed");
public static readonly EventId DestinationMatchingToAffinityKeyNotFound = new EventId(39, "DestinationMatchingToAffinityKeyNotFound");
public static readonly EventId RequestAffinityHeaderHasMultipleValues = new EventId(40, "RequestAffinityHeaderHasMultipleValues");
public static readonly EventId AffinityResolutionFailureWasHandledProcessingWillBeContinued = new EventId(41, "AffinityResolutionFailureWasHandledProcessingWillBeContinued");
}
}
Loading