
io.servicetalk.concurrent.api.internal.ConnectablePayloadWriter 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.api.internal;
import io.servicetalk.concurrent.PublisherSource.Subscriber;
import io.servicetalk.concurrent.PublisherSource.Subscription;
import io.servicetalk.concurrent.api.Publisher;
import io.servicetalk.concurrent.internal.DuplicateSubscribeException;
import io.servicetalk.concurrent.internal.TerminalNotification;
import io.servicetalk.oio.api.PayloadWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.LockSupport;
import javax.annotation.Nullable;
import static io.servicetalk.concurrent.api.Publisher.failed;
import static io.servicetalk.concurrent.internal.FlowControlUtils.addWithOverflowProtection;
import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverCompleteFromSource;
import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverErrorFromSource;
import static io.servicetalk.concurrent.internal.SubscriberUtils.handleExceptionFromOnSubscribe;
import static io.servicetalk.concurrent.internal.SubscriberUtils.isRequestNValid;
import static io.servicetalk.concurrent.internal.SubscriberUtils.newExceptionForInvalidRequestN;
import static io.servicetalk.concurrent.internal.TerminalNotification.complete;
import static io.servicetalk.concurrent.internal.TerminalNotification.error;
import static io.servicetalk.concurrent.internal.ThrowableUtils.unknownStackTrace;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater;
/**
* A {@link PayloadWriter} that can be {@link #connect() connected} to a sink such that any data written on the
* {@link PayloadWriter} is eventually emitted to the connected {@link Publisher} {@link Subscriber}.
*
* @param The type of data for the {@link PayloadWriter}.
*/
public final class ConnectablePayloadWriter implements PayloadWriter {
private static final long REQUESTN_ABOUT_TO_PARK = Long.MIN_VALUE;
private static final long REQUESTN_TERMINATED = REQUESTN_ABOUT_TO_PARK + 1;
@SuppressWarnings("rawtypes")
private static final AtomicLongFieldUpdater requestedUpdater =
AtomicLongFieldUpdater.newUpdater(ConnectablePayloadWriter.class, "requested");
@SuppressWarnings("rawtypes")
private static final AtomicReferenceFieldUpdater closedUpdater =
newUpdater(ConnectablePayloadWriter.class, TerminalNotification.class, "closed");
@SuppressWarnings("rawtypes")
private static final AtomicReferenceFieldUpdater stateUpdater =
newUpdater(ConnectablePayloadWriter.class, Object.class, "state");
/**
* A field that assumes various states:
*
* - {@link State#DISCONNECTED} - waiting for {@link #connect()} to be called
* - {@link State#CONNECTING} - {@link #connect()} has been called, but no {@link Subscriber} yet.
* - {@link State#WAITING_FOR_CONNECTED} - the writer thread is waiting for the {@link Subscriber}.
* - {@link State#CONNECTED} - {@link Publisher} created, logically connected but awaiting {@link Subscriber}
* - {@link Subscriber} - connected to the {@link Subscriber}, waiting for items to be emitted or termination
* - {@link State#TERMINATING} - we have {@link #close()}, but not yet delivered to the {@link Subscriber}
* - {@link State#TERMINATED} - we have delivered a terminal signal to the {@link Subscriber}
*
*/
private volatile Object state = State.DISCONNECTED;
private volatile long requested;
@Nullable
private volatile TerminalNotification closed;
/**
* The writer thread that maybe blocked on {@link LockSupport#park()}.
* This does not need to be volatile because visibility is provided by modifications to {@link #state} or
* {@link #requested}.
*/
@Nullable
private Thread writerThread;
@Override
public void write(final T t) throws IOException {
for (;;) {
final long requested = this.requested;
if (requested > 0) {
if (requestedUpdater.compareAndSet(this, requested, requested - 1)) {
break;
}
} else if (requested >= 0) {
waitForRequestNDemand();
break;
} else {
// In the event that we have been terminated don't bother trying to wait. More importantly we don't
// want to reset the requested state to REQUESTN_ABOUT_TO_PARK because it may have been set to
// REQUESTN_TERMINATED.
processClosed();
}
}
final Subscriber super T> s = waitForSubscriber();
try {
s.onNext(t);
} catch (final Throwable cause) {
closed = error(cause);
requested = REQUESTN_TERMINATED;
state = State.TERMINATED;
s.onError(cause);
throw cause;
}
}
@Override
public void flush() throws IOException {
// We currently don't queue any data at this layer, so there is nothing to do here.
verifyOpen();
}
@Override
public void close() throws IOException {
close0(null);
}
@Override
public void close(final Throwable cause) throws IOException {
close0(requireNonNull(cause));
}
private void close0(@Nullable Throwable cause) {
// Set closed before state, because the Subscriber thread depends upon this ordering in the event it needs to
// terminate the Subscriber.
TerminalNotification terminal = cause == null ? complete() : error(cause);
if (closedUpdater.compareAndSet(this, null, terminal)) {
// We need to terminate requested or else we may block indefinitely on subsequent calls to write in
// waitForRequestNDemand.
requested = REQUESTN_TERMINATED;
for (;;) {
final Object currState = state;
if (currState instanceof Subscriber) {
if (stateUpdater.compareAndSet(this, currState, State.TERMINATED)) {
terminal.terminate((Subscriber>) currState);
break;
}
} else if (currState == State.TERMINATED ||
stateUpdater.compareAndSet(this, currState, State.TERMINATING)) {
assert currState != State.WAITING_FOR_CONNECTED;
break;
}
}
} else {
Object currState = stateUpdater.getAndSet(this, State.TERMINATED);
if (currState instanceof Subscriber) {
final TerminalNotification currClosed = closed;
assert currClosed != null;
currClosed.terminate((Subscriber>) currState);
}
}
}
/**
* Connects this {@link PayloadWriter} to the returned {@link Publisher} such that any data written to this
* {@link PayloadWriter} is eventually delivered to a {@link Subscriber} of the returned {@link Publisher}.
*
* @return {@link Publisher} that will emit all data written to this {@link PayloadWriter} to its
* {@link Subscriber}. Only a single active {@link Subscriber} is allowed for this {@link Publisher}.
*/
public Publisher connect() {
return stateUpdater.compareAndSet(this, State.DISCONNECTED, State.CONNECTING) ? new ConnectedPublisher<>(this) :
failed(new IllegalStateException("Stream state " + state + " is not valid for connect."));
}
private void verifyOpen() throws IOException {
TerminalNotification currClosed = closed;
if (currClosed != null) {
processClosed(currClosed);
}
}
private void processClosed() throws IOException {
TerminalNotification currClosed = closed;
assert currClosed != null;
processClosed(currClosed);
}
private void processClosed(TerminalNotification currClosed) throws IOException {
Object currState = stateUpdater.getAndSet(this, State.TERMINATED);
if (currState instanceof Subscriber && currClosed.cause() != ConnectedPublisher.CONNECTED_PUBLISHER_CANCELLED) {
currClosed.terminate((Subscriber>) currState);
}
throw newAlreadyClosed(currClosed.cause());
}
private static IOException newAlreadyClosed(@Nullable Throwable cause) {
return new IOException("Already closed", cause);
}
private void waitForRequestNDemand() throws IOException {
writerThread = Thread.currentThread();
final long oldRequested = requestedUpdater.getAndSet(this, REQUESTN_ABOUT_TO_PARK);
if (oldRequested == 0) {
for (;;) {
LockSupport.park();
final long requested = this.requested;
if (requested > 0) {
if (requestedUpdater.compareAndSet(this, requested, requested - 1)) {
writerThread = null;
break;
}
} else if (requested != REQUESTN_ABOUT_TO_PARK) {
writerThread = null;
processClosed();
}
}
} else if (oldRequested > 0) {
writerThread = null;
waitForRequestNDemandAvoidPark(oldRequested);
} else {
writerThread = null;
processClosed();
}
}
private void waitForRequestNDemandAvoidPark(final long oldRequested) throws IOException {
for (;;) {
final long requested = this.requested;
if (requested == REQUESTN_ABOUT_TO_PARK) {
if (requestedUpdater.compareAndSet(this, REQUESTN_ABOUT_TO_PARK, oldRequested - 1)) {
break;
}
} else if (requested < 0) {
processClosed();
} else if (requestedUpdater.compareAndSet(this, requested,
addWithOverflowProtection(oldRequested - 1, requested))) {
break;
}
}
}
@SuppressWarnings("unchecked")
private Subscriber super T> waitForSubscriber() throws IOException {
final Object currState = state;
return currState instanceof Subscriber ? (Subscriber super T>) currState : waitForSubscriberSlowPath();
}
@SuppressWarnings("unchecked")
private Subscriber super T> waitForSubscriberSlowPath() throws IOException {
writerThread = Thread.currentThread();
for (;;) {
final Object currState = state;
if (currState instanceof Subscriber) {
writerThread = null;
return (Subscriber super T>) currState;
} else if (currState == State.TERMINATED || currState == State.TERMINATING) {
// If the subscriber is not handed off the the writer thread then the writer thread is not responsible
// for delivering the terminal event to the Subscriber (because it never has a reference to it), and
// the thread processing the subscribe(..) call will terminate the Subscriber instead of handing it off.
writerThread = null;
final TerminalNotification localClosed = closed;
assert localClosed != null;
throw newAlreadyClosed(localClosed.cause());
} else if (stateUpdater.compareAndSet(this, currState, State.WAITING_FOR_CONNECTED)) {
LockSupport.park();
}
}
}
private static final class ConnectedPublisher extends Publisher {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectedPublisher.class);
private static final IOException CONNECTED_PUBLISHER_CANCELLED = unknownStackTrace(
new IOException("Connected Publisher cancel()"), ConnectablePayloadWriter.class, "cancel()");
private final ConnectablePayloadWriter outer;
ConnectedPublisher(final ConnectablePayloadWriter outer) {
this.outer = outer;
}
@Override
protected void handleSubscribe(final Subscriber super T> subscriber) {
if (!stateUpdater.compareAndSet(outer, State.CONNECTING, State.CONNECTED)) {
if (stateUpdater.compareAndSet(outer, State.TERMINATING, State.TERMINATED)) {
deliverCompleteFromSource(subscriber);
} else {
deliverErrorFromSource(subscriber, new DuplicateSubscribeException(outer.state, subscriber));
}
return;
}
try {
subscriber.onSubscribe(new Subscription() {
@Override
public void request(final long n) {
if (isRequestNValid(n)) {
for (;;) {
final long requested = outer.requested;
if (requested >= 0) {
if (requestedUpdater.compareAndSet(outer, requested,
addWithOverflowProtection(requested, n))) {
break;
}
} else if (requested == REQUESTN_ABOUT_TO_PARK) {
// It is possible the writer thread did a GaS to make REQUESTN_ABOUT_TO_PARK visible
// but if the Get returned some positive demand it will write something and
// atomically decrement the demand. So we should do a CaS here to be sure we don't
// overwrite this value and lose demand.
if (requestedUpdater.compareAndSet(outer, REQUESTN_ABOUT_TO_PARK, n)) {
tryWakeupWriterThread();
break;
}
} else {
break;
}
}
} else if (closedUpdater.compareAndSet(outer, null,
error(newExceptionForInvalidRequestN(n)))) {
terminateRequestN();
} else {
LOGGER.warn("invalid request({}), but already closed.", n);
}
}
@Override
public void cancel() {
if (closedUpdater.compareAndSet(outer, null, error(CONNECTED_PUBLISHER_CANCELLED))) {
terminateRequestN();
}
}
});
} catch (Throwable cause) {
handleExceptionFromOnSubscribe(subscriber, cause);
} finally {
// Make the Subscriber available after this thread is done interacting with it to avoid concurrent
// invocation.
for (;;) {
final Object currState = outer.state;
if (currState == State.CONNECTED) {
if (stateUpdater.compareAndSet(outer, State.CONNECTED, subscriber)) {
break;
}
} else if (currState == State.WAITING_FOR_CONNECTED) {
if (stateUpdater.compareAndSet(outer, State.WAITING_FOR_CONNECTED, subscriber)) {
final Thread writerThread = outer.writerThread;
assert writerThread != null;
LockSupport.unpark(writerThread);
break;
}
} else {
TerminalNotification currClosed = outer.closed;
assert currClosed != null;
currClosed.terminate(subscriber);
break;
}
}
}
}
private void terminateRequestN() {
outer.requested = REQUESTN_TERMINATED;
tryWakeupWriterThread();
}
private void tryWakeupWriterThread() {
final Thread writerThread = outer.writerThread;
if (writerThread != null) {
LockSupport.unpark(writerThread);
}
}
}
private enum State {
DISCONNECTED, CONNECTING, WAITING_FOR_CONNECTED, CONNECTED, TERMINATING, TERMINATED
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy