RxJava повторить при странном поведении

Я играю с оператором RxJava retryWhen. В интернете об этом очень мало, и единственное, что заслуживает упоминания, это. Это также недостаточно для изучения различных вариантов использования, которые я хотел бы понять. Я также добавил асинхронное выполнение и повторил попытку, чтобы сделать ее более реалистичной.

Моя установка проста: у меня есть класс ChuckNorrisJokesRepository это возвращает случайное количество шуток Чака Норриса из файла JSON. Мой тестируемый класс ChuckNorrisJokesService что показано ниже. Интересующие меня варианты использования следующие:

  1. Успешно с 1-й попытки (без попыток)
  2. Сбой после 1 попытки
  3. Пытается повторить 3 раза, но успешно на 2-м, следовательно, не повторяет 3-й раз
  4. Успешно на 3-й повтор

Примечание: проект доступен на моем 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;
                });
    }
}

Для краткости я покажу тестовый пример Спока только для первого варианта использования. Смотрите мой 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, Что мне не хватает?

PS: Полное раскрытие - я также разместил этот вопрос на RxJava GitHub.

1 ответ

Решение

Я решил это после нескольких часов поиска неисправностей и с помощью члена ReactiveX Дэвида Карнока.

retryWhen это сложный, возможно, даже глючный оператор. Официальный документ и хотя бы один ответ здесь используют range оператор, который завершается немедленно, если нет повторных попыток. Смотрите мое обсуждение с Дэвидом Карнок.

Код доступен на моем GitHub вместе со следующими тестами:

  1. Успешно с 1-й попытки (без попыток)
  2. Сбой после 1 попытки
  3. Пытается повторить 3 раза, но успешно на 2-м, следовательно, не повторяет 3-й раз
  4. Успешно на 3-й повтор
Другие вопросы по тегам