9

One method is a standard async method, like this one :

private static async Task AutoRetryHandlerAsync_Worker(Func<Task<bool>> taskToRun,...)

I have tested two implementations, one that use await and the other uses .Wait()

The two implementations are not equal at all because the same tests are failing with the await version but not the Wait() one.

The goal of this method is to "execute a Task returned by the input function, and retry by executing the same function until it works" (with limitations to stop automatically if a certain number of tries is reached).

This works:

private static async Task AutoRetryHandlerAsync_Worker(Func<Task<bool>> taskToRun,...)
{
    try {
       await taskToRun();
    }
    catch(Exception) 
   {
       // Execute later, and wait the result to complete
       await Task.Delay(currentDelayMs).ContinueWith(t =>
       {
            // Wait for the recursive call to complete
            AutoRetryHandlerAsync_Worker(taskToRun).Wait();
       });

       // Stop
       return;
    }    
}

And this (with async t => and the usage of await instead of t => and the usage of .Wait() doesn't work at all because the result of the recursive call is not awaited before the final return; is executed :

private static async Task AutoRetryHandlerAsync_Worker(Func<Task<bool>> taskToRun,...)
{
    try {
       await taskToRun();
    }
    catch(Exception) 
   {
       // Execute later, and wait the result to complete
       await Task.Delay(currentDelayMs).ContinueWith(async t =>
       {
            // Wait for the recursive call to complete
            await AutoRetryHandlerAsync_Worker(taskToRun);
       });

       // Stop
       return;
    }    
}

I'm trying to understand why this simple change does change everything, when it's supposed to do the exact same thing : waiting the ContinueWith completion.

If I extract the task ran by the ContinueWith method, I do see the state of the ContinueWith function passing to "ranToCompletion" before the return of the inner await completes.

Why? Isn't it supposed to be awaited?


Concrete testable behaviour

public static void Main(string[] args)
{
    long o = 0;
    Task.Run(async () =>
    {
        // #1 await
        await Task.Delay(1000).ContinueWith(async t =>
        {
            // #2 await
            await Task.Delay(1000).ContinueWith(t2 => {
                o = 10;
            });
        });
        var hello = o;
    });


    Task.Delay(10000).Wait();
}

Why does var hello = o; is reached before o=10?

Isn't the #1 await supposed to hang on before the execution can continue?

Micaël Félix
  • 2,697
  • 5
  • 34
  • 46
  • `await` is the way for your method to say "I've got thing's I'll need to do later, once this task has finished, but for now, *I don't need to tie up this thread*, maybe my caller can make some use of it". Precisely the opposite of `Wait` "I can't do anything until this task finishes. I'm going to hog this thread doing nothing until it's done" – Damien_The_Unbeliever Apr 21 '17 at 12:26
  • Yes but the await is supposed to pause the current thread execution, so why does the "return;" is reached even if there is an await before it? I am adding a concrete testable example at the end of this post. – Micaël Félix Apr 21 '17 at 12:28
  • 1
    No, it *doesn't* pause the thread. It hands control back to its caller. That's the point of these mechanisms - to keep threads doing *useful work* rather than idly waiting for other threads/IO to do work. – Damien_The_Unbeliever Apr 21 '17 at 12:29
  • Does my answer to this question help you? [If async-await doesn't create any additional threads, then how does it make applications responsive?](http://stackoverflow.com/questions/37419572/if-async-await-doesnt-create-any-additional-threads-then-how-does-it-make-appl/37419845#37419845) – Lasse V. Karlsen Apr 21 '17 at 12:31
  • @LasseV.Karlsen your example clearly states in step 5 that the code executed 2s later is the "code is all the code after the await", in the case I explain here, the code is not executed after the completion (of the Task.Delay(1000) or the continuewith), it's executed before the inner tasks awaited are completed (I don't know if I explain things easily, that may be fuzzy). – Micaël Félix Apr 21 '17 at 12:34
  • @Damien_The_Unbeliever even if there's some kind of resume of control, do you agree that it is NOT meant to execute the code follwing an await until the awaited task complete? – Micaël Félix Apr 21 '17 at 12:36
  • 8
    `ContinueWith` doesn't handle "async lambda" and doesn't wait for the task returned by the lambda to complete, instead it simply returns the task back through ContinueWith and lets someone else await it. You will need `await await` to handle this. – Lasse V. Karlsen Apr 21 '17 at 12:38
  • @LasseV.Karlsen THANK YOU!!! – Micaël Félix Apr 21 '17 at 12:39
  • 1
    @LasseV.Karlsen give a full answer comments are rarely fully read – BRAHIM Kamel Apr 21 '17 at 12:55
  • @BRAHIMKamel Yes and this usage of `await await` is not really common, in fact I didn't see any example at all of `ContinueWith` with double await. – Micaël Félix Apr 21 '17 at 13:00
  • 1
    @MicaëlFélix it's just a matter of return type. In your case the `ContinueWith` internal method retruns a `Task`. You could also do `var myTask = await Task.Delay(1000).ContinueWith(async t => ...);` and then `await myTask;` If you won't await at all the original return type is `Task` – Ofir Winegarten Apr 21 '17 at 13:08
  • 1
    Not an answer to the question, but you can do an easier and probably more performant implementation with a simply while loop instead of the manual continuation and recursion: `while (true) { try { await taskToRun(); return; } catch (Exception) { await Task.Delay(currentDelayMs); } }` – Matthias247 Apr 21 '17 at 13:11
  • In the second option where you `Wait` within the `ContinueWith`, the internal method returns a void and not a Task. – Ofir Winegarten Apr 21 '17 at 13:11
  • 1
    Also note that the `.Wait()` approach will block a thread on the threadpool (where `Task.Delay()` finishes) until `taskToRun()` has finished. Depending on the concurrency level and the duration of `taskToRun` this might not be the ideal solution. – Matthias247 Apr 21 '17 at 13:15
  • @Matthias247 yes, the use of .Wait() was only used to fix the unexpected behaviour with ContinueWith returning Task. – Micaël Félix Apr 21 '17 at 13:18
  • `I didn't see any example at all of ContinueWith with double await.` Because `ContinueWith` is a [dangerous, low-level method](https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html) with the same [pitfalls as `StartNew`](https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html). You should use `await` *instead of* `ContinueWith`. – Stephen Cleary Apr 21 '17 at 15:21

1 Answers1

6

The lambda syntax obscures the fact that you ContinueWith(async void ...).

async void methods are not awaited and any errors they throw will go unobserved.

And to your base question, retrying from within a catch is not a recommended practice anyway. Too much going on, catch blocks should be simple. And bluntly retrying for all exception types is also very suspect. You ought to have an idea what errors could warrant a retry, and let the rest pass.

Go for simplicity and readability:

while (count++ < N)
{
   try
   {          
      MainAction();
      break;      
   }
   catch(MoreSpecificException ex) { /* Log or Ignore */ }

   Delay();
}
H H
  • 263,252
  • 30
  • 330
  • 514