rxjava: Kann ich retry () aber mit Verzögerung verwenden?

90

Ich verwende rxjava in meiner Android-App, um Netzwerkanforderungen asynchron zu verarbeiten. Jetzt möchte ich eine fehlgeschlagene Netzwerkanforderung erst nach Ablauf einer bestimmten Zeit wiederholen.

Gibt es eine Möglichkeit, retry () auf einem Observable zu verwenden, aber erst nach einer bestimmten Verzögerung erneut zu versuchen?

Gibt es eine Möglichkeit, dem Observable mitzuteilen, dass es gerade wiederholt wird (im Gegensatz zum ersten Versuch)?

Ich habe mir debounce () / throttleWithTimeout () angesehen, aber sie scheinen etwas anderes zu tun.

Bearbeiten:

Ich glaube, ich habe einen Weg gefunden, dies zu tun, aber ich würde mich entweder für die Bestätigung interessieren, dass dies der richtige Weg ist, oder für andere, bessere Wege.

Was ich tue, ist Folgendes: In der call () -Methode meines Observable.OnSubscribe lasse ich den Thread einfach für die gewünschte Zeit schlafen, bevor ich die Methode Subscribers onError () aufrufe. Um es alle 1000 Millisekunden erneut zu versuchen, mache ich ungefähr so:

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

Da diese Methode ohnehin auf einem E / A-Thread ausgeführt wird, wird die Benutzeroberfläche nicht blockiert. Das einzige Problem, das ich sehen kann, ist, dass selbst der erste Fehler mit Verzögerung gemeldet wird, sodass die Verzögerung auch dann vorhanden ist, wenn keine Wiederholung () erfolgt. Ich würde es besser finden, wenn die Verzögerung nicht nach einem Fehler, sondern vor einem erneuten Versuch angewendet würde (aber natürlich nicht vor dem ersten Versuch).

david.mihola
quelle

Antworten:

168

Du kannst den ... benutzen retryWhen() Operator jedem Observable eine Wiederholungslogik hinzufügen.

Die folgende Klasse enthält die Wiederholungslogik:

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

Verwendung:

// Add retry logic to existing observable.
// Retry max of 3 times with a delay of 2 seconds.
observable
    .retryWhen(new RetryWithDelay(3, 2000));
kjones
quelle
2
Error:(73, 20) error: incompatible types: RetryWithDelay cannot be converted to Func1<? super Observable<? extends Throwable>,? extends Observable<?>>
Nima G
3
@nima Ich hatte das gleiche Problem, ändern RetryWithDelaydiese: pastebin.com/6SiZeKnC
user1480019
2
Es sieht so aus, als ob sich der RxJava-Operator retryWhen geändert hat, seit ich das ursprünglich geschrieben habe. Ich werde die Antwort aktualisieren.
Kjones
3
Sie sollten diese Antwort aktualisieren, um RxJava 2
Vishnu M.
1
Wie würde die rxjava 2-Version für Kotlin aussehen?
Gabriel Sanmartin
18

Inspiriert von Pauls Antwort und wenn Sie sich nicht mit retryWhenProblemen befassen , die von Abhijit Sarkar angegeben wurden , ist der einfachste Weg, die erneute Anmeldung mit rxJava2 bedingungslos zu verzögern:

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

Möglicherweise möchten Sie weitere Beispiele und Erklärungen zu retryWhen und repeatWhen sehen .

McX
quelle
14

Dieses Beispiel funktioniert mit jxjava 2.2.2:

Wiederholen Sie dies unverzüglich:

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

Wiederholen Sie den Vorgang mit Verzögerung:

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."));

Unsere Quellensingle schlägt fehl, wenn someConnection.send () fehlschlägt. In diesem Fall wird der Fehler durch die Beobachtung von Fehlern in retryWhen ausgegeben. Wir verzögern diese Emission um 300 ms und senden sie zurück, um einen erneuten Versuch zu signalisieren. take (5) garantiert, dass unsere beobachtbare Signalisierung endet, nachdem wir fünf Fehler erhalten haben. retryWhen die Beendigung sieht und nach dem fünften Fehler nicht erneut versucht wird.

Erunafailaro
quelle
9

Dies ist eine Lösung, die auf den von mir gesehenen Snippets von Ben Christensen, RetryWhen Example und RetryWhenTestsConditional basiert (ich musste wechseln n.getThrowable(), ndamit es funktioniert). Ich habe evant / gradle-retrolambda verwendet , damit die Lambda-Notation auf Android funktioniert, aber Sie müssen keine Lambdas verwenden (obwohl dies sehr zu empfehlen ist). Für die Verzögerung habe ich exponentielles Backoff implementiert, aber Sie können dort jede gewünschte Backoff-Logik einstecken. Der Vollständigkeit halber habe ich die Operatoren subscribeOnund hinzugefügt observeOn. Ich benutze ReactiveX / RxAndroid für die 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
quelle
2
Das sieht elegant aus, aber ich verwende keine Lamba-Funktionen. Wie kann ich ohne Lambas schreiben? @ Amitai-Hoze
Ericn
Wie schreibe ich es auch so, dass ich diese Wiederholungsfunktion für andere ObservableObjekte wiederverwenden kann ?
Ericn
egal, ich habe eine kjonesLösung verwendet und sie funktioniert perfekt für mich, danke
Ericn
8

Anstatt MyRequestObservable.retry zu verwenden, verwende ich eine Wrapper-Funktion retryObservable (MyRequestObservable, retrycount, Sekunden), die eine neue Observable zurückgibt, die die Indirektion für die Verzögerung behandelt, damit ich dies tun kann

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
quelle
Es tut mir schrecklich leid, dass ich nicht früher geantwortet habe - irgendwie habe ich die Benachrichtigung von SO verpasst, dass es eine Antwort auf meine Frage gab ... Ich habe Ihre Antwort positiv bewertet, weil mir die Idee gefällt, aber ich bin mir nicht sicher, ob - gemäß den Prinzipien von SO - Ich sollte die Antwort akzeptieren, da es sich eher um eine Problemumgehung als um eine direkte Antwort handelt. Aber ich denke, da Sie eine
Problemumgehung geben,
5

retryWhenist ein komplizierter, vielleicht sogar fehlerhafter Bediener. Das offizielle Dokument und mindestens eine Antwort hier verwenden den rangeOperator, der fehlschlägt, wenn keine erneuten Versuche durchgeführt werden müssen. Siehe meine Diskussion mit ReactiveX-Mitglied David Karnok.

Ich verbessern auf kjones' Antwort , indem flatMapauf concatMapund durch eine Zugabe von RetryDelayStrategyKlasse. flatMapDie Reihenfolge der Emissionen bleibt dabei nicht erhalten concatMap, was für Verzögerungen beim Zurücksetzen wichtig ist. Die RetryDelayStrategy, wie der Name schon sagt, wollen wir die Benutzer von verschiedenen Arten der Erzeugung von Wiederholungsverzögerungen wählen, einschließlich Back-off. Der Code ist auf meinem GitHub verfügbar und enthält die folgenden Testfälle:

  1. Erfolg beim ersten Versuch (keine erneuten Versuche)
  2. Schlägt nach 1 erneuten Versuch fehl
  3. Versuche, es 3 Mal zu wiederholen, sind jedoch am 2. erfolgreich, daher wird das 3. Mal nicht wiederholt
  4. Erfolgreich bei 3. Wiederholung

Siehe setRandomJokesMethode.

Abhijit Sarkar
quelle
Wenn Sie versuchen, den Github-Link zu öffnen, erhalten Sie eine 404.
Christopher
1
@Christopher behoben.
Abhijit Sarkar
3

Mit RxJava Version 1.0+ können Sie jetzt zipWith verwenden, um einen erneuten Versuch mit Verzögerung zu erzielen.

Hinzufügen von Änderungen zu kjones Antwort.

Geändert

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
quelle
3

Gleiche Antwort wie von kjones, jedoch auf die neueste Version aktualisiert. Für RxJava 2.x- Version: ('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);
            }
        });
    }
}

Verwendung:

// Wiederholungslogik zu vorhandenem Observable hinzufügen. // Maximal 3 Mal mit einer Verzögerung von 2 Sekunden wiederholen.

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

Basierend auf kjones hier beantworten ist Kotlin Version von RxJava 2.x Neuversuch mit einer Verzögerung als Erweiterung. Ersetzen Observable, um dieselbe Erweiterung für zu erstellen 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)
            }
        }
    }
}

Dann benutze es einfach auf beobachtbar observable.retryWithDelay(3, 1000)

JuliusScript
quelle
Ist es möglich, dies auch durch zu ersetzen Single?
Papps
2
@Papps Ja, das sollte funktionieren, beachte nur, dass flatMapes verwendet werden muss Flowable.timerund Flowable.error obwohl die Funktion ist Single<T>.retryWithDelay.
JuliusScript
1

Sie können eine Verzögerung in der Observable hinzufügen, die im Operator retryWhen zurückgegeben wird

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

Weitere Beispiele finden Sie hier. https://github.com/politrons/reactive

paul
quelle
0

Mach es einfach so:

                  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
quelle
0

Für Kotlin & RxJava1 Version

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
quelle
0

(Kotlin) Ich habe den Code mit exponentiellem Backoff und angewandter Verteidigungsemission von Observable.range () ein wenig verbessert:

    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
quelle
0

Für den Fall, dass Sie die Anzahl der Wiederholungen ausdrucken müssen, können Sie das Beispiel auf der Wiki-Seite von Rxjava unter https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators verwenden

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));
Engel Koh
quelle