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

software.amazon.awssdk.http.nio.netty.internal.RunnableRequest Maven / Gradle / Ivy

Go to download

A single bundled dependency that includes all service and dependent JARs with third-party libraries relocated to different namespaces.

There is a newer version: 2.5.20
Show newest version
/*
 * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal;

import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.REQUEST_CONTEXT_KEY;
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.RESPONSE_COMPLETE_KEY;

import com.typesafe.netty.http.HttpStreamsClientHandler;
import com.typesafe.netty.http.StreamedHttpRequest;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutException;
import io.netty.handler.timeout.WriteTimeoutHandler;
import io.netty.util.concurrent.Future;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.Protocol;
import software.amazon.awssdk.http.async.AbortableRunnable;
import software.amazon.awssdk.http.nio.netty.internal.http2.Http2ToHttpInboundAdapter;
import software.amazon.awssdk.http.nio.netty.internal.http2.HttpToHttp2OutboundAdapter;
import software.amazon.awssdk.http.nio.netty.internal.utils.ChannelUtils;
import software.amazon.awssdk.utils.FunctionalUtils.UnsafeRunnable;

@SdkInternalApi
public final class RunnableRequest implements AbortableRunnable {

    private static final Logger log = LoggerFactory.getLogger(RunnableRequest.class);
    private final RequestContext context;
    private volatile Channel channel;

    public RunnableRequest(RequestContext context) {
        this.context = context;
    }

    @Override
    public void run() {
        context.channelPool().acquire().addListener((Future channelFuture) -> {
            if (channelFuture.isSuccess()) {
                try {
                    channel = channelFuture.getNow();
                    channel.attr(REQUEST_CONTEXT_KEY).set(context);
                    channel.attr(RESPONSE_COMPLETE_KEY).set(false);
                    makeRequest(context.nettyRequest());
                } catch (Exception e) {
                    handleFailure(() -> "Failed to make request to " + endpoint(), e);
                }
            } else {
                handleFailure(() -> "Failed to create connection to " + endpoint(), channelFuture.cause());
            }
        });
    }

    @Override
    public void abort() {
        if (channel != null) {
            closeAndRelease(channel);
        }
    }

    private void makeRequest(HttpRequest request) {
        log.debug("Writing request: {}", request);

        runOrFail(() -> {
            configurePipeline();
            writeRequest(request);
        },
            () -> "Failed to make request to " + endpoint());
    }

    private void configurePipeline() {
        Protocol protocol = ChannelAttributeKey.getProtocolNow(channel);
        if (Protocol.HTTP2.equals(protocol)) {
            channel.pipeline().addLast(new Http2ToHttpInboundAdapter());
            channel.pipeline().addLast(new HttpToHttp2OutboundAdapter());
        } else if (!Protocol.HTTP1_1.equals(protocol)) {
            throw new RuntimeException("Unknown protocol: " + protocol);
        }
        channel.config().setOption(ChannelOption.AUTO_READ, false);
        channel.pipeline().addLast(new HttpStreamsClientHandler());
        channel.pipeline().addLast(new ResponseHandler());
    }

    private void writeRequest(HttpRequest request) {
        channel.pipeline().addFirst(new WriteTimeoutHandler(context.configuration().writeTimeoutMillis(),
                                                            TimeUnit.MILLISECONDS));

        channel.writeAndFlush(new StreamedRequest(request, context.sdkRequestProvider(), channel))
               .addListener(wireCall -> {
                   // Done writing so remove the idle write timeout handler
                   ChannelUtils.removeIfExists(channel.pipeline(), WriteTimeoutHandler.class);
                   if (wireCall.isSuccess()) {
                       // Starting read so add the idle read timeout handler, removed when channel is released
                       channel.pipeline().addFirst(new ReadTimeoutHandler(context.configuration().readTimeoutMillis(),
                                                                          TimeUnit.MILLISECONDS));
                       // Auto-read is turned off so trigger an explicit read to give control to HttpStreamsClientHandler
                       channel.read();
                   } else {
                       handleFailure(() -> "Failed to make request to " + endpoint(), wireCall.cause());
                   }
               });
    }

    private URI endpoint() {
        return context.sdkRequest().getUri();
    }

    private void runOrFail(Runnable runnable, Supplier errorMsgSupplier) {
        try {
            runnable.run();
        } catch (Exception e) {
            handleFailure(errorMsgSupplier, e);
        }
    }

    private void handleFailure(Supplier msg, Throwable cause) {
        log.error(msg.get(), cause);
        Throwable throwable = decorateException(cause);
        runAndLogError("Exception thrown from AsyncResponseHandler",
            () -> context.handler().exceptionOccurred(throwable));
        if (channel != null) {
            runAndLogError("Unable to release channel back to the pool.",
                () -> closeAndRelease(channel));
        }
    }

    private Throwable decorateException(Throwable originalCause) {
        if (isAcquireTimeoutException(originalCause)) {
            return new Throwable(getMessageForAcquireTimeoutException(), originalCause);
        } else if (isTooManyPendingAcquiresException(originalCause)) {
            return new Throwable(getMessageForTooManyAcquireOperationsError(), originalCause);
        } else if (originalCause instanceof ReadTimeoutException) {
            // wrap it with IOException to be retried by SDK
            return new IOException("Read timed out", originalCause);
        } else if (originalCause instanceof WriteTimeoutException) {
            return new IOException("Write timed out", originalCause);
        }

        return originalCause;
    }

    private boolean isAcquireTimeoutException(Throwable originalCause) {
        return originalCause instanceof TimeoutException && originalCause.getMessage().contains("Acquire operation took longer");
    }

    private boolean isTooManyPendingAcquiresException(Throwable originalCause) {
        return originalCause instanceof IllegalStateException &&
               originalCause.getMessage().contains("Too many outstanding acquire operations");
    }

    private String getMessageForAcquireTimeoutException() {
        return "Acquire operation took longer than the configured maximum time. This indicates that a request cannot get a "
                + "connection from the pool within the specified maximum time. This can be due to high request rate.\n"

                + "Consider taking any of the following actions to mitigate the issue: increase max connections, "
                + "increase acquire timeout, or slowing the request rate.\n"

                + "Increasing the max connections can increase client throughput (unless the network interface is already "
                + "fully utilized), but can eventually start to hit operation system limitations on the number of file "
                + "descriptors used by the process. If you already are fully utilizing your network interface or cannot "
                + "further increase your connection count, increasing the acquire timeout gives extra time for requests to "
                + "acquire a connection before timing out. If the connections doesn't free up, the subsequent requests "
                + "will still timeout.\n"

                + "If the above mechanisms are not able to fix the issue, try smoothing out your requests so that large "
                + "traffic bursts cannot overload the client, being more efficient with the number of times you need to "
                + "call AWS, or by increasing the number of hosts sending requests.";
    }

    private String getMessageForTooManyAcquireOperationsError() {
        return "Maximum pending connection acquisitions exceeded. The request rate is too high for the client to keep up.\n"

                + "Consider taking any of the following actions to mitigate the issue: increase max connections, "
                + "increase max pending acquire count, decrease pool lease timeout, or slowing the request rate.\n"

                + "Increasing the max connections can increase client throughput (unless the network interface is already "
                + "fully utilized), but can eventually start to hit operation system limitations on the number of file "
                + "descriptors used by the process. If you already are fully utilizing your network interface or cannot "
                + "further increase your connection count, increasing the pending acquire count allows extra requests to be "
                + "buffered by the client, but can cause additional request latency and higher memory usage. If your request"
                + " latency or memory usage is already too high, decreasing the lease timeout will allow requests to fail "
                + "more quickly, reducing the number of pending connection acquisitions, but likely won't decrease the total "
                + "number of failed requests.\n"

                + "If the above mechanisms are not able to fix the issue, try smoothing out your requests so that large "
                + "traffic bursts cannot overload the client, being more efficient with the number of times you need to call "
                + "AWS, or by increasing the number of hosts sending requests.";
    }

    private static void closeAndRelease(Channel channel) {
        RequestContext requestCtx = channel.attr(REQUEST_CONTEXT_KEY).get();
        channel.close().addListener(ignored -> requestCtx.channelPool().release(channel));
    }

    /**
     * Runs a given {@link UnsafeRunnable} and logs an error without throwing.
     *
     * @param errorMsg Message to log with exception thrown.
     * @param runnable Action to perform.
     */
    private static void runAndLogError(String errorMsg, UnsafeRunnable runnable) {
        try {
            runnable.run();
        } catch (Exception e) {
            log.error(errorMsg, e);
        }
    }

    /**
     * Just delegates to {@link HttpRequest} for all methods.
     */
    static class DelegateHttpRequest implements HttpRequest {
        protected final HttpRequest request;

        DelegateHttpRequest(HttpRequest request) {
            this.request = request;
        }

        @Override
        public HttpRequest setMethod(HttpMethod method) {
            this.request.setMethod(method);
            return this;
        }

        @Override
        public HttpRequest setUri(String uri) {
            this.request.setUri(uri);
            return this;
        }

        @Override
        public HttpMethod getMethod() {
            return this.request.method();
        }

        @Override
        public HttpMethod method() {
            return request.method();
        }

        @Override
        public String getUri() {
            return this.request.uri();
        }

        @Override
        public String uri() {
            return request.uri();
        }

        @Override
        public HttpVersion getProtocolVersion() {
            return this.request.protocolVersion();
        }

        @Override
        public HttpVersion protocolVersion() {
            return request.protocolVersion();
        }

        @Override
        public HttpRequest setProtocolVersion(HttpVersion version) {
            this.request.setProtocolVersion(version);
            return this;
        }

        @Override
        public HttpHeaders headers() {
            return this.request.headers();
        }

        @Override
        public DecoderResult getDecoderResult() {
            return this.request.decoderResult();
        }

        @Override
        public DecoderResult decoderResult() {
            return request.decoderResult();
        }

        @Override
        public void setDecoderResult(DecoderResult result) {
            this.request.setDecoderResult(result);
        }

        @Override
        public String toString() {
            return this.getClass().getName() + "(" + this.request.toString() + ")";
        }
    }

    /**
     * Decorator around {@link StreamedHttpRequest} to adapt a publisher of {@link ByteBuffer} (i.e. {@link
     * software.amazon.awssdk.http.async.SdkHttpRequestProvider}) to a publisher of {@link HttpContent}.
     * 

* This publisher also prevents the adapted publisher from publishing more content to the subscriber than * the specified 'Content-Length' of the request. */ private static class StreamedRequest extends DelegateHttpRequest implements StreamedHttpRequest { private final Publisher publisher; private final Channel channel; private final Optional requestContentLength; private long written = 0L; private boolean done; private Subscription subscription; StreamedRequest(HttpRequest request, Publisher publisher, Channel channel) { super(request); this.publisher = publisher; this.channel = channel; this.requestContentLength = contentLength(request); } @Override public void subscribe(Subscriber subscriber) { publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { StreamedRequest.this.subscription = subscription; subscriber.onSubscribe(subscription); } @Override public void onNext(ByteBuffer byteBuffer) { if (done) { return; } int newLimit = clampedBufferLimit(byteBuffer.remaining()); byteBuffer.limit(newLimit); ByteBuf buffer = channel.alloc().buffer(byteBuffer.remaining()); buffer.writeBytes(byteBuffer); HttpContent content = new DefaultHttpContent(buffer); subscriber.onNext(content); written += newLimit; if (!shouldContinuePublishing()) { done = true; subscription.cancel(); subscriber.onComplete(); } } @Override public void onError(Throwable t) { if (!done) { done = true; subscriber.onError(t); } } @Override public void onComplete() { if (!done) { done = true; subscriber.onComplete(); } } }); } private int clampedBufferLimit(int bufLen) { return requestContentLength.map(cl -> (int) Math.min(cl - written, bufLen) ).orElse(bufLen); } private boolean shouldContinuePublishing() { return requestContentLength.map(cl -> written < cl).orElse(true); } private static Optional contentLength(HttpRequest request) { String value = request.headers().get("Content-Length"); if (value != null) { try { return Optional.of(Long.parseLong(value)); } catch (NumberFormatException e) { log.warn("Unable to parse 'Content-Length' header. Treating it as non existent."); } } return Optional.empty(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy