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

io.servicetalk.http.netty.SpliceFlatStreamToMetaSingle Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2018-2019, 2021-2024 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.http.netty;

import io.servicetalk.buffer.api.Buffer;
import io.servicetalk.concurrent.Cancellable;
import io.servicetalk.concurrent.PublisherSource;
import io.servicetalk.concurrent.PublisherSource.Subscription;
import io.servicetalk.concurrent.SingleSource.Subscriber;
import io.servicetalk.concurrent.api.Publisher;
import io.servicetalk.concurrent.api.PublisherToSingleOperator;
import io.servicetalk.concurrent.api.Single;
import io.servicetalk.concurrent.api.internal.SubscribablePublisher;
import io.servicetalk.concurrent.internal.DelayedSubscription;
import io.servicetalk.concurrent.internal.DuplicateSubscribeException;
import io.servicetalk.http.api.HttpResponseMetaData;
import io.servicetalk.http.api.StreamingHttpResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.BiFunction;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static io.servicetalk.concurrent.Cancellable.IGNORE_CANCEL;
import static io.servicetalk.concurrent.internal.EmptySubscriptions.EMPTY_SUBSCRIPTION;
import static io.servicetalk.concurrent.internal.SubscriberUtils.checkDuplicateSubscription;
import static io.servicetalk.concurrent.internal.SubscriberUtils.handleExceptionFromOnSubscribe;
import static java.util.Objects.requireNonNull;

/**
 * This class is responsible for splicing a {@link Publisher}<{@link Object}> with a common {@link Payload}
 * into a {@link Data}<{@link Payload}> eg. {@link StreamingHttpResponse}<{@link Buffer}>.
 *
 * @param  type of container, eg. {@link StreamingHttpResponse}<{@link Buffer}>
 * @param  type of meta-data in front of the stream of {@link Payload}, eg. {@link HttpResponseMetaData}
 * @param  type of payload inside the {@link Data}, eg. {@link Buffer}
 */
final class SpliceFlatStreamToMetaSingle implements PublisherToSingleOperator {
    private static final Logger LOGGER = LoggerFactory.getLogger(SpliceFlatStreamToMetaSingle.class);
    private final BiFunction, Data> packer;

    /**
     * Operator splicing a {@link Publisher}<{@link Object}> with a common {@link Payload} and {@link
     * MetaData} header as first element into a {@link Data}<{@link Payload}> eg. {@link
     * StreamingHttpResponse}<{@link Buffer}>.
     *
     * @param packer function to pack the {@link Publisher}<{@link Payload}> and {@link MetaData} into a
     * {@link Data}
     */
    SpliceFlatStreamToMetaSingle(BiFunction, Data> packer) {
        this.packer = requireNonNull(packer);
    }

    @Override
    public PublisherSource.Subscriber apply(Subscriber subscriber) {
        return new SplicingSubscriber<>(this, subscriber);
    }

    private static final class SplicingSubscriber
            implements PublisherSource.Subscriber {

        @SuppressWarnings("rawtypes")
        private static final AtomicReferenceFieldUpdater maybePayloadSubUpdater =
                AtomicReferenceFieldUpdater.newUpdater(SplicingSubscriber.class, Object.class, "maybePayloadSub");

        private static final String CANCELED = "CANCELED";
        private static final String PENDING = "PENDING";
        private static final String EMPTY_COMPLETED = "EMPTY_COMPLETED";
        private static final String EMPTY_COMPLETED_DELIVERED = "EMPTY_COMPLETED_DELIVERED";

        /**
         * A field that assumes various types and states depending on the state of the operator.
         * 

* One of

    *
  • {@code null} – initial pending state before the {@link Single} is completed
  • *
  • {@link PublisherSource.Subscriber}<{@link Payload}> - when subscribed to the payload
  • *
  • {@link #CANCELED} - when the {@link Single} is canceled prematurely
  • *
  • {@link #PENDING} - when the {@link Single} will complete and {@link Payload} pending subscribe
  • *
  • {@link #EMPTY_COMPLETED} - when the stream completed prematurely (empty) payload
  • *
  • {@link #EMPTY_COMPLETED_DELIVERED} - when the premature (empty) completion event was delivered to a * subscriber
  • *
  • {@link Throwable} - the error that occurred in the stream
  • *
*/ @Nullable @SuppressWarnings("unused") private volatile Object maybePayloadSub; /** * Once a {@link #maybePayloadSub} is set to a {@link PublisherSource.Subscriber} we cache a copy in a * non-volatile field to allow caching in register and avoid instanceof and casting on the hot path. */ @Nullable private PublisherSource.Subscriber payloadSubscriber; /** * Indicates whether the meta-data has been observed. */ private boolean metaSeenInOnNext; /** * The {@link Subscription} before wrapping to pass it to the downstream {@link PublisherSource.Subscriber}. * Doesn't need to be {@code volatile}, as it should be visible wrt JMM according to * ) subscriber; payloadSubscriber.onNext(payload); } } } else { ensureResultSubscriberOnSubscribe(); MetaData meta = (MetaData) obj; // When the upstream Publisher is canceled we don't give it to any Payload Subscribers metaSeenInOnNext = true; final Data data; try { final Publisher payload; if (maybePayloadSubUpdater.compareAndSet(this, null, PENDING)) { payload = newPayloadPublisher(); } else { final Object maybePayloadSub = this.maybePayloadSub; assert maybePayloadSub == CANCELED : "Expected CANCELED but got: " + maybePayloadSub; boolean cas = maybePayloadSubUpdater.compareAndSet(this, CANCELED, EMPTY_COMPLETED_DELIVERED); assert cas : "Could not transition from CANCELED to EMPTY_COMPLETED_DELIVERED"; payload = Publisher.failed(StacklessCancellationException.newInstance( "Canceled prematurely from SplicingSubscriber.cancelData(..), current state: " + maybePayloadSub, getClass(), "onNext(...)")); } data = parent.packer.apply(meta, payload); assert data != null : "Packer function must return non-null Data"; } catch (Throwable t) { assert rawSubscription != null : "Expected rawSubscription but got null"; // We know that there is nothing else that can happen on this stream as we are not sending the // data to the dataSubscriber. rawSubscription.cancel(); // Since we update our internal state before calling parent.packer, if parent.packer throws, // it will cause the assumptions to break in onError(). So, we catch and handle the error ourselves // as opposed to let the source call onError. dataSubscriber.onError(t); return; } dataSubscriber.onSuccess(data); } } @Nonnull private Publisher newPayloadPublisher() { return new SubscribablePublisher() { @Override protected void handleSubscribe(PublisherSource.Subscriber newSubscriber) { final DelayedSubscription delayedSubscription = new DelayedSubscription(); // newSubscriber.onSubscribe MUST be called before making newSubscriber visible below with the CAS // on maybePayloadSubUpdater. Otherwise there is a potential for concurrent invocation on the // Subscriber which is not allowed by the Reactive Streams specification. try { newSubscriber.onSubscribe(delayedSubscription); } catch (Throwable t) { handleExceptionFromOnSubscribe(newSubscriber, t); if (maybePayloadSubUpdater.compareAndSet(SplicingSubscriber.this, PENDING, EMPTY_COMPLETED_DELIVERED)) { final Subscription subscription = rawSubscription; assert subscription != null : "Expected rawSubscription but got null"; subscription.cancel(); } return; } if (maybePayloadSubUpdater.compareAndSet(SplicingSubscriber.this, PENDING, newSubscriber)) { assert rawSubscription != null : "Expected rawSubscription but got null"; delayedSubscription.delayedSubscription(rawSubscription); } else { // Entering this branch means either a duplicate subscriber or a stream that completed or failed // without a subscriber present. The consequence is that unless we've seen payload data we may // not send onComplete() or onError() to the original subscriber, but that is OK as long as one // subscriber of them gets the correct signal and all others get a duplicate subscriber error. final Object maybeSubscriber = SplicingSubscriber.this.maybePayloadSub; delayedSubscription.delayedSubscription(EMPTY_SUBSCRIPTION); if (maybeSubscriber == EMPTY_COMPLETED && maybePayloadSubUpdater .compareAndSet(SplicingSubscriber.this, EMPTY_COMPLETED, EMPTY_COMPLETED_DELIVERED)) { // Prematurely completed (header + empty payload) newSubscriber.onComplete(); } else if (maybeSubscriber instanceof Throwable && maybePayloadSubUpdater .compareAndSet(SplicingSubscriber.this, maybeSubscriber, EMPTY_COMPLETED_DELIVERED)) { // Premature error newSubscriber.onError((Throwable) maybeSubscriber); } else if (maybeSubscriber == CANCELED && maybePayloadSubUpdater .compareAndSet(SplicingSubscriber.this, maybeSubscriber, EMPTY_COMPLETED_DELIVERED)) { // Premature cancel, capture the full caller stack-trace to understand which code path // subscribes to the payload after cancellation. newSubscriber.onError(new CancellationException( "Canceled prematurely from SplicingSubscriber.cancelData(..), current state: " + maybeSubscriber)); } else { // Existing subscriber or terminal event consumed by other subscriber (COMPLETED_DELIVERED) newSubscriber.onError(new DuplicateSubscribeException(maybeSubscriber, newSubscriber, "HTTP request/response payload can only be subscribed to once")); } } } }; } @SuppressWarnings("unchecked") @Override public void onError(Throwable t) { if (payloadSubscriber != null) { // We have a subscriber that has seen onNext() payloadSubscriber.onError(t); } else { final Object maybeSubscriber = maybePayloadSubUpdater.getAndSet(this, t); if (!metaSeenInOnNext) { ensureResultSubscriberOnSubscribe(); dataSubscriber.onError(t); } else if (maybeSubscriber instanceof PublisherSource.Subscriber) { if (maybePayloadSubUpdater.compareAndSet(this, t, EMPTY_COMPLETED_DELIVERED)) { ((PublisherSource.Subscriber) maybeSubscriber).onError(t); } else { terminateWithIllegalStateException((PublisherSource.Subscriber) maybeSubscriber); } } else if (maybeSubscriber == EMPTY_COMPLETED_DELIVERED) { LOGGER.debug("Discarding a terminal error from upstream because the payload publisher was " + "already terminated", t); } else { LOGGER.debug("Terminal error queued for delayed delivery to the payload publisher. " + "If the payload is not subscribed, this event will not be delivered.", t); } } } @SuppressWarnings("unchecked") @Override public void onComplete() { if (payloadSubscriber != null) { // We have a subscriber that has seen onNext() payloadSubscriber.onComplete(); } else { final Object maybeSubscriber = maybePayloadSubUpdater.getAndSet(this, EMPTY_COMPLETED); if (maybeSubscriber instanceof PublisherSource.Subscriber) { if (maybePayloadSubUpdater.compareAndSet(this, EMPTY_COMPLETED, EMPTY_COMPLETED_DELIVERED)) { ((PublisherSource.Subscriber) maybeSubscriber).onComplete(); } else { terminateWithIllegalStateException((PublisherSource.Subscriber) maybeSubscriber); } } else if (!metaSeenInOnNext) { ensureResultSubscriberOnSubscribe(); dataSubscriber.onError(new IllegalStateException( "Stream unexpectedly completed without emitting any items")); } else if (maybeSubscriber == EMPTY_COMPLETED_DELIVERED) { LOGGER.debug("Discarding a terminal complete from upstream because the payload publisher was " + "already terminated"); } } } private void ensureResultSubscriberOnSubscribe() { assert !metaSeenInOnNext : "Already seen meta-data"; if (!onSubscribeSent) { onSubscribeSent = true; // Since we are going to deliver data or a terminal signal right after this, // there is no need for this to be cancellable. dataSubscriber.onSubscribe(IGNORE_CANCEL); } } private void terminateWithIllegalStateException(PublisherSource.Subscriber subscriber) { subscriber.onError(new IllegalStateException("Duplicate Subscribers are not allowed. Existing: " + subscriber + ", failed the race with a duplicate, but neither has seen onNext()")); } } }