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

com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer Maven / Gradle / Ivy

/**
 * Copyright (C) 2013-2018 Expedia Inc.
 *
 * 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 com.hotels.styx.client.netty.connectionpool;

import com.google.common.annotations.VisibleForTesting;
import com.hotels.styx.api.client.Origin;
import com.hotels.styx.api.netty.exceptions.ResponseTimeoutException;
import com.hotels.styx.client.netty.ConsumerDisconnectedException;
import com.hotels.styx.common.StateMachine;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Subscriber;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.LongUnaryOperator;

import static com.google.common.base.Objects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer.ProducerState.BUFFERING;
import static com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer.ProducerState.BUFFERING_COMPLETED;
import static com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer.ProducerState.COMPLETED;
import static com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer.ProducerState.EMITTING_BUFFERED_CONTENT;
import static com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer.ProducerState.STREAMING;
import static com.hotels.styx.client.netty.connectionpool.FlowControllingHttpContentProducer.ProducerState.TERMINATED;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static rx.internal.operators.BackpressureUtils.getAndAddRequest;

class FlowControllingHttpContentProducer {
    private static final Logger LOGGER = LoggerFactory.getLogger(FlowControllingHttpContentProducer.class);

    private final StateMachine stateMachine;
    private final String loggingPrefix;

    private final Runnable askForMore;
    private final Runnable onCompleteAction;
    private final Consumer onTerminateAction;

    private final Queue readQueue = new ConcurrentLinkedDeque<>();
    private final AtomicLong requested = new AtomicLong(Long.MAX_VALUE);
    private final AtomicLong receivedChunks = new AtomicLong(0);
    private final AtomicLong receivedBytes = new AtomicLong(0);
    private final AtomicLong emittedChunks = new AtomicLong(0);
    private final AtomicLong emittedBytes = new AtomicLong(0);

    private volatile Subscriber contentSubscriber;

    enum ProducerState {
        BUFFERING,
        STREAMING,
        BUFFERING_COMPLETED,
        EMITTING_BUFFERED_CONTENT,
        COMPLETED,
        TERMINATED
    }

    FlowControllingHttpContentProducer(
            Runnable askForMore,
            Runnable onCompleteAction,
            Consumer onTerminateAction,
            String loggingPrefix,
            Origin origin) {
        this.askForMore = requireNonNull(askForMore);
        this.onCompleteAction = requireNonNull(onCompleteAction);
        this.onTerminateAction = requireNonNull(onTerminateAction);

        this.stateMachine = new StateMachine.Builder()
                .initialState(BUFFERING)
                .transition(BUFFERING, RxBackpressureRequestEvent.class, this::rxBackpressureRequestBeforeSubscription)
                .transition(BUFFERING, ContentChunkEvent.class, this::contentChunkEventWhileBuffering)
                .transition(BUFFERING, ChannelInactiveEvent.class, this::releaseAndTerminate)
                .transition(BUFFERING, ChannelExceptionEvent.class, this::releaseAndTerminate)
                .transition(BUFFERING, ChannelIdleEvent.class, this::releaseAndTerminate)
                .transition(BUFFERING, ContentSubscribedEvent.class, this::contentSubscribedEventWhileBuffering)
                .transition(BUFFERING, ContentEndEvent.class, this::contentEndEventWhileBuffering)

                .transition(BUFFERING_COMPLETED, RxBackpressureRequestEvent.class, this::rxBackpressureRequestBeforeSubscription)
                .transition(BUFFERING_COMPLETED, ContentChunkEvent.class, this::spuriousContentChunkEvent)
                .transition(BUFFERING_COMPLETED, ChannelInactiveEvent.class, s -> BUFFERING_COMPLETED)
                .transition(BUFFERING_COMPLETED, ChannelExceptionEvent.class, s -> BUFFERING_COMPLETED)
                .transition(BUFFERING_COMPLETED, ChannelIdleEvent.class, this::releaseAndTerminate)
                .transition(BUFFERING_COMPLETED, ContentSubscribedEvent.class, this::contentSubscribedEventWhileBufferingCompleted)
                .transition(BUFFERING_COMPLETED, ContentEndEvent.class, s -> BUFFERING_COMPLETED)

                .transition(STREAMING, RxBackpressureRequestEvent.class, this::rxBackpressureRequestEventWhileStreaming)
                .transition(STREAMING, ContentChunkEvent.class, this::contentChunkEventWhileStreaming)
                .transition(STREAMING, ChannelInactiveEvent.class, e -> emitErrorAndTerminate(e.cause()))
                .transition(STREAMING, ChannelExceptionEvent.class, e -> emitErrorAndTerminate(e.cause()))
                .transition(STREAMING, ChannelIdleEvent.class, e -> emitErrorAndTerminate(new ResponseTimeoutException(origin)))
                .transition(STREAMING, ContentSubscribedEvent.class, this::contentSubscribedEventWhileStreaming)
                .transition(STREAMING, ContentEndEvent.class, this::contentEndEventWhileStreaming)
                .transition(STREAMING, UnsubscribeEvent.class, this::emitErrorAndTerminateOnPrematureUnsubscription)

                .transition(EMITTING_BUFFERED_CONTENT, RxBackpressureRequestEvent.class, this::rxBackpressureRequestEventWhileEmittingBufferedContent)
                .transition(EMITTING_BUFFERED_CONTENT, ContentChunkEvent.class, this::spuriousContentChunkEvent)
                .transition(EMITTING_BUFFERED_CONTENT, ChannelInactiveEvent.class, s -> EMITTING_BUFFERED_CONTENT)
                .transition(EMITTING_BUFFERED_CONTENT, ChannelExceptionEvent.class, s -> EMITTING_BUFFERED_CONTENT)
                .transition(EMITTING_BUFFERED_CONTENT, ChannelIdleEvent.class, e -> emitErrorAndTerminate(e.cause()))
                .transition(EMITTING_BUFFERED_CONTENT, ContentSubscribedEvent.class, this::contentSubscribedEventWhileEmittingBufferedContent)
                .transition(EMITTING_BUFFERED_CONTENT, ContentEndEvent.class, this::contentEndEventWhileEmittingBufferedContent)
                .transition(EMITTING_BUFFERED_CONTENT, UnsubscribeEvent.class, this::emitErrorAndTerminateOnPrematureUnsubscription)

                .transition(COMPLETED, ContentChunkEvent.class, this::spuriousContentChunkEvent)
                .transition(COMPLETED, UnsubscribeEvent.class, ev -> COMPLETED)
                .transition(COMPLETED, RxBackpressureRequestEvent.class, ev -> COMPLETED)
                .transition(COMPLETED, ContentSubscribedEvent.class, this::contentSubscribedInCompletedState)

                .transition(TERMINATED, ContentChunkEvent.class, this::spuriousContentChunkEvent)
                .transition(TERMINATED, ContentSubscribedEvent.class, this::contentSubscribedInTerminatedState)
                .transition(TERMINATED, RxBackpressureRequestEvent.class, ev -> TERMINATED)

                .onInappropriateEvent((state, event) -> {
                    LOGGER.warn(warningMessage("Inappropriate event=" + event.getClass().getSimpleName()));
                    return state;
                })
                .build();

        this.loggingPrefix = loggingPrefix;
    }

    /*
     * BUFFERING state event handlers
     */
    private ProducerState rxBackpressureRequestBeforeSubscription(RxBackpressureRequestEvent event) {
        // This can occur before the actual content subscribe event. This occurs if the subscriber
        // has called request() before actually having subscribed to the content observable. In this
        // case just initialise the request count with requested N value.
        requested.compareAndSet(Long.MAX_VALUE, 0);
        getAndAddRequest(requested, event.n());
        return this.state();
    }

    private ProducerState contentChunkEventWhileBuffering(ContentChunkEvent event) {
        receivedBytes.addAndGet(event.chunk.readableBytes());
        receivedChunks.incrementAndGet();
        readQueue.add(event.chunk);
        return BUFFERING;
    }

    private  ProducerState releaseAndTerminate(CausalEvent event) {
        releaseBuffers();
        onTerminateAction.accept(event.cause());
        return TERMINATED;
    }

    private ProducerState contentSubscribedEventWhileBuffering(ContentSubscribedEvent event) {
        this.contentSubscriber = event.subscriber;
        emitChunks(this.contentSubscriber);
        return STREAMING;
    }

    private ProducerState contentEndEventWhileBuffering(ContentEndEvent event) {
        return BUFFERING_COMPLETED;
    }

    /*
     * BUFFERING_COMPLETED event handlers
     */

    private ProducerState spuriousContentChunkEvent(ContentChunkEvent event) {
        // Should not occur because content has already been fully consumed.
        LOGGER.warn(warningMessage("Spurious content chunk."));
        ReferenceCountUtil.release(event.chunk);
        return this.state();
    }


    private ProducerState contentSubscribedEventWhileBufferingCompleted(ContentSubscribedEvent event) {
        this.contentSubscriber = event.subscriber;
        if (readQueue.size() == 0) {
            this.contentSubscriber.onCompleted();
            this.onCompleteAction.run();
            return COMPLETED;
        }

        emitChunks(this.contentSubscriber);

        if (readQueue.size() > 0) {
            return EMITTING_BUFFERED_CONTENT;
        } else {
            this.contentSubscriber.onCompleted();
            this.onCompleteAction.run();
            return COMPLETED;
        }
    }


    /*
     * STREAMING event handlers
     */
    private ProducerState rxBackpressureRequestEventWhileStreaming(RxBackpressureRequestEvent event) {
        requested.compareAndSet(Long.MAX_VALUE, 0);
        getAndAddRequest(requested, event.n());

        if (requested.get() <= 1) {
            askForMore.run();
        }
        emitChunks(contentSubscriber);

        return STREAMING;
    }

    private ProducerState contentChunkEventWhileStreaming(ContentChunkEvent event) {
        receivedBytes.addAndGet(event.chunk.readableBytes());
        receivedChunks.incrementAndGet();
        readQueue.add(event.chunk);
        emitChunks(contentSubscriber);
        return STREAMING;
    }

    private ProducerState emitErrorAndTerminate(Throwable cause) {
        releaseBuffers();
        contentSubscriber.onError(cause);
        onTerminateAction.accept(cause);
        return TERMINATED;

    }

    private ProducerState contentSubscribedEventWhileStreaming(ContentSubscribedEvent event) {
        // Subscription is already in place in Streaming state. Therefore this is a second subscription,
        // which is not allowed.
        releaseBuffers();
        IllegalStateException cause = new IllegalStateException(
                format("Secondary subscription occurred. producerState=%s. connection=%s",
                        state(), loggingPrefix));
        contentSubscriber.onError(cause);
        event.subscriber.onError(cause);
        onTerminateAction.accept(cause);
        return TERMINATED;
    }

    private ProducerState contentSubscribedInCompletedState(ContentSubscribedEvent event) {
        event.subscriber.onError(new IllegalStateException(
                format("Secondary subscription occurred. producerState=%s. connection=%s", state(), loggingPrefix)));
        return COMPLETED;
    }

    private ProducerState contentSubscribedInTerminatedState(ContentSubscribedEvent event) {
        event.subscriber.onError(new IllegalStateException(
                format("Secondary subscription occurred. producerState=%s. connection=%s", state(), loggingPrefix)));
        return TERMINATED;
    }

    private ProducerState contentEndEventWhileStreaming(ContentEndEvent event) {
        if (readQueue.size() > 0) {
            return EMITTING_BUFFERED_CONTENT;
        } else {
            this.contentSubscriber.onCompleted();
            this.onCompleteAction.run();
            return COMPLETED;
        }
    }

    private ProducerState emitErrorAndTerminateOnPrematureUnsubscription(UnsubscribeEvent event) {
        return emitErrorAndTerminate(
                new ConsumerDisconnectedException(
                        format("The consumer unsubscribed. connection=%s", loggingPrefix),
                        this.state().toString()));
    }

    /*
     * EMITTING_BUFFERED_CONTENT event handlers
     */
    private ProducerState rxBackpressureRequestEventWhileEmittingBufferedContent(RxBackpressureRequestEvent event) {
        requested.compareAndSet(Long.MAX_VALUE, 0);
        getAndAddRequest(requested, event.n());
        if (requested.get() <= 1) {
            askForMore.run();
        }
        emitChunks(contentSubscriber);
        if (readQueue.size() == 0) {
            LOGGER.debug("{} -> {}", new Object[]{this.state(), COMPLETED});
            this.contentSubscriber.onCompleted();
            this.onCompleteAction.run();
            return COMPLETED;
        } else {
            return EMITTING_BUFFERED_CONTENT;
        }
    }

    private ProducerState contentSubscribedEventWhileEmittingBufferedContent(ContentSubscribedEvent event) {
        // A second subscription occurred for the content observable that already
        // has a subscriber. Something is probably gone badly wrong. Therefore tear
        // everything down.

        releaseBuffers();

        event.subscriber.onError(
                new IllegalStateException(
                        format("Content observable is already subscribed. producerState=%s, connection=%s",
                                state(), loggingPrefix)));
        contentSubscriber.onError(
                new IllegalStateException(
                        format("Secondary subscription occurred. producerState=%s, connection=%s",
                                state(), loggingPrefix)));
        onTerminateAction.accept(
                new IllegalStateException(
                        format("Secondary content subscription detected. producerState=%s. connection=%s",
                                state(), loggingPrefix)));

        return TERMINATED;
    }

    private ProducerState contentEndEventWhileEmittingBufferedContent(ContentEndEvent event) {
        // Does not happen, because last HTTP content is already received.
        return EMITTING_BUFFERED_CONTENT;
    }


    /*
     * Event injector methods:
     */
    void newChunk(ByteBuf content) {
        stateMachine.handle(new ContentChunkEvent(content));
    }

    void lastHttpContent() {
        stateMachine.handle(new ContentEndEvent());
    }

    void channelException(Throwable cause) {
        stateMachine.handle(new ChannelExceptionEvent(cause));
    }

    void channelInactive(Throwable cause) {
        stateMachine.handle(new ChannelInactiveEvent(cause));
    }

    void idleStateEvent(Throwable cause) {
        stateMachine.handle(new ChannelIdleEvent(cause));
    }

    void request(long n) {
        stateMachine.handle(new RxBackpressureRequestEvent(n));
    }

    void onSubscribed(Subscriber subscriber) {
        if (inSubscribedState()) {
            LOGGER.warn(warningMessage("Secondary content subscription"));
        }
        stateMachine.handle(new ContentSubscribedEvent(subscriber));
    }

    private boolean inSubscribedState() {
        return state() == COMPLETED || state() == STREAMING || state() == EMITTING_BUFFERED_CONTENT || state() == TERMINATED;
    }

    void unsubscribe() {
        stateMachine.handle(new UnsubscribeEvent());
    }

    long emittedBytes() {
        return emittedBytes.get();
    }

    long emittedChunks() {
        return emittedChunks.get();
    }

    long receivedBytes() {
        return receivedBytes.get();
    }

    long receivedChunks() {
        return receivedChunks.get();
    }

    /*
     * Helper methods:
     */
    private void releaseBuffers() {
        ByteBuf value;
        while ((value = this.readQueue.poll()) != null) {
            ReferenceCountUtil.release(value);
        }
    }

    // This must not be run with locks held:
    private void emitChunks(Subscriber downstream) {
        LongUnaryOperator decrementIfBackpressureEnabled = current -> current == Long.MAX_VALUE ? current : current > 0 ? current - 1 : 0;
        LongUnaryOperator incrementIfBackpressureEnabled = current -> current == Long.MAX_VALUE ? current : current + 1;

        while (requested.getAndUpdate(decrementIfBackpressureEnabled) > 0) {
            ByteBuf value = this.readQueue.poll();
            if (value == null) {
                requested.getAndUpdate(incrementIfBackpressureEnabled);
                break;
            }
            emittedBytes.addAndGet(value.readableBytes());
            emittedChunks.incrementAndGet();
            downstream.onNext(value);

        }
    }

    @VisibleForTesting
    ProducerState state() {
        return stateMachine.currentState();
    }

    private String warningMessage(String msg) {
        return format("message=\"%s\", prefix=%s, state=%s, receivedChunks=%d, receivedBytes=%d, emittedChunks=%d, emittedBytes=%d",
                msg, loggingPrefix, state(), receivedChunks.get(), receivedBytes.get(), emittedChunks.get(), emittedBytes.get());
    }

    private static final class ContentChunkEvent {
        private final ByteBuf chunk;

        ContentChunkEvent(ByteBuf chunk) {
            this.chunk = checkNotNull(chunk);
        }

        @Override
        public String toString() {
            return toStringHelper(this)
                    .add("chunk", chunk)
                    .toString();
        }
    }

    private static final class ContentEndEvent {
        @Override
        public String toString() {
            return toStringHelper(this)
                    .toString();
        }
    }

    private static final class ContentSubscribedEvent {
        private final Subscriber subscriber;

        ContentSubscribedEvent(Subscriber subscriber) {
            this.subscriber = checkNotNull(subscriber);
        }

        @Override
        public String toString() {
            return toStringHelper(this)
                    .add("subscriber", subscriber)
                    .toString();
        }
    }

    private static final class RxBackpressureRequestEvent {
        private final long n;

        RxBackpressureRequestEvent(long n) {
            this.n = n;
        }

        long n() {
            return n;
        }

        @Override
        public String toString() {
            return toStringHelper(this)
                    .add("n", n)
                    .toString();
        }
    }

    private interface CausalEvent {
        Throwable cause();
    }

    private static final class ChannelInactiveEvent implements CausalEvent {
        private final Throwable cause;

        ChannelInactiveEvent(Throwable cause) {
            this.cause = checkNotNull(cause);
        }

        @Override
        public Throwable cause() {
            return cause;
        }

        @Override
        public String toString() {
            return toStringHelper(this)
                    .add("cause", cause)
                    .toString();
        }
    }

    private static final class ChannelExceptionEvent implements CausalEvent {
        private final Throwable cause;

        ChannelExceptionEvent(Throwable cause) {
            this.cause = checkNotNull(cause);
        }

        @Override
        public Throwable cause() {
            return cause;
        }

        @Override
        public String toString() {
            return toStringHelper(this)
                    .add("cause", cause)
                    .toString();
        }
    }

    private static final class ChannelIdleEvent implements CausalEvent {
        private final Throwable cause;

        private ChannelIdleEvent(Throwable cause) {
            this.cause = cause;
        }

        @Override
        public Throwable cause() {
            return cause;
        }

        @Override
        public String toString() {
            return toStringHelper(this)
                    .add("cause", cause)
                    .toString();
        }
    }

    private static class UnsubscribeEvent {
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy