Home > OS >  How to implement a mechanism in C# where a task is started only if previous task isn't finished
How to implement a mechanism in C# where a task is started only if previous task isn't finished

Time:03-04

I have a use case where my service talks to a server which has multiple fallback servers. I am trying to develop a mechanism in which I talk to the primary server first, but it doesn't respond in x seconds, I try another fallback server and so on and so forth until any one of them responds.

With my investigation I found I could possibly use Timer() or LinkedCancellationToken but none of them are quite fit for my use case.

Timer() would schedule something for later, but I want to schedule it only if primary server doesn't respond within limit.

LinkedCancellationToken would let me cancel the call if any of the linked cancellationTokens are cancelled, but that's not my use case.

Any ideas on how I could possibly implement this?

CodePudding user response:

This is a fairly straightforward use of promises a loop.

public async Task<Connection> ConnectAsync()
{
    foreach (var srv in Servers)
    {
        var cts = new CancellationTokenSource(ConnectionTimeout);
        try
        {
            return await srv.ConnectAsync(cts.Token);
        }
        catch (OperationCancelledException)
        {
            // no-op
        }
    }
    
    throw new NoServerRespondedException();
}

Start each attempt, if it times out you move on to the next. If all timeout, fail.

If you don't want to cancel the earlier requests, then Task.WhenAny is your friend.

public async Task<Connection> ConnectAsync()
{
    var cts = new CancellationTokenSource();
    var inProgress = new List<Task<Connection>>();
    foreach (var srv in Servers)
    {
        // Ensure task does not complete before success
        inProgress.Add(async () => 
        {
            try
            {
                return await srv.ConnectAsync(cts.Token)).ConfigureAsync(false);
            }
            catch (OperationCancelledException) { /* no-op */}
            catch (Exception ex)
            {
                // TODO: Log connection failure

                // prevent this task from completing before cancellation
                var cancellationTask = new TaskCompletionSource<bool>();
                cts.Token.Register(() => cancellationTask.TrySetResult(true));
                await cancellationTask.Task;
            }
        });

        var completedTask = Task.WhenAny(inProgress.Concat(new []{ Task.Delay(ConnectionInterval) });
        if (completedTask is Task<Connection> connectedTask)
        {
            cts.Cancel();
            return await connectedTask;
        }
    }
    
    cts.Cancel();
    throw new NoServerRespondedException();
}

The above will also return on failure of any task. If you want to ignore failures of

CodePudding user response:

In principle, this is pretty simple: you can await Task.WhenAny(...), passing in the tasks you've created plus another "delay" task that will end when you're tired of waiting and ready to try another server.

Of course, best practice says you should never leave any tasks unawaited, and you should cancel the other requests once you've received a good request. That can make things a little more complicated:

async Task<Response> GetFirstServerResponse(IEnumerable<string> serverNames, CancellationToken cancellationToken)
{
    var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    var tasks = new List<Task<Response>>();
    try
    {
        foreach (var serverName in serverNames)
        {
            tasks.Add(TalkToServer(serverName, linkedTokenSource.Token));
            await Task.WhenAny(tasks.Append(Task.Delay(delay)));
            var completedTask = tasks.FirstOrDefault(t => t.IsCompleted);
            if(completedTask != null)
            {
                return await completedTask;
            }
        }
        var firstCompleted = await Task.WhenAny(tasks);
        return await firstCompleted;
    }
    finally
    {
        // Make all the incomplete tasks stop trying.
        linkedTokenSource.Cancel();
        try
        {
            // Never leave tasks unawaited.
            // Since we cancelled these, they should all finish very quickly now.
            await Task.WhenAll(tasks);
        }
        catch (OperationCanceledException) // ignore tasks that got cancelled.
        {
        }
    }
}

Play around with it in this dotnetfiddle.

Depending on your requirements, you may need to tweak this. For example, I'm assuming if a request ends in error, then you want to bail and surface that error. If you expect certain types of intermittent errors may occur from one server but not from others, and you want to ignore errors as long as any request succeeds, you'll need to make some changes to this code.

CodePudding user response:

You could create a little helper method to handle the logic

public static async Task Foo(Func<Task> method1, Func<Task> method2, int timeoutSecs) {
    var taskToCheck = method1();
    await Task.Delay(timeoutSecs * 1000);
    if (!taskToCheck.IsCompleted)
        await method2();
}

Would work, or you could pass the task itself instead of method1 if you prefer. If you need the results of either task you can change the signature, the only problem is that my method should probably bail if method1 finishes early instead of wasting time waiting - I'll be back in 5 with an edit

Ok, It's not the cleanest but this should work. The empty try catches are assuming you will do the error unwrapping elsewhere, these methods just start the Tasks

public static async Task Foo(Func<Task> method1, Func<Task> method2, int timeoutSecs)
{
    var taskToCheck = method1();
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    _ = CancelTokenAfterTaskCompleted(taskToCheck, tokenSource);
    try
    {
        await Task.Delay(timeoutSecs * 1000, tokenSource.Token);
    }
    catch { }
    if (!taskToCheck.IsCompleted)
        await method2();
}
private static async Task CancelTokenAfterTaskCompleted(Task taskToCheck, CancellationTokenSource ts) {
    try { await taskToCheck; } catch { } finally {
        ts.Cancel();
    }
}

Another edit, because I'm avoiding work and felt like a nice little challenge. Let noone say I don't know how to overcomplicate things (:<

public static async Task<string> Foo(Dictionary<string, Func<Task<bool>>> connectionMethods, int timeoutSecs, int masterTimeout)
{
    CancellationTokenSource completedTokenSource = new CancellationTokenSource();
    Dictionary<string, Task<bool>> startedTasks = new Dictionary<string, Task<bool>>();
    foreach (var function in connectionMethods)
    {
        CancellationTokenSource finishedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(completedTokenSource.Token);
        var taskToCheck = function.Value();
        _ = CancelTokenAfterTaskCompleted(taskToCheck, finishedTokenSource);
        _ = CancelTokenAfterTaskReturnedTrue(taskToCheck, completedTokenSource);
        startedTasks[function.Key] = taskToCheck;
        try
        {
            await Task.Delay(timeoutSecs * 1000, finishedTokenSource.Token);
        }
        catch { }
        if (completedTokenSource.IsCancellationRequested)
            break;
        if (!taskToCheck.IsCompleted || taskToCheck.IsFaulted || !(await taskToCheck)) //is the task still running, failed or returned false
            continue;
        return function.Key;
    }
    if (!completedTokenSource.IsCancellationRequested)
        try
        {
            await Task.Delay(masterTimeout * 1000, completedTokenSource.Token);
        }
        catch { }
    foreach (var taskSet in startedTasks)
        if (taskSet.Value.IsCompleted && !taskSet.Value.IsFaulted && (await taskSet.Value))
            return taskSet.Key;
    throw new Exception($"None of the methods finished within the alloted timespan");
}
private static async Task CancelTokenAfterTaskCompleted(Task taskToCheck, CancellationTokenSource ts)
{
    try { await taskToCheck; }
    catch { }
    finally
    {
        ts.Cancel();
    }
}
private static async Task CancelTokenAfterTaskReturnedTrue(Task<bool> taskToCheck, CancellationTokenSource ts)
{
    try
    {
        if (await taskToCheck)
            ts.Cancel();
    }
    catch { }
}

this version takes in a dictionary of keys and connection delegates (which return true on successful connection). It then progressively goes through each using the logic you wanted, but at the same time keeps track of whether or not any of the started tasks have succeeded (using a second cancellation token source - I kept two because if you get an error 404 for example, you'd want to move on to the next method immediately, but the main cancellationToken should only end when one succeeds). I've really gotta stop wasting time now and get back to work, hope you enjoy my overcomplicated nightmare of a method.

  • Related