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

rx.internal.operators.OperatorPivot 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.internal.operators;

import java.util.Collections;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;

import rx.Observable.OnSubscribe;
import rx.Observable.Operator;
import rx.Observer;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.observables.GroupedObservable;
import rx.observers.SerializedObserver;
import rx.subscriptions.Subscriptions;

public final class OperatorPivot implements Operator>, GroupedObservable>> {

    @Override
    public Subscriber>> call(final Subscriber>> child) {
        final AtomicReference state = new AtomicReference(State.create());
        final PivotSubscriber pivotSubscriber = new PivotSubscriber(child, state);
        child.add(Subscriptions.create(new Action0() {

            @Override
            public void call() {
                State current;
                State newState = null;
                do {
                    current = state.get();
                    newState = current.unsubscribe();
                } while (!state.compareAndSet(current, newState));

                // If all outer/inner groups are completed/unsubscribed then we can shut it all down
                if (newState.shouldComplete()) {
                    pivotSubscriber.groups.completeAll(newState);
                }
                // otherwise it is just marked as unsubscribed and groups being completed/unsubscribed will allow it to cleanup
            }

        }));

        return pivotSubscriber;
    }

    private static final class PivotSubscriber extends Subscriber>> {
        private final Subscriber>> child;
        private final AtomicReference state;
        private final GroupState groups;

        private PivotSubscriber(Subscriber>> child, AtomicReference state) {
            this.child = child;
            this.state = state;
            this.groups = new GroupState(this, child);
        }

        @Override
        public void onCompleted() {
            State current;
            State newState = null;
            do {
                current = state.get();
                newState = current.complete();
            } while (!state.compareAndSet(current, newState));

            // special case for empty (no groups emitted) or all groups already done
            if (newState.shouldComplete()) {
                groups.completeAll(newState);
            }
        }

        @Override
        public void onError(Throwable e) {
            // we immediately tear everything down if we receive an error
            child.onError(e);
        }

        @Override
        public void onNext(final GroupedObservable> k1Group) {
            groups.startK1Group(state, k1Group.getKey());
            k1Group.unsafeSubscribe(new Subscriber>(this) {

                @Override
                public void onCompleted() {
                    groups.completeK1Group(state, k1Group.getKey());
                }

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

                @Override
                public void onNext(final GroupedObservable k2Group) {
                    /*
                     * In this scope once pivoted, k2 == outer and k2.k1 == inner
                     */
                    final Inner inner = groups.getOrCreateFor(state, child, k1Group.getKey(), k2Group.getKey());
                    if (inner == null) {
                        // we have been unsubscribed
                        return;
                    }
                    k2Group.unsafeSubscribe(new Subscriber(this) {

                        @Override
                        public void onCompleted() {
                            /*
                             * we don't actually propagate onCompleted to the 'inner.subscriber' here
                             * since multiple upstream groups will be sent to a single downstream
                             * and a single upstream group completing does not indicate total completion
                             */
                        }

                        @Override
                        public void onError(Throwable e) {
                            inner.subscriber.onError(e);
                        }

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

                    });

                }

            });

        }

    }

    private static final class GroupState {
        private final ConcurrentHashMap, Inner> innerSubjects = new ConcurrentHashMap, Inner>();
        private final ConcurrentHashMap> outerSubjects = new ConcurrentHashMap>();
        private final Subscription parentSubscription;
        private final Subscriber>> child;
        /** Indicates a terminal state. */
        volatile int completed;
        /** Field updater for completed. */
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater COMPLETED_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupState.class, "completed");

        public GroupState(Subscription parentSubscription, Subscriber>> child) {
            this.parentSubscription = parentSubscription;
            this.child = child;
        }

        public void startK1Group(AtomicReference state, K1 key) {
            State current;
            State newState;
            do {
                current = state.get();
                newState = current.addK1(key);
            } while (!state.compareAndSet(current, newState));
        }

        public void completeK1Group(AtomicReference state, K1 key) {
            State current;
            State newState = null;
            do {
                current = state.get();
                newState = current.removeK1(key);
            } while (!state.compareAndSet(current, newState));

            if (newState.shouldComplete()) {
                completeAll(newState);
            }
        }

        public void startK1K2Group(AtomicReference state, KeyPair keyPair) {
            State current;
            State newState;
            do {
                current = state.get();
                newState = current.addK1k2(keyPair);
            } while (!state.compareAndSet(current, newState));
        }

        public void completeK1K2Group(AtomicReference state, KeyPair keyPair) {
            State current;
            State newState = null;
            do {
                current = state.get();
                newState = current.removeK1k2(keyPair);
            } while (!state.compareAndSet(current, newState));

            if (newState.shouldComplete()) {
                completeAll(newState);
            }
        }

        public void completeAll(State state) {
            if (COMPLETED_UPDATER.compareAndSet(this, 0, 1)) {
                /*
                 * after we are completely done emitting we can now shut down the groups
                 */
                for (Entry> outer : outerSubjects.entrySet()) {
                    outer.getValue().subscriber.onCompleted();
                }
                for (Entry, Inner> inner : innerSubjects.entrySet()) {
                    inner.getValue().subscriber.onCompleted();
                }
                // unsubscribe eagerly
                if (state.unsubscribed) {
                    parentSubscription.unsubscribe(); // unsubscribe from parent
                }
                child.onCompleted();
            }
        }

        private Inner getOrCreateFor(final AtomicReference state, final Subscriber>> child, K1 key1, K2 key2) {
            Outer outer = getOrCreateOuter(state, child, key2);
            if (outer == null) {
                // we have been unsubscribed
                return null;
            }

            Inner orCreateInnerSubject = getOrCreateInnerSubject(state, outer, key1, key2);
            return orCreateInnerSubject;
        }

        private Inner getOrCreateInnerSubject(final AtomicReference state, final Outer outer, final K1 key1, final K2 key2) {
            KeyPair keyPair = new KeyPair(key1, key2);
            Inner innerSubject = innerSubjects.get(keyPair);
            if (innerSubject != null) {
                return innerSubject;
            } else {
                Inner newInner = Inner.create(this, state, outer, keyPair);
                Inner existing = innerSubjects.putIfAbsent(keyPair, newInner);
                if (existing != null) {
                    // we lost the race to create so return the one that did
                    return existing;
                } else {
                    startK1K2Group(state, keyPair);
                    outer.subscriber.onNext(newInner.group);
                    return newInner;
                }
            }
        }

