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

io.servicetalk.concurrent.internal.ConcurrentTerminalSubscriber Maven / Gradle / Ivy

/*
 * Copyright © 2019 Apple Inc. and the ServiceTalk project authors
 *
 * 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.servicetalk.concurrent.internal;

import io.servicetalk.concurrent.PublisherSource;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import javax.annotation.Nullable;

import static io.servicetalk.concurrent.PublisherSource.Subscriber;
import static io.servicetalk.concurrent.PublisherSource.Subscription;
import static java.util.Objects.requireNonNull;

/**
 * A {@link Subscriber} that allows for concurrent delivery of terminal events.
 *
 * @param  The type of {@link Subscriber}.
 */
public final class ConcurrentTerminalSubscriber implements Subscriber {
    private static final int SUBSCRIBER_STATE_INVALID = Integer.MIN_VALUE;
    private static final int SUBSCRIBER_STATE_WAITING_ON_SUBSCRIBE = -1;
    private static final int SUBSCRIBER_STATE_IDLE = 0;
    private static final int SUBSCRIBER_STATE_ON_NEXT = 1;
    private static final int SUBSCRIBER_STATE_TERMINATING = 2;
    private static final int SUBSCRIBER_STATE_TERMINATED = 3;

    private static final AtomicIntegerFieldUpdater stateUpdater =
            AtomicIntegerFieldUpdater.newUpdater(ConcurrentTerminalSubscriber.class, "state");

    private final Subscriber delegate;
    @Nullable
    private TerminalNotification terminalNotification;
    private volatile int state;

    /**
     * Create a new instance.
     *
     * @param delegate The {@link Subscriber} to delegate all signals to.
     */
    public ConcurrentTerminalSubscriber(Subscriber delegate) {
        this(delegate, true);
    }

    /**
     * Create a new instance.
     *
     * @param delegate The {@link Subscriber} to delegate all signals to.
     * @param concurrentOnSubscribe {@code false} to not guard for concurrency on
     * {@link Subscriber#onSubscribe(PublisherSource.Subscription)}. {@code true} means that
     * {@link Subscriber#onSubscribe(PublisherSource.Subscription)} will be protected against concurrent invocation with
     * terminal methods.
     */
    public ConcurrentTerminalSubscriber(Subscriber delegate, boolean concurrentOnSubscribe) {
        this.delegate = requireNonNull(delegate);
        state = concurrentOnSubscribe ? SUBSCRIBER_STATE_WAITING_ON_SUBSCRIBE : SUBSCRIBER_STATE_IDLE;
    }

    @Override
    public void onSubscribe(final Subscription subscription) {
        final boolean wasWaiting = state == SUBSCRIBER_STATE_WAITING_ON_SUBSCRIBE;

        try {
            delegate.onSubscribe(subscription);
        } finally {
            if (wasWaiting) {
                for (;;) {
                    final int localState = state;
                    if (localState == SUBSCRIBER_STATE_WAITING_ON_SUBSCRIBE) {
                        if (stateUpdater.compareAndSet(this, SUBSCRIBER_STATE_WAITING_ON_SUBSCRIBE,
                                SUBSCRIBER_STATE_IDLE)) {
                            break;
                        }
                    } else if (localState == SUBSCRIBER_STATE_TERMINATING) {
                        if (stateUpdater.compareAndSet(this, SUBSCRIBER_STATE_TERMINATING,
                                SUBSCRIBER_STATE_TERMINATED)) {
                            assert terminalNotification != null;
                            terminalNotification.terminate(delegate);
                            break;
                        }
                    } else {
                        break;
                    }
                }
            }
        }
    }

    @Override
    public void onNext(@Nullable final T t) {
        int originalState = SUBSCRIBER_STATE_INVALID;
        for (;;) {
            final int localState = state;
            if (localState == SUBSCRIBER_STATE_IDLE || localState == SUBSCRIBER_STATE_WAITING_ON_SUBSCRIBE) {
                if (stateUpdater.compareAndSet(this, localState, SUBSCRIBER_STATE_ON_NEXT)) {
                    originalState = localState;
                    break;
                }
            } else if (localState == SUBSCRIBER_STATE_ON_NEXT) {
                // Allow reentry because we don't want to drop data.
                break;
            } else {
                // The only possible state is TERMINATED. We don't have to worry about concurrency for
                // Subscriber#onNext.
                return;
            }
        }
        try {
            delegate.onNext(t);
        } finally {
            if (originalState != SUBSCRIBER_STATE_INVALID) {
                for (;;) {
                    final int localState = state;
                    if (localState == SUBSCRIBER_STATE_ON_NEXT) {
                        if (stateUpdater.compareAndSet(this, SUBSCRIBER_STATE_ON_NEXT, originalState)) {
                            break;
                        }
                    } else if (localState == SUBSCRIBER_STATE_TERMINATING) {
                        if (stateUpdater.compareAndSet(this, SUBSCRIBER_STATE_TERMINATING,
                                SUBSCRIBER_STATE_TERMINATED)) {
                            assert terminalNotification != null;
                            terminalNotification.terminate(delegate);
                            break;
                        }
                    } else {
                        break;
                    }
                }
            }
        }
    }

    @Override
    public void onError(final Throwable t) {
        processOnError(t);
    }

    /**
     * Attempt to process {@link #onError(Throwable)}.
     *
     * @param t The error to process.
     * @return {@code true} if the terminal signal was propagated to the delegate {@link Subscriber}.
     */
    public boolean processOnError(final Throwable t) {
        for (;;) {
            final int localState = state;
            if (localState == SUBSCRIBER_STATE_TERMINATED || localState == SUBSCRIBER_STATE_TERMINATING) {
                return false;
            } else {
                // We may overwrite the terminalNotification if there is concurrency on this method, but there is no
                // guarantee about what terminal notification will be propagated in the event of concurrency anyways.
                terminalNotification = TerminalNotification.error(t);
                if (stateUpdater.compareAndSet(this, localState, SUBSCRIBER_STATE_TERMINATING)) {
                    // We only propagate the terminal event here if the localState was SUBSCRIBER_STATE_IDLE, because
                    // otherwise this means we maybe interacting with the Subscriber on another thread.
                    if (localState == SUBSCRIBER_STATE_IDLE &&
                            stateUpdater.compareAndSet(this, SUBSCRIBER_STATE_TERMINATING,
                                    SUBSCRIBER_STATE_TERMINATED)) {
                        delegate.onError(t);
                        return true;
                    }
                    return false;
                }
            }
        }
    }

    @Override
    public void onComplete() {
        processOnComplete();
    }

    /**
     * Attempt to process {@link #onComplete()}.
     *
     * @return {@code true} if the terminal signal was propagated to the delegate {@link Subscriber}.
     */
    public boolean processOnComplete() {
        for (;;) {
            final int localState = state;
            if (localState == SUBSCRIBER_STATE_TERMINATED || localState == SUBSCRIBER_STATE_TERMINATING) {
                return false;
            } else {
                // We may overwrite the terminalNotification if there is concurrency on this method, but there is no
                // guarantee about what terminal notification will be propagated in the event of concurrency anyways.
                terminalNotification = TerminalNotification.complete();
                if (stateUpdater.compareAndSet(this, localState, SUBSCRIBER_STATE_TERMINATING)) {
                    // We only propagate the terminal event here if the localState was SUBSCRIBER_STATE_IDLE, because
                    // otherwise this means we maybe interacting with the Subscriber on another thread.
                    if (localState == SUBSCRIBER_STATE_IDLE &&
                            stateUpdater.compareAndSet(this, SUBSCRIBER_STATE_TERMINATING,
                                    SUBSCRIBER_STATE_TERMINATED)) {
                        delegate.onComplete();
                        return true;
                    }
                    return false;
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy