Vraag Probeer een taak meerdere keren opnieuw op basis van gebruikersinvoer in het geval van een uitzondering in de taak


Alle serviceaanvragen in mijn toepassing worden geïmplementeerd als taken.Wanneer er ooit een taak wordt verholpen, moet ik de gebruiker een dialoogvenster presenteren om de laatste bewerking opnieuw uit te voeren. Als de gebruiker opnieuw probeert, moet het programma de taak opnieuw proberen, anders de de uitvoering van het programma moet worden voortgezet na het loggen van de uitzondering. Iedereen heeft een idee op hoog niveau over hoe deze functionaliteit moet worden geïmplementeerd?


10
2018-05-07 23:04


oorsprong


antwoorden:


UPDATE 5/2017

C # 6 uitzonderingsfilters maken het catch clausule veel eenvoudiger:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch when (retryCount-- > 0){}
        }
    }

en een recursieve versie:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch when (retryCount-- > 0){}
        return await Retry(func, retryCount);
    }

ORIGINAL

Er zijn veel manieren om een ​​Retry-functie te coderen: u kunt recursie of taak-iteratie gebruiken. Er was een discussie in de Griekse .NET-gebruikersgroep een tijdje terug op de verschillende manieren om precies dit te doen.
Als u F # gebruikt, kunt u ook Async-constructies gebruiken. Helaas kunt u de async / await-constructies niet gebruiken in ten minste de Async CTP, omdat de code die door de compiler wordt gegenereerd, niet van meerdere wachttijden of mogelijke rethrows in catch-blokken houdt.

De recursieve versie is misschien de eenvoudigste manier om een ​​Retry in C # te bouwen. De volgende versie gebruikt Unwrap niet en voegt een optionele vertraging toe voordat opnieuw wordt geprobeerd:

private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    Task.Factory.StartNewDelayed(delay).ContinueWith(t =>
                    {
                        Retry(func, retryCount - 1, delay,tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

De StartNewDelayed functie komt van de ParallelExtensionsExtras bemonstert en gebruikt een timer om een ​​TaskCompletionSource te activeren wanneer de time-out optreedt.

De F # -versie is een stuk eenvoudiger:

let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = 
let rec retry' retryCount = 
    async {
        try
            let! result = asyncComputation  
            return result
        with exn ->
            if retryCount = 0 then
                return raise exn
            else
                return! retry' (retryCount - 1)
    }
retry' retryCount

Helaas is het niet mogelijk om iets vergelijkbaars te schrijven in C # met async / await van de Async CTP, omdat de compiler niet op verwachte instructies in een catch-blok wacht. De volgende poging mislukt ook silenty, omdat de runtime niet graag een await tegenkomt na een uitzondering:

private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await TaskEx.Run(func);
                return result;
            }
            catch 
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

Wat betreft het vragen aan de gebruiker, kunt u Retry aanpassen om een ​​functie aan te roepen die de gebruiker vraagt ​​en een taak door een TaskCompletionSource retourneert om de volgende stap te activeren wanneer de gebruiker antwoordt, bijvoorbeeld:

 private static Task<bool> AskUser()
    {
        var tcs = new TaskCompletionSource<bool>();
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine(@"Error Occured, continue? Y\N");
            var response = Console.ReadKey();
            tcs.SetResult(response.KeyChar=='y');

        });
        return tcs.Task;
    }

    private static Task<T> RetryAsk<T>(Func<T> func, int retryCount,  TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    AskUser().ContinueWith(t =>
                    {
                        if (t.Result)
                            RetryAsk(func, retryCount - 1, tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

Met alle voortzettingen kun je zien waarom een ​​asynchrone versie van Retry zo wenselijk is.

BIJWERKEN:

In Visual Studio 2012 Beta werken de volgende twee versies:

Een versie met een while-lus:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

en een recursieve versie:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch
        {
            if (retryCount == 0)
                throw;
        }
        return await Retry(func, --retryCount);
    }

32
2018-05-08 07:28



Hier is een riffed versie van Het uitstekende antwoord van Panagiotis Kanavos die ik heb getest en gebruik in productie.

Het behandelt enkele dingen die belangrijk voor me waren:

  • Wilt u kunnen beslissen of u opnieuw wilt proberen op basis van het aantal voorafgaande pogingen en uitzondering van de huidige poging
  • Ik wil niet vertrouwen op async (minder omgevingsbeperkingen)
  • Wil je het resultaat hebben Exception in het geval van mislukking omvatten details van elke poging


static Task<T> RetryWhile<T>(
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry )
{
    return RetryWhile<T>( func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>() );
}

static Task<T> RetryWhile<T>( 
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry, 
    TaskCompletionSource<T> tcs, 
    int previousAttempts, IEnumerable<Exception> previousExceptions )
{
    func( previousAttempts ).ContinueWith( antecedent =>
    {
        if ( antecedent.IsFaulted )
        {
            var antecedentException = antecedent.Exception;
            var allSoFar = previousExceptions
                .Concat( antecedentException.Flatten().InnerExceptions );
            if ( shouldRetry( antecedentException, previousAttempts ) )
                RetryWhile( func,shouldRetry,previousAttempts+1, tcs, allSoFar);
            else
                tcs.SetException( allLoggedExceptions );
        }
        else
            tcs.SetResult( antecedent.Result );
    }, TaskContinuationOptions.ExecuteSynchronously );
    return tcs.Task;
}

4
2018-05-03 08:15



Op het hoogste niveau vind ik het helpen om een ​​functiehandtekening te maken van wat je hebt en wat je wilt.

Jij hebt:

  • Een functie die u een taak geeft (Func<Task>). We gebruiken de functie omdat taken zelf over het algemeen niet opnieuw kunnen worden geprobeerd.
  • Een functie die bepaalt of de algehele taak is voltooid of opnieuw moet worden geprobeerd (Func<Task, bool>)

Jij wil:

  • Een algemene taak

Dus je hebt een functie zoals:

Task Retry(Func<Task> action, Func<Task, bool> shouldRetry);

Uitbreiding van de oefening binnen de functie, taken hebben ongeveer 2 handelingen met hen te doen, hun toestand te lezen en ContinueWith. Om je eigen taken te maken, TaskCompletionSource is een goed startpunt. Een eerste poging kan er ongeveer zo uitzien:

//error checking
var result = new TaskCompletionSource<object>();
action().ContinueWith((t) => 
  {
    if (shouldRetry(t))
        action();
    else
    {
        if (t.IsFaulted)
            result.TrySetException(t.Exception);
        //and similar for Canceled and RunToCompletion
    }
  });

Het voor de hand liggende probleem hier is dat slechts 1 keer opnieuw proberen zal gebeuren. Om dat te omzeilen, moet je een manier maken waarop de functie zichzelf kan noemen. De gebruikelijke manier om dit met lambda's te doen is zoiets als dit:

//error checking
var result = new TaskCompletionSource<object>();

Func<Task, Task> retryRec = null; //declare, then assign
retryRec = (t) => { if (shouldRetry(t))
                        return action().ContinueWith(retryRec).Unwrap();
                    else
                    {
                        if (t.IsFaulted) 
                            result.TrySetException(t.Exception);
                        //and so on
                        return result.Task; //need to return something
                     }
                  };
 action().ContinueWith(retryRec);
 return result.Task;

2
2018-05-08 00:52