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

com.couchbase.client.core.utils.UnicastAutoReleaseSubject Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
/**
 * Copyright (c) 2015 Couchbase, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING
 * IN THE SOFTWARE.
 */
/**
 * 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 com.couchbase.client.core.utils;

import com.couchbase.client.deps.io.netty.util.ReferenceCountUtil;
import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.internal.operators.BufferUntilSubscriber;
import rx.observers.Subscribers;
import rx.schedulers.Schedulers;
import rx.subjects.Subject;
import rx.subscriptions.Subscriptions;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

/**
 * This Subject can be used to auto-release reference counted objects when not subscribed before.
 *
 * The implementation is originally based on a Subject in RxNetty, but adapted to fit the needs of the core. For
 * example it does not always do auto releasing which is something that can clash with proper subscribers and as a
 * result it never increases the reference count of the ref counted object.
 *
 * While this subject can also be used with non reference counted objects it doesn't make much sense since they
 * can properly be garbage collected by the JVM and do not leak.
 *
 * @author Nitesh Kant (Netflix)
 * @author Michael Nitschinger
 * @since 1.1.1
 */
public final class UnicastAutoReleaseSubject extends Subject {

    private final State state;
    private volatile Observable timeoutScheduler;

    private UnicastAutoReleaseSubject(State state) {
        super(new OnSubscribeAction(state));
        this.state = state;
        timeoutScheduler = Observable.empty();
    }

    private UnicastAutoReleaseSubject(final State state, long noSubscriptionTimeout, TimeUnit timeUnit,
        Scheduler scheduler) {
        super(new OnSubscribeAction(state));
        this.state = state;
        timeoutScheduler = Observable.interval(noSubscriptionTimeout, timeUnit, scheduler).take(1);
    }

    /**
     * Creates a new {@link UnicastAutoReleaseSubject} without a no subscription timeout.
     * This can cause a memory leak in case no one ever subscribes to this subject. See
     * {@link UnicastAutoReleaseSubject} for details.
     *
     * @param onUnsubscribe An action to be invoked when the sole subscriber to this {@link Subject} unsubscribes.
     * @param  The type emitted and received by this subject.
     *
     * @return The new instance of {@link UnicastAutoReleaseSubject}
     */
    public static  UnicastAutoReleaseSubject createWithoutNoSubscriptionTimeout(Action0 onUnsubscribe) {
        State state = new State(onUnsubscribe);
        return new UnicastAutoReleaseSubject(state);
    }

    /**
     * Creates a new {@link UnicastAutoReleaseSubject} without a no subscription timeout.
     * This can cause a memory leak in case no one ever subscribes to this subject. See
     * {@link UnicastAutoReleaseSubject} for details.
     *
     * @param  The type emitted and received by this subject.
     *
     * @return The new instance of {@link UnicastAutoReleaseSubject}
     */
    public static  UnicastAutoReleaseSubject createWithoutNoSubscriptionTimeout() {
        return createWithoutNoSubscriptionTimeout(null);
    }

    public static  UnicastAutoReleaseSubject create(long noSubscriptionTimeout, TimeUnit timeUnit) {
        return create(noSubscriptionTimeout, timeUnit, (Action0)null);
    }

    public static  UnicastAutoReleaseSubject create(long noSubscriptionTimeout, TimeUnit timeUnit,
        Action0 onUnsubscribe) {
        return create(noSubscriptionTimeout, timeUnit, Schedulers.computation(), onUnsubscribe);
    }

    public static  UnicastAutoReleaseSubject create(long noSubscriptionTimeout, TimeUnit timeUnit,
        Scheduler timeoutScheduler) {
        return create(noSubscriptionTimeout, timeUnit, timeoutScheduler, null);
    }

    public static  UnicastAutoReleaseSubject create(long noSubscriptionTimeout, TimeUnit timeUnit,
        Scheduler timeoutScheduler, Action0 onUnsubscribe) {
        State state = new State(onUnsubscribe);
        return new UnicastAutoReleaseSubject(state, noSubscriptionTimeout, timeUnit, timeoutScheduler);
    }

    /**
     * This will eagerly dispose this {@link Subject} without waiting for the no subscription timeout period,
     * if configured.
     *
     * This must be invoked when the caller is sure that no one will subscribe to this subject. Any subscriber after
     * this call will receive an error that the subject is disposed.
     *
     * @return {@code true} if the subject was disposed by this call (if and only if there was no subscription).
     */
    public boolean disposeIfNotSubscribed() {
        if (state.casState(State.STATES.UNSUBSCRIBED, State.STATES.DISPOSED)) {
            state.bufferedSubject.lift(new AutoReleaseByteBufOperator()).subscribe(Subscribers.empty()); // Drain all items so that ByteBuf gets released.
            return true;
        }
        return false;
    }

    /** The common state. */
    private static final class State {

        /**
         * Following are the only possible state transitions:
         * UNSUBSCRIBED -> SUBSCRIBED
         * UNSUBSCRIBED -> DISPOSED
         */
        private enum STATES {
            UNSUBSCRIBED /*Initial*/, SUBSCRIBED /*Terminal state*/, DISPOSED/*Terminal state*/
        }

        /** Field updater for state. */
        private static final AtomicIntegerFieldUpdater STATE_UPDATER
                = AtomicIntegerFieldUpdater.newUpdater(State.class, "state");
        /** Field updater for timeoutScheduled. */
        private static final AtomicIntegerFieldUpdater TIMEOUT_SCHEDULED_UPDATER
                = AtomicIntegerFieldUpdater.newUpdater(State.class, "timeoutScheduled");

        private final Action0 onUnsubscribe;
        private final Subject bufferedSubject;

        private volatile Subscription timeoutSubscription;
        @SuppressWarnings("unused") private volatile int timeoutScheduled; // Boolean
        private volatile int state = STATES.UNSUBSCRIBED.ordinal(); /*Values are the ordinals of STATES enum*/

        public State(Action0 onUnsubscribe) {
            this.onUnsubscribe = onUnsubscribe;
            bufferedSubject = BufferUntilSubscriber.create();
        }

        public boolean casState(STATES expected, STATES next) {
            return STATE_UPDATER.compareAndSet(this, expected.ordinal(), next.ordinal());
        }

        public boolean casTimeoutScheduled() {
            return TIMEOUT_SCHEDULED_UPDATER.compareAndSet(this, 0, 1);
        }

        public void setTimeoutSubscription(Subscription subscription) {
            timeoutSubscription = subscription;
        }

        public void unsubscribeTimeoutSubscription() {
            if (timeoutSubscription != null) {
                timeoutSubscription.unsubscribe();
            }
        }
    }

    private static final class OnSubscribeAction implements OnSubscribe {

        private final State state;

        public OnSubscribeAction(State state) {
            this.state = state;
        }

        @Override
        public void call(final Subscriber subscriber) {
            if (state.casState(State.STATES.UNSUBSCRIBED, State.STATES.SUBSCRIBED)) {

                subscriber.add(Subscriptions.create(new Action0() {
                    @Override
                    public void call() {
                        if (null != state.onUnsubscribe) {
                            state.onUnsubscribe.call();
                        }
                    }
                }));

                state.bufferedSubject.subscribe(subscriber);
                state.unsubscribeTimeoutSubscription();

            } else if(State.STATES.SUBSCRIBED.ordinal() == state.state) {
                subscriber.onError(new IllegalStateException("This Observable can only have one subscription. "
                    + "Use Observable.publish() if you want to multicast."));
            } else if(State.STATES.DISPOSED.ordinal() == state.state) {
                subscriber.onError(new IllegalStateException("The Content of this Observable is already released. "
                    + "Subscribe earlier or tune the CouchbaseEnvironment#autoreleaseAfter() setting."));
            }
        }
    }

    private static class AutoReleaseByteBufOperator implements Operator {
        @Override
        public Subscriber call(final Subscriber subscriber) {
            return new Subscriber() {
                @Override
                public void onCompleted() {
                    subscriber.onCompleted();
                }

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

                @Override
                public void onNext(I t) {
                    try {
                        subscriber.onNext(t);
                    } finally {
                        ReferenceCountUtil.release(t);
                    }
                }
            };
        }
    }

    @Override
    public void onCompleted() {
        state.bufferedSubject.onCompleted();
    }

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

    @Override
    public void onNext(T t) {
        state.bufferedSubject.onNext(t);

        // Schedule timeout once and when not subscribed yet.
        if (state.casTimeoutScheduled() && state.state == State.STATES.UNSUBSCRIBED.ordinal()) {
            state.setTimeoutSubscription(timeoutScheduler.subscribe(new Action1() { // Schedule timeout after the first content arrives.
                @Override
                public void call(Long aLong) {
                    disposeIfNotSubscribed();
                }
            }));
        }
    }

    @Override
    public boolean hasObservers() {
        return state.state == State.STATES.SUBSCRIBED.ordinal();
    }

}