rxjava: Posso usare retry () ma con ritardo?


91

Sto usando rxjava nella mia app Android per gestire le richieste di rete in modo asincrono. Ora vorrei ritentare una richiesta di rete non riuscita solo dopo che è trascorso un certo tempo.

C'è un modo per usare retry () su un Observable ma per riprovare solo dopo un certo ritardo?

Esiste un modo per far sapere all'Osservabile che è attualmente in corso un nuovo tentativo (invece di provare per la prima volta)?

Ho dato un'occhiata a debounce () / throttleWithTimeout () ma sembra che stiano facendo qualcosa di diverso.

Modificare:

Penso di aver trovato un modo per farlo, ma sarei interessato a confermare che questo è il modo corretto per farlo o ad altri modi migliori.

Quello che sto facendo è questo: nel metodo call () del mio Observable.OnSubscribe, prima di chiamare il metodo Subscribers onError (), ho semplicemente lasciato dormire il Thread per il tempo desiderato. Quindi, per riprovare ogni 1000 millisecondi, faccio qualcosa del genere:

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

Poiché questo metodo è comunque in esecuzione su un thread IO, non blocca l'interfaccia utente. L'unico problema che posso vedere è che anche il primo errore viene segnalato con ritardo, quindi il ritardo è presente anche se non c'è retry (). Mi piacerebbe che il ritardo non fosse applicato dopo un errore ma prima di un nuovo tentativo (ma non prima del primo tentativo, ovviamente).

Risposte:


169

È possibile utilizzare l' retryWhen()operatore per aggiungere la logica di ripetizione a qualsiasi Observable.

La seguente classe contiene la logica di ripetizione:

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

Utilizzo:

// Add retry logic to existing observable.
// Retry max of 3 times with a delay of 2 seconds.
observable
    .retryWhen(new RetryWithDelay(3, 2000));

2
Error:(73, 20) error: incompatible types: RetryWithDelay cannot be converted to Func1<? super Observable<? extends Throwable>,? extends Observable<?>>
Nima G

3
@nima ho avuto lo stesso problema, cambia RetryWithDelayin questo: pastebin.com/6SiZeKnC
user1480019

2
Sembra che l'operatore RxJava retryWhen sia cambiato da quando l'ho scritto originariamente. Farò aggiornare la risposta.
kjones

3
È necessario aggiornare questa risposta per conformarsi a RxJava 2
Vishnu M.

1
come sarebbe la versione rxjava 2 a cercare kotlin?
Gabriel Sanmartin

18

Ispirato dalla risposta di Paul , e se non sei interessato ai retryWhenproblemi dichiarati da Abhijit Sarkar , il modo più semplice per ritardare incondizionatamente la sottoscrizione con rxJava2 è:

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

Potresti voler vedere più esempi e spiegazioni su retryWhen e repeatWhen .


14

Questo esempio funziona con jxjava 2.2.2:

Riprova senza indugio:

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

Riprova con ritardo:

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

Il nostro singolo sorgente fallisce se someConnection.send () fallisce. Quando ciò accade, l'osservabile degli errori all'interno di retryWhen emette l'errore. Ritardiamo l'emissione di 300 ms e la rimandiamo indietro per segnalare un nuovo tentativo. take (5) garantisce che la nostra segnalazione di osservabile terminerà dopo aver ricevuto cinque errori. retryQuando vede la terminazione e non riprova dopo il quinto errore.


9

Questa è una soluzione basata sugli snippet di Ben Christensen che ho visto, RetryWhen Example e RetryWhenTestsConditional (ho dovuto cambiare n.getThrowable()in nperché funzionasse). Ho usato evant / gradle-retrolambda per far funzionare la notazione lambda su Android, ma non devi usare lambda (anche se è altamente raccomandato). Per il ritardo ho implementato il backoff esponenziale, ma puoi inserire qualsiasi logica di backoff che desideri lì. Per completezza ho aggiunto gli operatori subscribeOne observeOn. Sto usando ReactiveX / RxAndroid per 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);

2
sembra elegante ma non sto usando le funzioni lamba, come posso scrivere senza lamba? @ amitai-hoze
ericn

anche come lo scrivo in modo tale da poter riutilizzare questa funzione di riprova per altri Observableoggetti?
ericn

non importa, ho usato la kjonessoluzione e sta funzionando perfettamente per me, grazie
ericn

8

invece di usare MyRequestObservable.retry io uso una funzione wrapper retryObservable (MyRequestObservable, retrycount, seconds) che restituisce un nuovo Observable che gestisce l'indirizzamento per il ritardo così posso farlo

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

                }
            });

        }

    });

}

Sono terribilmente dispiaciuto per non aver risposto prima - in qualche modo mi sono perso la notifica da SO che c'era una risposta alla mia domanda ... Ho votato positivamente la tua risposta perché mi piace l'idea, ma non sono sicuro che - secondo i principi di SO - Dovrei accettare la risposta in quanto è più una soluzione alternativa che una risposta diretta. Ma immagino, dato che stai dando una soluzione alternativa, la risposta alla mia domanda iniziale è "no, non puoi" ...
david.mihola

5

retryWhenè un operatore complicato, forse anche buggato. Il documento ufficiale e almeno una risposta qui usano l' rangeoperatore, che fallirà se non ci sono nuovi tentativi da effettuare. Guarda la mia discussione con il membro di ReactiveX David Karnok.

Ho migliorato la risposta di kjones passando flatMapa concatMape aggiungendo una RetryDelayStrategyclasse. flatMapnon conserva l'ordine di emissione mentre lo concatMapfa, il che è importante per i ritardi con back-off. Il RetryDelayStrategy, come indica il nome, consente all'utente di scegliere tra varie modalità di generazione di ritardi tra i tentativi, incluso il backoff. Il codice è disponibile sul mio GitHub completo dei seguenti casi di test:

  1. Ha successo al 1 ° tentativo (nessun tentativo)
  2. Non riesce dopo 1 tentativo
  3. Tenta di riprovare 3 volte ma riesce il 2 °, quindi non riprova la 3 ° volta
  4. Riuscirà al terzo tentativo

Vedi setRandomJokesmetodo.


3

Ora con RxJava versione 1.0+ puoi usare zipWith per riprovare con ritardo.

Aggiunta di modifiche alla risposta di kjones .

Modificato

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

3

Stessa risposta di kjones ma aggiornata all'ultima versione Per la versione 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);
            }
        });
    }
}

Utilizzo:

// Aggiunge la logica di ripetizione all'osservabile esistente. // Riprova massimo 3 volte con un ritardo di 2 secondi.

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

3

Sulla base della risposta di kjones, ecco la versione Kotlin di RxJava 2.x che riprova con un ritardo come estensione. Sostituisci Observableper creare la stessa estensione per 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)
            }
        }
    }
}

Quindi usalo su osservabile observable.retryWithDelay(3, 1000)


È possibile sostituire anche questo con Single?
Papps

2
@Papps Sì, dovrebbe funzionare, tieni presente che flatMapdovrai usare Flowable.timere Flowable.error anche se la funzione è Single<T>.retryWithDelay.
JuliusScript

1

È possibile aggiungere un ritardo all'Observable restituito nell'operatore retryWhen

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

Puoi vedere altri esempi qui. https://github.com/politrons/reactive


0

Fallo semplicemente in questo modo:

                  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) {

                                }
                            });

0

Per la versione Kotlin e 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
                    }
                })
    }
}

0

(Kotlin) Ho leggermente migliorato il codice con backoff esponenziale e emissione di difesa applicata di 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") }
}

0

nel caso in cui sia necessario stampare il conteggio dei tentativi, è possibile utilizzare l'esempio fornito nella pagina wiki di 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));
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.