Καλώς ορίσατε στο dotNETZone.gr - Σύνδεση | Εγγραφή | Βοήθεια
σε

 

Αρχική σελίδα Ιστολόγια Συζητήσεις Εκθέσεις Φωτογραφιών Αρχειοθήκες

Ασύγχρονο Retry Function

Îåêßíçóå áðü ôï ìÝëïò Παναγιώτης Καναβός. Τελευταία δημοσίευση από το μέλος Παναγιώτης Καναβός στις 16-05-2012, 11:00. Υπάρχουν 10 απαντήσεις.
Ταξινόμηση Δημοσιεύσεων: Προηγούμενο Επόμενο
  •  15-09-2011, 17:01 67309

    Ασύγχρονο Retry Function

    Αυτό τον καιρό δουλεύω με ένα Rest API και θέλω να κάνω Retry αν κάποια κλήση αποτυγχάνει για ένα ορισμένο αριθμό επαναλήψεων. Βρήκα μία συζήτηση στο StackOverflow όπου ο Jon Skeet προτείνει τον παρακάτω κώδικα:

    public static Func<T> Retry(Func<T> original, int retryCount)
    {
        return () =>
        {
            while (true)
            {
                try
                {
                    return original();
                }
                catch (Exception e)
                {
                    if (retryCount == 0)
                    {
                        throw;
                    }
                    // TODO: Logging
                    retryCount--;
                }
            }
        };
    }

    Έτσι μπορώ π.χ. να καλέσω την DowloadString του WebClient 5 φορές:
    var func = Retry(() =>
    	base.DownloadString(address), 
    	5);
    	
    return func();
    Φυσικά ο πραγματικός κώδικας δεν είναι τόσο απλός καθώς πρέπει να ελέγξω για τα κατάλληλα WebExceptions και να κάνω retry μόνο αν πρόκειται για timeout, αλλά η γενική ιδέα είναι ότι με αυτό τον τρόπο φτιάχνω εύκολα ένα function που κάνει retry.

    Ο κώδικας αυτός όμως τρέχει σε ένα thread κι εγώ θα ήθελα (όπως και αυτός που κάνει την αρχική ερώτηση) κάθε επανάληψη να τρέχει σε διαφορετικό thread επειδή τα Rest calls κρατάνε πολύ, ειδικά αν συμβεί timeout. Μία ιδέα θα ήταν να φτιάξω ένα Task για κάθε επανάληψη και να περιμένω με τη Wait για το αποτέλεσμα. Αυτό όμως σημαίνει ότι το thread που καλεί τη Retry θα πρέπει να περιμένει κάθε φορά που εκτελείται η επανάληψη. Ο κώδικας θα είναι κάπως έτσι:

    private Func<T> Retry<T>(Func<T> original, int retryCount)
    {
            return () =>
            {                
            	while (true)
                    {
                        try
                        {
                            var task = Task.Factory.StartNew(original);
                            task.Wait();
                            return task.Result;
                        }
                        catch (Exception e)
                        {
                            retryCount--;   
                        }
                    }
    	};
    }

    Ο κώδικας αυτός δεν μου αρέσει και πολύ και θα προτιμούσα κάτι που δεν χρειάζεται Wait για να δουλέψει. Σκέφτεται κανείς κάποιο καλύτερο τρόπο ?


    Παναγιώτης Καναβός, Freelancer
    Twitter: http://www.twitter.com/pkanavos
    Δημοσίευση στην κατηγορία:
  •  15-09-2011, 17:18 67310 σε απάντηση της 67309

    Απ: Παράλληλο Retry Function

    Είτε με τον ένα, είτε με τον άλλο τρόπο, εφόσον πρόκειται για Retry (δηλαδή fail το προηγούμενο και try again) θα πρέπει να περιμένεις για να δεις αμα απέτυχε ή πέτυχε, αλλοιώς θες κώδικα για να δεις εάν πετύχανε και τα δύο κλπ.. Και το Wait γιατι δεν σ'αρεσει; Μια χαρά φαίνεται.


    Παναγιώτης Κεφαλίδης

    "Για να επιτύχεις, θα πρέπει το πάθος σου για την επιτυχία να είναι μεγαλύτερο απο τον φόβο σου για την αποτυχία"

    Οι απαντήσεις παρέχονται για συγκεκριμένες ερωτήσεις και χωρίς καμιά εγγύηση. Παρακαλώ διαβάστε τους όρους χρήσης.
  •  15-09-2011, 18:05 67313 σε απάντηση της 67310

    Απ: Παράλληλο Retry Function

    Το Wait σημαίνει καταρχήν ότι το thread που καλεί θα μπλοκάρει σε ένα ή περισσότερα σημεία. Επιπλέον, με εμποδίζει να να αλλάξω το signature της μεθόδου και να επιστρέψω π.χ. Task<T>. Να επιστρέψω απευθείας το task δεν γίνεται γιατί δεν έχω τρόπο να δω αν έχει πετύχει ή όχι. Θα μπορούσα για παράδειγμα να βάλω ένα ContinueWith αλλά εκεί μέσα δεν μπορώ να κάνω κάποιο loop ή επανεκτέλεση του ίδιου task.

    Το ιδανικό θα ήταν να γίνεται όλο το Retry ως ανεξάρτητα tasks και το function να μου επιστρέφει ένα task που θα καλύπτει όλα τα άλλα. Έτσι θα μπορούσα π.χ. να αποθηκεύσω τα αποτελέσματα σε ένα ContinueWith αντί να κάνω wait και να περιμένω.

    Παναγιώτης Καναβός, Freelancer
    Twitter: http://www.twitter.com/pkanavos
  •  15-09-2011, 18:06 67314 σε απάντηση της 67309

    Απ: Παράλληλο Retry Function

    Νομίζω ότι αυτό που χρειάζεσαι είναι Async composition, δηλαδή να αποφύγεις τις blocking κλήσεις.


    Μια απλή λύση σε F#

    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


    Palladinos Nick
    Software Engineer
    -----------------------
    The limits of my language mean the limits of my world. (Ludwig Wittgenstein)
  •  15-09-2011, 19:55 67319 σε απάντηση της 67313

    Απ: Παράλληλο Retry Function

    Νίκο, αμα ήταν να το κάνουμε σε άλλη γλώσσα ή με "πειραματικές" εκδόσεις (για την C#), τότε οκ.. Το θέμα οτι πρέπει να είναι ασύγχρονα, το ξέραμε απο την αρχή, αλλά η C#, ασύγχρονα έχει στην 5.. ή αμα περάσεις την CTP.. που δεν νομίζω να το θες (εννοώ με παρόμοιο syntax όπως η F#). Αλλοιώς η υλοποίηση πάει σε F# που λέει ο Νίκος, και το βγάζεις σαν Librabry?
    Παναγιώτης Κεφαλίδης

    "Για να επιτύχεις, θα πρέπει το πάθος σου για την επιτυχία να είναι μεγαλύτερο απο τον φόβο σου για την αποτυχία"

    Οι απαντήσεις παρέχονται για συγκεκριμένες ερωτήσεις και χωρίς καμιά εγγύηση. Παρακαλώ διαβάστε τους όρους χρήσης.
  •  15-09-2011, 20:27 67320 σε απάντηση της 67319

    Απ: Παράλληλο Retry Function

    And  this is my attempt in C#

            private Task<T> Retry<T>(Func<T> original, int retryCount)
            {
                return 
                    Task.Factory.StartNew(() => original()).ContinueWith(_original =>
    { if (_original.IsFaulted) { if (retryCount == 0) throw _original.Exception; return Retry(original, retryCount - 1); } else return Task.Factory.StartNew(() => _original.Result); }).Unwrap(); }





    Palladinos Nick
    Software Engineer
    -----------------------
    The limits of my language mean the limits of my world. (Ludwig Wittgenstein)
  •  15-09-2011, 20:34 67321 σε απάντηση της 67320

    Απ: Παράλληλο Retry Function

    Α να γεια σου ντε! Big Smile
    Παναγιώτης Κεφαλίδης

    "Για να επιτύχεις, θα πρέπει το πάθος σου για την επιτυχία να είναι μεγαλύτερο απο τον φόβο σου για την αποτυχία"

    Οι απαντήσεις παρέχονται για συγκεκριμένες ερωτήσεις και χωρίς καμιά εγγύηση. Παρακαλώ διαβάστε τους όρους χρήσης.
  •  20-09-2011, 12:22 67418 σε απάντηση της 67320

    Απ: Παράλληλο Retry Function

    Το σκάλισα λίγο παραπάνω και βρήκα ένα τρόπο να αποφύγω το Unwrap αλλά και το return Task.Factory.StartNew(() => _original.Result); το οποίο υπάρχει μόνο για να γυρίσει το έτοιμο αποτέλεσμα σε μορφή Task. Ο παρακάτω κώδικας χρησιμοποιεί το TaskCompletionSource, μία κλάση η οποία επιστρέφει ένα task το οποίο ολοκληρώνεται μόνο αν κάποιος καλέσει τις μεθόδους SetResult, SetException, SetCancelled :

    private static Task<T> Retry<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);
                Retry(func, retryCount - 1,tcs);
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    }

    To default property μπαίνει για να μην αλλάξει ο τρόπος κλήσης της Retry. Νομίζω ότι αυτή η μορφή είναι λίγο καθαρότερη αν ξέρεις τί κάνει το TaskCompletionSource

    Τέλος, άλλος ένας τρόπος είναι να χρησιμοποιηθεί η Task.Factory.Iterate από τα ParallelExtensionsExtras, η οποία εκτελεί κάθε Task σε ένα IEnumerable<Task>. Εδώ το τρυκ είναι να μετατρέψεις τα retry σε ένα iterator όπως φαίνεται παρακάτω:

    private static Task<T> RetryIterativeTCS<T>(Func<T> func, int retryCount)
    {
    	var tcs=new TaskCompletionSource<T>();
    	Task.Factory.Iterate(RetryIterator(func, retryCount, tcs));
    	return tcs.Task;
    }
    
    private static IEnumerable<Task<T>> RetryIterator<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs)
    {
    	while (true)
    	{
    		var task = Task<T>.Factory.StartNew(original);
    		yield return task;
    
    		if (task.IsFaulted)
    		{
    			if (retryCount == 0)
    			{
    				tcs.SetException(task.Exception.InnerExceptions);
    				break;
    			}
    			retryCount--;
    		}
    		else
    		{
    			tcs.SetResult(task.Result);
    			break;
    		}
    	}
    }

    To TCS μπαίνει για να μπορέσω να πάρω το αποτέλεσμα του function αν πετύχει, καθώς η Iterate επιστρέφει απλά ένα Task.

    Η αλήθεια είναι ότι τον τελευταίο τρόπο τον έγραψα πιο πολύ για να δω πως γίνεται καθώς έχει μερικά προβλήματα: 
    • Χρειάζεται άλλο ένα function, το RetryIterator
    • Χρειάζεται άλλο ένα library, το ParallelExtensionExtras
    • Χρειάζεται την Task.Factory.Iterate, η οποία ως υλοποίηση είναι λίγο πιο βαρειά από ένα StartNew
    • Η χρήση του iterator με αυτό τον τρόπο μπερδεύει όποιον δεν την έχει συνηθίσει
    Από την άλλη, είναι ευκολότερο να προσθέσει κανείς επιπλέον δυνατότητες στον RetryIterator, όπως αυξανόμενη καθυστέρηση μεταξύ των retry, χωρίς να πρέπει να προσθέσει παραμέτρους (π.χ. delay) στο signature της Retry.

    Σχόλια?

    Παναγιώτης Καναβός, Freelancer
    Twitter: http://www.twitter.com/pkanavos
  •  28-04-2012, 22:13 70154 σε απάντηση της 67418

    Απ: Παράλληλο Retry Function

    Παναγιώτη η πρώτη σου τεχνική (δεν δοκίμασα την δεύτερη), κολλάει κάπου και τρέχει τον κώδικα για πάντα εφόσον συνεχίσει να "σκάει"

    Παραθέτω εδώ την έκδοση του Νίκου αλλά με καθορισμό ενός Action σε περίπτωση που μετά το τέλος των retry δεν έχει επιτύχει, ώστε να μην κάνει throw o finalizer

            public static Task<T> Retry<T>(Func<T> original, int tryCount, Action<Exception> ifAllRetriesFail, int delayMs = 0)
            {
                return
                    Task.Factory.StartNew(original).ContinueWith(_original =>
                    {
                        if (_original.IsFaulted)
                        {
                            // call this so that TPL thinks we've caught all the exceptions
                            var flattened = _original.Exception.Flatten();
     
                            // assume it's faulted because of the options we specify
                            // for ContinueWith()
                            if (tryCount == 1)
                            // this is one so we control the total number 
                            // of tries and not retries
                            {
                                ifAllRetriesFail(flattened.InnerException);
                                return Task.Factory.StartNew(() => default(T));
                            }
                            // sleep baby
                            Thread.Sleep(delayMs);
                            return Retry(original, tryCount - 1, ifAllRetriesFail);
                        }
                        return Task<T>.Factory.StartNew(() => _original.Result);
                    }).Unwrap();
            }

  •  07-05-2012, 12:48 70213 σε απάντηση της 70154

    Απ: Παράλληλο Retry Function

    Έχεις δίκιο, στον έλεγχο του RetryCount λείπει το else. Έπρεπε να είναι

    if (retryCount == 0)
        tcs.SetException(_original.Exception.InnerExceptions);
     else
        Retry(func, retryCount - 1, tcs);

    αντί για

    if (retryCount == 0)
        tcs.SetException(_original.Exception.InnerExceptions);
    Retry(func, retryCount - 1, tcs);

     

    Στον κώδικα του Νίκου έχεις κάνει μία "κακή" αλλαγή. Η προσθήκη του Thread.Sleep θα προκαλέσει πρόβλημα γιατί αχρηστεύει  ένα thread από το thread pool. Σκοπός του TPL είναι να απαλλαγούμε από απευθείας παρεμβάσεις στα threads.

    Αν θέλεις να προσθέσεις κάποια καθυστέρηση, θα πρέπει να χρησιμοποιήσεις tasks για να κάνεις κάτι σαν την StartNewDelayed από τα ParallelExtensionExtras για να εκτελέσεις το Retry αφού περάσει το timeout. Σε αυτή την περίπτωση ο κώδικας αλλάζει στο παρακάτω:

     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;
            }

    Αυτό που προσπαθώ να κάνω να παίξει και κάτι πάει στραβά, είναι το Retry με async/await. Κανονικά ο παρακάτω κώδικας  θα έπρεπε να παίζει αλλά κρεμάει χωρίς καν unhandled exception, όταν κάνω compile σε Visual Studio 2010 με Async CTP.

    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--;
            }
        }
    }

     

    Υποθέτω ότι το rewriting που κάνει ο compiler τουλάχιστον στο Async CTP αποτυγχάνει όταν δει παραπάνω από δύο exceptions στο catch. Επίσης, δεν μπορώ να έχω await μέσα στο catch, κάνοντας το Delay επίσης δύσκολο. Κρίμα, αν έπαιζε το Retry θα ήταν τόσο καθαρό όσο η αρχική έκδοση!

    Όταν ο compiler συναντάει τα async/await keywords στην ουσία δημιουργεί ένα task iterator ο οποίος τρέχει ως ξεχωριστό task τον κώδικα μεταξύ των awaits. Τα πράγματα γίνονται δύσκολα όταν υπάρχει catch/finally και παρότι ο compiler κάνει αρκετά καλή δουλειά, δεν μπορείς να έχεις await μέσα σε catch ή finally, ενώ όπως φαίνεται παραπάνω, τα απανωτά exception προκαλούν εγκεφαλικό.

    Όπως θα πει και ο Νίκος, το async της F# είναι ανώτερο από το αντίστοιχο του .NET Sad


    Παναγιώτης Καναβός, Freelancer
    Twitter: http://www.twitter.com/pkanavos
  •  16-05-2012, 11:00 70257 σε απάντηση της 70213

    Απ: Παράλληλο Retry Function

    Δοκίμασα το Retry με async στο Visual Studio 2011 και η μορφή με το while δουλεύει, ενώ η recursive μορφή που αντιστοιχεί στην F# θέλει μία μικρή αλλαγή.

    Το παρακάτω δουλεύει:

            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--;
                    }
                }
            }

    Αυτή η έκδοση όμως δεν δουλεύει, γιατί δεν γίνεται δεκτό το await μέσα στο catch:

            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);
                }
            }

    Ευτυχώς, μπορούμε άνετα να κάνουμε το Retry έξω από το catch, οπότε η παρακάτω μορφή δουλεύει:

            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);
            }

     

     


    Παναγιώτης Καναβός, Freelancer
    Twitter: http://www.twitter.com/pkanavos
Προβολή Τροφοδοσίας RSS με μορφή XML
Με χρήση του Community Server (Commercial Edition), από την Telligent Systems