rxjava: Чи можу я використовувати retry (), але із затримкою?


91

Я використовую rxjava у своєму додатку Android для асинхронної обробки мережевих запитів. Тепер я хотів би повторити спробу невдалого мережевого запиту лише після того, як пройде певний час.

Чи є спосіб використовувати retry () для Observable, але повторити спробу лише після певної затримки?

Чи є спосіб повідомити спостережуваного, що зараз переглядається (на відміну від того, що судили вперше)?

Я подивився debounce () / throttleWithTimeout (), але вони, здається, роблять щось інше.

Редагувати:

Думаю, я знайшов один із способів зробити це, але мене зацікавить або підтвердження, що це правильний спосіб, або інші, кращі способи.

Що я роблю, це: у методі call () мого Observable.OnSubscribe, перед тим як зателефонувати методу Subscribers onError (), я просто дозволяю потоку спати протягом бажаної кількості часу. Отже, щоб повторити спробу кожні 1000 мілісекунд, я роблю щось подібне:

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

Оскільки цей метод у будь-якому випадку працює на потоці вводу-виводу, він не блокує інтерфейс користувача. Єдина проблема, яку я бачу, полягає в тому, що навіть про першу помилку повідомляється із затримкою, тому затримка є, навіть якщо немає повторної спроби (). Я хотів би, щоб затримка була застосована не після помилки, а перед повторною спробою (але, очевидно, не перед першою спробою).

Відповіді:


169

Ви можете використовувати retryWhen()оператор, щоб додати логіку повтору до будь-якого Observable.

Наступний клас містить логіку повторення:

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

Використання:

// 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 У мене була та ж проблема, змініть RetryWithDelayна це: pastebin.com/6SiZeKnC
user1480019

2
Схоже, повторна спроба RxJavaWhen змінилася після того, як я це написав. Я отримаю відповідь оновленою.
kjones

3
Вам слід оновити цю відповідь, щоб відповідати RxJava 2
Vishnu M.

1
як би версія rxjava 2 шукала kotlin?
Габріель Санмартін,

18

Натхненний відповіддю Павла , і якщо вас не турбують retryWhenпроблеми, заявлені Абхіджітом Саркаром , найпростіший спосіб відкласти повторну підписку на rxJava2 безумовно:

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

Можливо, ви захочете побачити більше зразків та пояснень щодо retryWhen і repeatWhen .


14

Цей приклад працює з jxjava 2.2.2:

Повторіть спробу без затримки:

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

Повторіть спробу із затримкою:

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

Наш вихідний сингл не вдається, якщо someConnection.send () не вдається. Коли це трапляється, спостережувані збої всередині повторюють, коли видає помилку. Ми затримуємо цей викид на 300 мс і відправляємо його назад, щоб сигналізувати про повторну спробу. прийняти (5) гарантує, що наша сигналізація, що спостерігається, припиниться після того, як ми отримаємо п'ять помилок. повторити спробу Коли побачить завершення і не повторить після п’ятої помилки.


9

Це рішення засноване на фрагментах Бена Крістенсена, які я бачив, RetryWhen Example та RetryWhenTestsConditional (мені довелося змінити n.getThrowable()на, nщоб це працювало). Я використовував evant / gradle-retrolambda, щоб зробити позначення лямбда на Android, але вам не потрібно використовувати лямбди (хоча це настійно рекомендується). Для затримки я реалізував експоненціальне відсікання, але ви можете підключити туди будь-яку логіку відмови. Для повноти я додав оператори subscribeOnі observeOn. Я використовую ReactiveX / RxAndroid для 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
це виглядає елегантно, але я не використовую функції лямба, як я можу писати без лямбасів? @ amitai-hoze
ericn

також як мені написати це так, щоб я міг повторно використовувати цю функцію повтору для інших Observableоб'єктів?
ericn

нічого, я використовував kjonesрішення, і воно ідеально підходить для мене, дякую
ericn

8

замість того, щоб використовувати MyRequestObservable.retry, я використовую функцію обгортки retryObservable (MyRequestObservable, retrycount, секунди), яка повертає новий Observable, який обробляє непряму затримку, щоб я міг зробити

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

                }
            });

        }

    });

}

Мені страшенно шкода, що я не відповів раніше - я якось пропустив повідомлення від SO про те, що була відповідь на моє запитання ... Я підтримав вашу відповідь, тому що мені подобається ця ідея, але я не впевнений, чи відповідно до принципів SO - Я повинен прийняти відповідь, оскільки це швидше обхідний шлях, ніж пряма відповідь. Але я гадаю, оскільки ви даєте обхідний шлях, відповідь на моє початкове запитання: "ні, ви не можете" ...
david.mihola

5

retryWhenє складним, можливо, навіть глючним оператором. Офіційний документ і принаймні одна відповідь тут використовують rangeоператор, який не вдасться, якщо не буде зроблено спроб. Дивіться мою дискусію з учасником ReactiveX Девідом Карноком.

Я поліпшив відповідь kjones 'шляхом зміни flatMapдо concatMapі шляхом додавання RetryDelayStrategyкласу. flatMapне зберігає порядок викидів, поки concatMapце робить, що важливо для затримок із зворотним відходом. Як RetryDelayStrategyвказує назва, давайте користувачеві вибрати один із різних режимів генерації затримок повторної спроби, включаючи зворотне відключення. Код доступний на моєму GitHub у комплекті з наступними тестами:

  1. Успіх з першої спроби (без спроб)
  2. Помилка після 1 повторної спроби
  3. Спроба повторити спробу 3 рази, але успішна 2-го, отже, не повторюється 3-й раз
  4. Вдається з 3-ї повторної спроби

Див. setRandomJokesМетод.


3

Тепер із RxJava версії 1.0+ ви можете використовувати zipWith для повторної спроби із затримкою.

Додавання змін до відповіді kjones .

Модифікований

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

Та сама відповідь, що й у kjones, але оновлена ​​до останньої версії Для версії 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);
            }
        });
    }
}

Використання:

// Додаємо логіку повторних спроб до існуючих спостережуваних. // Повторіть макс. 3 рази із затримкою 2 секунди.

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

3

На основі відповіді kjones тут є версія Kotx RxJava 2.x повторна спроба із затримкою як розширення. Замінити, Observableщоб створити те саме розширення для 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)
            }
        }
    }
}

Тоді просто використовуйте його на спостережуваному observable.retryWithDelay(3, 1000)


Чи можна замінити це Singleтакож?
Паппс,

2
@Papps Так, це повинно працювати, просто зверніть увагу, що flatMapтут доведеться використовувати, Flowable.timerі Flowable.error хоча ця функція є Single<T>.retryWithDelay.
JuliusScript

1

Ви можете додати затримку в Observable, що повертається в операції 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);
}

Більше прикладів ви можете побачити тут. https://github.com/politrons/reactive


0

Просто зробіть це так:

                  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

Для версій 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
                    }
                })
    }
}

0

(Котлін) Я трохи вдосконалив код з експоненціальним відходом і застосував захисне випромінювання 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

у випадку, коли вам потрібно роздрукувати кількість повторних спроб, ви можете скористатися прикладом, наведеним на вікі-сторінці 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));
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.