rxjava: Puis-je utiliser retry () mais avec un délai?

91

J'utilise rxjava dans mon application Android pour gérer les demandes réseau de manière asynchrone. Maintenant, je voudrais réessayer une demande réseau ayant échoué seulement après un certain temps.

Existe-t-il un moyen d'utiliser retry () sur un observable mais de ne réessayer qu'après un certain délai?

Existe-t-il un moyen de faire savoir à l'Observable qu'il est actuellement réessayé (par opposition à essayé pour la première fois)?

J'ai jeté un coup d'œil à debounce () / throttleWithTimeout () mais ils semblent faire quelque chose de différent.

Éditer:

Je pense avoir trouvé un moyen de le faire, mais je serais intéressé soit par la confirmation que c'est la bonne façon de le faire, soit par d'autres, de meilleures façons.

Voici ce que je fais: dans la méthode call () de mon Observable.OnSubscribe, avant d'appeler la méthode Subscribers onError (), je laisse simplement le Thread dormir pendant la durée souhaitée. Donc, pour réessayer toutes les 1000 millisecondes, je fais quelque chose comme ceci:

@Override
public void call(Subscriber<? super List<ProductNode>> subscriber) {
    try {
        Log.d(TAG, "trying to load all products with pid: " + pid);
        subscriber.onNext(productClient.getProductNodesForParentId(pid));
        subscriber.onCompleted();
    } catch (Exception e) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e.printStackTrace();
        }
        subscriber.onError(e);
    }
}

Étant donné que cette méthode s'exécute de toute façon sur un thread IO, elle ne bloque pas l'interface utilisateur. Le seul problème que je peux voir est que même la première erreur est signalée avec un délai, donc le délai est là même s'il n'y a pas de nouvelle tentative (). Je préfère que le délai ne soit pas appliqué après une erreur mais plutôt avant une nouvelle tentative (mais pas avant le premier essai, évidemment).

david.mihola
la source

Réponses:

169

Vous pouvez utiliser l' retryWhen()opérateur pour ajouter une logique de nouvelle tentative à n'importe quel observable.

La classe suivante contient la logique de nouvelle tentative:

RxJava 2.x

public class RetryWithDelay implements Function<Observable<? extends Throwable>, Observable<?>> {
    private final int maxRetries;
    private final int retryDelayMillis;
    private int retryCount;

    public RetryWithDelay(final int maxRetries, final int retryDelayMillis) {
        this.maxRetries = maxRetries;
        this.retryDelayMillis = retryDelayMillis;
        this.retryCount = 0;
    }

    @Override
    public Observable<?> apply(final Observable<? extends Throwable> attempts) {
        return attempts
                .flatMap(new Function<Throwable, Observable<?>>() {
                    @Override
                    public Observable<?> apply(final Throwable throwable) {
                        if (++retryCount < maxRetries) {
                            // When this Observable calls onNext, the original
                            // Observable will be retried (i.e. re-subscribed).
                            return Observable.timer(retryDelayMillis,
                                    TimeUnit.MILLISECONDS);
                        }

                        // Max retries hit. Just pass the error along.
                        return Observable.error(throwable);
                    }
                });
    }
}

RxJava 1.x

public class RetryWithDelay implements
        Func1<Observable<? extends Throwable>, Observable<?>> {

    private final int maxRetries;
    private final int retryDelayMillis;
    private int retryCount;

    public RetryWithDelay(final int maxRetries, final int retryDelayMillis) {
        this.maxRetries = maxRetries;
        this.retryDelayMillis = retryDelayMillis;
        this.retryCount = 0;
    }

    @Override
    public Observable<?> call(Observable<? extends Throwable> attempts) {
        return attempts
                .flatMap(new Func1<Throwable, Observable<?>>() {
                    @Override
                    public Observable<?> call(Throwable throwable) {
                        if (++retryCount < maxRetries) {
                            // When this Observable calls onNext, the original
                            // Observable will be retried (i.e. re-subscribed).
                            return Observable.timer(retryDelayMillis,
                                    TimeUnit.MILLISECONDS);
                        }

                        // Max retries hit. Just pass the error along.
                        return Observable.error(throwable);
                    }
                });
    }
}

