/* * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package io.reactivex.netty.util; import rx.Subscriber; import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; import rx.internal.util.BackpressureDrainManager; import rx.internal.util.BackpressureDrainManager.BackpressureQueueCallback; import rx.subjects.Subject; import rx.subscriptions.Subscriptions; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; /** * An {@code Observable} that only supports a single active subscriber and buffers messages on back pressure and when * there is no active subscription. * * <h2>Add new messages</h2> * * New messages can be added to this subject via * <ul> <li>{@link #onNext(Object)}: This throws an error if there is a buffer overflow.</li> <li>{@link #offerNext(Object)}: This returns {@code false} if there is a buffer overflow.</li> </ul> * * <h2>Backpressure</h2> * * This subject supports backpressure from the only concurrent subscriber it can have at any time. The buffer limits * that are specified while creating the subject is the maximum buffer that is allowed during backpressure. * * @param <T> The type of objects accepted by this subject. */ @Experimental public class UnicastBufferingSubject<T> extends Subject<T, T> { private final State<T> state; protected UnicastBufferingSubject(OnSubscribe<T> onSubscribe, State<T> state) { super(onSubscribe); this.state = state; } public static <T> UnicastBufferingSubject<T> create(long bufferSize) { final State<T> state = new State<>(bufferSize); return new UnicastBufferingSubject<>(new OnSubscribe<T>() { @Override public void call(Subscriber<? super T> subscriber) { state.registerSubscriber(subscriber); } }, state); } public boolean isTerminated() { synchronized (state) { if (null != state.producer) { return state.producer.isTerminated(); } else { return state.terminatedBeforeSubscribe; } } } @Override public boolean hasObservers() { return null != state.subscriber; } @Override public void onCompleted() { BackpressureDrainManager p = null; /* If a subscriber is active, send the completion to the subscriber, else store it to be delivered to the * buffered producer post subscription.*/ synchronized (state) { if (null != state.producer) { p = state.producer; } else { state.terminatedBeforeSubscribe = true; state.errorBeforeSubscribe = null; } } /*Send callbacks outside the sync block*/ if (null != p) { p.terminateAndDrain(); } } @Override public void onError(Throwable e) { BackpressureDrainManager p = null; /* If a subscriber is active, send the completion to the subscriber, else store it to be delivered to the * buffered producer post subscription.*/ synchronized (state) { if (null != state.producer) { p = state.producer; } else { state.terminatedBeforeSubscribe = true; state.errorBeforeSubscribe = e; } } /*Send callbacks outside the sync block*/ if (null != p) { p.terminateAndDrain(e); } } @Override public void onNext(T t) { try { addNext(t); } catch (MissingBackpressureException e) { throw Exceptions.propagate(e); } } private void addNext(T next) throws MissingBackpressureException { if (isTerminated()) { throw new IllegalStateException("Observable is already completed."); } /*Check for overflow*/ while (true) { final long currentSize = state.currentSize.get(); final long newSize = currentSize + 1; if (newSize > state.maxBufferedCount) { throw new MissingBackpressureException("Max buffer limit exceeded. Current size: " + currentSize); } if (state.currentSize.compareAndSet(currentSize, newSize)) { break; } } state.nexts.add(next); BackpressureDrainManager p = null; /*Drain the producer, if a subscriber is active.*/ synchronized (state) { if (null != state.producer) { p = state.producer; } } if (null != p) { p.drain(); } } /** * Offers the passed item to this subject. Same as {@link #onNext(Object)} just that this method does not throw an * exception in case of buffer overflow, instead returns a {@code false}. * * @param next Next item to offer. * * @return {@code true} if the item was accepted, {@code false} if the subject is already terminated or the buffer * is full. */ public boolean offerNext(T next) { try { addNext(next); return true; } catch (MissingBackpressureException e) { return false; } } private static final class State<T> { private final ConcurrentLinkedQueue<T> nexts; private final BackpressureQueueCallbackImpl queueCallback; private final AtomicLong currentSize = new AtomicLong(); private final long maxBufferedCount; private volatile Subscriber<? super T> subscriber; private volatile BackpressureDrainManager producer; private volatile Throwable errorBeforeSubscribe; private volatile boolean terminatedBeforeSubscribe; private State(long maxBufferedCount) { this.maxBufferedCount = maxBufferedCount; nexts = new ConcurrentLinkedQueue<>(); queueCallback = new BackpressureQueueCallbackImpl(); } public void registerSubscriber(final Subscriber<? super T> subscriber) { boolean _shdSubscribe = false; boolean _terminated = false; Throwable _terminalError = null; BackpressureDrainManager p = null; synchronized (this) { if (null == this.subscriber) { this.subscriber = subscriber; _shdSubscribe = true; _terminated = terminatedBeforeSubscribe; _terminalError = errorBeforeSubscribe; p = new BackpressureDrainManager(queueCallback); producer = p; } } if (_shdSubscribe) { subscriber.add(Subscriptions.create(new Action0() { @Override public void call() { synchronized (State.this) { State.this.subscriber = null; State.this.producer = null; /** * Why not clear the terminate-before-subscribe state? * It can be so that there are multiple subscribers and the first subscriber did not * completely consume the events and hence on unsubscribe clears the terminal state. * The new subscriber will never get the terminal state in this case. */ } } })); subscriber.setProducer(p); if (_terminated) { p.terminateAndDrain(_terminalError); } } else { subscriber.onError(new IllegalStateException("Only one subscriber is allowed.")); } } /** * Shared {@link BackpressureQueueCallback} for all producers (subscribers) as there is no state in this * callback. */ private class BackpressureQueueCallbackImpl implements BackpressureQueueCallback { @Override public Object peek() { return nexts.peek(); } @Override public Object poll() { T poll = nexts.poll(); if (null != poll) { currentSize.decrementAndGet(); } return poll; } @Override public boolean accept(Object next) { @SuppressWarnings("unchecked") T t = (T) next; subscriber.onNext(t); return false; } @Override public void complete(Throwable exception) { if (null == exception) { subscriber.onCompleted(); } else { subscriber.onError(exception); } } } } }