-
Notifications
You must be signed in to change notification settings - Fork 160
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
improvement: Add cancellation support for long running queries #1271
Conversation
I'm not sure how I could test that but I'm open to suggestions |
@microsoft-github-policy-service agree |
@rpallares could you create a separate issue describing the PageSize issue in detail? |
Hi, I'm note sure it's necessary to create a new one because all these existing issues already details that comportement:
There's probably more related as #423 seems to come from very old issue and even older pr #324 I also saw references inside https://github.com/OData/WebApi
If any brealing change is required, odata team should really consider add it in the next major version. Thanks |
Here's a suggestion of how you could test this. The general idea is to pass a cancellation token for which you have the Look at the First, I think it will be easier to create unit tests if the Create a test async enumerable data source that will generate the sample data to serialize and will handle the cancellation token that will be passed by the serializer: private static async IAsyncEnumerable<Address> GetAsyncAddressesDataSource(
[EnumeratorCancellation] CancellationTolen cancellationToken = default)
{
for (int i = 0; i < 1000; i++)
{
yield return new Address { City = "City" };
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
}
} Ideally the enumeration will be cancelled before it runs through all those iterations. If we abort the request before we start serializing, then we're guaranteed the enumeration will be cancelled (if cancellation token is propagated correctly). If we cancel the token concurrently with serialization, it's possible it might run through the whole loop before cancellation is triggered even if the code is correct. So this could be a flaky test if we do it that way, but the Now we'll need a way to create trigger the cancellation of the token from the test. One way to do this would be to create an instance of Alternatively, we could inject an override the cancellation token in the First, we use the var request = RequestFactory.Create();
// ...
var addresses = GetAsyncAddressesDataSource();
var type = typeof(IAsyncEnumerable<Address>);
ODataSerializationContext writeContext = new ODataSerializationContext
{
Model = model,
Type = type ,
Request = request
};
// in this example we abort before we run serialization
writeContext.Request.HttpContext.Abort();
await Assert.ThrowAsync<OperationCanceledException>(() => serializer.WriteObjectAsync(addresses, type, writer, writeContext); In the example above we abort the request before serialization starts, so we're guaranteed the enumeration will be cancelled if the code is implemented correctly. We could also abort after the serialization has started, which I think is more representative of real-world use cases. While technically flaky, I think that's unlikely to manifest in practice given the large number of iterations in the loop: var serializationTask = serializer.WriteObjectAsync(addresses, type, writer, writeContext);
request.HttpContext.Abort();
await Assert.ThrowsAsync<OperationCanceledException>(() => serializationTask); This test will fail because this test public static HttpRequest Create()
{
HttpContext context = new DefaultHttpContext();
return context.Request;
} I've looked at the implementation of private class TestRequestLifeTimeFeature : IHttpRequestLifetimeFeature
{
private CancellationTokenSource _cts = new CancellationTokenSource();
public CancellationToken RequestAborted { get => _cts.Token; set => throw new NotImplementedException(); }
public void Abort()
{
_cts.Cancel();
}
} This could be an inner helper class inside And finally, we can update the request factory to inject this feature: public static HttpRequest Create()
{
FeatureCollection features = new();
features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
HttpContext context = new DefaultHttpContext(features);
return context.Request;
} |
Please add the tests like the ones suggested by @habbes above #1271 (comment) |
Hi, yes. @habbes gave great very detailled suggestions. |
I changed a bit against @habbes suggested to be able to correctly dispose the CancellationTokenSource. |
@habbes , any official news regarding the pagesize topic? |
…ncellation-support
The change looks good to me, simpler and requires less changes. |
@rpallares I agree that the page size issue is longstanding issue that we should finally solve. We are looking to invest in more perf improvements to the library and this is one that should definitely be part of that. I can't give an ETA yet, but I'm bringing this to the attention of the team. We also want to get rid of places where synchronous I/O is forced even when the data source is async, and this is one of such bottlenecks. |
Thank you @rpallares for your contribution |
Co-authored-by: Rafael Pallares <[email protected]>
Hi,
I open this PR to support cancellation for long running queries.
It's inpired from #423.
The root problem is related to PageSize feature that imply synchornous queries.
This force us to not use it and expose directly IAsyncEnumerable.
The counterpart is clients are able to totally dump our database, and if not wanted, it's still nice to cancel thé request.
That issue force to not apply Security guidance for ASP.NET Core Web API OData suggestions to not sacrifice performance.
To reduce a bit risks a bit cancellation must be supported to at least reduce ressource consumption when the client timeout or cancel on long running queries.
The PageSize issue should be also fixed.