Наблюдаемый, повторите попытку при ошибке и кеше, только если завершено

мы можем использовать оператор cache(), чтобы избежать выполнения длинной задачи (http-запроса) несколько раз и повторно использовать ее результат:

Observable apiCall = createApiCallObservable().cache(); // notice the .cache()

---------------------------------------------
// the first time we need it
apiCall.andSomeOtherStuff()
               .subscribe(subscriberA);

---------------------------------------------
//in the future when we need it again
apiCall.andSomeDifferentStuff()
               .subscribe(subscriberB);

В первый раз выполняется HTTP-запрос, но во второй раз, поскольку мы использовали оператор cache(), запрос не будет выполнен, но мы сможем повторно использовать первый результат.

Это отлично работает, когда первый запрос завершается успешно. Но если onError вызывается с первой попытки, то в следующий раз, когда новый подписчик подписывается на одно и то же наблюдаемое, onError будет вызван снова, не повторив запрос http.

То, что мы пытаемся сделать, состоит в том, что если onError вызывается в первый раз, то в следующий раз, когда кто-то присоединяется к тому же наблюдаемому, http-запрос будет предпринят с нуля. т.е. наблюдаемый будет кэшировать только успешные вызовы api, то есть те, для которых был вызван onCompleted.

Любые идеи о том, как действовать? Мы пробовали использовать операторы retry() и cache() без особого успеха.

Ответ 1

Это решение, которое мы получили после расширения решения akarnokd:

public class OnErrorRetryCache<T> {

    public static <T> Observable<T> from(Observable<T> source) {
         return new OnErrorRetryCache<>(source).deferred;
    }

    private final Observable<T> deferred;
    private final Semaphore singlePermit = new Semaphore(1);

    private Observable<T> cache = null;
    private Observable<T> inProgress = null;

    private OnErrorRetryCache(Observable<T> source) {
        deferred = Observable.defer(() -> createWhenObserverSubscribes(source));
    }

    private Observable<T> createWhenObserverSubscribes(Observable<T> source) 
    {
        singlePermit.acquireUninterruptibly();

        Observable<T> cached = cache;
        if (cached != null) {
            singlePermit.release();
            return cached;
        }

        inProgress = source
                .doOnCompleted(this::onSuccess)
                .doOnTerminate(this::onTermination)
                .replay()
                .autoConnect();

        return inProgress;
    }

    private void onSuccess() {
        cache = inProgress;
    }

    private void onTermination() {
        inProgress = null;
        singlePermit.release();
    }
}

Нам нужно было кешировать результат http-запроса от Retrofit. Таким образом, это было создано, с наблюдаемой, которая испускает единственный элемент в памяти.

Если наблюдатель подписывался во время выполнения http-запроса, мы хотели, чтобы он ждал и не выполнял запрос дважды, если только не выполнялся текущий запрос. Для этого семафор разрешает единый доступ к блоку, который создает или возвращает кешируемую наблюдаемую информацию, и если создается новая наблюдаемая, мы ждем, пока она не завершится. Тесты на вышесказанное можно найти здесь

Ответ 2

Ну, для всех, кого все еще интересуют, я думаю, что у меня есть лучший способ добиться этого с помощью rx.

Главное примечание - использовать onErrorResumeNext, что позволит вам заменить Observable в случае ошибки. поэтому он должен выглядеть примерно так:

Observable<Object> apiCall = createApiCallObservable().cache(1);
//future call
apiCall.onErrorResumeNext(new Func1<Throwable, Observable<? extends Object>>() {
    public Observable<? extends Object> call(Throwable throwable) {
        return  createApiCallObservable();
        }
    });

Таким образом, если первый вызов не удался, будущий вызов просто вызовет его (только один раз).

но каждый другой вызывающий, который попытается использовать первый наблюдаемый, потерпит неудачу и сделает другой запрос.

вы сделали ссылку на исходное наблюдаемое, просто обновите его.

так, ленивый геттер:

Observable<Object> apiCall;
private Observable<Object> getCachedApiCall() {
    if ( apiCall == null){
        apiCall = createApiCallObservable().cache(1);
    }
    return apiCall;
}

теперь, получатель, который будет повторять попытку, если предыдущий был сбой:

private Observable<Object> getRetryableCachedApiCall() {
    return getCachedApiCall().onErrorResumeNext(new Func1<Throwable, Observable<? extends Object>>() {
        public Observable<? extends Object> call(Throwable throwable) {
            apiCall = null;
            return getCachedApiCall();
        }
    });
}

Обратите внимание, что он будет повторять только один раз для каждого вызова.

Итак, теперь ваш код будет выглядеть примерно так:

---------------------------------------------
// the first time we need it - this will be without a retry if you want..
getCachedApiCall().andSomeOtherStuff()
               .subscribe(subscriberA);

---------------------------------------------
//in the future when we need it again - for any other call so we will have a retry
getRetryableCachedApiCall().andSomeDifferentStuff()
               .subscribe(subscriberB);

Ответ 3

Вам нужно выполнить некоторую обработку состояния. Вот как я сделал бы это:

public class CachedRetry {

    public static final class OnErrorRetryCache<T> {
        final AtomicReference<Observable<T>> cached = 
                new AtomicReference<>();

        final Observable<T> result;

        public OnErrorRetryCache(Observable<T> source) {
            result = Observable.defer(() -> {
                for (;;) {
                    Observable<T> conn = cached.get();
                    if (conn != null) {
                        return conn;
                    }
                    Observable<T> next = source
                            .doOnError(e -> cached.set(null))
                            .replay()
                            .autoConnect();

                    if (cached.compareAndSet(null, next)) {
                        return next;
                    }
                }
            });
        }

        public Observable<T> get() {
            return result;
        }
    }

    public static void main(String[] args) {
        AtomicInteger calls = new AtomicInteger();
        Observable<Integer> source = Observable
                .just(1)
                .doOnSubscribe(() -> 
                    System.out.println("Subscriptions: " + (1 + calls.get())))
                .flatMap(v -> {
                    if (calls.getAndIncrement() == 0) {
                        return Observable.error(new RuntimeException());
                    }
                    return Observable.just(42);
                });

        Observable<Integer> o = new OnErrorRetryCache<>(source).get();

        o.subscribe(System.out::println, 
                Throwable::printStackTrace, 
                () -> System.out.println("Done"));

        o.subscribe(System.out::println, 
                Throwable::printStackTrace, 
                () -> System.out.println("Done"));

        o.subscribe(System.out::println, 
                Throwable::printStackTrace, 
                () -> System.out.println("Done"));
    }
}

Он работает путем кэширования полностью успешного источника и возвращает его всем. В противном случае (частично) сбойный источник создаст кеш, и следующий наблюдатель вызова вызовет повторную подписку.

Ответ 4

Решение Платона - на высоте! В случае, если кому-то нужна версия Kotlin с функцией расширения и параметризованным размером кэша, вот она.

class OnErrorRetryCache<T> constructor(source: Flowable<T>, private val retries: Int? = null) {

val deferred: Flowable<T>
private val singlePermit = Semaphore(1)

private var cache: Flowable<T>? = null
private var inProgress: Flowable<T>? = null

init {
    deferred = Flowable.defer { createWhenObserverSubscribes(source) }
}

private fun createWhenObserverSubscribes(source: Flowable<T>): Flowable<T> {
    singlePermit.acquireUninterruptibly()

    val cached = cache
    if (cached != null) {
        singlePermit.release()
        return cached
    }

    inProgress = source
            .doOnComplete(::onSuccess)
            .doOnTerminate(::onTermination)
            .let {
                when (retries) {
                    null -> it.replay()
                    else -> it.replay(retries)
                }
            }
            .autoConnect()

    return inProgress!!
}

private fun onSuccess() {
    cache = inProgress
}

private fun onTermination() {
    inProgress = null
    singlePermit.release()
}

}

fun <T> Flowable<T>.onErrorRetryCache(retries: Int? = null) = OnErrorRetryCache(this, retries).deferred

И быстрый тест, чтобы доказать, как это работает:

@Test
fun 'when source fails for the first time, new observables just resubscribe'() {

    val cacheSize = 2
    val error = Exception()
    var shouldFail = true //only fail on the first subscription

    val observable = Flowable.defer {
        when (shouldFail) {
            true -> Flowable.just(1, 2, 3, 4)
                    .doOnNext { shouldFail = false }
                    .concatWith(Flowable.error(error))
            false -> Flowable.just(5, 6, 7, 8)
        }
    }.onErrorRetryCache(cacheSize)

    val test1 = observable.test()
    val test2 = observable.test()
    val test3 = observable.test()

    test1.assertValues(1, 2, 3, 4).assertError(error) //fails the first time
    test2.assertValues(5, 6, 7, 8).assertNoErrors() //then resubscribes and gets whole stream from source
    test3.assertValues(7, 8).assertNoErrors() //another subscriber joins in and gets the 2 last cached values

}

Ответ 5

Рассматривали ли вы использование AsyncSubject для реализации кэша для сетевого запроса? Я сделал пример приложения RxApp, чтобы проверить, как он может работать. Я использую модель singleton для получения ответа от сети. Это позволяет кэшировать ответы, получать доступ к данным из нескольких фрагментов, подписываться на ожидающий запрос, а также предоставлять макетные данные для автоматических тестов пользовательского интерфейса.