        private Outer getOrCreateOuter(final AtomicReference state, final Subscriber>> child, final K2 key2) {
            Outer outerSubject = outerSubjects.get(key2);
            if (outerSubject != null) {
                return outerSubject;
            } else {
                // this group doesn't exist
                if (child.isUnsubscribed()) {
                    // we have been unsubscribed on the outer so won't send any  more groups 
                    return null;
                }

                Outer newOuter = Outer. create(key2);
                Outer existing = outerSubjects.putIfAbsent(key2, newOuter);
                if (existing != null) {
                    // we lost the race to create so return the one that did
                    return existing;
                } else {
                    child.onNext(newOuter.group);
                    return newOuter;
                }
            }
        }
    }

    private static final class Inner {

        private final Observer subscriber;
        private final GroupedObservable group;

        private Inner(BufferUntilSubscriber subscriber, GroupedObservable group) {
            // since multiple threads are being pivoted we need to make sure this is serialized
            this.subscriber = new SerializedObserver(subscriber);
            this.group = group;
        }

        public static  Inner create(final GroupState groupState, final AtomicReference state, final Outer outer, final KeyPair keyPair) {
            final BufferUntilSubscriber subject = BufferUntilSubscriber.create();
            GroupedObservable group = new GroupedObservable(keyPair.k1, new OnSubscribe() {

                @Override
                public void call(final Subscriber o) {
                    o.add(Subscriptions.create(new Action0() {

                        @Override
                        public void call() {
                            groupState.completeK1K2Group(state, keyPair);
                        }

                    }));
                    subject.unsafeSubscribe(new Subscriber(o) {

                        @Override
                        public void onCompleted() {
                            groupState.completeK1K2Group(state, keyPair);
                            o.onCompleted();
                        }

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

                        @Override
                        public void onNext(T t) {
                            if (!isUnsubscribed()) {
                                o.onNext(t);
                            }
                        }

                    });
                }

            });
            return new Inner(subject, group);
        }
    }

    private static final class Outer {

        private final Observer> subscriber;
        private final GroupedObservable> group;

        private Outer(BufferUntilSubscriber> subscriber, GroupedObservable> group) {
            // since multiple threads are being pivoted we need to make sure this is serialized
            this.subscriber = new SerializedObserver>(subscriber);
            this.group = group;
        }

        public static  Outer create(final K2 key2) {
            final BufferUntilSubscriber> subject = BufferUntilSubscriber.create();
            GroupedObservable> group = new GroupedObservable>(key2, new OnSubscribe>() {

                @Override
                public void call(final Subscriber> o) {
                    subject.unsafeSubscribe(new Subscriber>(o) {

                        @Override
                        public void onCompleted() {
                            o.onCompleted();
                        }

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

                        @Override
                        public void onNext(GroupedObservable t) {
                            if (!isUnsubscribed()) {
                                o.onNext(t);
                            }
                        }

                    });
                }

            });

            return new Outer(subject, group);
        }
    }

    private static final class State {
        private final boolean unsubscribed;
        private final boolean completed;
        private final Set k1Keys;
        private final Set> k1k2Keys;

        private State(boolean completed, boolean unsubscribed, Set k1Keys, Set> k1k2Keys) {
            this.completed = completed;
            this.unsubscribed = unsubscribed;
            this.k1Keys = k1Keys;
            this.k1k2Keys = k1k2Keys;
        }

        public static State create() {
            return new State(false, false, Collections.emptySet(), Collections.> emptySet());
        }

        public State addK1(Object key) {
            Set newKeys = new HashSet(k1Keys);
            newKeys.add(key);
            return new State(completed, unsubscribed, newKeys, k1k2Keys);
        }

        public State removeK1(Object key) {
            Set newKeys = new HashSet(k1Keys);
            newKeys.remove(key);
            return new State(completed, unsubscribed, newKeys, k1k2Keys);
        }

        public State addK1k2(KeyPair key) {
            Set> newKeys = new HashSet>(k1k2Keys);
            newKeys.add(key);
            return new State(completed, unsubscribed, k1Keys, newKeys);
        }

        public State removeK1k2(KeyPair key) {
            Set> newKeys = new HashSet>(k1k2Keys);
            newKeys.remove(key);
            return new State(completed, unsubscribed, k1Keys, newKeys);
        }

        public State complete() {
            return new State(true, unsubscribed, k1Keys, k1k2Keys);
        }

        public State unsubscribe() {
            return new State(completed, true, k1Keys, k1k2Keys);
        }

        public boolean shouldComplete() {
            if (k1Keys.isEmpty() && completed) {
                return true;
            } else if (unsubscribed) {
                // if unsubscribed and all groups are completed/unsubscribed we can complete
                return k1k2Keys.isEmpty();
            } else {
                return false;
            }
        }

        @Override
        public String toString() {
            return "State =>  k1: " + k1Keys.size() + " k1k2: " + k1k2Keys.size() + " completed: " + completed + " unsubscribed: " + unsubscribed;
        }
    }

    private static final class KeyPair {
        private final K1 k1;
        private final K2 k2;

        KeyPair(K1 k1, K2 k2) {
            this.k1 = k1;
            this.k2 = k2;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((k1 == null) ? 0 : k1.hashCode());
            result = prime * result + ((k2 == null) ? 0 : k2.hashCode());
            return result;
        }

        @SuppressWarnings("rawtypes")
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            KeyPair other = (KeyPair) obj;
            if (k1 == null) {
                if (other.k1 != null)
                    return false;
            } else if (!k1.equals(other.k1))
                return false;
            if (k2 == null) {
                if (other.k2 != null)
                    return false;
            } else if (!k2.equals(other.k2))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return k2 + "." + k1;
        }

    }

}