com.vmware.dcp.common.http.netty.NettyHttpServiceClient.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.dcp.common.http.netty.NettyHttpServiceClient.java

Source

/*
 * Copyright (c) 2014-2015 VMware, Inc. 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.  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.vmware.dcp.common.http.netty;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.ClientCookieDecoder;
import io.netty.handler.codec.http.Cookie;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;

import com.vmware.dcp.common.Operation;
import com.vmware.dcp.common.Operation.AuthorizationContext;
import com.vmware.dcp.common.Operation.CompletionHandler;
import com.vmware.dcp.common.Service.Action;
import com.vmware.dcp.common.ServiceClient;
import com.vmware.dcp.common.ServiceErrorResponse;
import com.vmware.dcp.common.ServiceErrorResponse.ErrorDetail;
import com.vmware.dcp.common.ServiceHost;
import com.vmware.dcp.common.UriUtils;
import com.vmware.dcp.common.Utils;
import com.vmware.dcp.services.common.ServiceUriPaths;
import com.vmware.dcp.services.common.authn.AuthenticationConstants;

/**
 * Asynchronous request / response client with concurrent connection management
 */
public class NettyHttpServiceClient implements ServiceClient {
    /**
     * Number of maximum parallel connections to a remote host. Idle connections are groomed but if
     * this limit is set too high, and we are talking to many remote hosts, we can possibly exceed
     * the process file descriptor limit
     */
    public static final int DEFAULT_CONNECTIONS_PER_HOST = 128;

    public static final int MAX_REQUEST_SIZE = 1024 * 1024;

    public static final Logger LOGGER = Logger.getLogger(ServiceClient.class.getName());
    private static final String ENV_VAR_NAME_HTTP_PROXY = "http_proxy";

    private URI httpProxy;
    private String userAgent;

    private NettyChannelPool sslChannelPool;
    private NettyChannelPool channelPool;

    private ScheduledExecutorService scheduledExecutor;
    private ExecutorService executor;

    private SSLContext sslContext;

    private ServiceHost host;

    private HttpRequestCallbackService callbackService;

    CookieJar cookieJar = new CookieJar();

    private boolean isStarted;

    public static ServiceClient create(String userAgent, ExecutorService executor,
            ScheduledExecutorService scheduledExecutor) throws URISyntaxException {
        return create(userAgent, executor, scheduledExecutor, null);
    }

    public static ServiceClient create(String userAgent, ExecutorService executor,
            ScheduledExecutorService scheduledExecutor, ServiceHost host) throws URISyntaxException {
        NettyHttpServiceClient sc = new NettyHttpServiceClient();
        sc.userAgent = userAgent;
        sc.executor = executor;
        sc.scheduledExecutor = scheduledExecutor;
        sc.host = host;
        sc.channelPool = new NettyChannelPool(executor);
        String proxy = System.getenv(ENV_VAR_NAME_HTTP_PROXY);
        if (proxy != null) {
            sc.setHttpProxy(new URI(proxy));
        }

        return sc.setConnectionLimitPerHost(DEFAULT_CONNECTIONS_PER_HOST);
    }

    private String buildThreadTag() {
        if (this.host != null) {
            return UriUtils.extendUri(this.host.getUri(), "netty-client").toString();
        }
        return getClass().getSimpleName() + ":" + Utils.getNowMicrosUtc();
    }

    @Override
    public void start() {
        synchronized (this) {
            if (this.isStarted) {
                return;
            }
            this.isStarted = true;
        }

        this.channelPool.setThreadTag(buildThreadTag());
        this.channelPool.setThreadCount(Utils.DEFAULT_THREAD_COUNT / 2);
        this.channelPool.start();

        if (this.sslContext != null) {
            this.sslChannelPool = new NettyChannelPool(this.executor);
            this.sslChannelPool.setThreadTag(buildThreadTag());
            this.sslChannelPool.setThreadCount(2);
            this.sslChannelPool.setSSLContext(this.sslContext);
            this.sslChannelPool.start();
        }

        if (this.host != null) {
            Operation startCallbackPost = Operation
                    .createPost(UriUtils.buildUri(this.host, ServiceUriPaths.CORE_CALLBACKS));
            this.callbackService = new HttpRequestCallbackService();
            this.host.startService(startCallbackPost, this.callbackService);
        }
    }

