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

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

/*
 * Copyright © 2018 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.Subscription;

import java.util.concurrent.atomic.AtomicLongFieldUpdater;

import static io.servicetalk.concurrent.internal.ConcurrentUtils.releaseReentrantLock;
import static io.servicetalk.concurrent.internal.ConcurrentUtils.tryAcquireReentrantLock;
import static io.servicetalk.concurrent.internal.SubscriberUtils.isRequestNValid;
import static java.util.Objects.requireNonNull;

/**
 * This class prevents concurrent invocation of {@link Subscription} methods and preserves the
 * Reactive Streams, 2.7
 *  rule when there is a possibility of concurrency.
 */
public class ConcurrentSubscription implements Subscription {
    private static final AtomicLongFieldUpdater pendingDemandUpdater =
            AtomicLongFieldUpdater.newUpdater(ConcurrentSubscription.class, "pendingDemand");
    /**
     * Typically usage of this lock follows the acquire/try/finally{release} usage pattern. However in this case we do
     * not follow this pattern for the following reasons:
     * 
    *
  1. If {@link #subscription} throws, the associated asynchronous source is considered invalid and should be * cleaned up externally relative to this {@link ConcurrentSubscription}.
  2. *
  3. If the previous item is true, then it is OK to poison the lock if {@link #subscription} throws, because * it is considered invalid and no further interaction is required for clean (it is done externally).
  4. *
*/ private static final AtomicLongFieldUpdater subscriptionLockUpdater = AtomicLongFieldUpdater.newUpdater(ConcurrentSubscription.class, "subscriptionLock"); private static final long CANCELLED = Long.MIN_VALUE; private final Subscription subscription; private volatile long pendingDemand; @SuppressWarnings("unused") private volatile long subscriptionLock; /** * New instance. * @param subscription {@link Subscription} to wrap. */ protected ConcurrentSubscription(Subscription subscription) { this.subscription = requireNonNull(subscription); } /** * Wrap a {@link Subscription} to make it thread safe when concurrent access may exists between * {@link Subscription#request(long)} and {@link Subscription#cancel()}. * @param subscription The subscription to wrap. * @return A {@link Subscription} that will enforce the threading constraints in a concurrent environment. */ public static ConcurrentSubscription wrap(Subscription subscription) { return subscription instanceof ConcurrentSubscription ? (ConcurrentSubscription) subscription : new ConcurrentSubscription(subscription); } @Override public void request(long n) { final long acquireId = tryAcquireReentrantLock(subscriptionLockUpdater, this); if (acquireId != 0) { // fast path (no concurrency) just deliver demand without adding to pending. subscription.request(n); if (!releaseReentrantLock(subscriptionLockUpdater, acquireId, this)) { // if we failed to release the lock there was concurrent invocation, and we need to drain. drainPending(); } } else { // slow path (concurrency detected) add pending demand and try to re-acquire lock and process demand. addPending(n); drainPending(); } } @Override public void cancel() { pendingDemand = CANCELLED; if (tryAcquireReentrantLock(subscriptionLockUpdater, this) != 0) { subscription.cancel(); // poison subscriptionLockUpdater } } private void addPending(long n) { if (!isRequestNValid(n)) { pendingDemand = mapInvalidRequestN(n); } else { pendingDemandUpdater.accumulateAndGet(this, n, FlowControlUtils::addWithOverflowProtectionIfNotNegative); } } private void drainPending() { long acquireId; do { acquireId = tryAcquireReentrantLock(subscriptionLockUpdater, this); if (acquireId == 0) { break; } final long prevPendingDemand = pendingDemandUpdater.getAndSet(this, 0); if (prevPendingDemand == CANCELLED) { subscription.cancel(); } else if (prevPendingDemand != 0) { subscription.request(prevPendingDemand); } } while (!releaseReentrantLock(subscriptionLockUpdater, acquireId, this)); } private static long mapInvalidRequestN(long n) { // We map zero to a negative number because zero could later be overwritten by a subsequent legit value of // n, and we want to ensure the invalid use gets propagated. return n == CANCELLED ? CANCELLED + 1 : n == 0 ? -1 : n; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy