RxJava重试当奇怪的行为

时间:2016-07-17 00:29:48

标签: java rx-java reactive-programming

我正在玩RxJava retryWhen运算符。在互联网上发现它很少,唯一值得提及的是this。这也无法探索我想要理解的各种用例。我还投入异步执行并使用后退重试以使其更加真实。

我的设置很简单:我有一个类ChuckNorrisJokesRepository,可以从JSON文件中返回随机数量的Chuck Norris笑话。我正在测试的课程是ChuckNorrisJokesService,如下所示。我感兴趣的用例如下:

  1. 第一次尝试成功(无重试)
  2. 1次重试后失败
  3. 尝试重试3次,但第2次成功,因此不会第3次重试
  4. 第三次重试成功
  5. 注意:该项目可在GitHub上找到。

    ChuckNorrisJokesService.java

    @Slf4j
    @Builder
    public class ChuckNorrisJokesService {
        @Getter
        private final AtomicReference<Jokes> jokes = new AtomicReference<>(new Jokes());
    
        private final Scheduler scheduler;
        private final ChuckNorrisJokesRepository jokesRepository;
        private final CountDownLatch latch;
        private final int numRetries;
        private final Map<String, List<String>> threads;
    
        public static class ChuckNorrisJokesServiceBuilder {
            public ChuckNorrisJokesService build() {
                if (scheduler == null) {
                    scheduler = Schedulers.io();
                }
    
                if (jokesRepository == null) {
                    jokesRepository = new ChuckNorrisJokesRepository();
                }
    
                if (threads == null) {
                    threads = new ConcurrentHashMap<>();
                }
    
                requireNonNull(latch, "CountDownLatch must not be null.");
    
                return new ChuckNorrisJokesService(scheduler, jokesRepository, latch, numRetries, threads);
            }
        }
    
        public void setRandomJokes(int numJokes) {
            mergeThreadNames("getRandomJokes");
    
            Observable.fromCallable(() -> {
                log.debug("fromCallable - before call. Latch: {}.", latch.getCount());
                mergeThreadNames("fromCallable");
                latch.countDown();
    
                List<Joke> randomJokes = jokesRepository.getRandomJokes(numJokes);
                log.debug("fromCallable - after call. Latch: {}.", latch.getCount());
    
                return randomJokes;
            }).retryWhen(errors ->
                    errors.zipWith(Observable.range(1, numRetries), (n, i) -> i).flatMap(retryCount -> {
                        log.debug("retryWhen. retryCount: {}.", retryCount);
                        mergeThreadNames("retryWhen");
    
                        return Observable.timer(retryCount, TimeUnit.SECONDS);
                    }))
                    .subscribeOn(scheduler)
                    .subscribe(j -> {
                                log.debug("onNext. Latch: {}.", latch.getCount());
                                mergeThreadNames("onNext");
    
                                jokes.set(new Jokes("success", j));
                                latch.countDown();
                            },
                            ex -> {
                                log.error("onError. Latch: {}.", latch.getCount(), ex);
                                mergeThreadNames("onError");
                            },
                            () -> {
                                log.debug("onCompleted. Latch: {}.", latch.getCount());
                                mergeThreadNames("onCompleted");
    
                                latch.countDown();
                            }
                    );
        }
    
        private void mergeThreadNames(String methodName) {
            threads.merge(methodName,
                    new ArrayList<>(Arrays.asList(Thread.currentThread().getName())),
                    (value, newValue) -> {
                        value.addAll(newValue);
    
                        return value;
                    });
        }
    }
    

    为简洁起见,我只展示第一个用例的Spock测试用例。有关其他测试用例,请参阅我的GitHub

    def "succeeds on 1st attempt"() {
        setup:
        CountDownLatch latch = new CountDownLatch(2)
        Map<String, List<String>> threads = Mock(Map)
        ChuckNorrisJokesService service = ChuckNorrisJokesService.builder()
                .latch(latch)
                .threads(threads)
                .build()
    
        when:
        service.setRandomJokes(3)
        latch.await(2, TimeUnit.SECONDS)
    
        Jokes jokes = service.jokes.get()
    
        then:
        jokes.status == 'success'
        jokes.count() == 3
    
        1 * threads.merge('getRandomJokes', *_)
        1 * threads.merge('fromCallable', *_)
        0 * threads.merge('retryWhen', *_)
        1 * threads.merge('onNext', *_)
        0 * threads.merge('onError', *_)
        1 * threads.merge('onCompleted', *_)
    }
    

    这失败了:

    Too few invocations for:
    
    1 * threads.merge('fromCallable', *_)   (0 invocations)
    1 * threads.merge('onNext', *_)   (0 invocations)
    

    我期待的是fromCallable被调用一次,成功,onNext被调用一次,然后是onCompleted。我错过了什么?

    P.S。:完全披露 - 我也在RxJava GitHub上发布了这个问题。

1 个答案:

答案 0 :(得分:1)

经过几个小时的故障排除并在ReactiveX会员David Karnok的帮助下解决了这个问题。

retryWhen是一个复杂的,甚至是错误的运营商。官方文档和至少一个答案使用range运算符,如果没有重试,则立即完成。请与David Karnok见discussion

我的GitHub上提供了以下测试用例的代码:

  1. 第一次尝试成功(无重试)
  2. 1次重试后失败
  3. 尝试重试3次,但第2次成功,因此不会第3次重试
  4. 第三次重试成功