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

rx.internal.operators.OperatorPublish Maven / Gradle / Ivy

There is a newer version: 1.3.8
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.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;

import rx.Observable;
import rx.Producer;
import rx.Subscriber;
import rx.Subscription;
import rx.exceptions.CompositeException;
import rx.exceptions.Exceptions;
import rx.exceptions.MissingBackpressureException;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.internal.util.RxRingBuffer;
import rx.observables.ConnectableObservable;
import rx.subscriptions.Subscriptions;

public class OperatorPublish extends ConnectableObservable {
    final Observable source;
    private final RequestHandler requestHandler;

    public static  ConnectableObservable create(Observable source) {
        return new OperatorPublish(source);
    }

    public static  Observable create(final Observable source, final Func1, ? extends Observable> selector) {
        return Observable.create(new OnSubscribe() {

            @Override
            public void call(final Subscriber child) {
                OperatorPublish op = new OperatorPublish(source);
                selector.call(op).unsafeSubscribe(child);
                op.connect(new Action1() {

                    @Override
                    public void call(Subscription sub) {
                        child.add(sub);
                    }

                });
            }

        });
    }

    private OperatorPublish(Observable source) {
        this(source, new Object(), new RequestHandler());
    }

    private OperatorPublish(Observable source, final Object guard, final RequestHandler requestHandler) {
        super(new OnSubscribe() {
            @Override
            public void call(final Subscriber subscriber) {
                subscriber.setProducer(new Producer() {

                    @Override
                    public void request(long n) {
                        requestHandler.requestFromChildSubscriber(subscriber, n);
                    }

                });
                subscriber.add(Subscriptions.create(new Action0() {

                    @Override
                    public void call() {
                        requestHandler.state.removeSubscriber(subscriber);
                    }

                }));
            }
        });
        this.source = source;
        this.requestHandler = requestHandler;
    }

    @Override
    public void connect(Action1 connection) {
        // each time we connect we create a new Subscription
        boolean shouldSubscribe = false;
        
        // subscription is the state of whether we are connected or not
        OriginSubscriber origin = requestHandler.state.getOrigin();
        if (origin == null) {
            shouldSubscribe = true;
            requestHandler.state.setOrigin(new OriginSubscriber(requestHandler));
        }

        // in the lock above we determined we should subscribe, do it now outside the lock
        if (shouldSubscribe) {
            // register a subscription that will shut this down
            connection.call(Subscriptions.create(new Action0() {
                @Override
                public void call() {
                    OriginSubscriber s = requestHandler.state.getOrigin();
                    requestHandler.state.setOrigin(null);
                    if (s != null) {
                        s.unsubscribe();
                    }
                }
            }));

            // now that everything is hooked up let's subscribe
            // as long as the subscription is not null (which can happen if already unsubscribed)
            OriginSubscriber os = requestHandler.state.getOrigin();
            if (os != null) {
                source.unsafeSubscribe(os);
            }
        }
    }

    private static class OriginSubscriber extends Subscriber {

        private final RequestHandler requestHandler;
        private final AtomicLong originOutstanding = new AtomicLong();
        private final long THRESHOLD = RxRingBuffer.SIZE / 4;
        private final RxRingBuffer buffer = RxRingBuffer.getSpmcInstance();

        OriginSubscriber(RequestHandler requestHandler) {
            this.requestHandler = requestHandler;
            add(buffer);
        }

        @Override
        public void onStart() {
            requestMore(RxRingBuffer.SIZE);
        }

        private void requestMore(long r) {
            originOutstanding.addAndGet(r);
            request(r);
        }

        @Override
        public void onCompleted() {
            try {
                requestHandler.emit(requestHandler.notifier.completed());
            } catch (MissingBackpressureException e) {
                onError(e);
            }
        }

        @Override
        public void onError(Throwable e) {
            List errors = null;
            for (Subscriber subscriber : requestHandler.state.getSubscribers()) {
                try {
                    subscriber.onError(e);
                } catch (Throwable e2) {
                    if (errors == null) {
                        errors = new ArrayList();
                    }
                    errors.add(e2);
                }
            }
            if (errors != null) {
                if (errors.size() == 1) {
                    Exceptions.propagate(errors.get(0));
                } else {
                    throw new CompositeException("Errors while emitting onError", errors);
                }
            }
        }

        @Override
        public void onNext(T t) {
            try {
                requestHandler.emit(requestHandler.notifier.next(t));
            } catch (MissingBackpressureException e) {
                onError(e);
            }
        }

    }

    /**
     * Synchronized mutable state.
     * 
     * benjchristensen => I have not figured out a non-blocking approach to this that doesn't involve massive object allocation overhead
     * with a complicated state machine so I'm sticking with mutex locks and just trying to make sure the work done while holding the
     * lock is small (such as never emitting data).
     * 
     * This does however mean we can't rely on a reference to State being consistent. For example, it can end up with a null OriginSubscriber. 
     * 
     * @param 
     */
    private static class State {
        private long outstandingRequests = -1;
        private long emittedSinceRequest = 0;
        private OriginSubscriber origin;
        // using AtomicLong to simplify mutating it, not for thread-safety since we're synchronizing access to this class
        // using LinkedHashMap so the order of Subscribers having onNext invoked is deterministic (same each time the code is run)
        private final Map, AtomicLong> ss = new LinkedHashMap, AtomicLong>();
        @SuppressWarnings("unchecked")
        private Subscriber[] subscribers = new Subscriber[0];