Usage:

// Add retry logic to existing observable.
// Retry max of 3 times with a delay of 2 seconds.
observable
    .retryWhen(new RetryWithDelay(3, 2000));
kjones
la source
2
Error:(73, 20) error: incompatible types: RetryWithDelay cannot be converted to Func1<? super Observable<? extends Throwable>,? extends Observable<?>>
Nima G
3
@nima J'ai eu le même problème, changez RetryWithDelaypour ceci: pastebin.com/6SiZeKnC
user1480019
2
On dirait que l'opérateur retryWhen RxJava a changé depuis que j'ai écrit ceci. Je vais mettre à jour la réponse.
kjones
3
Vous devez mettre à jour cette réponse pour vous conformer à RxJava 2
Vishnu M.
1
à quoi ressemblerait la version rxjava 2 pour kotlin?
Gabriel Sanmartin
18

Inspiré par la réponse de Paul , et si vous n'êtes pas concerné par les retryWhenproblèmes évoqués par Abhijit Sarkar , le moyen le plus simple de retarder le réabonnement avec rxJava2 sans condition est:

source.retryWhen(throwables -> throwables.delay(1, TimeUnit.SECONDS))

Vous voudrez peut-être voir plus d'exemples et d'explications sur retryWhen et repeatWhen .

McX
la source
14

Cet exemple fonctionne avec jxjava 2.2.2:

Réessayez sans tarder:

