Java tutorial
/* * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.iep.http; import com.netflix.config.ConfigurationManager; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.impl.Preconditions; import com.netflix.spectator.sandbox.HttpLogEntry; import io.reactivex.netty.client.CompositePoolLimitDeterminationStrategy; import io.reactivex.netty.client.MaxConnectionsBasedStrategy; import io.reactivex.netty.client.PoolLimitDeterminationStrategy; import org.apache.commons.configuration.Configuration; import rx.functions.Actions; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.timeout.ReadTimeoutException; import io.reactivex.netty.RxNetty; import io.reactivex.netty.pipeline.PipelineConfigurator; import io.reactivex.netty.pipeline.PipelineConfiguratorComposite; import io.reactivex.netty.pipeline.ssl.DefaultFactories; import io.reactivex.netty.protocol.http.client.HttpClient; import io.reactivex.netty.protocol.http.client.HttpClientBuilder; import io.reactivex.netty.protocol.http.client.HttpClientPipelineConfigurator; import io.reactivex.netty.protocol.http.client.HttpClientRequest; import io.reactivex.netty.protocol.http.client.HttpClientResponse; import rx.Observable; import rx.functions.Action1; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Singleton; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.GZIPOutputStream; /** * Helper for some simple uses of rxnetty with eureka. Only intended for use within the spectator * plugin. */ @Singleton public final class RxHttp { private static final Logger LOGGER = LoggerFactory.getLogger(RxHttp.class); private static final String APPLICATION_JSON = "application/json"; private static final int MIN_COMPRESS_SIZE = 512; private static final AtomicInteger NEXT_THREAD_ID = new AtomicInteger(0); private final ConcurrentHashMap<String, PoolLimitDeterminationStrategy> poolLimits = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Server, HttpClient<ByteBuf, ByteBuf>> clients = new ConcurrentHashMap<>(); private ScheduledExecutorService executor; private final Configuration config; private final ServerRegistry serverRegistry; /** * Create a new instance using the specified server registry. Calls using client side * load-balancing (niws:// or vip:// URIs) need a server registry to lookup the set of servers * to balance over. * * @deprecated Use {@link RxHttp#RxHttp(Configuration, ServerRegistry)} instead. */ @Deprecated public RxHttp(ServerRegistry serverRegistry) { this(ConfigurationManager.getConfigInstance(), serverRegistry); } /** * Create a new instance using the specified server registry. Calls using client side * load-balancing (niws:// or vip:// URIs) need a server registry to lookup the set of servers * to balance over. */ @Inject public RxHttp(Configuration config, ServerRegistry serverRegistry) { this.config = config; this.serverRegistry = serverRegistry; } /** * Setup the background tasks for cleaning up connections. */ @PostConstruct public void start() { LOGGER.info("starting up backround cleanup threads"); executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "spectator-rxhttp-" + NEXT_THREAD_ID.getAndIncrement()); t.setDaemon(true); return t; } }); Runnable task = new Runnable() { @Override public void run() { try { LOGGER.debug("executing cleanup for {} clients", clients.size()); for (Map.Entry<Server, HttpClient<ByteBuf, ByteBuf>> entry : clients.entrySet()) { final Server s = entry.getKey(); if (s.isRegistered() && !serverRegistry.isStillAvailable(s)) { LOGGER.debug("cleaning up client for {}", s); clients.remove(s); entry.getValue().shutdown(); } } LOGGER.debug("cleanup complete with {} clients remaining", clients.size()); } catch (Exception e) { LOGGER.warn("connection cleanup task failed", e); } } }; final long cleanupFreq = Spectator.config().getLong("spectator.http.cleanupFrequency", 60); executor.scheduleWithFixedDelay(task, 0L, cleanupFreq, TimeUnit.SECONDS); } /** * Shutdown all connections that are currently open. */ @PreDestroy public void stop() { LOGGER.info("shutting down backround cleanup threads"); executor.shutdown(); for (HttpClient<ByteBuf, ByteBuf> client : clients.values()) { client.shutdown(); } } private static HttpClientRequest<ByteBuf> compress(ClientConfig clientCfg, HttpClientRequest<ByteBuf> req, byte[] entity) { if (entity.length >= MIN_COMPRESS_SIZE && clientCfg.gzipEnabled()) { req.withHeader(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) { gzip.write(entity); } catch (IOException e) { // This isn't expected to occur throw new RuntimeException("failed to gzip request payload", e); } req.withContent(baos.toByteArray()); } else { req.withContent(entity); } return req; } /** Create a log entry for an rxnetty request. */ public static HttpLogEntry create(HttpClientRequest<ByteBuf> req) { HttpLogEntry entry = new HttpLogEntry().withMethod(req.getMethod().name()) .withRequestUri(URI.create(req.getUri())) .withRequestContentLength(req.getHeaders().getContentLength(-1)); for (Map.Entry<String, String> h : req.getHeaders().entries()) { entry.withRequestHeader(h.getKey(), h.getValue()); } return entry; } private static HttpLogEntry create(ClientConfig cfg, HttpClientRequest<ByteBuf> req) { return create(req).withClientName(cfg.name()).withOriginalUri(cfg.originalUri()) .withMaxAttempts(cfg.numRetries() + 1); } private static void update(HttpLogEntry entry, HttpClientResponse<ByteBuf> res) { int code = res.getStatus().code(); boolean canRetry = (code == 429 || code >= 500); entry.mark("received-response").withStatusCode(code).withStatusReason(res.getStatus().reasonPhrase()) .withResponseContentLength(res.getHeaders().getContentLength(-1)).withCanRetry(canRetry); for (Map.Entry<String, String> h : res.getHeaders().entries()) { entry.withResponseHeader(h.getKey(), h.getValue()); } } private void update(HttpLogEntry entry, Throwable t) { boolean canRetry = (t instanceof ConnectException || t instanceof ReadTimeoutException); entry.mark("received-error").withException(t).withCanRetry(canRetry); } /** * Perform a GET request. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> get(String uri) { return submit(HttpClientRequest.createGet(uri)); } /** * Perform a GET request. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> get(URI uri) { return submit(HttpClientRequest.createGet(uri.toString())); } /** * Perform a GET request expecting a JSON response. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> getJson(String uri) { return getJson(URI.create(uri)); } /** * Perform a GET request expecting a JSON response. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> getJson(URI uri) { final HttpClientRequest<ByteBuf> req = HttpClientRequest.createGet(uri.toString()) .withHeader(HttpHeaders.Names.ACCEPT, APPLICATION_JSON); return submit(req); } /** * Perform a POST request. * * @param uri * Location to send the data. * @param contentType * MIME type for the request payload. * @param entity * Data to send. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> post(URI uri, String contentType, byte[] entity) { final HttpClientRequest<ByteBuf> req = HttpClientRequest.createPost(uri.toString()) .withHeader(HttpHeaders.Names.CONTENT_TYPE, contentType); return submit(req, entity); } /** * Perform a POST request with {@code Content-Type: application/json}. * * @param uri * Location to send the data. * @param entity * Data to send. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> postJson(URI uri, byte[] entity) { final HttpClientRequest<ByteBuf> req = HttpClientRequest.createPost(uri.toString()) .withHeader(HttpHeaders.Names.CONTENT_TYPE, APPLICATION_JSON) .withHeader(HttpHeaders.Names.ACCEPT, APPLICATION_JSON); return submit(req, entity); } /** * Perform a POST request with {@code Content-Type: application/json}. * * @param uri * Location to send the data. * @param entity * Data to send. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> postJson(URI uri, String entity) { return postJson(uri, getBytes(entity)); } /** * Perform a POST request with form data. The body will be extracted from the query string * in the URI. * * @param uri * Location to send the data. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> postForm(URI uri) { Preconditions.checkNotNull(uri.getRawQuery(), "uri.query"); byte[] entity = getBytes(uri.getRawQuery()); return post(uri, HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED, entity); } /** * Perform a PUT request. * * @param uri * Location to send the data. * @param contentType * MIME type for the request payload. * @param entity * Data to send. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> put(URI uri, String contentType, byte[] entity) { final HttpClientRequest<ByteBuf> req = HttpClientRequest.createPut(uri.toString()) .withHeader(HttpHeaders.Names.CONTENT_TYPE, contentType); return submit(req, entity); } /** * Perform a PUT request with {@code Content-Type: application/json}. * * @param uri * Location to send the data. * @param entity * Data to send. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> putJson(URI uri, byte[] entity) { final HttpClientRequest<ByteBuf> req = HttpClientRequest.createPut(uri.toString()) .withHeader(HttpHeaders.Names.CONTENT_TYPE, APPLICATION_JSON) .withHeader(HttpHeaders.Names.ACCEPT, APPLICATION_JSON); return submit(req, entity); } /** * Perform a PUT request with {@code Content-Type: application/json}. * * @param uri * Location to send the data. * @param entity * Data to send. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> putJson(URI uri, String entity) { return putJson(uri, getBytes(entity)); } /** * Perform a DELETE request. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> delete(String uri) { return submit(HttpClientRequest.createDelete(uri)); } /** * Perform a DELETE request. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> delete(URI uri) { return submit(HttpClientRequest.createDelete(uri.toString())); } /** * Perform a DELETE request expecting a JSON response. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> deleteJson(String uri) { return deleteJson(URI.create(uri)); } /** * Perform a DELETE request expecting a JSON response. * * @param uri * Location to send the request. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> deleteJson(URI uri) { final HttpClientRequest<ByteBuf> req = HttpClientRequest.createDelete(uri.toString()) .withHeader(HttpHeaders.Names.ACCEPT, APPLICATION_JSON); return submit(req); } /** * Submit an HTTP request. * * @param req * Request to execute. Note the content should be passed in separately not already passed * to the request. The RxNetty request object doesn't provide a way to get the content * out via the public api, so we need to keep it separate in case a new request object must * be created. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> submit(HttpClientRequest<ByteBuf> req) { return submit(req, (byte[]) null); } /** * Submit an HTTP request. * * @param req * Request to execute. Note the content should be passed in separately not already passed * to the request. The RxNetty request object doesn't provide a way to get the content * out via the public api, so we need to keep it separate in case a new request object must * be created. * @param entity * Content data or null if no content is needed for the request body. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> submit(HttpClientRequest<ByteBuf> req, String entity) { return submit(req, (entity == null) ? null : getBytes(entity)); } /** * Submit an HTTP request. * * @param req * Request to execute. Note the content should be passed in separately not already passed * to the request. The RxNetty request object doesn't provide a way to get the content * out via the public api, so we need to keep it separate in case a new request object must * be created. * @param entity * Content data or null if no content is needed for the request body. * @return * Observable with the response of the request. */ public Observable<HttpClientResponse<ByteBuf>> submit(HttpClientRequest<ByteBuf> req, byte[] entity) { final URI uri = URI.create(req.getUri()); final ClientConfig clientCfg = ClientConfig.fromUri(config, uri); final List<Server> servers = getServers(clientCfg); final String reqUri = clientCfg.relativeUri(); final HttpClientRequest<ByteBuf> newReq = copy(req, reqUri); final HttpClientRequest<ByteBuf> finalReq = (entity == null) ? newReq : compress(clientCfg, newReq, entity); return execute(clientCfg, servers, finalReq); } /** * Execute an HTTP request. * * @param clientCfg * Configuration settings for the request. * @param servers * List of servers to attempt. The servers will be tried in order until a successful * response or a non-retriable error occurs. For status codes 429 and 503 the * {@code Retry-After} header is honored. Otherwise back-off will be based on the * {@code RetryDelay} config setting. * @param req * Request to execute. * @return * Observable with the response of the request. */ Observable<HttpClientResponse<ByteBuf>> execute(final ClientConfig clientCfg, final List<Server> servers, final HttpClientRequest<ByteBuf> req) { final HttpLogEntry entry = create(clientCfg, req); if (servers.isEmpty()) { final String msg = "empty server list for client " + clientCfg.name(); return Observable.error(new IllegalStateException(msg)); } if (clientCfg.gzipEnabled()) { req.withHeader(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); } final RequestContext context = new RequestContext(this, entry, req, clientCfg, servers.get(0)); final long backoffMillis = clientCfg.retryDelay(); Observable<HttpClientResponse<ByteBuf>> observable = execute(context).flatMap(new RedirectHandler(context)); for (int i = 1; i < servers.size(); ++i) { final RequestContext ctxt = context.withServer(servers.get(i)); final long delay = backoffMillis << (i - 1); final int attempt = i + 1; observable = observable.flatMap(new RedirectHandler(ctxt)) .flatMap(new StatusRetryHandler(ctxt, attempt, delay)) .onErrorResumeNext(new ErrorRetryHandler(ctxt, attempt)); } return observable; } /** * Execute an HTTP request. * * @param context * Context associated with the request. * @return * Observable with the response of the request. */ Observable<HttpClientResponse<ByteBuf>> execute(final RequestContext context) { final HttpLogEntry entry = context.entry(); final HttpClient<ByteBuf, ByteBuf> client = getClient(context); entry.mark("start"); entry.withRemoteAddr(context.server().host()); entry.withRemotePort(context.server().port()); return client.submit(context.request()).doOnNext(res -> { update(entry, res); HttpLogEntry.logClientRequest(entry); }).doOnError(throwable -> { update(entry, throwable); HttpLogEntry.logClientRequest(entry); }).doOnTerminate(Actions.empty()); } private HttpClient<ByteBuf, ByteBuf> getClient(final RequestContext context) { HttpClient<ByteBuf, ByteBuf> c = clients.get(context.server()); if (c == null) { c = newClient(context); HttpClient<ByteBuf, ByteBuf> tmp = clients.putIfAbsent(context.server(), c); if (tmp != null) { c.shutdown(); c = tmp; } } return c; } private HttpClient<ByteBuf, ByteBuf> newClient(final RequestContext context) { final Server server = context.server(); final ClientConfig clientCfg = context.config(); HttpClient.HttpClientConfig config = new HttpClient.HttpClientConfig.Builder() .readTimeout(clientCfg.readTimeout(), TimeUnit.MILLISECONDS).userAgent(clientCfg.userAgent()) .build(); PipelineConfiguratorComposite<HttpClientResponse<ByteBuf>, HttpClientRequest<ByteBuf>> pipelineCfg = new PipelineConfiguratorComposite<HttpClientResponse<ByteBuf>, HttpClientRequest<ByteBuf>>( new HttpClientPipelineConfigurator<ByteBuf, ByteBuf>(), new HttpDecompressionConfigurator()); HttpClientBuilder<ByteBuf, ByteBuf> builder = RxNetty .<ByteBuf, ByteBuf>newHttpClientBuilder(server.host(), server.port()) .pipelineConfigurator(pipelineCfg).config(config).withName(clientCfg.name()) .channelOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientCfg.connectTimeout()); if (clientCfg.wireLoggingEnabled()) { builder.enableWireLogging(clientCfg.wireLoggingLevel()); } final int idleTimeout = clientCfg.idleConnectionsTimeoutMillis(); if (idleTimeout == 0) { builder.withNoConnectionPooling(); } else { builder.withConnectionPoolLimitStrategy(getPoolLimitStrategy(clientCfg)) .withIdleConnectionsTimeoutMillis(idleTimeout); } if (server.isSecure()) { builder.withSslEngineFactory(DefaultFactories.trustAll()); } return builder.build(); } private PoolLimitDeterminationStrategy getPoolLimitStrategy(ClientConfig clientCfg) { PoolLimitDeterminationStrategy totalStrategy = poolLimits.get(clientCfg.name()); if (totalStrategy == null) { totalStrategy = new MaxConnectionsBasedStrategy(clientCfg.maxConnectionsTotal()); PoolLimitDeterminationStrategy tmp = poolLimits.putIfAbsent(clientCfg.name(), totalStrategy); if (tmp != null) { totalStrategy = tmp; } } return new CompositePoolLimitDeterminationStrategy( new MaxConnectionsBasedStrategy(clientCfg.maxConnectionsPerHost()), totalStrategy); } private List<Server> getServers(ClientConfig clientCfg) { List<Server> servers; if (clientCfg.uri().isAbsolute()) { servers = getServersForUri(clientCfg, clientCfg.uri()); } else { servers = serverRegistry.getServers(clientCfg.vip(), clientCfg); } return servers; } private List<Server> getServersForUri(ClientConfig clientCfg, URI uri) { final int numRetries = clientCfg.numRetries(); final boolean secure = "https".equals(uri.getScheme()); List<Server> servers = new ArrayList<>(); servers.add(new Server(uri.getHost(), getPort(uri), secure)); for (int i = 0; i < numRetries; ++i) { servers.add(new Server(uri.getHost(), getPort(uri), secure)); } return servers; } /** * Create a copy of a request object. It can only copy the method, uri, and headers so should * not be used for any request with a content already specified. */ static HttpClientRequest<ByteBuf> copy(HttpClientRequest<ByteBuf> req, String uri) { HttpClientRequest<ByteBuf> newReq = HttpClientRequest.create(req.getHttpVersion(), req.getMethod(), uri); for (Map.Entry<String, String> h : req.getHeaders().entries()) { newReq.withHeader(h.getKey(), h.getValue()); } return newReq; } /** We expect UTF-8 to always be supported. */ private static byte[] getBytes(String s) { try { return s.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * Return the port taking care of handling defaults for http and https if not explicit in the * uri. */ static int getPort(URI uri) { final int defaultPort = ("https".equals(uri.getScheme())) ? 443 : 80; return (uri.getPort() <= 0) ? defaultPort : uri.getPort(); } private static class HttpDecompressionConfigurator implements PipelineConfigurator<ByteBuf, ByteBuf> { @Override public void configureNewPipeline(ChannelPipeline pipeline) { pipeline.addLast("deflater", new HttpContentDecompressor()); } } }