All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rx.subjects.ReplaySubject Maven / Gradle / Ivy

There is a newer version: 0.20.7
Show newest version
/**
 * Copyright 2014 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 rx.subjects;

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import rx.Notification;
import rx.Observer;
import rx.functions.Action1;
import rx.subjects.SubjectSubscriptionManager.SubjectObserver;

/**
 * Subject that retains all events and will replay them to an {@link Observer} that subscribes.
 * 

* *

* Example usage: *

*

 {@code

 * ReplaySubject subject = ReplaySubject.create();
  subject.onNext("one");
  subject.onNext("two");
  subject.onNext("three");
  subject.onCompleted();

  // both of the following will get the onNext/onCompleted calls from above
  subject.subscribe(observer1);
  subject.subscribe(observer2);

  } 
 * 
 * @param 
 */
public final class ReplaySubject extends Subject {
    public static  ReplaySubject create() {
        return create(16);
    }

    public static  ReplaySubject create(int initialCapacity) {
        final SubjectSubscriptionManager subscriptionManager = new SubjectSubscriptionManager();
        final ReplayState state = new ReplayState(initialCapacity);

        OnSubscribe onSubscribe = subscriptionManager.getOnSubscribeFunc(
                /**
                 * This function executes at beginning of subscription.
                 * We want to replay history with the subscribing thread
                 * before the Observer gets registered.
                 * 
                 * This will always run, even if Subject is in terminal state.
                 */
                new Action1>() {

                    @Override
                    public void call(SubjectObserver o) {
                        // replay history for this observer using the subscribing thread
                        int lastIndex = replayObserverFromIndex(state.history, 0, o);

                        // now that it is caught up add to observers
                        state.replayState.put(o, lastIndex);
                    }
                },
                /**
                 * This function executes if the Subject is terminated.
                 */
                new Action1>() {

                    @Override
                    public void call(SubjectObserver o) {
                        Integer idx = state.replayState.remove(o);
                        // we will finish replaying if there is anything left
                        replayObserverFromIndex(state.history, idx, o);
                    }
                }, 
                new Action1>() {
                    @Override
                    public void call(SubjectObserver o) {
                        state.replayState.remove(o);
                    }
                });

        return new ReplaySubject(onSubscribe, subscriptionManager, state);
    }

    private static class ReplayState {
        // single-producer, multi-consumer
        final History history;
        // each Observer is tracked here for what events they have received
        final ConcurrentHashMap, Integer> replayState;

        public ReplayState(int initialCapacity) {
            history = new History(initialCapacity);
            replayState = new ConcurrentHashMap, Integer>();
        }
    }

    private final SubjectSubscriptionManager subscriptionManager;
    private final ReplayState state;

    protected ReplaySubject(OnSubscribe onSubscribe, SubjectSubscriptionManager subscriptionManager, ReplayState state) {
        super(onSubscribe);
        this.subscriptionManager = subscriptionManager;
        this.state = state;
    }

    @Override
    public void onCompleted() {
        subscriptionManager.terminate(new Action1>>() {

            @Override
            public void call(Collection> observers) {
                state.history.complete(Notification.createOnCompleted());
                for (SubjectObserver o : observers) {
                    if (caughtUp(o)) {
                        o.onCompleted();
                    }
                }
            }
        });
    }

    @Override
    public void onError(final Throwable e) {
        subscriptionManager.terminate(new Action1>>() {

            @Override
            public void call(Collection> observers) {
                state.history.complete(Notification.createOnError(e));
                for (SubjectObserver o : observers) {
                    if (caughtUp(o)) {
                        o.onError(e);
                    }
                }
            }
        });
    }

    @Override
    public void onNext(T v) {
        if (state.history.terminalValue.get() != null) {
            return;
        }
        state.history.next(v);
        for (SubjectObserver o : subscriptionManager.rawSnapshot()) {
            if (caughtUp(o)) {
                o.onNext(v);
            }
        }
    }

    /*
     * This is not very elegant but resulted in non-trivial performance improvement by
     * eliminating the 'replay' code-path on the normal fast-path of emitting values.
     * 
     * With this method: 16,151,174 ops/sec
     * Without: 8,632,358 ops/sec
     */
    private boolean caughtUp(SubjectObserver o) {
        if (!o.caughtUp) {
            o.caughtUp = true;
            replayObserver(o);
            return false;
        } else {
            // it was caught up so proceed the "raw route"
            return true;
        }
    }

    private void replayObserver(SubjectObserver observer) {
        Integer lastEmittedLink = state.replayState.get(observer);
        if (lastEmittedLink != null) {
            int l = replayObserverFromIndex(state.history, lastEmittedLink, observer);
            state.replayState.put(observer, l);
        } else {
            throw new IllegalStateException("failed to find lastEmittedLink for: " + observer);
        }
    }

    private static  int replayObserverFromIndex(History history, Integer l, SubjectObserver observer) {
        while (l < history.index.get()) {
            observer.onNext(history.list.get(l));
            l++;
        }
        if (history.terminalValue.get() != null) {
            history.terminalValue.get().accept(observer);
        }

        return l;
    }

    /**
     * NOT thread-safe for multi-writer. Assumes single-writer.
     * Is thread-safe for multi-reader.
     * 
     * @param 
     */
    private static class History {
        private final AtomicInteger index;
        private final ArrayList list;
        private final AtomicReference> terminalValue;

        public History(int initialCapacity) {
            index = new AtomicInteger(0);
            list = new ArrayList(initialCapacity);
            terminalValue = new AtomicReference>();
        }

        public boolean next(T n) {
            if (terminalValue.get() == null) {
                list.add(n);
                index.getAndIncrement();
                return true;
            } else {
                return false;
            }
        }

        public void complete(Notification n) {
            terminalValue.set(n);
        }
    }
    /**
     * @return Returns the number of subscribers.
     */
    /* Support test.*/ int subscriberCount() {
        return state.replayState.size();
    }
}