Single.just(somePaylodData)
   .map(data -> someConnection.send(data))
   .retry(5)
   .doOnSuccess(status -> log.info("Yay! {}", status);

Réessayer avec un délai:

Single.just(somePaylodData)
   .map(data -> someConnection.send(data))
   .retryWhen((Flowable<Throwable> f) -> f.take(5).delay(300, TimeUnit.MILLISECONDS))
   .doOnSuccess(status -> log.info("Yay! {}", status)
   .doOnError((Throwable error) 
                -> log.error("I tried five times with a 300ms break" 
                             + " delay in between. But it was in vain."));

Notre unique source échoue si someConnection.send () échoue. Lorsque cela se produit, l'observable des échecs à l'intérieur de retryWhen émet l'erreur. Nous retardons cette émission de 300 ms et la renvoyons pour signaler une nouvelle tentative. take (5) garantit que notre observable de signalisation se terminera après avoir reçu cinq erreurs. retryWhen voit l'arrêt et ne réessaye pas après le cinquième échec.

Erunafailaro
la source
9

Il s'agit d'une solution basée sur les extraits de Ben Christensen que j'ai vus, RetryWhen Example et RetryWhenTestsConditional (j'ai dû changer n.getThrowable()pour nque cela fonctionne). J'ai utilisé evant / gradle-retrolambda pour faire fonctionner la notation lambda sur Android, mais vous n'avez pas besoin d'utiliser lambdas (bien que cela soit fortement recommandé). Pour le délai, j'ai implémenté un back-off exponentiel, mais vous pouvez y brancher la logique de backoff que vous souhaitez. Pour être complet, j'ai ajouté les opérateurs subscribeOnet observeOn. J'utilise ReactiveX / RxAndroid pour le AndroidSchedulers.mainThread().

int ATTEMPT_COUNT = 10;

public class Tuple<X, Y> {
    public final X x;
    public final Y y;

    public Tuple(X x, Y y) {
        this.x = x;
        this.y = y;
    }
}


observable
    .subscribeOn(Schedulers.io())
    .retryWhen(
            attempts -> {
                return attempts.zipWith(Observable.range(1, ATTEMPT_COUNT + 1), (n, i) -> new Tuple<Throwable, Integer>(n, i))
                .flatMap(
                        ni -> {
                            if (ni.y > ATTEMPT_COUNT)
                                return Observable.error(ni.x);
                            return Observable.timer((long) Math.pow(2, ni.y), TimeUnit.SECONDS);
                        });
            })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(subscriber);
david-hoze
la source
2
cela a l'air élégant mais je n'utilise pas les fonctions lamba, comment puis-je écrire sans lambas? @ amitai-hoze
ericn
comment puis-je l'écrire de manière à pouvoir réutiliser cette fonction de nouvelle tentative pour d'autres Observableobjets?
ericn
tant pis, j'ai utilisé une kjonessolution et ça marche parfait pour moi, merci
ericn
8

au lieu d'utiliser MyRequestObservable.retry J'utilise une fonction wrapper retryObservable (MyRequestObservable, retrycount, seconds) qui renvoie un nouvel Observable qui gère l'indirection pour le délai afin que je puisse faire

retryObservable(restApi.getObservableStuff(), 3, 30)
    .subscribe(new Action1<BonusIndividualList>(){
        @Override
        public void call(BonusIndividualList arg0) 
        {
            //success!
        }
    }, 
    new Action1<Throwable>(){
        @Override
        public void call(Throwable arg0) { 
           // failed after the 3 retries !
        }}); 


// wrapper code
private static <T> Observable<T> retryObservable(
        final Observable<T> requestObservable, final int nbRetry,
        final long seconds) {

    return Observable.create(new Observable.OnSubscribe<T>() {

        @Override
        public void call(final Subscriber<? super T> subscriber) {
            requestObservable.subscribe(new Action1<T>() {

                @Override
                public void call(T arg0) {
                    subscriber.onNext(arg0);
                    subscriber.onCompleted();
                }
            },

            new Action1<Throwable>() {
                @Override
                public void call(Throwable error) {

                    if (nbRetry > 0) {
                        Observable.just(requestObservable)
                                .delay(seconds, TimeUnit.SECONDS)
                                .observeOn(mainThread())
                                .subscribe(new Action1<Observable<T>>(){
                                    @Override
                                    public void call(Observable<T> observable){
                                        retryObservable(observable,
                                                nbRetry - 1, seconds)
                                                .subscribe(subscriber);
                                    }
                                });
                    } else {
                        // still fail after retries
                        subscriber.onError(error);
                    }

                }
            });

        }

    });

}
Alexis Contour
la source
Je suis terriblement désolé de ne pas avoir répondu plus tôt - en quelque sorte, j'ai manqué la notification de SO indiquant qu'il y avait une réponse à ma question ... J'ai voté pour votre réponse parce que j'aime l'idée, mais je ne suis pas sûr si - selon les principes de SO - Je devrais accepter la réponse car il s'agit plus d'une solution de contournement que d'une réponse directe. Mais je suppose que, puisque vous donnez une solution de contournement, la réponse à ma question initiale est "non, vous ne pouvez pas" ...
david.mihola
5

retryWhenest un opérateur compliqué, peut-être même bogué. Le document officiel et au moins une réponse ici utilisent l' rangeopérateur, qui échouera s'il n'y a pas de nouvelle tentative. Voir ma discussion avec David Karnok, membre de ReactiveX.

Je me suis amélioré sur la réponse de kjones en changeant flatMapà concatMapet en y ajoutant une RetryDelayStrategyclasse. flatMapne préserve pas l'ordre d'émission tandis queconcatMap fait, ce qui est important pour les retards avec recul. Le RetryDelayStrategy, comme son nom l'indique, permet à l'utilisateur de choisir parmi différents modes de génération de délais de relance, y compris l'arrêt. Le code est disponible sur mon GitHub avec les cas de test suivants:

  1. Réussit à la 1ère tentative (aucune nouvelle tentative)
  2. Échoue après 1 nouvelle tentative
  3. Tente de réessayer 3 fois mais réussit le 2e et ne réessaye donc pas la 3e fois
  4. Réussit à la troisième tentative

Voir la setRandomJokesméthode.

Abhijit Sarkar
la source
3

Maintenant, avec la version 1.0+ de RxJava, vous pouvez utiliser zipWith pour effectuer une nouvelle tentative avec un délai.

Ajout de modifications à la réponse kjones .

Modifié

public class RetryWithDelay implements 
                            Func1<Observable<? extends Throwable>, Observable<?>> {

    private final int MAX_RETRIES;
    private final int DELAY_DURATION;
    private final int START_RETRY;

    /**
     * Provide number of retries and seconds to be delayed between retry.
     *
     * @param maxRetries             Number of retries.
     * @param delayDurationInSeconds Seconds to be delays in each retry.
     */
    public RetryWithDelay(int maxRetries, int delayDurationInSeconds) {
        MAX_RETRIES = maxRetries;
        DELAY_DURATION = delayDurationInSeconds;
        START_RETRY = 1;
    }

    @Override
    public Observable<?> call(Observable<? extends Throwable> observable) {
        return observable
                .delay(DELAY_DURATION, TimeUnit.SECONDS)
                .zipWith(Observable.range(START_RETRY, MAX_RETRIES), 
                         new Func2<Throwable, Integer, Integer>() {
                             @Override
                             public Integer call(Throwable throwable, Integer attempt) {
                                  return attempt;
                             }
                         });
    }
}
Omkar
la source
3

Même réponse que depuis kjones mais mise à jour vers la dernière version Pour la version RxJava 2.x : ('io.reactivex.rxjava2: rxjava: 2.1.3')

public class RetryWithDelay implements Function<Flowable<Throwable>, Publisher<?>> {

    private final int maxRetries;
    private final long retryDelayMillis;
    private int retryCount;

    public RetryWithDelay(final int maxRetries, final int retryDelayMillis) {
        this.maxRetries = maxRetries;
        this.retryDelayMillis = retryDelayMillis;
        this.retryCount = 0;
    }

    @Override
    public Publisher<?> apply(Flowable<Throwable> throwableFlowable) throws Exception {
        return throwableFlowable.flatMap(new Function<Throwable, Publisher<?>>() {
            @Override
            public Publisher<?> apply(Throwable throwable) throws Exception {
                if (++retryCount < maxRetries) {
                    // When this Observable calls onNext, the original
                    // Observable will be retried (i.e. re-subscribed).
                    return Flowable.timer(retryDelayMillis,
                            TimeUnit.MILLISECONDS);
                }

                // Max retries hit. Just pass the error along.
                return Flowable.error(throwable);
            }
        });
    }
}

Usage:

// Ajout d'une logique de nouvelle tentative à l'observable existante. // Réessayez au maximum 3 fois avec un délai de 2 secondes.

observable
    .retryWhen(new RetryWithDelay(3, 2000));
Mihuilk
la source
3

Basé sur la réponse de kjones, voici la version Kotlin de RxJava 2.x réessayer avec un retard comme extension. Remplacez Observablepour créer la même extension pour Flowable.

fun <T> Observable<T>.retryWithDelay(maxRetries: Int, retryDelayMillis: Int): Observable<T> {
    var retryCount = 0

    return retryWhen { thObservable ->
        thObservable.flatMap { throwable ->
            if (++retryCount < maxRetries) {
                Observable.timer(retryDelayMillis.toLong(), TimeUnit.MILLISECONDS)
            } else {
                Observable.error(throwable)
            }
        }
    }
}

Ensuite, utilisez-le simplement sur observable observable.retryWithDelay(3, 1000)

JuliusScript
la source
Est-il possible de le remplacer Singleégalement?
Papps
2
@Papps Ouais cela devrait fonctionner, notez simplement flatMapqu'il faudra utiliser Flowable.timeret Flowable.error même si la fonction est Single<T>.retryWithDelay.
JuliusScript
1

Vous pouvez ajouter un délai dans l'observable renvoyé lors de la nouvelle tentative.

          /**
 * Here we can see how onErrorResumeNext works and emit an item in case that an error occur in the pipeline and an exception is propagated
 */
@Test
public void observableOnErrorResumeNext() {
    Subscription subscription = Observable.just(null)
                                          .map(Object::toString)
                                          .doOnError(failure -> System.out.println("Error:" + failure.getCause()))
                                          .retryWhen(errors -> errors.doOnNext(o -> count++)
                                                                     .flatMap(t -> count > 3 ? Observable.error(t) : Observable.just(null).delay(100, TimeUnit.MILLISECONDS)),
                                                     Schedulers.newThread())
                                          .onErrorResumeNext(t -> {
                                              System.out.println("Error after all retries:" + t.getCause());
                                              return Observable.just("I save the world for extinction!");
                                          })
                                          .subscribe(s -> System.out.println(s));
    new TestSubscriber((Observer) subscription).awaitTerminalEvent(500, TimeUnit.MILLISECONDS);
}

Vous pouvez voir plus d'exemples ici. https://github.com/politrons/reactive

Paul
la source
0

Faites-le simplement comme ceci:

                  Observable.just("")
                            .delay(2, TimeUnit.SECONDS) //delay
                            .flatMap(new Func1<String, Observable<File>>() {
                                @Override
                                public Observable<File> call(String s) {
                                    L.from(TAG).d("postAvatar=");

                                    File file = PhotoPickUtil.getTempFile();
                                    if (file.length() <= 0) {
                                        throw new NullPointerException();
                                    }
                                    return Observable.just(file);
                                }
                            })
                            .retry(6)
                            .subscribe(new Action1<File>() {
                                @Override
                                public void call(File file) {
                                    postAvatar(file);
                                }
                            }, new Action1<Throwable>() {
                                @Override
                                public void call(Throwable throwable) {

                                }
                            });
Allen Vork
la source
0

Pour la version Kotlin & RxJava1

class RetryWithDelay(private val MAX_RETRIES: Int, private val DELAY_DURATION_IN_SECONDS: Long)
    : Function1<Observable<out Throwable>, Observable<*>> {

    private val START_RETRY: Int = 1

    override fun invoke(observable: Observable<out Throwable>): Observable<*> {
        return observable.delay(DELAY_DURATION_IN_SECONDS, TimeUnit.SECONDS)
            .zipWith(Observable.range(START_RETRY, MAX_RETRIES),
                object : Function2<Throwable, Int, Int> {
                    override fun invoke(throwable: Throwable, attempt: Int): Int {
                        return attempt
                    }
                })
    }
}
Cody
la source
0

(Kotlin) J'ai un peu amélioré le code avec un ralentissement exponentiel et une émission de défense appliquée de Observable.range ():

    fun testOnRetryWithDelayExponentialBackoff() {
    val interval = 1
    val maxCount = 3
    val ai = AtomicInteger(1);
    val source = Observable.create<Unit> { emitter ->
        val attempt = ai.getAndIncrement()
        println("Subscribe ${attempt}")
        if (attempt >= maxCount) {
            emitter.onNext(Unit)
            emitter.onComplete()
        }
        emitter.onError(RuntimeException("Test $attempt"))
    }

    // Below implementation of "retryWhen" function, remove all "println()" for real code.
    val sourceWithRetry: Observable<Unit> = source.retryWhen { throwableRx ->
        throwableRx.doOnNext({ println("Error: $it") })
                .zipWith(Observable.range(1, maxCount)
                        .concatMap { Observable.just(it).delay(0, TimeUnit.MILLISECONDS) },
                        BiFunction { t1: Throwable, t2: Int -> t1 to t2 }
                )
                .flatMap { pair ->
                    if (pair.second >= maxCount) {
                        Observable.error(pair.first)
                    } else {
                        val delay = interval * 2F.pow(pair.second)
                        println("retry delay: $delay")
                        Observable.timer(delay.toLong(), TimeUnit.SECONDS)
                    }
                }
    }

    //Code to print the result in terminal.
    sourceWithRetry
            .doOnComplete { println("Complete") }
            .doOnError({ println("Final Error: $it") })
            .blockingForEach { println("$it") }
}
ultraon
la source
0

dans le cas où vous devez imprimer le nombre de tentatives, vous pouvez utiliser l'exemple fourni sur la page wiki de Rxjava https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators

observable.retryWhen(errors ->
    // Count and increment the number of errors.
    errors.map(error -> 1).scan((i, j) -> i + j)  
       .doOnNext(errorCount -> System.out.println(" -> query errors #: " + errorCount))
       // Limit the maximum number of retries.
       .takeWhile(errorCount -> errorCount < retryCounts)   
       // Signal resubscribe event after some delay.
       .flatMapSingle(errorCount -> Single.timer(errorCount, TimeUnit.SECONDS));
Angel Koh
la source