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

com.microsoft.rest.v2.http.NettyClient Maven / Gradle / Ivy

/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See License.txt in the project root for
 * license information.
 */

package com.microsoft.rest.v2.http;

import com.microsoft.rest.v2.util.FlowableUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOption;
import io.netty.channel.MultithreadEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.pool.AbstractChannelPoolHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslContext;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.reactivex.Flowable;
import io.reactivex.FlowableSubscriber;
import io.reactivex.Single;
import io.reactivex.SingleEmitter;
import io.reactivex.disposables.Disposable;
import io.reactivex.exceptions.Exceptions;
import io.reactivex.internal.fuseable.SimplePlainQueue;
import io.reactivex.internal.queue.SpscLinkedArrayQueue;
import io.reactivex.internal.subscriptions.SubscriptionHelper;
import io.reactivex.internal.util.BackpressureHelper;
import io.reactivex.plugins.RxJavaPlugins;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import static com.microsoft.rest.v2.util.FlowableUtil.ensureLength;


/**
 * An HttpClient that is implemented using Netty.
 */
public final class NettyClient extends HttpClient {
    private final NettyAdapter adapter;
    private final HttpClientConfiguration configuration;

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

    /**
     * Creates NettyClient.
     *
     * @param configuration
     *            the HTTP client configuration.
     * @param adapter
     *            the adapter to Netty
     */
    private NettyClient(HttpClientConfiguration configuration, NettyAdapter adapter) {
        this.adapter = adapter;
        this.configuration = configuration != null ? configuration : new HttpClientConfiguration(null);
    }

    @Override
    public Single sendRequestAsync(final HttpRequest request) {
        return adapter.sendRequestInternalAsync(request, configuration);
    }

    private static final class NettyAdapter {
        private static final String EPOLL_GROUP_CLASS_NAME = "io.netty.channel.epoll.EpollEventLoopGroup";
        private static final String EPOLL_SOCKET_CLASS_NAME = "io.netty.channel.epoll.EpollSocketChannel";

        private static final String KQUEUE_GROUP_CLASS_NAME = "io.netty.channel.kqueue.KQueueEventLoopGroup";
        private static final String KQUEUE_SOCKET_CLASS_NAME = "io.netty.channel.kqueue.KQueueSocketChannel";

        private final MultithreadEventLoopGroup eventLoopGroup;
        private final SharedChannelPool channelPool;

        public Future shutdownGracefully() {
            channelPool.close();
            return eventLoopGroup.shutdownGracefully();
        }

        private static final class TransportConfig {
            final MultithreadEventLoopGroup eventLoopGroup;
            final Class channelClass;

            private TransportConfig(MultithreadEventLoopGroup eventLoopGroup,
                    Class channelClass) {
                this.eventLoopGroup = eventLoopGroup;
                this.channelClass = channelClass;
            }
        }

        private static MultithreadEventLoopGroup loadEventLoopGroup(String className, int size)
                throws ReflectiveOperationException {
            Class cls = Class.forName(className);
            ThreadFactory factory = new DefaultThreadFactory(cls, true);
            MultithreadEventLoopGroup result = (MultithreadEventLoopGroup) cls
                    .getConstructor(Integer.TYPE, ThreadFactory.class).newInstance(size, factory);
            return result;
        }

        @SuppressWarnings("unchecked")
        private static TransportConfig loadTransport(int groupSize) {
            TransportConfig result = null;
            try {
                final String osName = System.getProperty("os.name");
                if (osName.contains("Linux")) {
                    result = new TransportConfig(loadEventLoopGroup(EPOLL_GROUP_CLASS_NAME, groupSize),
                            (Class) Class.forName(EPOLL_SOCKET_CLASS_NAME));
                } else if (osName.contains("Mac")) {
                    result = new TransportConfig(loadEventLoopGroup(KQUEUE_GROUP_CLASS_NAME, groupSize),
                            (Class) Class.forName(KQUEUE_SOCKET_CLASS_NAME));
                }
            } catch (Exception e) {
                String message = e.getMessage();
                if (message == null) {
                    Throwable cause = e.getCause();
                    if (cause != null) {
                        message = cause.getMessage();
                    }
                }
                LoggerFactory.getLogger(NettyAdapter.class)
                        .debug("Exception when obtaining native EventLoopGroup and SocketChannel: " + message);
            }

            if (result == null) {
                result = new TransportConfig(
                        new NioEventLoopGroup(groupSize, new DefaultThreadFactory(NioEventLoopGroup.class, true)),
                        NioSocketChannel.class);
            }

            return result;
        }

        private static SharedChannelPool createChannelPool(Bootstrap bootstrap, TransportConfig config,
                SharedChannelPoolOptions options, SslContext sslContext) {
            bootstrap.group(config.eventLoopGroup);
            bootstrap.channel(config.channelClass);
            bootstrap.option(ChannelOption.AUTO_READ, false);
            bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
            bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) TimeUnit.MINUTES.toMillis(3L));
            return new SharedChannelPool(bootstrap, config.eventLoopGroup, new AbstractChannelPoolHandler() {
                @Override
                public synchronized void channelCreated(Channel ch) throws Exception {
                    // Why is it necessary to have "synchronized" to prevent NRE in pipeline().get(Class)?
                    // Is channelCreated not run on the eventLoop assigned to the channel?
                    ch.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
                    ch.pipeline().addLast("HttpClientInboundHandler", new HttpClientInboundHandler());
                }
            }, options, sslContext);
        }

        private NettyAdapter() {
            TransportConfig config = loadTransport(0);
            this.eventLoopGroup = config.eventLoopGroup;
            this.channelPool = createChannelPool(new Bootstrap(), config,
                    new SharedChannelPoolOptions().withPoolSize(eventLoopGroup.executorCount() * 16).withIdleChannelKeepAliveDurationInSec(60), null);
        }

        private NettyAdapter(Bootstrap baseBootstrap, int eventLoopGroupSize, SharedChannelPoolOptions options, SslContext sslContext) {
            TransportConfig config = loadTransport(eventLoopGroupSize);
            this.eventLoopGroup = config.eventLoopGroup;
            this.channelPool = createChannelPool(baseBootstrap, config, options, sslContext);
        }

        private Single sendRequestInternalAsync(final HttpRequest request, final HttpClientConfiguration configuration) {
            addHeaders(request);

            // Creates cold observable from an emitter
            return Single.create((SingleEmitter responseEmitter) -> {
                AcquisitionListener listener = new AcquisitionListener(channelPool, request, responseEmitter);
                responseEmitter.setDisposable(listener);
                channelPool.acquire(request.url().toURI(), configuration.proxy()).addListener(listener);
            });
        }
    }

    private static void addHeaders(final HttpRequest request) {
        request.withHeader(io.netty.handler.codec.http.HttpHeaderNames.HOST.toString(), request.url().getHost())
               .withHeader(io.netty.handler.codec.http.HttpHeaderNames.CONNECTION.toString(),
                        io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE.toString());
    }

    private static final class AcquisitionListener
            implements GenericFutureListener>, Disposable {

        private final SharedChannelPool channelPool;
        private final HttpRequest request;
        private final SingleEmitter responseEmitter;

        // state is tracked to ensure that any races between write, read,
        // disposal, cancel, and request are properly handled via a serialized state machine.
        private final AtomicInteger state = new AtomicInteger(ACQUIRING_NOT_DISPOSED);

        private static final int MAX_SEND_BUF_SIZE = 1024 * 64;

        private static final int ACQUIRING_NOT_DISPOSED = 0;
        private static final int ACQUIRING_DISPOSED = 1;
        private static final int ACQUIRED_CONTENT_NOT_SUBSCRIBED = 2;
        private static final int ACQUIRED_CONTENT_SUBSCRIBED = 3;
        private static final int ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED = 4;
        private static final int ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED = 5;
        private static final int CHANNEL_RELEASED = 6;

        // synchronized by `state`
        private Channel channel;

        //synchronized by `state`
        private ResponseContentFlowable content;

        private volatile boolean finishedWritingRequestBody;
        private volatile RequestSubscriber requestSubscriber;

        AcquisitionListener(
                SharedChannelPool channelPool,
                HttpRequest request,
                SingleEmitter responseEmitter) {
            this.channelPool = channelPool;
            this.request = request;
            this.responseEmitter = responseEmitter;
        }

        @Override
        public void operationComplete(Future cf) {
            if (!cf.isSuccess()) {
                emitError(cf.cause());
                return;
            }
            channel = (Channel) cf.getNow();
            while (true) {
                int s = state.get();
                if (s == ACQUIRING_DISPOSED) {
                    if (transition(ACQUIRING_DISPOSED, CHANNEL_RELEASED)) {
                        LOGGER.debug("Channel disposed on acquisition");
                        channelPool.closeAndRelease(channel);
                        return;
                    }
                } else if (s == ACQUIRING_NOT_DISPOSED) {
                    if (transition(ACQUIRING_NOT_DISPOSED, ACQUIRED_CONTENT_NOT_SUBSCRIBED)) {
                        break;
                    }
                } else {
                    return;
                }
            }

            final HttpClientInboundHandler inboundHandler =
                    channel.pipeline().get(HttpClientInboundHandler.class);
            inboundHandler.setFields(responseEmitter, this);
            //TODO do we need a memory barrier here to ensure vis of responseEmitter in other threads?

            try {

                final DefaultHttpRequest raw = createDefaultHttpRequest(request);

                writeRequest(raw);

                if (request.body() == null) {
                    writeBodyEnd();
                } else {
                    requestSubscriber = new RequestSubscriber(inboundHandler);
                    String contentLengthHeader = request.headers().value("content-length");
                    try {
                        long contentLength = Long.parseLong(contentLengthHeader);
                        request.body()
                                .flatMap(bb -> bb.remaining() > MAX_SEND_BUF_SIZE ? FlowableUtil.split(bb, MAX_SEND_BUF_SIZE) : Flowable.just(bb))
                                .compose(ensureLength(contentLength))
                                .subscribe(requestSubscriber);
                    } catch (NumberFormatException e) {
                        String message = String.format(
                                "Content-Length was expected to be a valid long but was \"%s\"", contentLengthHeader);
                        throw new IllegalArgumentException(message, e);
                    }
                }
            } catch (Exception e) {
                emitError(e);
            }
        }
        
        private final class RequestSubscriber implements FlowableSubscriber, GenericFutureListener> {
            Subscription subscription;

            // we need a done flag because an onNext emission can throw and emit an Error
            // event though the onNext cancels the subscription that is best endeavours for the
            // upstream so we need to be defensive about terminal events that follow
            private boolean done;

            private final HttpClientInboundHandler inboundHandler;

            RequestSubscriber(HttpClientInboundHandler inboundHandler) {
                this.inboundHandler = inboundHandler;
            }

            @Override
            public void onSubscribe(Subscription s) {
                subscription = s;
                inboundHandler.requestContentSubscription = subscription;
                subscription.request(1);
            }

            @Override
            public void onNext(ByteBuffer buf) {
                if (done) {
                    return;
                }

                try {
                    // Always dispatching writes on the event loop prevents data corruption on macOS.
                    // Since channel.write always dispatches to the event loop itself if needed internally,
                    // it seems fine to do it here.
                    channel.eventLoop().execute(() -> {
                        try {
                            channel.write(Unpooled.wrappedBuffer(buf)).addListener(this);
                            if (channel.isWritable()) {
                                subscription.request(1);
                            } else {
                                channel.flush();
                            }
                        } catch (Exception e) {
                            subscription.cancel();
                            onError(e);
                        }
                    });
                } catch (Exception e) {
                    subscription.cancel();
                    onError(e);
                }
            }

            @Override
            public void onError(Throwable t) {
                // TODO should we wrap the throwable so that the client
                // knows that the error occurred from the request body?
                if (done) {
                    RxJavaPlugins.onError(t);
                    return;
                }
                done = true;
                emitError(t);
            }

            @Override
            public void onComplete() {
                if (done) {
                    return;
                }
                done = true;
                try {
                    writeBodyEnd();
                } catch (Exception e) {
                    emitError(e);
                }
            }
            
            @Override
            public void operationComplete(Future future) throws Exception {
                if (!future.isSuccess()) {
                    subscription.cancel();
                    done = true;
                    emitError(future.cause());
                }
            }

            void channelWritable(boolean writable) {
                if (writable) {
                    subscription.request(1);
                }
            }

        }

        private void writeRequest(final DefaultHttpRequest raw) {
            channel.eventLoop().execute(() ->
                    channel //
                            .write(raw) //
                            .addListener((Future future) -> {
                                if (!future.isSuccess()) {
                                    emitError(future.cause());
                                }
                            })
            );
        }

        private void writeBodyEnd() {
            channel.eventLoop().execute(() -> channel //
                    .writeAndFlush(DefaultLastHttpContent.EMPTY_LAST_CONTENT) //
                    .addListener((Future future) -> {
                        if (future.isSuccess()) {
                            finishedWritingRequestBody = true;
                            // reads the response status code and headers and may also read some of the
                            // response body which will be buffered in ResponseContentFlowable
                            channel.read();
                        } else {
                            emitError(future.cause());
                        }
                    }));
        }

        private boolean transition(int from, int to) {
            return state.compareAndSet(from, to);
        }

        void emitError(Throwable throwable) {
            while (true) {
                if (throwable == null) {
                    throwable = new IOException("Unknown error in Netty client");
                }
                if (channel != null) {
                    LOGGER.warn("Error emitted on channel {}. Message: {}", channel.id(), throwable.getMessage());
                } else {
                    LOGGER.warn("Error emitted before channel is created. Message: {}", throwable.getMessage());
                }
                LOGGER.debug("Stack trace: ", new Exception());
                channelPool.dump();
                int s = state.get();
                if (s == ACQUIRING_NOT_DISPOSED) {
                    if (transition(ACQUIRING_NOT_DISPOSED, ACQUIRING_DISPOSED)) {
                        LOGGER.debug("Channel disposed before response is subscribed");
                        responseEmitter.onError(throwable);
                        break;
                    }
                } else if (s == ACQUIRED_CONTENT_SUBSCRIBED) {
                    if (transition(ACQUIRED_CONTENT_SUBSCRIBED, CHANNEL_RELEASED)) {
                        LOGGER.debug("Channel disposed after content is subscribed");
                        content.onError(throwable);
                        closeAndReleaseChannel();
                        break;
                    }
                } else if (s == ACQUIRED_CONTENT_NOT_SUBSCRIBED) {
                    if (transition(ACQUIRED_CONTENT_NOT_SUBSCRIBED, CHANNEL_RELEASED)) {
                        LOGGER.debug("Channel disposed before content is subscribed");
                        responseEmitter.onError(throwable);
                        closeAndReleaseChannel();
                        break;
                    }
                } else if (s == ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED) {
                    if (transition(ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED, CHANNEL_RELEASED)) {
                        LOGGER.debug("Channel disposed after content is subscribed with response emitter disposed");
                        content.onError(throwable);
                        closeAndReleaseChannel();
                        break;
                    }
                } else if (s == ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED) {
                    if (transition(ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED, CHANNEL_RELEASED)) {
                        LOGGER.debug("Channel disposed before content is subscribed with response emitter disposed");
                        closeAndReleaseChannel();
                        throw Exceptions.propagate(throwable);
                    }
                } else {
                    LOGGER.debug("Channel disposed at state {}", s);
                    closeAndReleaseChannel();
                    break;
                }
            }
        }

        /**
         * Returns false if and only if content subscription should be immediately
         * cancelled.
         *
         * @param content the content that was subscribed to
         * @return false if and only if content subscription should be immediately
         *     cancelled
         */
        boolean contentSubscribed(ResponseContentFlowable content) {
            while (true) {
                int s = state.get();
                if (s == ACQUIRED_CONTENT_NOT_SUBSCRIBED) {
                    if (transition(ACQUIRED_CONTENT_NOT_SUBSCRIBED, ACQUIRED_CONTENT_SUBSCRIBED)) {
                        this.content = content;
                        return true;
                    }
                } else if (s == ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED) {
                    if (transition(ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED, ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED)) {
                        this.content = content;
                        return true;
                    }
                } else {
                    return false;
                }
            }
        }

        private void releaseChannel(boolean cancelled) {
            if (!cancelled && finishedWritingRequestBody) {
                Future release = channelPool.release(channel);
                if (!release.isSuccess()) {
                    emitError(release.cause());
                }
            } else {
                LOGGER.debug("Channel disposed on cancellation or request body reading interrupted");
                closeAndReleaseChannel();
            }
        }

        /**
         * Is called when content flowable terminates or is cancelled.
         *
         **/
        void contentDone(boolean cancelled) {
            while (true) {
                int s = state.get();
                if (s == ACQUIRED_CONTENT_SUBSCRIBED) {
                    if (transition(ACQUIRED_CONTENT_SUBSCRIBED, CHANNEL_RELEASED)) {
                        releaseChannel(cancelled);
                        return;
                    }
                } else if (s == ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED) {
                    if (transition(ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED, CHANNEL_RELEASED)) {
                        releaseChannel(cancelled);
                        return;
                    }
                } else {
                    return;
                }
            }
        }

        @Override
        public void dispose() {
            while (true) {
                int s = state.get();
                if (s == ACQUIRING_NOT_DISPOSED) {
                    if (transition(ACQUIRING_NOT_DISPOSED, ACQUIRING_DISPOSED)) {
                        // haven't got the channel to be able to release it yet
                        // but check in operationComplete will release it
                        return;
                    }
                } else if (s == ACQUIRING_DISPOSED) {
                    if (transition(ACQUIRING_DISPOSED, CHANNEL_RELEASED)) {
                        // error emitted during channel acquisition, channel may
                        // or may not be available
                        LOGGER.debug("Channel disposed on ACQUIRING_DISPOSED");
                        closeAndReleaseChannel();
                        return;
                    }
                } else if (s == ACQUIRED_CONTENT_NOT_SUBSCRIBED) {
                    if (transition(ACQUIRED_CONTENT_NOT_SUBSCRIBED, ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED)) {
                        // response emitted but before content is subscribed.
                        // likely a Flowable response that's not yet
                        // been subscribed
                        return;
                    }
                } else if (s == ACQUIRED_CONTENT_SUBSCRIBED) {
                    if (transition(ACQUIRED_CONTENT_SUBSCRIBED, ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED)) {
                        // response emitted and content is already subscribed
                        return;
                    }
//                } else if (s == ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED) {
//                    if (transition(ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED, CHANNEL_RELEASED)) {
//                        LOGGER.debug("Channel disposed on ACQUIRED_DISPOSED_CONTENT_NOT_SUBSCRIBED");
//                        closeAndReleaseChannel();
//                        return;
//                    }
//                } else if (s == ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED) {
//                    if (transition(ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED, CHANNEL_RELEASED)) {
//                        LOGGER.debug("Channel disposed on ACQUIRED_DISPOSED_CONTENT_SUBSCRIBED");
//                        closeAndReleaseChannel();
//                        return;
//                    }
                } else {
                    return;
                }
            }
        }

        private void closeAndReleaseChannel() {
            if (channel != null) {
                channelPool.closeAndRelease(channel);
            }
        }

        @Override
        public boolean isDisposed() {
            return state.get() >= 4;
        }

        public void channelWritable(boolean writable) {
            if (requestSubscriber != null) {
                requestSubscriber.channelWritable(writable);
            }
        }

    }

    private static DefaultHttpRequest createDefaultHttpRequest(HttpRequest request) {
        final DefaultHttpRequest raw = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
                HttpMethod.valueOf(request.httpMethod().toString()), request.url().toString());

        for (HttpHeader header : request.headers()) {
            raw.headers().add(header.name(), header.value());
        }
        return raw;
    }


    /**
     * Emits HTTP response content from Netty.
     */
    private static final class ResponseContentFlowable extends Flowable implements Subscription {

        // single producer, single consumer queue
        private final SimplePlainQueue queue = new SpscLinkedArrayQueue<>(16);
        private final Subscription channelSubscription;
        private final AtomicBoolean chunkRequested = new AtomicBoolean(true);
        private final AtomicLong requested = new AtomicLong();

        // work-in-progress counter
        private final AtomicInteger wip = new AtomicInteger(1); // set to 1 to disable drain till we are ready

        // ensures one subscriber only
        private final AtomicBoolean once = new AtomicBoolean();

        private Subscriber subscriber;

        // can be non-volatile because event methods onReceivedContent,
        // chunkComplete, onError are serialized and is only written and
        // read in those event methods
        private boolean done;

        private volatile boolean cancelled = false;

        // must be volatile otherwise parts of Throwable might not be visible to drain
        // loop (or suffer from word tearing)
        private volatile Throwable err;
        private final AcquisitionListener acquisitionListener;

        ResponseContentFlowable(AcquisitionListener acquisitionListener, Subscription channelSubscription) {
            this.acquisitionListener = acquisitionListener;
            this.channelSubscription = channelSubscription;
        }

        @Override
        protected void subscribeActual(Subscriber s) {
            if (once.compareAndSet(false, true)) {
                subscriber = s;
                s.onSubscribe(this);

                acquisitionListener.contentSubscribed(this);

                // now that subscriber has been set enable the drain loop
                wip.lazySet(0);

                // we call drain because requests could have happened asynchronously before
                // wip was set to 0 (which enables the drain loop)
                drain();
            } else {
                s.onSubscribe(SubscriptionHelper.CANCELLED);
                s.onError(new IllegalStateException("Multiple subscriptions not allowed for response content"));
            }
        }

        @Override
        public void request(long n) {
            if (SubscriptionHelper.validate(n)) {
                BackpressureHelper.add(requested, n);
                drain();
            }
        }

        @Override
        public void cancel() {
            cancelled = true;
            channelSubscription.cancel();
            drain();
        }

        //
        // EVENTS - serialized
        //

        void onReceivedContent(HttpContent data) {
            if (done) {
                RxJavaPlugins.onError(new IllegalStateException("data arrived after LastHttpContent"));
                return;
            }
            if (data instanceof LastHttpContent) {
                done = true;
            }
            if (cancelled) {
                data.release();
            } else {
                queue.offer(data);
                drain();
            }
        }

        void chunkCompleted() {
            if (done) {
                return;
            }
            chunkRequested.set(false);
            drain();
        }

        void onError(Throwable cause) {
            if (done) {
                RxJavaPlugins.onError(cause);
            }
            done = true;
            err = cause;
            drain();
        }

        void channelInactive() {
            if (!done) {
                done = true;
                err = new IOException("channel inactive");
                drain();
            }
        }

        //
        // PRIVATE METHODS
        //

        private void requestChunkOfByteBufsFromUpstream() {
            channelSubscription.request(1);
        }

        private void drain() {
            // Below is a non-blocking technique to ensure serialization (in-order
            // processing) of the block inside the if statement and also to ensure
            // no race conditions exist where items on the queue would be missed.
            //
            // wip = `work in progress` and follows a naming convention in RxJava
            //
            // `missed` is a clever little trick to ensure that we only do as many
            // loops as actually required. If `drain` is called say 10 times while
            // the `drain` loop is active then we notice that there are possibly
            // extra items on the queue that arrived just after we found none left
            // (and before the method exits). We don't need to loop around ten times
            // but only once because all items will be picked up from the queue in
            // one additional polling loop.
            if (wip.getAndIncrement() == 0) {
                // need to check cancelled even if there are no requests
                if (cancelled) {
                    releaseQueue();
                    acquisitionListener.contentDone(true);
                    return;
                }
                int missed = 1;
                while (true) {
                    long r = requested.get();
                    long e = 0;
                    while (e != r) {
                        // Note that an error can shortcut the emission of content that is currently on
                        // the queue. This is probably desirable generally because it prevents work that being done
                        // downstream that might be thrown away anyway due to the error.
                        Throwable error = err;
                        if (error != null) {
                            releaseQueue();
                            channelSubscription.cancel();
                            subscriber.onError(error);
                            acquisitionListener.contentDone(true);
                            return;
                        }
                        HttpContent o = queue.poll();
                        if (o != null) {
                            e++;
                            if (emitContent(o)) {
                                return;
                            }
                        } else {
                            // queue is empty so lets see if we need to request another chunk
                            // note that we can only request one chunk at a time because the
                            // method channel.read() ignores calls if a read is pending
                            if (chunkRequested.compareAndSet(false, true)) {
                                requestChunkOfByteBufsFromUpstream();
                            }
                            break;
                        }
                        if (cancelled) {
                            releaseQueue();
                            acquisitionListener.contentDone(true);
                            return;
                        }
                    }
                    if (e > 0) {
                        // it's tempting to use the result of this method to avoid
                        // another volatile read of requested but to avoid race conditions
                        // it's essential that requested is read again AFTER wip is changed.
                        BackpressureHelper.produced(requested, e);
                    }
                    missed = wip.addAndGet(-missed);
                    if (missed == 0) {
                        return;
                    }
                }
            }
        }

        // should only be called from the drain loop
        // returns true if complete
        private boolean emitContent(HttpContent data) {
            subscriber.onNext(data.content());
            if (data instanceof LastHttpContent) {
                // release queue defensively (event serialization and the done flag
                // should mean there are no more items on the queue)
                releaseQueue();
                subscriber.onComplete();
                acquisitionListener.contentDone(false);
                return true;
            } else {
                return false;
            }
        }

        // Should only be called from the drain loop. We want to poll
        // the whole queue and release the contents one by one so we
        // need to honor the single consumer aspect of the Spsc queue
        // to ensure proper visibility of the queued items.
        private void releaseQueue() {
            HttpContent c;
            while ((c = queue.poll()) != null) {
                c.release();
            }
        }
    }

    private static final class ChannelSubscription implements Subscription {

        private final AtomicReference channel;
        private final AcquisitionListener acquisitionListener;

        ChannelSubscription(AtomicReference channel, AcquisitionListener acquisitionListener) {
            this.channel = channel;
            this.acquisitionListener = acquisitionListener;
        }

        @Override
        public void request(long n) {
            assert n == 1 : "requests must be one at a time!";
            Channel c = channel.get();
            if (c != null) {
                c.read();
            }
        }

        @Override
        public void cancel() {
            acquisitionListener.contentDone(true);
        }
    }

    private static final class HttpClientInboundHandler extends ChannelInboundHandlerAdapter {
        private SingleEmitter responseEmitter;

        // TODO does this need to be volatile
        private volatile ResponseContentFlowable contentEmitter;
        private AcquisitionListener acquisitionListener;
        //TODO may not need to be volatile, depends on eventLoop involvement
        private volatile Subscription requestContentSubscription;

        private AtomicReference channel = new AtomicReference();

        HttpClientInboundHandler() {
        }

        void setFields(SingleEmitter responseEmitter, AcquisitionListener acquisitionListener) {
            // this will be called before request has been initiated
            this.responseEmitter = responseEmitter;
            this.acquisitionListener = acquisitionListener;
            this.contentEmitter = new ResponseContentFlowable(
                    acquisitionListener, new ChannelSubscription(channel, acquisitionListener));
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            // TODO can ctx.channel() return a different object at some point in the lifecycle?
            channel.set(ctx.channel());
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            acquisitionListener.emitError(cause);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            if (contentEmitter != null) {
                // It doesn't seem like this should be possible since we set this volatile field
                // before we begin writing request content, but it can happen under high load
                contentEmitter.chunkCompleted();
            }
        }

        @Override
        public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
            acquisitionListener.channelWritable(ctx.channel().isWritable());
            super.channelWritabilityChanged(ctx);
        }

        @Override
        public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof io.netty.handler.codec.http.HttpResponse) {
                io.netty.handler.codec.http.HttpResponse response = (io.netty.handler.codec.http.HttpResponse) msg;

                if (response.decoderResult().isFailure()) {
                    exceptionCaught(ctx, response.decoderResult().cause());
                    return;
                }

                responseEmitter.onSuccess(new NettyResponse(response, contentEmitter));
            }
            else if (msg instanceof HttpContent) {
                HttpContent content = (HttpContent) msg;

                contentEmitter.onReceivedContent(content);
                if (msg instanceof LastHttpContent) {
                    acquisitionListener.contentDone(false);
                }
            } else {
                exceptionCaught(ctx, new IllegalStateException("Unexpected message type: " + msg.getClass().getName()));
            }
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            if (contentEmitter != null) {
                contentEmitter.channelInactive();
            }
            super.channelInactive(ctx);
        }

    }

    /**
     * Dump the channel pool.
     */
    public void dumpChannelPool() {
        adapter.channelPool.dump();
    }

    /**
     * The factory for creating a NettyClient.
     */
    public static class Factory implements HttpClientFactory {
        private final NettyAdapter adapter;

        /**
         * Create a Netty client factory with default settings.
         */
        public Factory() {
            this.adapter = new NettyAdapter();
        }

        /**
         * Create a Netty client factory, specifying the channel pool options.
         *
         * @param options the options to configure the channel pool
         */
        public Factory(SharedChannelPoolOptions options) {
            this(new Bootstrap(), 0, options, null);
        }

        /**
         * Create a Netty client factory, specifying the event loop group size and the
         * channel pool size.
         *
         * @param baseBootstrap
         *            a channel Bootstrap to use as a basis for channel creation
         * @param eventLoopGroupSize
         *            the number of event loop executors
         * @param channelPoolSize
         *            the number of pooled channels (connections)
         */
        public Factory(Bootstrap baseBootstrap, int eventLoopGroupSize, int channelPoolSize) {
            this(baseBootstrap.clone(), eventLoopGroupSize, channelPoolSize, null);
        }

        /**
         * Create a Netty client factory, specifying the event loop group size and the
         * channel pool size.
         *
         * @param baseBootstrap
         *          a channel Bootstrap to use as a basis for channel creation
         * @param eventLoopGroupSize
         *          the number of event loop executors
         * @param channelPoolSize
         *          the number of pooled channels (connections)
         * @param sslContext
         *          An SslContext, can be null.
         */
        public Factory(Bootstrap baseBootstrap, int eventLoopGroupSize, int channelPoolSize, SslContext sslContext) {
            this(baseBootstrap, eventLoopGroupSize, new SharedChannelPoolOptions().withPoolSize(channelPoolSize), sslContext);
        }

        /**
         * Create a Netty client factory, specifying the event loop group size and the
         * channel pool options.
         *
         * @param baseBootstrap
         *          a channel Bootstrap to use as a basis for channel creation
         * @param eventLoopGroupSize
         *          the number of event loop executors
         * @param options
         *          the options to configure the channel pool
         * @param sslContext
         *          An SslContext, can be null.
         */
        public Factory(Bootstrap baseBootstrap, int eventLoopGroupSize, SharedChannelPoolOptions options, SslContext sslContext) {
            this.adapter = new NettyAdapter(baseBootstrap.clone(), eventLoopGroupSize, options, sslContext);
        }
        
        @Override
        public HttpClient create(final HttpClientConfiguration configuration) {
            return new NettyClient(configuration, adapter);
        }
        
        @Override
        public void close() {
            adapter.shutdownGracefully().awaitUninterruptibly();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy