
io.servicetalk.concurrent.internal.ThreadBasedSignalOffloader 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.Cancellable;
import io.servicetalk.concurrent.CompletableSource;
import io.servicetalk.concurrent.Executor;
import io.servicetalk.concurrent.PublisherSource.Subscriber;
import io.servicetalk.concurrent.PublisherSource.Subscription;
import io.servicetalk.concurrent.SingleSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Queue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverErrorFromSource;
import static io.servicetalk.concurrent.internal.SubscriberUtils.isRequestNValid;
import static io.servicetalk.concurrent.internal.SubscriberUtils.safeCancel;
import static io.servicetalk.concurrent.internal.SubscriberUtils.safeOnComplete;
import static io.servicetalk.concurrent.internal.SubscriberUtils.safeOnError;
import static io.servicetalk.concurrent.internal.SubscriberUtils.safeOnSuccess;
import static io.servicetalk.concurrent.internal.TerminalNotification.complete;
import static io.servicetalk.concurrent.internal.TerminalNotification.error;
import static io.servicetalk.utils.internal.PlatformDependent.newUnboundedSpscQueue;
import static java.lang.System.arraycopy;
import static java.lang.Thread.currentThread;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater;
import static java.util.concurrent.locks.LockSupport.park;
import static java.util.concurrent.locks.LockSupport.unpark;
/**
* An implementation of {@link SignalOffloader} that uses a single thread to offload all signals.
*/
final class ThreadBasedSignalOffloader implements SignalOffloader, Runnable {
private static final int INDEX_INIT = -1;
private static final int INDEX_OFFLOADER_TERMINATED = -2;
private static final Logger LOGGER = LoggerFactory.getLogger(ThreadBasedSignalOffloader.class);
private static final String UNKNOWN_EXECUTOR_THREAD_NAME = "signal-offloader-";
private static final AtomicIntegerFieldUpdater lastEntityIndexUpdater =
newUpdater(ThreadBasedSignalOffloader.class, "lastEntityIndex");
private final Executor executor;
private final int publisherSignalQueueInitialCapacity;
// Why not use a Queue?
// Typical usecase for this offloader is where entities are added as the execution chain is subscribed and then
// different events happen on these entities. Since, events are typically much more than the number of entities,
// which are proportional to the number of asynchronous operators in the execution chain, this approach means we
// do not have to mutate the queue once subscribe has traversed through all operators in the execution chain.
//
// If we use a queue, then we can selectively run the entities which have pending signals but we enqueue/dequeue
// from the queue on every signal.
// This approach iterates over all entities for each run and skip entities which have previously terminated.
private OffloadedEntity[] offloadedEntities;
private volatile int lastEntityIndex = INDEX_INIT;
@Nullable
private volatile Thread executorThread;
/**
* New instance.
*
* @param executor A {@link Executor} to use for offloading signals.
*/
ThreadBasedSignalOffloader(Executor executor) {
this(executor, 2, 2);
}
/**
* New instance.
*
* @param executor A {@link Consumer} representing an executor and hence provides the memory visibility guarantee:
* {@link Runnable} submitted to this consumer happens-before invoking {@link Runnable#run()} on that
* {@link Runnable}.
* @param expectedOffloadingEntities An approximation of the number of entities offloaded.
* @param publisherSignalQueueInitialCapacity Initial capacity for the queue of signals to a {@link Subscriber}.
*/
ThreadBasedSignalOffloader(final Executor executor, final int expectedOffloadingEntities,
final int publisherSignalQueueInitialCapacity) {
this.executor = requireNonNull(executor);
this.publisherSignalQueueInitialCapacity = publisherSignalQueueInitialCapacity;
offloadedEntities = new OffloadedEntity[expectedOffloadingEntities];
}
@Override
public Subscriber super T> offloadSubscriber(Subscriber super T> subscriber) {
return addOffloadedEntity(new OffloadedSubscriber<>(this, subscriber));
}
@Override
public SingleSource.Subscriber super T> offloadSubscriber(SingleSource.Subscriber super T> subscriber) {
return addOffloadedEntity(new OffloadedSingleSubscriber<>(this, subscriber));
}
@Override
public CompletableSource.Subscriber offloadSubscriber(CompletableSource.Subscriber subscriber) {
return addOffloadedEntity(new OffloadedCompletableSubscriber(this, subscriber));
}
@Override
public Subscriber super T> offloadSubscription(Subscriber super T> subscriber) {
return addOffloadedEntity(new OffloadedSubscription<>(this, subscriber));
}
@Override
public SingleSource.Subscriber super T> offloadCancellable(SingleSource.Subscriber super T> subscriber) {
return addOffloadedEntity(new OffloadedSingleCancellable<>(this, subscriber));
}
@Override
public CompletableSource.Subscriber offloadCancellable(CompletableSource.Subscriber subscriber) {
return addOffloadedEntity(new OffloadedCompletableCancellable(this, subscriber));
}
@Override
public void offloadSubscribe(
Subscriber super T> subscriber, Consumer> handleSubscribe) {
try {
addOffloadedEntity(new OffloadedSignalEntity<>(handleSubscribe, subscriber), true);
} catch (EnqueueForOffloadingFailed e) {
// Since we failed to enqueue for offloading, we are sure that Subscriber has not been signalled and hence
// safe to send the error.
deliverErrorFromSource(subscriber, e.getCause());
}
}
@Override
public void offloadSubscribe(SingleSource.Subscriber super T> subscriber,
Consumer> handleSubscribe) {
try {
addOffloadedEntity(new OffloadedSignalEntity<>(handleSubscribe, subscriber), true);
} catch (EnqueueForOffloadingFailed e) {
// Since we failed to enqueue for offloading, we are sure that Subscriber has not been signalled and hence
// safe to send the error.
deliverErrorFromSource(subscriber, e.getCause());
}
}
@Override
public void offloadSubscribe(CompletableSource.Subscriber subscriber,
Consumer handleSubscribe) {
try {
addOffloadedEntity(new OffloadedSignalEntity<>(handleSubscribe, subscriber), true);
} catch (EnqueueForOffloadingFailed e) {
// Since we failed to enqueue for offloading, we are sure that Subscriber has not been signalled and hence
// safe to send the error.
deliverErrorFromSource(subscriber, e.getCause());
}
}
@Override
public void offloadSignal(T signal, Consumer signalConsumer) {
// Since this is an independent offload, it lacks context of what it is trying to offload. We simply always
// offload here. If this offloading is also not required then one can use the immediate Executor.
addOffloadedEntity(new OffloadedSignalEntity<>(signalConsumer, signal));
}
@Override
public void run() {
assert executorThread == null; // This only runs once.
final Thread executorThread = currentThread();
this.executorThread = executorThread;
// There is a basic invariant here that none of the entities should start terminating before all entities are
// added, which is how the execution chain is processed:
//
// - Send subscribe() through the execution chain from the last operator to source.
// - Send onSubscribe() through the execution chain from the source to the last operator.
//
// Since, nothing can terminate without a Subscription which is sent through onSubscribe(),
// we will have all entities registered for offloading before we start terminating.
//
// There is one additional angle:
//
// handleSubscribe itself uses offloading and it terminates immediately.
// If it happens so that offloading of handleSubscribe() is the only registered entity and it finished without
// adding any more entities that will lead to issues. Today we add an offload for Subscription before we offload
// handleSubscribe so this case is not possible.
for (;;) {
// Volatile read will ensure "happens-before" between addition of entity and read by the run() method
// for this index.
final int lastIndex = lastEntityIndex;
assert lastIndex >= 0;
final OffloadedEntity[] entities = offloadedEntities;
int terminatedEntities = 0;
for (int i = 0; i <= lastIndex; i++) {
OffloadedEntity entity = entities[i];
// terminated state is only touched from within sendSignals so there are no concurrent mutation,
// hence no need for atomic operation.
if (!entity.isTerminated()) {
// sendSignals() never throws.
entity.sendSignals();
if (entity.isTerminated()) {
terminatedEntities++;
}
} else {
terminatedEntities++;
}
}
if (terminatedEntities == lastEntityIndex + 1) {
lastEntityIndex = INDEX_OFFLOADER_TERMINATED; // No more entities can be offloaded.
return;
} else if (lastIndex == lastEntityIndex) {
// If more entities are added, do not park, just run the loop again.
// park-unpark does not guarantee visibility between the entities and the run loop, so we depend on the
// following:
// -- Any changes to offloadedEntities are made visible by the write and read of volatile field
// lastEntityIndex
// -- Any changes to an offloaded entity is made visible by the write and read of the field "state" in
// that entity.
// By the above we make sure that whatever change happens to the entity before notifying the run loop,
// is made visible when the run loop signals that entity.
park(executorThread);
}
}
}
void notifyExecutor() {
notifyExecutor(executorThread);
}
void notifyExecutor(@Nullable Thread executorThread) {
// unpark is a noop if the passed thread is null.
unpark(executorThread);
}
private T addOffloadedEntity(T offloadedEntity) {
return addOffloadedEntity(offloadedEntity, false);
}
private T addOffloadedEntity(T offloadedEntity, boolean wrapEnqueueFailure) {
final int lastIndex = lastEntityIndex;
if (lastIndex == INDEX_OFFLOADER_TERMINATED) {
IllegalStateException iae = new IllegalStateException("Signal offloader: " +
executorThreadName() + " has already terminated.");
throw wrapEnqueueFailure ? new EnqueueForOffloadingFailed(iae) : iae;
}
final int nextIndex = lastIndex + 1;
if (nextIndex == offloadedEntities.length) {
OffloadedEntity[] nextValue = new OffloadedEntity[offloadedEntities.length * 2];
arraycopy(offloadedEntities, 0, nextValue, 0, offloadedEntities.length);
// Till we update the lastEntityIndex below, the new value of the array or the entity isn't visible to the
// run loop so there is no chance of accessing an index which has a null value.
offloadedEntities = nextValue;
}
offloadedEntities[nextIndex] = offloadedEntity;
// Volatile write will ensure "happens-before" between addition of entity and read by the run() method for
// this index.
if (!lastEntityIndexUpdater.compareAndSet(this, lastIndex, nextIndex)) {
if (lastEntityIndex == INDEX_OFFLOADER_TERMINATED) {
IllegalStateException iae = new IllegalStateException("Signal offloader: " +
executorThreadName() + " has already terminated.");
throw wrapEnqueueFailure ? new EnqueueForOffloadingFailed(iae) : iae;
}
// concurrent registration of entities is not allowed by this class as new offload entities are added in
// the subscribe path which are not called concurrently.
IllegalArgumentException iae = new IllegalArgumentException("Entity " + offloadedEntity +
" added concurrently for offloading signals.");
throw wrapEnqueueFailure ? new EnqueueForOffloadingFailed(iae) : iae;
}
if (nextIndex == 0) {
if (wrapEnqueueFailure) {
try {
executor.execute(this);
} catch (RejectedExecutionException re) {
throw new EnqueueForOffloadingFailed(re);
}
} else {
executor.execute(this);
}
} else {
final Thread executorThread = this.executorThread;
// If we are in the executor thread, it can only be when we are adding entities while sending offloaded
// signals. In such a case we are assured that run-loop will pick this new entity before getting parked.
// So, we do not need to notify the executor. Notifying will mean that the next park() call will immediately
// return and drain entities again which is not be required.
if (currentThread() != executorThread) {
notifyExecutor(executorThread);
}
}
return offloadedEntity;
}
private String executorThreadName() {
final Thread executorThread = this.executorThread;
return executorThread == null ? UNKNOWN_EXECUTOR_THREAD_NAME : executorThread.getName();
}
private interface OffloadedEntity {
/**
* Send any pending signals to the offloaded entity.
* This method should not throw.
*/
void sendSignals();
boolean isTerminated();
}
private static final class OffloadedSignalEntity implements OffloadedEntity {
private final Consumer signalConsumer;
private final T signal;
private boolean terminated;
OffloadedSignalEntity(Consumer signalConsumer, T signal) {
this.signalConsumer = signalConsumer;
this.signal = signal;
}
@Override
public void sendSignals() {
terminated = true;
// No concurrent executor signals so no need to protect against noop sendSignals0
try {
signalConsumer.accept(signal);
} catch (Throwable throwable) {
LOGGER.error("Ignored unexpected exception offloading signal: {} to consumer: {}", signal,
signalConsumer, throwable);
}
}
@Override
public boolean isTerminated() {
return terminated;
}
}
private abstract static class AbstractOffloadedEntity implements OffloadedEntity {
private static final AtomicIntegerFieldUpdater notifyUpdater =
newUpdater(AbstractOffloadedEntity.class, "notify");
private boolean terminated; // only accessed from the drain thread.
private final ThreadBasedSignalOffloader offloader;
@SuppressWarnings("unused")
private volatile int notify;
AbstractOffloadedEntity(ThreadBasedSignalOffloader offloader) {
this.offloader = offloader;
}
/**
* Send all pending signals for this entity and call {@link #setTerminated()} if it does not need to be invoked
* anymore.
*/
@Override
public final void sendSignals() {
// As with the CAS in notifyExecutor(), this CAS makes sure that writes to all normal fields in this
// offloaded entity happens-before calling notifyExecutor() and hence sendSignals0().
// A plain write to the volatile field will not provide guarantees for the load of fields so we need a
// LoadStore barrier (ref: http://gee.cs.oswego.edu/dl/jmm/cookbook.html).
//
// The two CASes together provides a StoreLoad barrier which provides a full-fence between the writes
// inside an entity and the reading of the same state inside sendSignals0()
if (notifyUpdater.compareAndSet(this, 1, 0)) {
sendSignals0();
}
}
@Override
public final boolean isTerminated() {
return terminated;
}
abstract void sendSignals0();
final void notifyExecutor() {
// We CAS this volatile field in order to make sure that all changes made to the entity by the signals
// happens-before signalling the run-loop and hence are made visible to the run-loop without adding explicit
// visibility guarantees.
// CAS adds a LoadStore barrier: Load1; LoadStore; Store2 such that writes to normal variables can not be
// reordered with the write to this volatile.
// (ref: http://gee.cs.oswego.edu/dl/jmm/cookbook.html)
// ========================================================================================================
// use of atomic conditional update operations CompareAndSwap (CAS) or LoadLinked/StoreConditional (LL/SC)
// that have the semantics of performing a volatile load followed by a volatile store.
// ========================================================================================================
if (notifyUpdater.compareAndSet(this, 0, 1)) {
offloader.notifyExecutor();
}
}
final void setTerminated() {
terminated = true;
}
}
private static final class OffloadedSubscriber extends AbstractOffloadedEntity implements Subscriber {
private static final Object NULL_ON_NEXT = new Object();
private final ThreadBasedSignalOffloader offloader;
private final Subscriber super T> original;
private final Queue
© 2015 - 2025 Weber Informatics LLC | Privacy Policy