Skip to content

Add short circuit in routing #46713

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

Merged
merged 11 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.ShortCircuit;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Routing;

public class EndpointRoutingShortCircuitBenchmark
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the result

Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
NormalEndpoint 665.7 ns 5.81 ns 4.53 ns 1,502,118.8 0.0343 - - 1 KB
ShortCircuitEndpoint 772.2 ns 9.12 ns 8.08 ns 1,295,049.2 0.0343 - - 1 KB

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's NormalEndpoints benchmark without your changes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's with my changes. I will add another benchmark with previous implementation too. I assume we don't need to have two different implementation in the code base, right? I just run it in my local?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, we just want to confirm the before-and-after to make sure this doesn't regress anything.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
New Implementation without short circuit 658.7 ns 4.03 ns 3.37 ns 1,518,082.2 0.0343 - - 1 KB
Old Implementation 639.7 ns 10.42 ns 12.41 ns 1,563,187.5 0.0343 - - 1 KB
New Implementation with short circuit 791.3 ns 3.24 ns 2.87 ns 1,263,689.7 0.0343 - - 1 KB

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1,518,082.2 vs 1,563,187.5? That's less than 3% on a microbenchmark. I doubt it would even show up on the end-to-end. Adding the endpoint execution middleware to this benchmark would make it more representative in all cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I looked at the wrong results.

I doubt it would even show up on the end-to-end

We don't have to guess, we can just run it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Implementation with short circuit

Why is this so much worse?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hitting the new code path that would normally not be hit until the EndpointMiddleware. If we include the EndpointMiddleware in the benchmark you shouldn't notice a difference.
https://github.com/dotnet/aspnetcore/pull/46713/files#diff-5019372fafa72e521b12fab1c28fd96919cb21c5294003b7777f5a46322cca54R113-R175
https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/EndpointMiddleware.cs#L34-L78

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the result with both endpoint routing middleware and endpoint middleware together.

Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
New Implementation without short circuit 766.3 ns 2.28 ns 1.90 ns 1,304,930.5 0.0343 - - 1 KB
New Implementation with short circuit 775.2 ns 3.44 ns 2.87 ns 1,290,051.6 0.0343 - - 1 KB
Old Implementation 728.6 ns 6.77 ns 5.28 ns 1,372,541.8 0.0343 - - 1 KB

{
private EndpointRoutingMiddleware _normalEndpointMiddleware;
private EndpointRoutingMiddleware _shortCircuitEndpointMiddleware;

[GlobalSetup]
public void Setup()
{
var normalEndpoint = new Endpoint(context => Task.CompletedTask, new EndpointMetadataCollection(), "normal");

_normalEndpointMiddleware = new EndpointRoutingMiddleware(
new BenchmarkMatcherFactory(normalEndpoint),
NullLogger<EndpointRoutingMiddleware>.Instance,
new BenchmarkEndpointRouteBuilder(),
new BenchmarkEndpointDataSource(),
new DiagnosticListener("benchmark"),
Options.Create(new RouteOptions()),
context => Task.CompletedTask);

var shortCircuitEndpoint = new Endpoint(context => Task.CompletedTask, new EndpointMetadataCollection(new ShortCircuitMetadata(200)), "shortcircuit");

_shortCircuitEndpointMiddleware = new EndpointRoutingMiddleware(
new BenchmarkMatcherFactory(shortCircuitEndpoint),
NullLogger<EndpointRoutingMiddleware>.Instance,
new BenchmarkEndpointRouteBuilder(),
new BenchmarkEndpointDataSource(),
new DiagnosticListener("benchmark"),
Options.Create(new RouteOptions()),
context => Task.CompletedTask);

}

[Benchmark]
public async Task NormalEndpoint()
{
var context = new DefaultHttpContext();
await _normalEndpointMiddleware.Invoke(context);
}

[Benchmark]
public async Task ShortCircuitEndpoint()
{
var context = new DefaultHttpContext();
await _shortCircuitEndpointMiddleware.Invoke(context);
}
}

