2
votes

I realize this is not the first thread on cancelling tasks, but I didn't find anything suitable so far which fits my needs.

Here's the case:

  • I've got a modular system in which modules are initialized by a controller class, dynamically based on configuration.
  • Each module implements a Connect() method. The method itself is black box, but for background info: some modules attempt to create Bluetooth connections.
  • If connecting takes too long, I want to cancel the attempt and retry it in 5 minutes.

At first I put the retry logic in the module implementation. That works fine, but becomes repetitive over modules and doesn't really belong to the core responsibility of the module. So I then considered putting the logic in the controller, but I'm struggling to find a nice implementation.

The issue here is that I not only want to cancel the task, I want to do it in a nice way. If there's objects to close / dispose, the module should be able to do so. Also, the module should be able to set its status to "Disconnected" instead of "Connecting" (and I rather have the module do that than have a public method so externally change the status). This was all easy when the Connect and timeout logic is inside the module, but becomes more challenging introducing an outside timeout.

Here's the flow I foresee when a module does not respond in time:

  • Controller creates a new module instance
  • Controller calls Connect() in order to connect the module
  • Controller also initializes a timer to monitor the module finishes connecting in 30 seconds
  • Timer goes off, module hasn't finished it's work yet
  • Connect() method should be notified of Cancellation, preferably by throwing an exception inside of the Connect method which can be caught and handled.
  • Exception handler can then clean-up things neatly and the method returns.

The above flow doesn't exist as far as I've found. Cancellation tokens work differently and require me to have events or polling in place. That's nice, but then I'm back to including this logic into my modules so why not just do the entire retry thing in there to begin with. So I wonder whether there is any sweet pattern out there that allows to me to this.

I'm building, by the way, for Windows 10 UWP. I haven't included any code on purpose because what I have at the moment is crap anyway.

1

1 Answers

1
votes

Don't get what is the problem with CancellationToken your async operation supports cancelation or doesn't there is no way to stop code that is executing unless you kill the thread or process. Here is a sample of timeout.

  public static async Task Run() 
            {
                var module = new Module();

                //No timeout
                await module.Connect(1, CancelAfter(2000));

                try
                {
                    // Timeout
                     await module.Connect(5, CancelAfter(1000));
                }
                catch (Exception)
                {

                    module.Dispose();
                }

            }

            public static CancellationToken CancelAfter(int millisecondsDelay) 
            {
                var token = new CancellationTokenSource();
                token.CancelAfter(millisecondsDelay);
                return token.Token;
            }

            public class Module : IDisposable
            {
                public async Task Connect(int count, CancellationToken cancel)
                {
                    for (int i = 0; i < count; i++)
                    {
                        //This is just to simulte some work Task.Delay can be canceled as well with Task.Delay(500,cancel)
                        await Task.Delay(500);
                        cancel.ThrowIfCancellationRequested();
                    }
                }

                public void Dispose()
                {

                }
            }

You can time out any task with

public static class TaskTimeout
    {
        public static async Task TimeoutAfter(this Task task, int millisecondsTimeout)
        {
            if (task != await Task.WhenAny(task, Task.Delay(millisecondsTimeout)))
            { throw new TimeoutException(); }
        }

        public static async Task<T> TimeoutAfter<T>(this Task<T> task, int millisecondsTimeout)
        {
            if (task != await Task.WhenAny(task, Task.Delay(millisecondsTimeout)))
            { throw new TimeoutException(); }
            else { return task.Result; }
        }
    }

But this won't cancel you connection only if Connect is cancelable it can be canceled. Most Async calls in .net have CancellationToken implemented so if your connection has it use that.