rxjava: ฉันสามารถใช้ retry () แต่มีความล่าช้าได้หรือไม่?


94

ฉันใช้ rxjava ในแอพ Android เพื่อจัดการคำขอเครือข่ายแบบอะซิงโครนัส ตอนนี้ฉันต้องการลองอีกครั้งคำขอเครือข่ายที่ล้มเหลวหลังจากผ่านไประยะหนึ่งแล้วเท่านั้น

มีวิธีใดบ้างในการใช้ retry () บน Observable แต่จะลองอีกครั้งหลังจากเกิดความล่าช้าบางอย่างเท่านั้น

มีวิธีใดที่จะแจ้งให้ผู้สังเกตการณ์ทราบว่ากำลังถูกลองใหม่ (ซึ่งตรงข้ามกับการทดลองในครั้งแรก) หรือไม่?

ฉันได้ดู debounce () / throttleWithTimeout () แต่ดูเหมือนว่าพวกเขาจะทำอะไรบางอย่างที่แตกต่างออกไป

แก้ไข:

ฉันคิดว่าฉันพบวิธีหนึ่งที่จะทำได้ แต่ฉันสนใจที่จะยืนยันว่านี่เป็นวิธีที่ถูกต้องในการทำหรือวิธีอื่นที่ดีกว่า

สิ่งที่ฉันกำลังทำอยู่นี้: ในวิธี call () ของ Observable OnSubscribe ของฉันก่อนที่ฉันจะเรียกใช้เมธอด Subscribers onError () ฉันเพียงแค่ปล่อยให้เธรดเข้าสู่โหมดสลีปตามระยะเวลาที่ต้องการ ดังนั้นในการลองใหม่ทุกๆ 1,000 มิลลิวินาทีฉันจะทำสิ่งนี้:

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

เนื่องจากวิธีนี้ทำงานบนเธรด IO อย่างไรก็ตามจึงไม่ได้ปิดกั้น UI ปัญหาเดียวที่ฉันเห็นคือแม้ข้อผิดพลาดแรกจะถูกรายงานด้วยความล่าช้าดังนั้นความล่าช้าจึงเกิดขึ้นแม้ว่าจะไม่มีการลองใหม่ () ฉันจะดีกว่านี้หากไม่ได้ใช้การหน่วงเวลาหลังจากเกิดข้อผิดพลาด แต่ก่อนที่จะลองใหม่ (แต่ไม่ใช่ก่อนการลองครั้งแรกอย่างเห็นได้ชัด)

คำตอบ:


173

คุณสามารถใช้ตัว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
ดูเหมือนว่า RxJava ลองใหม่เมื่อตัวดำเนินการเปลี่ยนไปตั้งแต่ฉันเขียนสิ่งนี้ครั้งแรก ฉันจะได้รับคำตอบที่อัปเดต
kjones

3
คุณควรอัปเดตคำตอบนี้เพื่อให้สอดคล้องกับ RxJava 2
Vishnu M.

1
rxjava 2 รุ่นจะมองหา kotlin ได้อย่างไร
Gabriel Sanmartin

20

ได้รับแรงบันดาลใจจากคำตอบของ Paulและหากคุณไม่กังวลกับretryWhenปัญหาที่Abhijit Sarkarระบุไว้วิธีที่ง่ายที่สุดในการชะลอการสมัครสมาชิกใหม่ด้วย rxJava2 โดยไม่มีเงื่อนไขคือ:

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

คุณอาจต้องการที่จะเห็นตัวอย่างเพิ่มเติมและคำอธิบายเกี่ยวกับretryWhen และ repeatWhen


15

ตัวอย่างนี้ใช้ได้กับ 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

นี่เป็นวิธีแก้ปัญหาตามตัวอย่างของ Ben Christensen ที่ฉันเห็นRetryWhen ExampleและRetryWhenTestsConditional (ฉันต้องเปลี่ยนn.getThrowable()เพื่อnให้มันทำงานได้) ฉันใช้ evant / gradle-retrolambdaเพื่อให้สัญกรณ์ lambda ทำงานบน Android แต่คุณไม่จำเป็นต้องใช้ lambdas (แม้ว่าจะแนะนำเป็นอย่างยิ่ง) สำหรับความล่าช้าฉันใช้การย้อนกลับแบบเอ็กซ์โพเนนเชียล แต่คุณสามารถเสียบตรรกะแบ็คออฟที่คุณต้องการได้ที่นั่น เพื่อความสมบูรณ์ฉันเพิ่ม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
มันดูหรูหรา แต่ฉันไม่ได้ใช้ฟังก์ชัน lamba ฉันจะเขียนโดยไม่มี lambas ได้อย่างไร @ amitai-hoze
ericn

ฉันจะเขียนมันเพื่อให้ฉันสามารถใช้ฟังก์ชั่นลองใหม่นี้ซ้ำกับObservableวัตถุอื่นได้อย่างไร
ericn

ไม่เป็นไรฉันใช้kjonesวิธีแก้ปัญหาแล้วและมันก็ออกมาสมบูรณ์แบบสำหรับฉันขอบคุณ
ericn

8

แทนที่จะใช้ MyRequestObservable.ry ฉันใช้ฟังก์ชัน wrapper retryObservable (MyRequestObservable, retrycount, seconds) ซึ่งส่งคืนค่า 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 David Karnok

ฉันปรับปรุงคำตอบของ kjones โดยเปลี่ยนflatMapเป็นconcatMapและเพิ่มRetryDelayStrategyคลาส flatMapไม่รักษาลำดับการปล่อยในขณะที่concatMapทำซึ่งเป็นสิ่งสำคัญสำหรับความล่าช้าด้วยการสำรอง RetryDelayStrategyเป็นชื่อที่บ่งชี้ให้ของผู้ใช้เลือกโหมดต่างๆของการสร้างความล่าช้าลองใหม่อีกครั้งรวมถึงการกลับมาปิด รหัสมีอยู่ในGitHubของฉันพร้อมกรณีทดสอบต่อไปนี้:

  1. ประสบความสำเร็จในความพยายามครั้งแรก (ไม่มีการลองใหม่)
  2. ล้มเหลวหลังจากลองอีกครั้ง 1 ครั้ง
  3. พยายามลองใหม่ 3 ครั้ง แต่สำเร็จในครั้งที่ 2 ดังนั้นจะไม่ลองครั้งที่ 3 อีกครั้ง
  4. ประสบความสำเร็จในการลองครั้งที่ 3

ดูsetRandomJokesวิธีการ


5

ตามคำตอบของkjonesที่นี่คือรุ่น Kotlin ของ RxJava 2.x ลองใหม่โดยมีการหน่วงเวลาเป็นส่วนขยาย แทนที่การสร้างนามสกุลเดียวกันObservableFlowable

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เช่นกัน?
Papps

2
@Papps ใช่ว่าจะใช้งานได้โปรดทราบว่าflatMapจะต้องใช้Flowable.timerและFlowable.error แม้ว่าฟังก์ชันจะเป็นSingle<T>.retryWithDelayอย่างไร
JuliusScript

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

1

คุณสามารถเพิ่มความล่าช้าใน Observable ที่ส่งคืนใน 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);
}

สามารถดูตัวอย่างเพิ่มเติมได้ที่นี่ 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

(Kotlin) ฉันปรับปรุงโค้ดเล็กน้อยด้วยการย้อนกลับแบบเอ็กซ์โปเนนเชียลและใช้การป้องกันการปล่อย 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));
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.