I was recently working at an async method which could possibly hang if some dependent stuff did not happen. To prevent the method from hanging, I wanted to implement some kind of timeout. Now, how can I make a task abort, given the following method signature?
1 | public class SomeClient |
Take One - Task Extension Method
The plan was to implement som kind of extension method to Task which could add the timeout-functionality.
1 | internal static class TaskExtensions |
And use it like this.
1 | public class SomeClient |
This design allowed that the internal Delay
-task would be cancelled if the user of my API canceled the method call. Nice! But it also had some mayor disadvantages:
- No task will be canceled if either of them finish successfully, which leads to having tasks running in the background for no reason, eating system resources.
- I had to make sure to pass the same cancellation token both to
DoStuffInnerAsync
andTimeoutAfter
, which might be something that could lead to mistakes further down.
Take Two - Expanding the Extension Method
To be able to cancel the TimeoutAfter
-task i needed a CancellationTokenSource
-instance, and pass its token to the TimeoutAfter
-method. And I also wanted the TimeoutAfter
-task to cancel if the user canceled the public API call.
This is what I came up with.
1 | internal static class TaskExtensions |
This is some seriously dangerous programming.
- By subscribing to cancel-events with
ct.Register(...)
I opened upp the possibility for memory leaks if I do not unsubscribe somehow. - Also, using
cts
(which can be disposed) in the delegate passed toct.Register(...)
might actually make my application crash ifct
was canceled outside of theTimeOutAfter
-method scope.
Register
returns a disposable something, which when disposed will unsubscribe. By adding the inner using-block, I fixed both of these problems.
This made it possible to cancel the Delay
-task when the actual task completed, but not the reverse. How should I solve the bigger problem, how to cancel the actual task if it would hang? Eating up system resources indefinitely…
Take Three - Changing the Public API
With much hesitation I finally decided to make a breaking change to the public API by replacing the CancellationToken
with a CancellationTokenSource
in the DoStuffAsync
-method.
1 | public class SomeClient |
Nice! But this still did not solve that I had make sure to pass the same cts
to both the actual task and the Delay
-task.
Final Solution - Doing the Obvious
Most things is really easy when you know the answer. By accident I stumbled upon that CancellationTokenSource
has a CancelAfter(...)
-method. This solves my problem entirely without the need to update my public API.
1 | var client = new SomeClient(); |
Easy peasy. I wish I had known about this earlier!