rxjava: ¿Puedo usar retry () pero con retraso?

91

Estoy usando rxjava en mi aplicación de Android para manejar solicitudes de red de forma asincrónica. Ahora me gustaría volver a intentar una solicitud de red fallida solo después de que haya pasado un cierto tiempo.

¿Hay alguna forma de usar retry () en un Observable pero reintentar solo después de un cierto retraso?

¿Hay alguna manera de hacerle saber al Observable que se está reintentando actualmente (en lugar de intentarlo por primera vez)?

Eché un vistazo a debounce () / throttleWithTimeout () pero parecen estar haciendo algo diferente.

Editar:

Creo que encontré una forma de hacerlo, pero me interesaría la confirmación de que esta es la forma correcta de hacerlo o de otras formas mejores.

Lo que estoy haciendo es esto: en el método call () de mi Observable.OnSubscribe, antes de llamar al método Subscribers onError (), simplemente dejo que el Thread duerma durante la cantidad de tiempo deseada. Entonces, para volver a intentarlo cada 1000 milisegundos, hago algo como esto:

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

Dado que este método se ejecuta en un subproceso IO de todos modos, no bloquea la interfaz de usuario. El único problema que puedo ver es que incluso el primer error se informa con retraso, por lo que el retraso existe incluso si no hay reintento (). Me gustaría más si el retraso no se aplicara después de un error, sino antes de un reintento (pero no antes del primer intento, obviamente).

david.mihola
fuente

Respuestas:

169

Puede usar el retryWhen()operador para agregar lógica de reintento a cualquier Observable.

La siguiente clase contiene la lógica de reintento:

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

Uso:

// Add retry logic to existing observable.
// Retry max of 3 times with a delay of 2 seconds.
observable
    .retryWhen(new RetryWithDelay(3, 2000));
kjones
fuente
2
Error:(73, 20) error: incompatible types: RetryWithDelay cannot be converted to Func1<? super Observable<? extends Throwable>,? extends Observable<?>>
Nima G
3
@nima Tuve el mismo problema, cambie RetryWithDelaya esto: pastebin.com/6SiZeKnC
user1480019
2
Parece que el operador RxJava retryWhen ha cambiado desde que escribí esto originalmente. Actualizaré la respuesta.
kjones
3
Debe actualizar esta respuesta para cumplir con RxJava 2
Vishnu M.
1
¿Cómo se vería la versión de rxjava 2 para kotlin?
Gabriel Sanmartin
18

Inspirado por la respuesta de Paul , y si no le preocupan los retryWhenproblemas indicados por Abhijit Sarkar , la forma más sencilla de retrasar la resuscripción con rxJava2 incondicionalmente es:

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

Es posible que desee ver más ejemplos y explicaciones sobre retryWhen y repeatWhen .

McX
fuente
14

Este ejemplo funciona con jxjava 2.2.2:

Vuelva a intentarlo sin demora:

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

Reintentar con retraso:

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

Nuestro único fuente falla si falla someConnection.send (). Cuando eso sucede, el observable de fallas dentro de retryWhen emite el error. Retrasamos esa emisión en 300 ms y la enviamos de vuelta para señalar un reintento. take (5) garantiza que nuestro observable de señalización terminará después de recibir cinco errores. retryWhen ve la terminación y no vuelve a intentarlo después del quinto error.

Erunafailaro
fuente
9

Esta es una solución basada en los fragmentos de código que vi de Ben Christensen, RetryWhen Example y RetryWhenTestsConditional (tuve que cambiar n.getThrowable()a npara que funcione). Solía Evant / Gradle-retrolambda para hacer el trabajo notación lambda en Android, pero usted no tiene que utilizar lambdas (aunque es muy recomendable). Para la demora, implementé un retroceso exponencial, pero puede conectar la lógica de retroceso que desee allí. Para completar, agregué los operadores subscribeOny observeOn. Estoy usando ReactiveX / RxAndroid para 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
fuente
2
esto se ve elegante pero no estoy usando funciones lamba, ¿cómo puedo escribir sin lambas? @ amitai-hoze
ericn
también, ¿cómo lo escribo de manera que pueda reutilizar esta función de reintento para otros Observableobjetos?
ericn
no importa, he usado una kjonessolución y me está funcionando perfecto, gracias
Eric
8

en lugar de usar MyRequestObservable.retry, uso una función contenedora retryObservable (MyRequestObservable, retrycount, seconds) que devuelve un nuevo Observable que maneja la indirección del retraso para que pueda hacerlo

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
fuente
Lamento mucho no haber respondido antes, de alguna manera me perdí la notificación de SO de que había una respuesta a mi pregunta ... voté a favor de su respuesta porque me gusta la idea, pero no estoy seguro de si, de acuerdo con los principios de SO - Debo aceptar la respuesta ya que es más una solución alternativa que una respuesta directa. Pero supongo que, dado que estás dando una solución, la respuesta a mi pregunta inicial es "no, no puedes" ...
david.mihola
5

retryWhenes un operador complicado, quizás incluso con errores. El documento oficial y al menos una respuesta aquí usan el rangeoperador, que fallará si no se realizan reintentos. Vea mi discusión con el miembro de ReactiveX, David Karnok.

Mejoré la respuesta de kjones cambiando flatMap a concatMapy añadiendo una RetryDelayStrategyclase. flatMapno conserva el orden de emisión mientras lo concatMaphace, lo cual es importante para retrasos con retroceso. El RetryDelayStrategy, como su nombre lo indica, permite que el usuario elija entre varios modos de generar retrasos en los reintentos, incluido el retroceso. El código está disponible en mi GitHub completo con los siguientes casos de prueba:

  1. Tiene éxito en el primer intento (sin reintentos)
  2. Falla después de 1 reintento
  3. Intenta reintentar 3 veces pero tiene éxito en la segunda, por lo tanto, no vuelve a intentarlo la tercera vez
  4. Tiene éxito en el tercer reintento

Ver setRandomJokesmétodo.

Abhijit Sarkar
fuente
3

Ahora, con la versión 1.0+ de RxJava, puede usar zipWith para lograr reintentar con retraso.

Añadiendo modificaciones a la respuesta de kjones .

Modificado

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

La misma respuesta que la de kjones pero actualizada a la última versión para la versión 2.x de RxJava : ('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);
            }
        });
    }
}

Uso:

// Agregue lógica de reintento al observable existente. // Reintentar un máximo de 3 veces con un retraso de 2 segundos.

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

Según la respuesta de kjones, aquí está la versión Kotlin de RxJava 2.x reintentar con un retraso como extensión. Reemplazar Observablepara crear la misma extensión para 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)
            }
        }
    }
}

Entonces utilícelo en observables observable.retryWithDelay(3, 1000)

JuliusScript
fuente
¿Es posible reemplazar esto Singletambién?
Papps
2
@Papps Sí, eso debería funcionar, solo tenga en cuenta que flatMaphabrá que usar Flowable.timery Flowable.error aunque la función sea Single<T>.retryWithDelay.
JuliusScript
1

Puede agregar un retraso en el Observable devuelto en el retryWhen Operator

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

Puedes ver más ejemplos aquí. https://github.com/politrons/reactive

Pablo
fuente
0

Simplemente hazlo así:

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

Para la versión Kotlin y 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
fuente
0

(Kotlin) He mejorado un poco el código con retroceso exponencial y emisión de defensa aplicada 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
fuente
0

en caso de que necesite imprimir el recuento de reintentos, puede usar el ejemplo proporcionado en la página 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));
Ángel Koh
fuente