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

etcd.client.HttpClient Maven / Gradle / Ivy

/*
 *   Copyright (c) 2014 Intellectual Reserve, 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 etcd.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Light HTTP client wrapper around Netty.
 */
// TODO Remove bad hosts from selection pool after error, return after timeout
class HttpClient {

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

	private static AttributeKey> ATTRIBUTE_KEY = AttributeKey.valueOf(HttpClient.class.getName() + "-attribute");
	private static AttributeKey REQUEST_KEY = AttributeKey.valueOf(HttpClient.class.getName() + "-request");

	private final EventLoopGroup eventLoopGroup;
	private final Bootstrap bootstrap;
	private final Executor executor;

	private final ServerList servers;
	private final boolean autoReconnect;
//	private final List channelPool = new ArrayList<>();
//
//	private final Object lock = new Object();

	public HttpClient(EventLoopGroup eventLoopGroup, Executor executor, ServerList servers, boolean autoReconnect) {
		this.eventLoopGroup = eventLoopGroup;
		this.executor = executor;
		this.servers = servers;
		this.autoReconnect = autoReconnect;
		bootstrap = new Bootstrap()
				.group(eventLoopGroup)
				.channel(NioSocketChannel.class)
				.option(ChannelOption.SO_REUSEADDR, true)
				.handler(new ChannelInitializer() {
					@Override
					protected void initChannel(SocketChannel channel) throws Exception {
						final ChannelPipeline pipeline = channel.pipeline();

						pipeline.addLast(
								new HttpClientCodec(4096, 8192, 8192, true),
								new HttpObjectAggregator(1024 * 1024),
								new HttpClientHandler());
					}
				})
				.validate();
	}

	public void send(FullHttpRequest request, Consumer completionHandler) {
		// TODO Add support for TLS
		// TODO Add support for TLS client authentication
		send(servers.serverIterator(), request, completionHandler);
	}

	private void send(Iterator serverIterator, FullHttpRequest request, Consumer completionHandler) {
		final ServerList.Server server = serverIterator.next();
		final URI address = server.getAddress();
		final ChannelFuture connectFuture = bootstrap.connect(address.getHost(), address.getPort());
		final FullHttpRequest requestCopy = request.copy();
		requestCopy.retain();
		final Channel channel = connectFuture.channel();
		channel.attr(REQUEST_KEY).set(requestCopy);
		channel.attr(ATTRIBUTE_KEY).set(completionHandler);
		connectFuture.addListener((future) -> {
			if (future.isSuccess()) {
				channel.writeAndFlush(request);
			} else {
				server.connectionFailed();
				if (autoReconnect && serverIterator.hasNext()) {
					send(serverIterator, request, completionHandler);
				} else {
					invokeCompletionHandler(completionHandler, new Response(null, new EtcdException(future.cause())));
				}
			}
		});
	}

	private void invokeCompletionHandler(Consumer completionHandler, Response response) {
		executor.execute(() -> completionHandler.accept(response));
	}

	public EventLoopGroup getEventLoopGroup() {
		return eventLoopGroup;
	}

	class HttpClientHandler extends ChannelInboundHandlerAdapter {
		@Override
		public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
			final FullHttpRequest request = ctx.channel().attr(REQUEST_KEY).getAndRemove();
			try {
				final Consumer completionCallbackHandler = ctx.channel().attr(ATTRIBUTE_KEY).getAndRemove();
				if (completionCallbackHandler == null) {
					throw new IllegalStateException("Received a response with nothing to handle it.");
				}
				final DefaultFullHttpResponse response = (DefaultFullHttpResponse) msg;

				if (response.getStatus().equals(HttpResponseStatus.MOVED_PERMANENTLY) ||
						response.getStatus().equals(HttpResponseStatus.TEMPORARY_REDIRECT)
						) {
					final URI locationUri = URI.create(response.headers().get(HttpHeaders.Names.LOCATION));
					final URI serverUri;
					if (locationUri.isAbsolute()) {
						serverUri = locationUri;
						request.headers().set(HttpHeaders.Names.HOST, serverUri.getHost());
					} else {
						final InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
						serverUri = URI.create("http://" + address.getHostString() + ":" + address.getPort());
					}
					request.setUri(locationUri.getPath() + (locationUri.getQuery() == null ? "" : locationUri.getQuery()));
					final Iterator serverIterator = Collections.singleton(new ServerList.Server(serverUri)).iterator();
					request.retain();
					send(serverIterator, request, completionCallbackHandler);
				} else {
					response.retain();
					invokeCompletionHandler(completionCallbackHandler, new Response(response, null));
				}
			} finally {
				request.release();
			}
			ctx.close();
		}

		@Override
		public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//			synchronized (lock) {
//				channelPool.remove(ctx.channel());
//			}
			final Consumer completionCallbackHandler = ctx.channel().attr(ATTRIBUTE_KEY).getAndRemove();
			if (completionCallbackHandler != null) {
				invokeCompletionHandler(completionCallbackHandler, new Response(null, new EtcdException("Connection closed unexpectedly")));
			}
		}

		@Override
		public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
			final Consumer completionCallbackHandler = ctx.channel().attr(ATTRIBUTE_KEY).getAndRemove();
			if (completionCallbackHandler != null) {
				final Response response;
				if (cause instanceof EtcdException) {
					response = new Response(null, (EtcdException)cause);
				} else {
					response = new Response(null, new EtcdException(cause));
				}
				invokeCompletionHandler(completionCallbackHandler, response);
			} else {
				LOGGER.error("Error processing server request", cause);
			}
			ctx.channel().close();
		}
	}

	class Response {
		private final DefaultFullHttpResponse response;
		private final EtcdException exception;

		Response(DefaultFullHttpResponse response, EtcdException exception) {
			this.response = response;
			this.exception = exception;
		}

		public DefaultFullHttpResponse getHttpResponse() {
			if (exception != null) {
				throw exception;
			}
			return response;
		}
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy