-
Notifications
You must be signed in to change notification settings - Fork 380
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
CancellationToken not cancelling #487
CancellationToken not cancelling #487
Conversation
Working solution pending tests cases. |
Closed, tests produce different results on each execution |
Updated tests to include timeout |
Included tests to validate issues solution. |
Core changes are set to a minimum. However, some issues are complicated to test. @jterry75 @galvesribeiro would you have a look on it? |
{ | ||
string line; | ||
while ((line = await reader.ReadLineAsync()) != null) | ||
string line = await await Task.WhenAny(reader.ReadLineAsync(), tcs.Task); |
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.
Fascinating idea. You seem to have an extra await
though.
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.
Yes, it looks like that.
await Task.WhenAny(task1, task2)
would return either task1 or task2.
Then, this task1 or task2 need to be awaited too.
Original idea from #343 by @suneption, I just adjusted it for tests to pass.
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.
Crazy. I did not know you could inline await :).
|
||
internal static async Task MonitorStreamAsync(Task<Stream> streamTask, DockerClient client, CancellationToken cancel, IProgress<string> progress) | ||
{ | ||
var tcs = new TaskCompletionSource<string>(); | ||
cancel.Register(() => tcs.TrySetCanceled(default)); |
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 in a using like the others?
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.
Should be like others by next commit. I'm running tests now before pushing again
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.
Just a couple of questions. Looks good
{ | ||
while (!cancel.IsCancellationRequested) | ||
{ | ||
string line = await await Task.WhenAny(reader.ReadLineAsync(), tcs.Task); |
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.
Alright one last question. When tcs.Task
is selected the 2nd await
will throw because there is no result. I assume this is the correct behavior and we want to throw the cancellation exception?
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.
When cancelling a token I expect to get an OpertionCancelledException. Otherwise it would mean task reached its end instead of being cancelled.
I have no knowledge about architecture or system designs, i just enjoy thinks working on the way I like. Do the minimum to not do it twice.
I've included several tests on this, expecting OperationCancelledException to be thrown and failing test otherwise.
There should be on IContainerOperationsTests
Edited: @jterry75 before removing Stream.Dispose, an ObjectDisposedException would be thrown most of the time, but also IOException and other unexpected behaviours. It took me a while to figure out how to test some parts, in my opinion no changes should be added without tests supporting them. Also there is so much more fun on it
Edit again:
By using (cancel.Register(() => tcs.TrySetCanceled(default)))
causes tcs.Task not to be selected because of finished but because of cancelled. Then, OperationCancelledException
is thrown.
A different use of TaskCompletionSource
is on GetContainerLogs_MonitorStream_Succeeds
test, where I validate task result to be what's being set within MonitorStream
handler
@galvesribeiro wins
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 don't think it will throw there. The tcs.Task
will be returned.
In this case I wouldn't assign to the line
variable directly. I would assign it to a Task and would check which task is that - the readline or the cancellation.
If that is the cancellation task, then yeah, it is cancelled. If it is the deadline Task, then you string line = await readLineTask();
.
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 should be a test behind this
Edit: @galvesribeiro you win
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.
Agreed.
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 listened to your advice. It's not exactly as you said but shares the same idea.
It helped fixing some problems when reading streams from containers created without a tty
Thank you @galvesribeiro
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.
LGTM
@galvesribeiro - Will you check my last question about throwing the cancellation exception here. As long as your good with that we can take this change IMO. |
@jterry75 @galvesribeiro to not merge this. Tested behavior is not right on
Expect unified behavior to be pushed soon |
// @dgvives: This test doesn't work on xUnit test project. Execution never finishes. | ||
// Same code on consoleApp works just fine |
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.
This may suggest a deadlock bug in the code, rather than an xUnit problem. It's common for async code to work in Console apps but break elsewhere, due to the difference in synchronization contexts. Unfortunately, I'm not an expert in how sync contexts and schedulers and such work, so I don't know where to suggest looking.
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 see, I'm creating some sort of web app to test this. Will update with test results
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.
@Emdot you are righ, JsonReader.ReadAsync can't be cancelled once serialization has started.
Looking for an alternative now
if (timeout != s_InfiniteTimeout) | ||
{ | ||
using (var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) | ||
{ | ||
timeoutTokenSource.CancelAfter(timeout); |
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 make sense to move the timeout complexity to a separate function? Then this whole method can be:
var request = PrepareRequest(method, path, queryString, headers, data);
return RunWithTimeoutAsync(
ct => _client.SendAsync(request, completionOption, ct),
cancellationToken, timeout);
A RunWithTimeoutAsync implementation might look like this:
public static async Task<T> TimeoutAfter<T>(Func<CancellationToken, Task<T>> makeTask, CancellationToken inputToken, TimeSpan timeout)
{
// Make a token that triggers either manually or when the input token triggers.
using var cts = CancellationTokenSource.CreateLinkedTokenSource(inputToken);
var timerTask = Task.Delay(timeout, cts.Token);
var mainTask = makeTask(cts.Token);
// Start both the timeout and the main task in parallel.
Task firstTask == await Task.WhenAny(timerTask, mainTask).ConfigureAwait(false);
// At this point, one of them has finished. Cancel the other one.
cts.Cancel();
// If the consumer canceled, put the output task in the canceled state and link it to the input token.
if (inputToken.IsCancellationRequested)
throw new OperationCanceledException(inputToken);
// If the timeout occurred first, throw a timed-out exception.
if (firstTask == timerTask)
throw new TimeoutException();
// Otherwise return the result of the main task. If it was canceled, this may throw
// a cancelation exception. If it faulted, it may throw some other exception.
return await mainTask.ConfigureAwait(false);
}
For added performance you could use a state parameter to avoid the closure. As a bonus, timeouts emit a separate exception than cancellation.
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.
@Emdot great suggestion.
I'll wait for this to get approved and continue with improvements.
|
Closed while research on stream.read not being cancelled is done |
When using I went over the source for JsonReader, StreamReader, Stream, and finally JsonSerializer until I found out this. private object ReadObject(JsonReader reader)
{
IDictionary<string, object?> expandoObject = new ExpandoObject();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
string propertyName = reader.Value!.ToString();
if (!reader.Read())
{
throw JsonSerializationException.Create(reader, "Unexpected end when reading ExpandoObject.");
}
object? v = ReadValue(reader);
expandoObject[propertyName] = v;
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
return expandoObject;
}
}
throw JsonSerializationException.Create(reader, "Unexpected end when reading ExpandoObject.");
} This isn't easy to find out, was difficult to figure out a solution, and turned even more complicated to test. |
@dgvives - Looks like you got it! |
@jterry75 - Yes, it has been a very interesting journey into task cancelling and tests |
Do you want to rebase all your commits into one just because it was a lot of change over them and then I'll do a final review and signoff. But looks good to me |
974e624
to
f0336bf
Compare
@jterry75 done |
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.
LGTM
@galvesribeiro - Want to take a look as well |
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.
Awesome work! Thank you for the contribution and the huge patience across the reviews.
@jterry75 do you think we have enough for a new release now? |
Fixes #236
Fixes #375
Fixes #416
Fixes #434
Closes #343
Closes #351
Closes #394
It solves execution stop on
DockerClient.Containers.WaitContainerAsync
waiting for the task to finish without being able to cancel.