internal class BenchmarkMatcherFactory : MatcherFactory
{
private readonly Endpoint _endpoint;

public BenchmarkMatcherFactory(Endpoint endpoint)
{
_endpoint = endpoint;
}

public override Matcher CreateMatcher(EndpointDataSource dataSource)
{
return new BenchmarkMatcher(_endpoint);
}

internal class BenchmarkMatcher : Matcher
{
private Endpoint _endpoint;

public BenchmarkMatcher(Endpoint endpoint)
{
_endpoint = endpoint;
}

public override Task MatchAsync(HttpContext httpContext)
{
httpContext.SetEndpoint(_endpoint);
return Task.CompletedTask;
}
}
}

internal class BenchmarkEndpointRouteBuilder : IEndpointRouteBuilder
{
public IServiceProvider ServiceProvider => throw new NotImplementedException();

public ICollection<EndpointDataSource> DataSources => new List<EndpointDataSource>();

public IApplicationBuilder CreateApplicationBuilder()
{
throw new NotImplementedException();
}
}
internal class BenchmarkEndpointDataSource : EndpointDataSource
{
public override IReadOnlyList<Endpoint> Endpoints => throw new NotImplementedException();

public override IChangeToken GetChangeToken()
{
throw new NotImplementedException();
}
}
1 change: 1 addition & 0 deletions src/Http/Routing/src/EndpointMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public Task Invoke(HttpContext httpContext)
var endpoint = httpContext.GetEndpoint();
if (endpoint is not null)
{
// This check should be kept in sync with the one in EndpointRoutingMiddleware
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null &&
Expand Down
104 changes: 103 additions & 1 deletion src/Http/Routing/src/EndpointRoutingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.ShortCircuit;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Routing;

Expand All @@ -19,7 +23,7 @@ internal sealed partial class EndpointRoutingMiddleware
private readonly EndpointDataSource _endpointDataSource;
private readonly DiagnosticListener _diagnosticListener;
private readonly RequestDelegate _next;

private readonly RouteOptions _routeOptions;
private Task<Matcher>? _initializationTask;

public EndpointRoutingMiddleware(
Expand All @@ -28,6 +32,7 @@ public EndpointRoutingMiddleware(
IEndpointRouteBuilder endpointRouteBuilder,
EndpointDataSource rootCompositeEndpointDataSource,
DiagnosticListener diagnosticListener,
IOptions<RouteOptions> routeOptions,
RequestDelegate next)
{
ArgumentNullException.ThrowIfNull(endpointRouteBuilder);
Expand All @@ -36,6 +41,7 @@ public EndpointRoutingMiddleware(
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
_next = next ?? throw new ArgumentNullException(nameof(next));
_routeOptions = routeOptions.Value;

// rootCompositeEndpointDataSource is a constructor parameter only so it always gets disposed by DI. This ensures that any
// disposable EndpointDataSources also get disposed. _endpointDataSource is a component of rootCompositeEndpointDataSource.
Expand Down Expand Up @@ -102,6 +108,12 @@ private Task SetRoutingAndContinue(HttpContext httpContext)
}

Log.MatchSuccess(_logger, endpoint);

var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
if (shortCircuitMetadata is not null)
{
return ExecuteShortCircuit(shortCircuitMetadata, endpoint, httpContext);
}
}

return _next(httpContext);
Expand All @@ -115,6 +127,75 @@ static void Write(DiagnosticListener diagnosticListener, HttpContext httpContext
}
}

private Task ExecuteShortCircuit(ShortCircuitMetadata shortCircuitMetadata, Endpoint endpoint, HttpContext httpContext)
{
// This check should be kept in sync with the one in EndpointMiddleware
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null)
{
ThrowCannotShortCircuitAnAuthRouteException(endpoint);
}

if (endpoint.Metadata.GetMetadata<ICorsMetadata>() is not null)
{
ThrowCannotShortCircuitACorsRouteException(endpoint);
}
}

if (shortCircuitMetadata.StatusCode.HasValue)
{
httpContext.Response.StatusCode = shortCircuitMetadata.StatusCode.Value;
}

if (endpoint.RequestDelegate is not null)
{
if (!_logger.IsEnabled(LogLevel.Information))
{
// Avoid the AwaitRequestTask state machine allocation if logging is disabled.
return endpoint.RequestDelegate(httpContext);
}

Log.ExecutingEndpoint(_logger, endpoint);

try
{
var requestTask = endpoint.RequestDelegate(httpContext);
if (!requestTask.IsCompletedSuccessfully)
{
return AwaitRequestTask(endpoint, requestTask, _logger);
}
}
catch
{
Log.ExecutedEndpoint(_logger, endpoint);
throw;
}

Log.ExecutedEndpoint(_logger, endpoint);

return Task.CompletedTask;

static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
{
try
{
await requestTask;
}
finally
{
Log.ExecutedEndpoint(logger, endpoint);
}
}

}
else
{
Log.ShortCircuitedEndpoint(_logger, endpoint);
}
return Task.CompletedTask;
}

// Initialization is async to avoid blocking threads while reflection and things
// of that nature take place.
//
Expand Down Expand Up @@ -165,6 +246,18 @@ private Task<Matcher> InitializeCoreAsync()
}
}

private static void ThrowCannotShortCircuitAnAuthRouteException(Endpoint endpoint)
{
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
"but this endpoint is marked with short circuit and it will execute on Routing Middleware.");
}

private static void ThrowCannotShortCircuitACorsRouteException(Endpoint endpoint)
{
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
"but this endpoint is marked with short circuit and it will execute on Routing Middleware.");
}

private static partial class Log
{
public static void MatchSuccess(ILogger logger, Endpoint endpoint)
Expand All @@ -181,5 +274,14 @@ public static void MatchSkipped(ILogger logger, Endpoint endpoint)

[LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")]
private static partial void MatchingSkipped(ILogger logger, string? endpointName);

[LoggerMessage(4, LogLevel.Information, "The endpoint '{EndpointName}' is being executed without running additional middleware.", EventName = "ExecutingEndpoint")]
public static partial void ExecutingEndpoint(ILogger logger, Endpoint endpointName);

[LoggerMessage(5, LogLevel.Information, "The endpoint '{EndpointName}' has been executed without running additional middleware.", EventName = "ExecutedEndpoint")]
public static partial void ExecutedEndpoint(ILogger logger, Endpoint endpointName);

[LoggerMessage(6, LogLevel.Information, "The endpoint '{EndpointName}' is being short circuited without running additional middleware or producing a response.", EventName = "ShortCircuitedEndpoint")]
public static partial void ShortCircuitedEndpoint(ILogger logger, Endpoint endpointName);
}
}
4 changes: 4 additions & 0 deletions src/Http/Routing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#nullable enable
Microsoft.AspNetCore.Routing.RouteHandlerServices
static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable<string!>! httpMethods, System.Func<System.Reflection.MethodInfo!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult!>! populateMetadata, System.Func<System.Delegate!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions!, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult!>! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions
Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions
static Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions.ShortCircuit(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int? statusCode = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
static Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions.MapShortCircuit(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! builder, int statusCode, params string![]! routePrefixes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
static Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRoutingCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Routing.ShortCircuit;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// Short circuit extension methods for <see cref="IEndpointConventionBuilder"/>.
/// </summary>
public static class RouteShortCircuitEndpointConventionBuilderExtensions
{
private static readonly ShortCircuitMetadata _200ShortCircuitMetadata = new ShortCircuitMetadata(200);
private static readonly ShortCircuitMetadata _401ShortCircuitMetadata = new ShortCircuitMetadata(401);
private static readonly ShortCircuitMetadata _404ShortCircuitMetadata = new ShortCircuitMetadata(404);
private static readonly ShortCircuitMetadata _nullShortCircuitMetadata = new ShortCircuitMetadata(null);

/// <summary>
/// Short circuit the endpoint(s).
/// The execution of the endpoint will happen in UseRouting middleware instead of UseEndpoint.
/// </summary>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="statusCode">The status code to set in the response.</param>
/// <returns>The original convention builder parameter.</returns>
public static IEndpointConventionBuilder ShortCircuit(this IEndpointConventionBuilder builder, int? statusCode = null)
{
var metadata = statusCode switch
{
200 => _200ShortCircuitMetadata,
401 => _401ShortCircuitMetadata,
404 => _404ShortCircuitMetadata,
null => _nullShortCircuitMetadata,
_ => new ShortCircuitMetadata(statusCode)
};

builder.Add(b => b.Metadata.Add(metadata));
return builder;
}
}
Loading