RxJava:重试地图操作

时间:2015-05-29 15:10:28

标签: java rx-java reactive-programming

我有一个Observable,其中每个项目的转换方式可能会导致异常,但可以重试。我不希望失败打破流,因为每个项目代表一个独立的事务。我能想出的最佳解决方案是:

    final AtomicLong errCount = new AtomicLong();
    Observable.from(ImmutableList.of(1L, 2L, 3L)).flatMap(new Func1<Long, Observable<Long>>() {
        @Override
        public Observable<Long> call(Long aLong) {
            return Observable.from(ImmutableList.of(aLong)).map(new Func1<Long, Long>() {
                @Override
                public Long call(Long aLong) {
                    if (aLong == 2 && errCount.getAndIncrement() < 1) {
                        throw new RuntimeException("retryable error");
                    }
                    return aLong * 100;
                }
            }).retry(2);
        }
    }).forEach(new Action1<Long>() {
        @Override
        public void call(Long aLong) {
            System.out.println(aLong);
        }
    });

// Desired output: 100, 200, 300 (not 100, 100, 200, 300)

问题:

  • 重试逻辑真的很冗长。
  • 如果任何项目在2次重试后失败,则流会中断(不再处理任何项目)。我想要一个简洁的方法来返回像Finagle的Try那样的异常和结果,所以我可以处理所有异常。

1 个答案:

答案 0 :(得分:1)

  

重试逻辑真的很冗长。

您可以通过切换到ImmutableList构造函数来完全避免使用Observable.just(t1, t2, t3)。这基本上做同样的事情,但不那么冗长。

我看到你是flatMapping以便将每个值转换为Observable。这样可以防止在取消订阅整个链时映射单个值时遇到onError。因此,当操作员抛出它时,将取消订阅该值的内部可观察链。否则,错误将导致取消订阅并重新订阅主外部可观察对象。

如果你想保持这种行为但是减少锅炉板(除了明显切换到Java8 lambdas之外),我可以想到两个选择。

首先,在重试后重新订阅并重复数据删除您的数据。如果您的值具有良好的hashcodeequals实现,则只有在该集合尚未包含该值时,才能使用过滤器附加到有状态集和onNext。

Observable.<Long> just(1L, 2L, 3L)
        .map(new Func1<Long, Long>() {
            @Override
            public Long call(Long aLong) {
                if (aLong == 2 && errCount.getAndIncrement() < 1) {
                    throw new RuntimeException("retryable error");
                }
                return aLong * 100;
            }})
        .retry(2)
        .filter(new Func1<Long, Boolean>() {
            Set<Long> state = null;

            @Override
            public Boolean call(Long a) {
                if (state == null)
                    state = new HashSet<Long>();
                if (!state.contains(a)) {
                    state.add(a);
                    return true;
                }
                return false;
            }})
        .forEach(new Action1<Long>() {
            @Override
            public void call(Long aLong) {
                System.out.println(aLong);
            }});

其次,您可以将您的observable切换为 resume ,从重新订阅时停止的位置。请注意,使用缓冲区运算符(observeOn,merge,flatMap)时,这可能会导致数据丢失问题。这是因为他们将继续以与下游消费者分离的方式从生产者那里消费。因此,您需要确保在重试之前不进行缓冲。如果要实现支持背压的可观察源,还有其他注意事项。

// Should resume right where it left off
resumableObservable.map(...).retry(2).observeOn()

// Don't do this. ObserveOn will buffer values and resume will lose data.
resumableObservable.map(...).observeOn().retry(2)

// Also bad if running async observables. Merging buffers so this could have data loss.
Observable.merge(resumableObservable.map(...)).retry(2)
  

如果任何项目在2次重试后失败,则流会中断(不再处理任何项目)。我想要一个简洁的方法来返回异常和结果,比如Finagle的Try,所以我可以处理所有异常。

您可以将不可靠的地图从Long -> Long更改为Long -> Tuple<Long, List<Exception>>。由于这是相当多的泛型并且很快变得麻烦,我建议使用不同的重试运算符变体,即retryWhen(Func1<Observable<Throwable>, Observable<?>>)。以下是如何在代码中使用它的示例。

}).retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>(){
@Override
public Observable<?> call(Observable<? extends Throwable> o) {
    final AtomicInteger count = new AtomicInteger();
    return o.filter(new Func1<Throwable, Boolean>() {
        @Override
        public Boolean call(Throwable t) {
            return t instanceof RuntimeException || count.getAndIncrement() < 5;
        }}).delay(1, TimeUnit.SECONDS, Schedulers.immediate());
}})

使用重试的好处是,您可以在非阻塞样式的一段时间后轻松实现延迟重试。