        public synchronized OriginSubscriber getOrigin() {
            return origin;
        }

        public synchronized void setOrigin(OriginSubscriber o) {
            this.origin = o;
        }

        public synchronized boolean canEmitWithDecrement() {
            if (outstandingRequests > 0) {
                outstandingRequests--;
                emittedSinceRequest++;
                return true;
            }
            return false;
        }

        public synchronized void incrementOutstandingAfterFailedEmit() {
            outstandingRequests++;
            emittedSinceRequest--;
        }

        public synchronized Subscriber[] getSubscribers() {
            return subscribers;
        }

        /**
         * @return long outstandingRequests
         */
        public synchronized long requestFromSubscriber(Subscriber subscriber, Long request) {
            AtomicLong r = ss.get(subscriber);
            if (r == null) {
                ss.put(subscriber, new AtomicLong(request));
            } else {
                if (r.get() != Long.MAX_VALUE) {
                    if (request == Long.MAX_VALUE) {
                        r.set(Long.MAX_VALUE);
                    } else {
                        r.addAndGet(request.longValue());
                    }
                }
            }

            return resetAfterSubscriberUpdate();
        }

        public synchronized void removeSubscriber(Subscriber subscriber) {
            ss.remove(subscriber);
            resetAfterSubscriberUpdate();
        }

        private long resetAfterSubscriberUpdate() {
            subscribers = new Subscriber[ss.size()];
            int i = 0;
            for (Subscriber s : ss.keySet()) {
                subscribers[i++] = s;
            }

            long lowest = -1;
            for (AtomicLong l : ss.values()) {
                // decrement all we have emitted since last request
                long c = l.addAndGet(-emittedSinceRequest);
                if (lowest == -1 || c < lowest) {
                    lowest = c;
                }
            }
            /*
             * when receiving a request from a subscriber we reset 'outstanding' to the lowest of all subscribers
             */
            outstandingRequests = lowest;
            emittedSinceRequest = 0;
            return outstandingRequests;
        }
    }

    private static class RequestHandler {
        private final NotificationLite notifier = NotificationLite.instance();
        
        private final State state = new State();
        @SuppressWarnings("unused")
        volatile long wip;
        @SuppressWarnings("rawtypes")
        static final AtomicLongFieldUpdater WIP = AtomicLongFieldUpdater.newUpdater(RequestHandler.class, "wip");

        public void requestFromChildSubscriber(Subscriber subscriber, Long request) {
            state.requestFromSubscriber(subscriber, request);
            OriginSubscriber originSubscriber = state.getOrigin();
            if(originSubscriber != null) {
                drainQueue(originSubscriber);
            }
        }

        public void emit(Object t) throws MissingBackpressureException {
            OriginSubscriber originSubscriber = state.getOrigin();
            if(originSubscriber == null) {
                // unsubscribed so break ... we are done
                return;
            }
            if (notifier.isCompleted(t)) {
                originSubscriber.buffer.onCompleted();
            } else {
                originSubscriber.buffer.onNext(notifier.getValue(t));
            }
            drainQueue(originSubscriber);
        }

        private void requestMoreAfterEmission(int emitted) {
            OriginSubscriber origin = state.getOrigin();
            if (emitted > 0 && origin != null) {
                long r = origin.originOutstanding.addAndGet(-emitted);
                if (r <= origin.THRESHOLD) {
                    origin.requestMore(RxRingBuffer.SIZE - origin.THRESHOLD);
                }
            }
        }

        public void drainQueue(OriginSubscriber originSubscriber) {
            if (WIP.getAndIncrement(this) == 0) {
                int emitted = 0;
                do {
                    /*
                     * Set to 1 otherwise it could have grown very large while in the last poll loop
                     * and then we can end up looping all those times again here before exiting even once we've drained
                     */
                    WIP.set(this, 1);
                    /**
                     * This is done in the most inefficient possible way right now and we can revisit the approach.
                     * If we want to batch this then we need to account for new subscribers arriving with a lower request count
                     * concurrently while iterating the batch ... or accept that they won't
                     */
                    while (true) {
                        boolean shouldEmit = state.canEmitWithDecrement();
                        if (!shouldEmit) {
                            break;
                        }
                        Object o = originSubscriber.buffer.poll();
                        if (o == null) {
                            // nothing in buffer so increment outstanding back again
                            state.incrementOutstandingAfterFailedEmit();
                            break;
                        }

                        if (notifier.isCompleted(o)) {
                            for (Subscriber s : state.getSubscribers()) {
                                notifier.accept(s, o);
                            }

                        } else {
                            for (Subscriber s : state.getSubscribers()) {
                                notifier.accept(s, o);
                            }
                        }
                        emitted++;
                    }
                } while (WIP.decrementAndGet(this) > 0);
                requestMoreAfterEmission(emitted);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy