-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Add short circuit in routing #46713
Conversation
Thanks for your PR, @Kahbazi. Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>(); | ||
if (shortCircuitMetadata is not null) | ||
{ | ||
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata) | ||
{ | ||
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null) | ||
{ | ||
ThrowMissingAuthMiddlewareException(endpoint); | ||
} | ||
|
||
if (endpoint.Metadata.GetMetadata<ICorsMetadata>() is not null) | ||
{ | ||
ThrowMissingCorsMiddlewareException(endpoint); | ||
} | ||
} | ||
|
||
if (shortCircuitMetadata.StatusCode.HasValue) | ||
{ | ||
httpContext.Response.StatusCode = shortCircuitMetadata.StatusCode.Value; | ||
} | ||
|
||
if (endpoint.RequestDelegate is not null) | ||
{ | ||
return endpoint.RequestDelegate(httpContext); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There must be a more efficient way to do this since it's a first class feature now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which part concerns you, endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking a bit further on this, I believe this will hurt performance in the common case (that your route matches) compared to the more exceptional case that you want to shortcircuit. I think that probably means that this should be opt-in, so that apps that don't have this concern don't have to pay the performance penalty.
An alternative design is to create and set a special type of endpoint ShortcutEndpoint
with a fixed delegate and a Shortcut
property, and cast to that instead of searching inside the metadata collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our goal should be to keep logic that is not related to endpoint selection outside of routing as much as possible. Code that provides specific behaviors should in general go into their own middleware so that devs get a chance of choosing when to apply the behavior and so that routing doesn't have to be aware of such concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What part is the perf overhead, endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
? Rather than separating the components, we could mitigate that cost instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If GetMetadata is too slow then we should look into improving it, It gets called a lot per request.
Alternatives:
- A new bool property on Endpoint?
- A derived Endpoint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach that is currently followed here is inheritance, see "Endpoint" and "RouteEndpoint".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inheritance doesn't seem like a good fit in this case, we'd want this behavior to apply to existing kinds of endpoints, especially RouteEndpoint's.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more this grows, the more it feels that the execution part should be in its own middleware, and we should keep the routing middleware as is.
It feels that this is a very specific behavior that is useful in a few cases but that everybody is now paying for.
Why does this deserve a special treatment within the endpoint routing middleware as compared to the authorization/authentication or other similar middleware. If the concern is that there needs to be an extra call in the pipeline that the user might forget, why can't it work similar to how the authentication middleware does and throw an exception if it detects the middleware is not present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the concern is that there needs to be an extra call in the pipeline that the user might forget, why can't it work similar to how the authentication middleware does and throw an exception if it detects the middleware is not present.
That ends up being the same check as this that gets called on every request.
The microbenchmarks do not show this to be significant overhead.
d530ba5
to
5443182
Compare
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
|
||
namespace Microsoft.AspNetCore.Routing; | ||
|
||
public class EndpointRoutingShortCircuitBenchmark |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointConventionBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointConventionBuilderExtensions.cs
Show resolved
Hide resolved
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
a0fb6a7
to
8abecae
Compare
group.Map(routePrefix, _shortCircuitDelegate).ShortCircuit(statusCode); | ||
} | ||
|
||
return new EndpointConventionBuilder(group); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not return the group?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree groups are convenient for this. The one problem I see is that even though the declared type is IEndpointConventionBuilder, someone could call (RouteGroupBuilder)app.MapShortCircuit(404, "/prefix1", "/prfix2") and then add additional routes.
While not exactly a problem, it's a little weird, and would prevent us from returning another IEndpointConventionBuilder that's not derived from the currently-sealed RouteGroupBuilder without going through the breaking change process since it'd be considered a behavioral breaking change to the return value. So probably better to be safe and copy the private CompositeEndpointConventionBuilder. Maybe we could make it shared source.
From #46713 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@halter73 is this really a concern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I missed this, but yes. If we think it's valuable to return a RouteGroupBuilder, we should make that the declared return type. You know people will end up casting it anyway if we let them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation is fine, if cautious. We can decide to remove the wrapper later if we think that caution is not needed. I don't think we want to change the API return type though. Publicly returning the group exposes an implementation detail and sets the wrong expectation about how that return type can be used. The group APIs don't make sense for this scenario. We only return the builder so that other metadata can be added like http method constraints.
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
@davidfowl, we addressed the feedback here and in API review. We'll keep this in the routing middleware for now. |
src/Http/Routing/src/ShortCircuit/RouteShortCircuitEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs
Outdated
Show resolved
Hide resolved
src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs
Outdated
Show resolved
Hide resolved
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
Thanks @Kahbazi ! |
@Kahbazi, this change will be considered for inclusion in the blog post for the release it'll ship in. Nice work! Please ensure that the original comment in this thread contains a clear explanation of what the change does, why it's important (what problem does it solve?), and, if relevant, include things like code samples and/or performance numbers. This content may not be exactly what goes into the blog post, but it will help the team putting together the announcement. Thanks! |
foreach (var routePrefix in routePrefixes) | ||
{ | ||
string route; | ||
if (routePrefix.EndsWith("/", StringComparison.OrdinalIgnoreCase)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it better if using if (routePrefix[^1] is '/')
instead
Fixes #46071