J'utilise rxjava dans mon application Android pour gérer les demandes réseau de manière asynchrone. Maintenant, j'aimerais réessayer une requête réseau ayant échoué uniquement après un certain temps.
Existe-t-il un moyen d'utiliser retry () sur un observable mais de ne le réessayer qu'après un certain délai?
Existe-t-il un moyen de faire savoir à l’Observable qu’il fait actuellement l’essai (au lieu d’être essayé pour la première fois)?
J'ai jeté un œil à debounce ()/throttleWithTimeout () mais ils semblent faire quelque chose de différent.
Modifier:
Je pense avoir trouvé une façon de le faire, mais je voudrais savoir si c'est la bonne façon de le faire ou pour d'autres, de meilleures façons.
Voici ce que je suis en train de faire: Dans la méthode call () de mon Observable.OnSubscribe, avant d'appeler la méthode Subscribers onError (), je laisse simplement le thread dormir pendant le temps souhaité. 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);
}
}
Comme cette méthode est exécutée sur un thread IO de toute façon, elle ne bloque pas l'interface utilisateur. Le seul problème que je peux voir, c'est que même la première erreur est signalée avec délai, donc le délai est là même s'il n'y a pas de nouvelle tentative (). Je préférerais 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).
Vous pouvez utiliser l'opérateur retryWhen()
pour ajouter une logique de nouvelle tentative à tout observable.
La classe suivante contient la logique de nouvelle tentative:
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);
}
});
}
}
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));
Inspiré par la réponse de Paul , et si vous n'êtes pas concerné par les problèmes retryWhen
énoncés par Abhijit Sarkar , le moyen le plus simple de retarder la réabonnement avec rxJava2 inconditionnellement est:
source.retryWhen(throwables -> throwables.delay(1, TimeUnit.SECONDS))
Vous voudrez peut-être voir plus d’échantillons et d’explications sur relancez-vous quand et répétez quand .
Ceci est une solution basée sur les extraits de Ben Christensen que j'ai vus, RetryWhen Exemple , et RetryWhenTestsConditional (Je devais changer n.getThrowable()
en n
pour que cela fonctionne). J'ai utilisé evant/gradle-retrolambda pour que la notation lambda fonctionne sous Android, mais vous n'êtes pas obligé d'utiliser lambdas (bien que cela soit fortement recommandé). Pour le retard, j’ai implémenté un back-off exponentiel, mais vous pouvez y brancher la logique de back-back que vous voulez. Pour être complet, j'ai ajouté les opérateurs subscribeOn
et observeOn
. J'utilise ReactiveX/RxAndroid pour la 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);
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 du délai afin que je puisse le 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);
}
}
});
}
});
}
Maintenant, avec la version 1.0+ de RxJava, vous pouvez utiliser zipWith pour obtenir de nouvelles tentatives avec délai.
Ajout de modifications à kjones answer.
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;
}
});
}
}
Cet exemple fonctionne avec jxjava 2.2.2:
Réessayer sans délai:
Single.just(somePaylodData)
.map(data -> someConnection.send(data))
.retry(5)
.doOnSuccess(status -> log.info("Yay! {}", status);
Réessayer avec 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 source unique échoue en cas d'échec de someConnection.send () . Lorsque cela se produit, l'observable échec 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 que nous ayons reçu cinq erreurs . réessayerLorsque la terminaison est affichée et ne réessaie pas après la cinquième défaillance.
Même réponse que dekjonesmais mise à jour avec la dernière version Pour 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);
}
});
}
}
Usage:
// Ajout de la logique de nouvelle tentative à l'observable existant . // Nouvelle tentative maximum de 3 fois avec un délai de 2 secondes.
observable
.retryWhen(new RetryWithDelay(3, 2000));
Vous pouvez ajouter un délai dans l’observable renvoyé lors de la nouvelle tentativeWhen 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);
}
Vous pouvez voir plus d'exemples ici. https://github.com/politrons/reactive
retryWhen
est un opérateur compliqué, peut-être même bogué. Le document officiel et au moins une réponse ici utilisent l'opérateur range
, qui échouera s'il n'y a aucune nouvelle tentative à effectuer. Voir mon discussion } _ avec David Karnok, membre de ReactiveX.
J'ai amélioré la réponse de kjones en remplaçant flatMap
par concatMap
et en ajoutant une classe RetryDelayStrategy
. flatMap
ne conserve pas l'ordre des émissions tandis que concatMap
le fait, ce qui est important pour les retards avec recul. La variable RetryDelayStrategy
, comme son nom l'indique, permet à l'utilisateur de choisir parmi différents modes de génération de retards de tentative, y compris le retrait de la mémoire . Le code est disponible sur my GitHub avec les cas de test suivants:
Voir la méthode setRandomJokes
.
(Kotlin) J'ai un peu amélioré le code avec un recul exponentiel et une défense appliquée émettant 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") }
}
Sur la base de kjones , la réponse ci-dessous correspond à la version Kotlin de RxJava 2.x, avec un délai comme extension. Remplacez Observable
pour 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.retryWithDelay(3, 1000)
observable
si vous devez imprimer le nombre de tentatives, vous pouvez utiliser l'exemple fourni dans la page de 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));
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
}
})
}
}