RxJava重试当奇怪的行为

Abh*_*kar 5 java reactive-programming rx-java

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

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

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

注意:该项目可以在我的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;
                });
    }
}
Run Code Online (Sandbox Code Playgroud)

为简洁起见,我只展示第一个用例的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', *_)
}
Run Code Online (Sandbox Code Playgroud)

这失败了:

Too few invocations for:

1 * threads.merge('fromCallable', *_)   (0 invocations)
1 * threads.merge('onNext', *_)   (0 invocations)
Run Code Online (Sandbox Code Playgroud)

我所期待的是,它fromCallable被称为一次,它成功,onNext被称为一次,然后是onCompleted.我错过了什么?

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

Abh*_*kar 1

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

retryWhen是一个复杂的,甚至可能是有缺陷的操作符。官方文档和这里至少有一个答案使用range运算符,如果没有重试,该运算符会立即完成。请参阅我与大卫·卡诺克的讨论

该代码可在我的GitHub上找到,并包含以下测试用例:

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