-
Notifications
You must be signed in to change notification settings - Fork 44
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 cancellation token support #46
base: master
Are you sure you want to change the base?
Add cancellation token support #46
Conversation
|
||
var task = Task.Run(async () => await taskClient.Listen(cancellation.Token), CancellationToken.None); | ||
await Task.Delay(waitTime, CancellationToken.None); | ||
cancellation.Cancel(); |
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.
Enqueued task (WaitForTaskClientCancellationAndWriteSemaphore) will be canceled at this moment.
up |
Thank you for including tests with your PR. I am starting to look over it. |
@@ -28,37 +29,59 @@ public async Task ItContinuesListeningWhenATaskThrowsAnException() | |||
|
|||
taskClient.CancelListen(); | |||
await task; | |||
|
|||
|
|||
TaskQueueTestFixture.EnsureSemaphore(semaphoreFile); |
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.
Looks like a bug in the test, good catch!
Gofer.NET.Tests/GivenATaskInfo.cs
Outdated
var taskInfo3b = GetTestTask(() => TestMethod3(new CancellationTokenSource().Token)); | ||
|
||
var taskInfo4a = GetTestTask(() => Console.WriteLine("hello")); | ||
var taskInfo4b = GetTestTask(() => Console.WriteLine("hello world")); |
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.
👍 System assemblies do present an uncovered test case here.
|
||
namespace Gofer.NET.Utils | ||
{ | ||
public class JsonSkipCancellationTokenConverter : JsonConverter |
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.
Unless you can provide a compelling reason why the cancellation token should be excluded from the serialization, this functionality should be removed altogether.
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've added cancellation support for enqueued tasks. Json.Net cannot serialize CancellationToken anyway so it was an easiest fix I've could imagine.
Gofer.NET.Utils/TaskInfo.cs
Outdated
@@ -43,7 +44,7 @@ public bool IsEquivalent(TaskInfo otherTaskInfo) | |||
} | |||
|
|||
for (var i=0; i<Args.Length; ++i) { | |||
if (!Args[i].Equals(otherTaskInfo.Args[i])) | |||
if (!Equals(Args[i], otherTaskInfo.Args[i])) |
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.
reason for this change?
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 will be a NulIRefException if Args[i] is null. Actually, I've catched one and did the change to mitigate it.
@ig-sinicyn Is the idea that upon canceling the cancellation token passed to TaskClient.Listen that the cancellation should be propagated to the workers? |
Yeah. I've started with adding cancellation to the client's Start()/Listen() methods. But then I've found that there is no way to notify workers about cancellation. So, I've added this feature, too. |
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.
@brthor Thanks for the good review! I've made requested changes, let me know if there are more:)
…token to the IsEquivalent() method
6318fc7
to
12f4e86
Compare
up |
I let this roll over in my brain for a couple days. So pretty much my interpretation of what you're doing is allowing users to create tasks that take a cancellation token as an argument and then plugging in a cancellation token tied to the particular The intention is good, that the tasks get the cancellation signal and can handle their own stopping. The implementation, whereby you plug in a value for the token based on an available CancellationToken type argument in the task makes me worried. Firstly, it requires digging into the args, as I already commented on, and doing special tricks with them on serialization and at invocation time. In general, this is a behavior I'd like to stay away from as much as possible because it can cause unexpected behavior for users who haven't taken the time to familiarize themselves with this special behavior and naively use a task with a CancellationToken argument for whatever reason. A broader form of the argument is that this special behavior will interfere with regular usage where no intention was presented by the user for a So, if the functionality is good, but the implementation has some issues with user-experience, what is the ideal implementation? In my thinking the ideal implementation accommodates the user who wants a So I make the following proposal to satisfy those requirements:
Rough mockup:
My preliminary research shows that this implementation is feasible, subject to some testing. This implementation provides the user a way of obtaining a CancellationToken tied to the TaskClient with a clear gesture that specifies their intention rather than providing a convention that may interfere with regular usage without that intention. Examining the call stack may seem a little overkill, but this method is likely to be used infrequently only at the start of a task, minimizing performance concerns. Also, assuming it works well when tested, it's likely to be fairly durable. Please let me know your thoughts. EDIT: See below comment on using AsyncLocal instead of examining the call stack. |
{ | ||
TaskQueue = taskQueue; | ||
OnError = onError; | ||
TaskScheduler = new TaskScheduler(TaskQueue); | ||
IsCanceled = false; | ||
} | ||
|
||
public async Task Listen() | ||
public Task Listen() |
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.
To clean up the mix of being able to pass in a cancellation token here and at the same time have a default cancellation token source I recommend the following:
-
Change the
Start(CancellationToken cancellation)
overload toStart(CancellationTokenSource cancellationTokenSource)
-
Inside of the
Listen(CancellationToken cancellation)
useCancellationTokenSource.CreateLinkedTokenSource(cancellation)
to create aCancellationTokenSource
to pass toStart()
-
Inside the
Listen()
method overload, do not call the other overload of Listen but instead duplicate the code and call theStart()
overload with no parameters. -
Inside the
Start()
overload with no parameters usenew CancellationTokenSource()
and use it to call the other overload of StartStart(CancellationToken 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.
Current implementation has a very low overhead so I'd prefer to keep it until there are some scenarios it does not fit nicely.
Official docs recommend to pass CancellationToken, not CancellationTokenSource. Linked cancellation token is a standard approach for supporting both external and internal cancellation. So there is nothing unusual and surprising for end-user code.
Upon a little more research it seems that examining the call stack and getting the actual calling object instance cannot easily be accomplished. The https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=netcore-3.1 In this case a private static field can be used to track the Local Cancellation Token (set during the Start method). Then the |
@brthor good catch and astonishing level of detail. Thanks for a great response 👍 I do agree with proposed changes, no objections here. Will push fix in a day or two. |
…place them with a AsyncLocal<CancellationToken>();
@brthor sorry for the nasty delay:( Had a hard month and literally no spare time at all. I've made changes you've suggested. Code is much more cleaner now and area of changes was reduced a lot. Win-win:) |
await Task.Delay(waitTime, CancellationToken.None); | ||
cancellation.Cancel(); | ||
await Task.Delay(waitTime, CancellationToken.None); | ||
await 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.
If a bug is introduced, and the task fails to exit on cancellation will this introduce a hanging test?
throw new InvalidOperationException("This method must be called from a task client callback"); | ||
try | ||
{ | ||
await Task.Delay(-1, token); |
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 guess the real potential for a test hang is here. Perhaps a polling loop with a 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.
Nice catch! Fixed with a timeout parameter.
|
||
_listenCancellationContext.Value = token; | ||
|
||
var executionTimer = Stopwatch.StartNew(); |
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 worries @ig-sinicyn . I think overall it's looking pretty good and the async local change does clean it up a lot! My only concern left is the potential test hang, I think it's a pretty quick fix though. I'm going to re-enable the CI on this so the tests will run. |
Looks like the CI is running but not posting status updates. Link is here: https://travis-ci.org/github/brthor/Gofer.NET Tests are passing 👍 |
…phore + Fix TimeSpan and DateTimeOffset parameter passing
Thanks for spotting, have missed it! I've fixed the issue with timeout parameter. As it turned out TimeSpan and DateTimeOffset args were not deserialized properly so I've fixed args deserialzation as well:) |
up) |
1 similar comment
up) |
+1 up |
This PR
TaskInfo
is created manuallyTests included.