1

I have a piece of code who's work is to update a local cache. There are two triggers to this cache update:

  1. At a fixed interval
  2. When requested

So here's a basic example on how I did this.

forceReloadEvents = new SerializedSubject<Long, Long>(PublishSubject.<Long> create());
dataUpdates = Observable
    .merge(forceReloadEvents, Observable.timer(0, pullInterval, TimeUnit.SECONDS))
    .flatMap(new Func1<Long, Observable<Boolean>>() {
        @Override
        public Observable<Boolean> call(Long t) {
            return reloadData(); // operation that may take long
        }
    })
    .publish();

dataUpdates.subscribe();
dataUpdates.connect();

Then later i have

public void forceReload() {
    final CountDownLatch cdl = new CountDownLatch(1);

    dataUpdates
        .take(1)
        .subscribe(
            new Action1<Boolean>() {
                @Override
                public void call(Boolean b) {
                    cdl.countDown();
                }
            }
        );

    forceReloadEvents.onNext(-1L);

    try {
        cdl.await();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

This works but the problem is when I start to have multiple concurrent calls to forceReload() : There will be no concurrent execution of reloadData() but the elements will queue up and the process will loop on reloading data until all the events sent to forceReloadEvents have been consumed even though forceReload() already completed due to previous events releasing the CountDownLatch.

I wanted to use onBackPressureDrop but it seems there's no induced backpressure and nothing is dropped. What I'd like is some way to force backpressure so that the merge understands that only one element can be processed at a time and that any subsequent event must be dropped until the current execution is done.

I thought about using buffer or throttleFirst too but I don't want to force a specific time between each event and i'd rather have this auto-scaling depending on the time it takes to reload the cache. You can think of it as throttleFirst until reloadData has completed.

Crystark
  • 3,693
  • 5
  • 40
  • 61

2 Answers2

2

Edit: based on the comments, you can have an AtomicBoolean as a gate in the flatMap to not start a reload until the gate is open again:

public class AvoidReloadStorm {
    static Observable<Boolean> reload() {
        return Observable.just(true)
        .doOnNext(v -> System.out.println("Reload started..."))
        .delay(10, TimeUnit.SECONDS)
        .doOnNext(v -> System.out.println("Reloaded"));
    }
    public static void main(String[] args) throws Exception {
        Subject<Long, Long> manual = PublishSubject.<Long>create().toSerialized();
        Observable<Long> timer = Observable.timer(0, 5, TimeUnit.SECONDS)
                .doOnNext(v -> System.out.println("Timer reload"));

        AtomicBoolean running = new AtomicBoolean();

        ConnectableObservable<Boolean> src = Observable
        .merge(manual.onBackpressureDrop(), timer.onBackpressureDrop())
        .observeOn(Schedulers.io())
        .flatMap(v -> {
            if (running.compareAndSet(false, true)) {
                return reload().doOnCompleted(() -> {
                    running.set(false);
                });
            }
            System.out.println("Reload rejected");
            return Observable.empty();
        }).publish();

        src.subscribe(System.out::println);

        src.connect();

        Thread.sleep(100000);
    }
}
akarnokd
  • 69,132
  • 14
  • 157
  • 192
  • I don't necessarily want to match the `onNext` with the emission of the successful reload. I just want to make sure i get a successful reload *after* i called onNext even if it's one that was currently running when i made the call to onNext. `throttleFirst` bothers me as if the reloading takes for instance only 500ms, then all calls to onNext between the 500th and 1000th millis will have no effect and `forceReload` will wait for the next event before the CDL is released. – Crystark Jun 15 '15 at 15:57
  • I put in throttleFirst to avoid reload storms, but you don't have to use it. – akarnokd Jun 15 '15 at 16:07
  • Well, that's exactly what i want to avoid: reload storms. But without defining a specific timer during which to avoid the reloads as `throttleFirst` does. I would like the `flatMap` to "generate backpressure" whenever it gets an event during it's execution of `reloadData`. That can be interpreted also as "throttle until the current `reloadData` ends". – Crystark Jun 15 '15 at 16:18
  • Adding the AtomicBoolean isn't sufficient. It seems all is queued up when I do `observeOn` so nothing is ever rejected. You've showed me the right path though and I got a solution working using `.filter()` before `observeOn`. See my response that I'l be posting in a sec. – Crystark Jun 16 '15 at 08:39
  • If I run my example, it prints rejected as expected. – akarnokd Jun 16 '15 at 09:33
  • Strange, maybe is it because you don't use manual events but only the timer ? I wasn't getting rejections sending 30 concurrent manual events. – Crystark Jun 16 '15 at 09:37
2

I made this work thanks to akarnokd!

Here's the solution I created based on his answer:

Observable<Long> forceReloadEvents = this.forceReloadEvents
    .asObservable()
    .onBackpressureDrop();

Observable<Long> periodicReload = Observable
    .timer(0, pullInterval, TimeUnit.SECONDS)
    .onBackpressureDrop();

final AtomicBoolean running = new AtomicBoolean();

dataUpdates = Observable
    .merge(forceReloadEvents, periodicReload)
    .filter(new Func1<Long, Boolean>() {
        @Override
        public Boolean call(Long t) {
            return running.compareAndSet(false, true);
        }
    })
    .observeOn(Schedulers.io())
    .flatMap(new Func1<Long, Observable<Boolean>>() {
        @Override
        public Observable<Boolean> call(Long t) {
            return reloadData();
        }
    })
    .doOnNext(new Action1<Boolean>() {
        @Override
        public void call(Boolean t) {
            running.set(false);
        }
    })
    .publish();

dataUpdates.subscribe();
dataUpdates.connect();

I'm not sure onBackpressureDrop is usefull here but I set it as a precaution.

The forceReload code does not change.

Crystark
  • 3,693
  • 5
  • 40
  • 61