    @Override
    public void stop() {
        this.channelPool.stop();
        if (this.sslChannelPool != null) {
            this.sslChannelPool.stop();
        }
        this.isStarted = false;
    }

    public ServiceClient setHttpProxy(URI proxy) {
        this.httpProxy = proxy;
        return this;
    }

    @Override
    public void send(Operation op) {
        sendSingleRequest(op);
    }

    private void sendSingleRequest(Operation op) {
        Operation clone = clone(op);
        if (clone == null) {
            return;
        }

        setCookies(clone);

        // Try to deliver operation to in-process service host
        if (!op.isRemote()) {
            if (this.host != null && this.host.handleRequest(clone)) {
                return;
            }
        }

        addAuthorizationContextCookie(clone);

        sendRemote(clone);
    }

    private void addAuthorizationContextCookie(Operation op) {
        AuthorizationContext ctx = op.getAuthorizationContext();
        if (ctx == null) {
            return;
        }

        String token = ctx.getToken();
        if (token == null) {
            return;
        }

        Map<String, String> cookies = op.getCookies();
        if (cookies == null) {
            cookies = new HashMap<>();
        }

        cookies.put(AuthenticationConstants.DCP_JWT_COOKIE, ctx.getToken());
        op.setCookies(cookies);
    }

    private void sendRemote(Operation op) {
        connect(op);
    }

    /**
     * Sends a request using the asynchronous HTTP pattern, allowing greater connection re-use. The
     * send method creates a lightweight service that serves as the callback URI for receiving the
     * completion status from the remote node. The callback URI is set as a header on the out bound
     * request.
     *
     * The remote node, if it detects the presence of the callback location header, will create a
     * new, local request, send it to the local service, and when that local request completes, it
     * will issues a PATCH to the callback service on this node. The original request will then be
     * completed and the client will see the response.
     *
     * The end result is that a TCP connection is not "blocked" while we wait for the remote node to
     * return a response (similar to the benefits of the asynchronous REST pattern for services that
     * implement it)
     */
    @Override
    public void sendWithCallback(Operation op) {
        sendWithCallbackSingleRequest(op);
    }

    private void sendWithCallbackSingleRequest(Operation req) {
        if (req.getExpirationMicrosUtc() == 0) {
            req.setExpiration(Utils.getNowMicrosUtc() + this.host.getOperationTimeoutMicros());
        }

        Operation op = clone(req);
        if (op == null) {
            return;
        }
        if (!req.isRemote() && this.host != null && this.host.handleRequest(op)) {
            // request was accepted by an in-process service host
            return;
        }

        // Queue operation, then send it to remote target. At some point later the remote host will send a PATCH
        // to the callback service to complete this pending operation
        URI u = this.callbackService.queueUntilCallback(op);
        Operation remoteOp = op.clone();
        remoteOp.setRequestCallbackLocation(u);
        remoteOp.setCompletion((o, e) -> {
            if (e != null) {
                // we do not remove the operation from the callback service, it will be removed on next maintenance
                op.setExpiration(0).fail(e);
                return;
            }
            // release reference to body, not needed
            op.setBody(null);
        });
        sendRemote(remoteOp);
    }

    private void setCookies(Operation clone) {
        // Extract cookies into cookie jar, regardless of where this operation ends up being handled.
        clone.nestCompletion((o, e) -> {
            if (e != null) {
                o.fail(e);
                return;
            }

            handleSetCookieHeaders(o);
            o.complete();
        });

        if (this.cookieJar.isEmpty()) {
            return;
        }

        // Set cookies for outbound request
        clone.setCookies(this.cookieJar.list(clone.getUri()));
    }

    private void handleSetCookieHeaders(Operation op) {
        String value = op.getResponseHeader(Operation.SET_COOKIE_HEADER);
        if (value == null) {
            return;
        }

        Cookie cookie = ClientCookieDecoder.decode(value);
        if (cookie == null) {
            return;
        }

        this.cookieJar.add(op.getUri(), cookie);
    }

    private void connect(Operation op) {
        URI uri = this.httpProxy == null ? op.getUri() : this.httpProxy;
        if (op.getUri().getHost().equals(ServiceHost.LOCAL_HOST)) {
            uri = op.getUri();
        }

        op.nestCompletion((o, e) -> {
            if (e != null) {
                op.setBody(ServiceErrorResponse.create(e, Operation.STATUS_CODE_BAD_REQUEST,
                        EnumSet.of(ErrorDetail.SHOULD_RETRY)));
                fail(e, op);
                return;
            }
            sendRequest(op);
        });

        int port = uri.getPort();
        NettyChannelPool pool = this.channelPool;

        if (uri.getScheme().equals(UriUtils.HTTP_SCHEME)) {
            if (port == -1) {
                port = UriUtils.HTTP_DEFAULT_PORT;
            }
        } else if (uri.getScheme().equals(UriUtils.HTTPS_SCHEME)) {
            if (port == -1) {
                port = UriUtils.HTTPS_DEFAULT_PORT;
            }
            pool = this.sslChannelPool;
        }

        pool.connectOrReuse(uri.getHost(), port, false, op);
    }

    private void sendRequest(Operation op) {
        if (!checkScheme(op)) {
            return;
        }

        try {
            byte[] body = Utils.encodeBody(op);
            String pathAndQuery;
            String path = op.getUri().getPath();
            String query = op.getUri().getQuery();
            path = path == null || path.isEmpty() ? "/" : path;
            if (query != null) {
                pathAndQuery = path + "?" + query;
            } else {
                pathAndQuery = path;
            }

            if (this.httpProxy != null) {
                pathAndQuery = op.getUri().toString();
            }

            HttpRequest request = null;
            HttpMethod method = HttpMethod.valueOf(op.getAction().toString());

            if (body == null || body.length == 0) {
                request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, pathAndQuery);
            } else {
                ByteBuf content = Unpooled.wrappedBuffer(body);
                request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, pathAndQuery, content);
            }

            for (Entry<String, String> nameValue : op.getRequestHeaders().entrySet()) {
                request.headers().set(nameValue.getKey(), nameValue.getValue());
            }

