Java tutorial
/* * 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; } }