3

TL;DR: How do you create an Observable that only creates a Subscription if it is cold and queues any other subscribe calls when it is hot?

I would like to create an Observable which can only execute a single Subscription at a time. If any other Subscribers subscribe to the Observable, I would like them to be queued to run when the Observable is completed (after onComplete).

I can build this construct myself by having some kind of stack and popping the stack on every onComplete - but it feels like this functionality already exists in RxJava.

Is there a way to limit subscriptions in this way?

(More on hot and cold observables)

bkach
  • 1,431
  • 1
  • 15
  • 24
  • That is not what 'hot observable' means. A hot observable is a observable that shares side effects between subscriptions. For example when it publishes data live. What you really want is a source that hands out observables that only yield once all previous observables are done. Q: Does the subscribe call need to be blocking, or is it good enough when the second Observable doesn't yield `onNext` until the previous one has called `onCompleted`? – Dorus Feb 09 '16 at 15:18
  • Also I'm not aware of a build in operator that does this for you, but I'm sure it should be possible to make this using existing operators. – Dorus Feb 09 '16 at 15:20
  • Another unclear part: Do you run an inner cold observable that each subscription subscribes to in turn, or is there a long running hot observable where the next subscription is only eligible for messages when the last one unsubscribes? – Dorus Feb 09 '16 at 17:57

3 Answers3

2

There are no built-in operators or combination of operators I can think of that can achieve this. Here is how I'd implement it:

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.*;

import rx.*;
import rx.observers.TestSubscriber;
import rx.subjects.PublishSubject;
import rx.subscriptions.Subscriptions;

public final class SequenceSubscribers<T> implements Observable.OnSubscribe<T> {

    final Observable<? extends T> source;

    final Queue<Subscriber<? super T>> queue;

    final AtomicInteger wip;

    volatile boolean active;

    public SequenceSubscribers(Observable<? extends T> source) {
        this.source = source;
        this.queue = new ConcurrentLinkedQueue<>();
        this.wip = new AtomicInteger();
    }

    @Override
    public void call(Subscriber<? super T> t) {
        SubscriberWrapper wrapper = new SubscriberWrapper(t);
        queue.add(wrapper);

        t.add(wrapper);
        t.add(Subscriptions.create(() -> wrapper.next()));

        drain();
    }

    void complete(SubscriberWrapper inner) {
        active = false;
        drain();
    }

    void drain() {
        if (wip.getAndIncrement() != 0) {
            return;
        }
        do {
            if (!active) {
                Subscriber<? super T> s = queue.poll();
                if (s != null && !s.isUnsubscribed()) {
                    active = true;
                    source.subscribe(s);
                }
            }
        } while (wip.decrementAndGet() != 0);
    }

    final class SubscriberWrapper extends Subscriber<T> {
        final Subscriber<? super T> actual;

        final AtomicBoolean once;

        public SubscriberWrapper(Subscriber<? super T> actual) {
            this.actual = actual;
            this.once = new AtomicBoolean();
        }

        @Override
        public void onNext(T t) {
            actual.onNext(t);
        }

        @Override
        public void onError(Throwable e) {
            actual.onError(e);
            next();
        }

        @Override
        public void onCompleted() {
            actual.onCompleted();
            next();
        }

        @Override
        public void setProducer(Producer p) {
            actual.setProducer(p);
        }

        void next() {
            if (once.compareAndSet(false, true)) {
                complete(this);
            }
        }
    }

    public static void main(String[] args) {
        PublishSubject<Integer> ps = PublishSubject.create();

        TestSubscriber<Integer> ts1 = TestSubscriber.create();
        TestSubscriber<Integer> ts2 = TestSubscriber.create();

        Observable<Integer> source = Observable.create(new SequenceSubscribers<>(ps));

        source.subscribe(ts1);
        source.subscribe(ts2);

        ps.onNext(1);
        ps.onNext(2);

        ts1.assertValues(1, 2);
        ts2.assertNoValues();

        ts1.unsubscribe();

        ps.onNext(3);
        ps.onNext(4);
        ps.onCompleted();

        ts1.assertValues(1, 2);
        ts2.assertValues(3, 4);
        ts2.assertCompleted();
    }
}
akarnokd
  • 69,132
  • 14
  • 157
  • 192
0

I assume you have an underlying cold observable that you want to subscribe at multiple times, but only once the previous subscription finished.

We can make smart use of the build in function delaySubscription that can delay new subscriptions. The next hurdle is to trigger the subscription when the previous subscription finishes. We do this with doOnUnsubscribe, that triggers both on unsubscribe, onError and onCompleted actions.

public class DelaySubscribe<T> {
    Observable<Integer> previouse = Observable.just(0);

    private DelaySubscribe() {

    }

    public static <T> Observable<T> makeDelayOb(Observable<T> cold) {
        return new DelaySubscribe<T>().obs(cold);
    }

    private Observable<T> obs(Observable<T> cold) {
        return Observable.create(ob -> {
            Observable<Integer> tmp = previouse;
            ReplaySubject<Integer> rep = ReplaySubject.create();
            previouse = rep;
            cold.delaySubscription(() -> tmp).doOnUnsubscribe(() -> {
                rep.onNext(0);
                rep.onCompleted();
            }).subscribe(ob);
        });
    }

Example usage:

    public static void main(String[] args) throws IOException, InterruptedException {
        Observable<Long> cold = Observable.interval(1, TimeUnit.SECONDS).take(2);
        Observable<Long> hot = makeDelayOb(cold);

        Func1<Integer, rx.Observer<Long>> obs = (Integer i) -> Observers.create(el -> System.out.println(i + "next: " + el),
                er -> System.out.println(i + "error: " + er), () -> System.out.println(i + "completed"));

        System.out.println("1");
        Subscription s = hot.subscribe(obs.call(1));
        System.out.println("2");
        hot.subscribe(obs.call(2));
        Thread.sleep(1500);
        s.unsubscribe();
        System.out.println("3");
        Thread.sleep(3500);
        hot.subscribe(obs.call(3));
        System.out.println("4");
        System.in.read();
    }
}

Output:

1
2
1next: 0
3
2next: 0
2next: 1
2completed
4
3next: 0
3next: 1
3completed
Dorus
  • 7,276
  • 1
  • 30
  • 36
0

I assume you have a hot observable with multiple subscriptions, but only want one subscription to receive events. Once the current subscription unsubscribes, the next one should start receiving.

What we can do is give every subscription and unique number, and keep a list of all subscriptions. Only the first subscription in the list receives events, the rest of them filters the events away.

public class SingleSubscribe {
    List<Integer> current = Collections.synchronizedList(new ArrayList<>());
    int max = 0;
    Object gate = new Object();

    private SingleSubscribe() {
    }

    public static <T> Transformer<T, T> singleSubscribe() {
        return new SingleSubscribe().obs();
    }

    private <T> Transformer<T, T> obs() {
        return (source) -> Observable.create((Subscriber<? super T> ob) -> {
            Integer me;
            synchronized (gate) {
                 me = max++;
            }
            current.add(me);
            source.doOnUnsubscribe(() -> current.remove(me)).filter(__ -> {
                return current.get(0) == me;
            }).subscribe(ob);
        });
    }

Example usage:

    public static void main(String[] args) throws InterruptedException, IOException {
        ConnectableObservable<Long> connectable = Observable.interval(500, TimeUnit.MILLISECONDS)
                .publish();
        Observable<Long> hot = connectable.compose(SingleSubscribe.<Long> singleSubscribe());
        Subscription sub = connectable.connect();

        Func1<Integer, rx.Observer<Long>> obs = (Integer i) -> Observers.create(el -> System.out.println(i + "next: " + el),
                er -> {
                    System.out.println(i + "error: " + er);
                    er.printStackTrace();
                } , () -> System.out.println(i + "completed"));

        System.out.println("1");
        Subscription s = hot.subscribe(obs.call(1));
        System.out.println("2");
        hot.take(4).subscribe(obs.call(2));
        Thread.sleep(1500);
        s.unsubscribe();
        System.out.println("3");
        Thread.sleep(500);
        hot.take(2).subscribe(obs.call(3));
        System.out.println("4");
        System.in.read();
        sub.unsubscribe();
    }
}

Output:

1
2
1next: 0
1next: 1
1next: 2
3
2next: 3
4
2next: 4
2next: 5
2next: 6
2completed
3next: 6
3next: 7
3completed

Notice there is a small flaw in the output: Because 2 unsubscribes right after it receives 6, but before 3 receives 6. After 2 unsubscribes, 3 is the next active observer and happily accept 6.

A solution would be to delay doOnUnsubscribe, easiest way is to schedule the action on a new thread scheduler:

source.doOnUnsubscribe(() -> Schedulers.newThread().createWorker().schedule(()-> current.remove(me)))

However, this means there is now a small chance the next item emitted is ignored when 2 unsubscribe, and the next item arrives before 3 is activated. Use the variation that is most suitable for you.

Lastly, this solution absolutely assumes the source to be hot. I'm not sure what will happen when you apply this operator to a cold observable, but the results could be unexpected.

Dorus
  • 7,276
  • 1
  • 30
  • 36