            request.headers().set(HttpHeaderNames.CONTENT_LENGTH, Long.toString(op.getContentLength()));
            request.headers().set(HttpHeaderNames.CONTENT_TYPE, op.getContentType());
            request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);

            if (op.getContextId() != null) {
                request.headers().set(Operation.CONTEXT_ID_HEADER, op.getContextId());
            }

            if (op.getReferer() != null) {
                request.headers().set(Operation.REFERER_HEADER, op.getReferer().toString());
            }

            if (op.getCookies() != null) {
                String header = CookieJar.encodeCookies(op.getCookies());
                request.headers().set(HttpHeaderNames.COOKIE, header);
            }

            request.headers().set(HttpHeaderNames.USER_AGENT, this.userAgent);

            request.headers().set(HttpHeaderNames.ACCEPT, Operation.MEDIA_TYPE_APPLICATION_JSON);

            request.headers().set(HttpHeaderNames.HOST,
                    op.getUri().getHost() + ((op.getUri().getPort() != -1) ? (":" + op.getUri().getPort()) : ""));

            op.nestCompletion((o, e) -> {
                if (e != null) {
                    fail(e, op);
                    return;
                }
                // After request is sent control is transferred to the
                // NettyHttpServerResponseHandler. The response handler will nest completions
                // and call complete() when response is received, which will invoke this completion
                op.complete();
            });

            op.getSocketContext().writeHttpRequest(request);
        } catch (Throwable e) {
            op.setBody(ServiceErrorResponse.create(e, Operation.STATUS_CODE_BAD_REQUEST,
                    EnumSet.of(ErrorDetail.SHOULD_RETRY)));
            fail(e, op);
        }
    }

    private boolean checkScheme(Operation op) {
        if (op.getUri().getScheme().equals(UriUtils.HTTP_SCHEME)) {
            return true;
        }

        String scheme = op.getUri().getScheme();

        if (scheme.equals(UriUtils.HTTPS_SCHEME)) {
            if (this.getSSLContext() == null) {
                fail(new IllegalArgumentException(
                        "HTTPS not enabled, set SSL context before starting client:" + op.getUri().getScheme()),
                        op);
                return false;
            } else {
                return true;
            }
        }

        fail(new IllegalArgumentException("scheme not supported:" + op.getUri().getScheme()), op);
        return false;
    }

    private void fail(Throwable e, Operation op) {
        boolean isRetryRequested = op.getRetryCount() > 0 && op.decrementRetriesRemaining() >= 0;

        NettyChannelContext ctx = (NettyChannelContext) op.getSocketContext();
        NettyChannelPool pool = this.channelPool;

        if (this.sslChannelPool != null && this.sslChannelPool.isContextInUse(ctx)) {
            pool = this.sslChannelPool;
        }
        pool.returnOrClose(ctx, !op.isKeepAlive());
        op.setSocketContext(null);

        if (this.scheduledExecutor.isShutdown()) {
            op.fail(new CancellationException());
            return;
        }

        if (op.getStatusCode() >= Operation.STATUS_CODE_SERVER_FAILURE_THRESHOLD) {
            isRetryRequested = false;
        }

        if (!isRetryRequested) {
            LOGGER.warning(String.format("(%d) Send of %d, from %s to %s failed with %s",
                    pool.getPendingRequestCount(op), op.getId(), op.getReferer(), op.getUri(), e.toString()));
            op.fail(e);
            return;
        }

        LOGGER.info(String.format("(%d) Retry %d of request %d from %s to %s due to %s",
                pool.getPendingRequestCount(op), op.getRetryCount() - op.getRetriesRemaining(), op.getId(),
                op.getReferer(), op.getUri(), e.toString()));

        int delaySeconds = op.getRetryCount() - op.getRetriesRemaining();

        op.setStatusCode(Operation.STATUS_CODE_OK);
        this.scheduledExecutor.schedule(() -> {
            connect(op);
        }, delaySeconds, TimeUnit.SECONDS);
    }

    private static Operation clone(Operation op) {

        Throwable e = null;
        if (op == null) {
            throw new IllegalArgumentException("Operation is required");
        }

        CompletionHandler c = op.getCompletion();

        if (op.getUri() == null) {
            e = new IllegalArgumentException("Uri is required");
        }

        if (op.getAction() == null) {
            e = new IllegalArgumentException("Action is required");
        }

        if (op.getReferer() == null) {
            e = new IllegalArgumentException("Referer is required");
        }

        boolean needsBody = op.getAction() != Action.GET && op.getAction() != Action.DELETE
                && op.getAction() != Action.POST;

        if (!op.hasBody() && needsBody) {
            e = new IllegalArgumentException("Body is required");
        }

        if (e != null) {
            if (c != null) {
                c.handle(op, e);
                return null;
            } else {
                throw new RuntimeException(e);
            }
        }
        return op.clone();
    }

    @Override
    public void handleMaintenance(Operation op) {
        if (this.sslChannelPool != null) {
            this.sslChannelPool.handleMaintenance(Operation.createPost(op.getUri()));
        }
        this.channelPool.handleMaintenance(op);
    }

    @Override
    public ServiceClient setConnectionLimitPerHost(int limit) {
        this.channelPool.setConnectionLimitPerHost(limit);
        if (this.sslChannelPool != null) {
            this.sslChannelPool.setConnectionLimitPerHost(limit);
        }
        return this;
    }

    @Override
    public int getConnectionLimitPerHost() {
        return this.channelPool.getConnectionLimitPerHost();
    }

    @Override
    public ServiceClient setSSLContext(SSLContext context) {
        this.sslContext = context;
        return this;
    }

    @Override
    public SSLContext getSSLContext() {
        return this.sslContext;
    }

}