/* * * Copyright 2013 LinkedIn Corp. 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.linkedin.databus.client.netty; import java.nio.channels.ClosedChannelException; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.log4j.Logger; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.group.ChannelGroup; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpChunkTrailer; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpVersion; import org.jboss.netty.handler.timeout.WriteTimeoutException; import org.jboss.netty.util.Timer; import com.linkedin.databus.client.ChunkedBodyReadableByteChannel; import com.linkedin.databus.client.DatabusServerConnection; import com.linkedin.databus.client.DatabusSourcesConnection; import com.linkedin.databus.client.pub.ServerInfo; import com.linkedin.databus.core.DbusConstants; import com.linkedin.databus.core.DbusEventFactory; import com.linkedin.databus.core.DbusPrettyLogUtils; import com.linkedin.databus.core.util.DbusHttpUtils; import com.linkedin.databus2.core.DatabusException; import com.linkedin.databus2.core.container.DatabusHttpHeaders; import com.linkedin.databus2.core.container.ExtendedReadTimeoutHandler; import com.linkedin.databus2.core.container.monitoring.mbean.ContainerStatisticsCollector; /** * Common functionality between {@link NettyHttpDatabusRelayConnection} and * {@link NettyHttpDatabusBootstrapConnection}. * */ public class AbstractNettyHttpConnection implements DatabusServerConnection { /** Connection state */ static enum State { INIT, CONNECTING, CONNECTED, CLOSING, CLOSED, ERROR } //State transitions // (INIT, connect()) --> CONNECTING // (INIT, close()) --> CLOSING // (CONNECTING, connect_error) --> INIT // (CONNECTING, connect_success) --> CONNECTED // (CONNECTING, close()) --> CLOSING // (CONNECTED, close()) --> CLOSING // (CLOSING, close()) --> CLOSING // (CLOSING, close_complete) --> CLOSED // (CLOSED, close()) --> CLOSED // (CLOSED, close_complete) --> CLOSED // (CLOSED, connect()) --> CONNECTING // Attempt for any other transition will result in going to state ERROR private final Lock _mutex = new ReentrantLock(); private final Condition _connectionClosed = _mutex.newCondition(); private final GenericHttpClientPipelineFactory _pipelineFactory; protected final ClientBootstrap _bootstrap; protected final ServerInfo _server; private final Logger _log; final private int _protocolVersion; protected volatile Channel _channel; private int _connectRetriesLeft; private State _state; private final GenericHttpResponseHandler _handler; String _hostHdr; String _svcHdr; public AbstractNettyHttpConnection(ServerInfo server, ClientBootstrap bootstrap, ContainerStatisticsCollector containerStatsCollector, Timer timeoutTimer, long writeTimeoutMs, long readTimeoutMs, ChannelGroup channelGroup, int protocolVersion, Logger log) { super(); _log = null != log ? log : Logger.getLogger(AbstractNettyHttpConnection.class); _server = server; _bootstrap = bootstrap; _protocolVersion = protocolVersion; _bootstrap.setOption("connectTimeoutMillis", DatabusSourcesConnection.CONNECT_TIMEOUT_MS); _handler = new GenericHttpResponseHandler(GenericHttpResponseHandler.KeepAliveType.KEEP_ALIVE, null); _pipelineFactory = new GenericHttpClientPipelineFactory( _handler, containerStatsCollector, timeoutTimer, writeTimeoutMs, readTimeoutMs, channelGroup); _bootstrap.setPipelineFactory(_pipelineFactory); _channel = null; _state = State.INIT; } @Override public int getProtocolVersion() { return _protocolVersion; } /** Closes the connection. Note: this method will block until the connection is actually closed */ @Override public void close() { _log.info("closing connection to: " + _server.getAddress()); final State newState = switchToClosing(); if (State.CLOSING != newState && State.CLOSED != newState) { return; } if (null == _channel || !_channel.isConnected()) { switchToClosed(); } else { _channel.close(); awaitForCloseUninterruptibly(); } } @Override public String getRemoteHost() { String host = getHostHdr(); return null != host ? host : DbusConstants.UNKNOWN_HOST; } @Override public String getRemoteService() { String service = getSvcHdr(); return null != service ? service : DbusConstants.UNKNOWN_SERVICE_ID; } private void awaitForCloseUninterruptibly() { _mutex.lock(); try { while (! isClosed()) { _connectionClosed.awaitUninterruptibly(); } } finally { _mutex.unlock(); } } /** * @return the logger used by this instance */ public Logger getLog() { return _log; } public boolean isInit() { _mutex.lock(); try { return State.INIT == _state; } finally { _mutex.unlock(); } } public boolean isClosingOrClosed() { _mutex.lock(); try { return State.CLOSING == _state || State.CLOSED == _state; } finally { _mutex.unlock(); } } public boolean isClosed() { _mutex.lock(); try { return State.CLOSED == _state; } finally { _mutex.unlock(); } } public boolean isClosing() { _mutex.lock(); try { return State.CLOSING == _state; } finally { _mutex.unlock(); } } public boolean isConnectingOrConnected() { _mutex.lock(); try { return State.CONNECTING == _state || State.CONNECTED == _state; } finally { _mutex.unlock(); } } public boolean isConnecting() { _mutex.lock(); try { return State.CONNECTING == _state; } finally { _mutex.unlock(); } } public boolean isConnected() { _mutex.lock(); try { return State.CONNECTED == _state; } finally { _mutex.unlock(); } } private void unexpectedTransition(State fromState, State toState) { _log.error("unexpected netty connection transition from " + fromState + " to " + toState); _state = State.ERROR; } /** * Attempt to switch to CLOSING state * @return the new state */ private State switchToClosing() { _mutex.lock(); try { switch (_state) { case CLOSING: case CLOSED: break; //NOOP default: _state = State.CLOSING; } return _state; } finally { _mutex.unlock(); } } /** * Switch to CLOSED state * @return the new state */ private State switchToClosed() { _mutex.lock(); try { switch (_state) { case CLOSING: case CLOSED: _state = State.CLOSED; break; default: unexpectedTransition(_state, State.CLOSED); break; } return _state; } finally { _mutex.unlock(); } } /** * Switch to CONNECTING state * @return the new state */ private State switchToConnecting() { _mutex.lock(); try { switch (_state) { case CLOSED: case INIT: _state = State.CONNECTING; break; default: unexpectedTransition(_state, State.CONNECTING); break; } return _state; } finally { _mutex.unlock(); } } /** * Switch to CONNECTED state * @return the new state */ private State switchToConnected() { _mutex.lock(); try { switch (_state) { case CONNECTING: _state = State.CONNECTED; break; case CLOSING: case CLOSED: break; //NOOP default: unexpectedTransition(_state, State.CONNECTED); break; } return _state; } finally { _mutex.unlock(); } } /** * Switch to INIT state * @return the new state */ private State switchToInit() { _mutex.lock(); try { switch (_state) { case CONNECTING: _state = State.INIT; break; default: unexpectedTransition(_state, State.INIT); break; } return _state; } finally { _mutex.unlock(); } } /** * Checks if there is network connection is available. Note that if the connection is in a * transitional state (CONNECTING, CLOSING), the method will still return true to avoid race * conditions where the caller will try to re-establish the connection while these transitions are * in progress. If the caller tries an operation that is not allowed on the channel, e.g. send data * over a closed channel, netty should be able to detect that and throw a ClosedChannelException. * */ protected boolean hasConnection() { return null != _channel && State.INIT != _state && State.CLOSED != _state; //return null != _channel && _channel.isConnected(); } /** For testing */ State getNetworkState() { return _state; } protected void connectWithListener(ConnectResultListener listener) { if (State.CONNECTING != switchToConnecting()) { listener.onConnectFailure(new RuntimeException("unable to connect")); } else { _connectRetriesLeft = DatabusSourcesConnection.MAX_CONNECT_RETRY_NUM; connectRetry(listener); } } private void connectRetry(ConnectResultListener listener) { _log.info("connecting: " + _server.toSimpleString()); if (isClosingOrClosed()) { listener.onConnectFailure(new ClosedChannelException()); return; } else if (! isConnecting()) { listener.onConnectFailure(new RuntimeException("unable to connect")); } else { //ChannelFuture future = _bootstrap.connect(_server.getAddress()); //future.addListener(new MyConnectListener(listener)); _handler.reset();// reset state to make sure new connection starts with a blank Handler state _handler.setConnectionListener(new AbstractNettyConnectListener(listener)); _bootstrap.connect(_server.getAddress()); } } protected void sendRequest(HttpRequest request, SendRequestResultListener listener, HttpResponseProcessor responseProcessor) { if (isClosingOrClosed()) { listener.onSendRequestFailure(request, new ClosedChannelException()); } else if (! isConnected()) { listener.onSendRequestFailure(request, new RuntimeException("unable to send request")); } else { try { setResponseProcessor(responseProcessor, listener); } catch (DatabusException e) { listener.onSendRequestFailure(request, e.getCause()); _channel.close(); return; } // Send the HTTP request. if (_channel.isConnected()) { //ChannelFuture future = _channel.write(request); //future.addListener(new MySendRequestListener(request, listener)); _channel.write(request); } else { _log.error("disconnect on request: " + request.getUri()); listener.onSendRequestFailure(request, new ClosedChannelException()); } } } /** * Creates an empty HttpRequest object with pre-filled standard headers * @param uriString the request URL * @return the request object */ protected HttpRequest createEmptyRequest(String uriString) { HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriString); request.setHeader(HttpHeaders.Names.HOST, _server.getAddress().toString()); request.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); request.setHeader(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); String hostHdr = DbusHttpUtils.getLocalHostName(); String svcHdr = DbusConstants.getServiceIdentifier(); if (! hostHdr.isEmpty()) { request.setHeader(DatabusHttpHeaders.DBUS_CLIENT_HOST_HDR, hostHdr); } if (! svcHdr.isEmpty()) { request.setHeader(DatabusHttpHeaders.DBUS_CLIENT_SERVICE_HDR, svcHdr); } return request; } protected void setConnectListener(ConnectResultListener l) { _handler.setConnectionListener(l); } protected void setResponseProcessor(HttpResponseProcessor responseProcessor, SendRequestResultListener l) throws DatabusException { _handler.setResponseProcessor(responseProcessor); _handler.setRequestListener(l); /* if (null != _channel) { ChannelPipeline channelPipeline = _channel.getPipeline(); channelPipeline.replace("handler", "handler", _handler); } */ assert(_channel != null); assert(_channel.getPipeline().get("handler")!=null); } protected GenericHttpResponseHandler getHandler() { return _handler; } protected boolean shouldIgnoreWriteTimeoutException(Throwable cause) { // special case - DDSDBUS-1497 // in case of WriteTimeoutException we will get close channel exception. // Since the timeout exception comes from a different thread (timer) we // may end up informing PullerThread twice - and causing it to create two new connections // Instead we just drop this exception and just react to close channel //If null == getHandler(), the error happened the connect attempt, i.e. the request was not //sent. boolean requestSent = null != getHandler() ? getHandler()._messageState.hasSentRequest() : false; if(requestSent && (cause instanceof WriteTimeoutException)) { _log.error("Got RequestFailure because of WriteTimeoutException. requestSent = " + requestSent); return true; } else { _log.error("The request has not been sent due to " + cause + " requestSent = " + requestSent); return false; } } protected void addConnectionTracking(HttpResponse response) throws Exception { boolean debugEnabled = _log.isDebugEnabled(); _hostHdr = response.getHeader(DatabusHttpHeaders.DBUS_SERVER_HOST_HDR); _svcHdr = response.getHeader(DatabusHttpHeaders.DBUS_SERVER_SERVICE_HDR); if (debugEnabled) { if (null != _hostHdr) { _log.debug("Received response for databus server host: " + _hostHdr); } if (null != _svcHdr) { _log.debug("Received response for databus server host: " + _svcHdr); } } return; } /** Internal listener to handle connect success and failures */ private class AbstractNettyConnectListener implements ConnectResultListener { private final ConnectResultListener _listener; public AbstractNettyConnectListener(ConnectResultListener listener) { super(); _listener = listener; } /** * @see com.linkedin.databus.client.netty.AbstractNettyHttpConnection.ConnectResultListener#onConnectSuccess(org.jboss.netty.channel.Channel) */ @Override public void onConnectSuccess(Channel channel) { if (State.CONNECTED != switchToConnected()) { _listener.onConnectFailure(new RuntimeException("unable to connect")); } else { _log.info("connected: " + _server.toSimpleString()); //_channel = future.getChannel(); //channel.getCloseFuture().addListener(new MyChannelCloseListener()); _handler.setCloseListener(new NettyChannelCloseListener()); _channel = channel; _listener.onConnectSuccess(channel); } } /** * @see com.linkedin.databus.client.netty.AbstractNettyHttpConnection.ConnectResultListener#onConnectFailure(java.lang.Throwable) */ @Override public void onConnectFailure(Throwable cause) { DbusPrettyLogUtils.logExceptionAtError("Connect cancelled/failed", cause, _log); switchToInit(); _listener.onConnectFailure(cause); } } /** Marks the connection as closed */ private class NettyChannelCloseListener implements ChannelCloseListener { @Override public void onChannelClose() { _mutex.lock(); try { switchToClosing(); switchToClosed(); _connectionClosed.signalAll(); _log.info("connection closed: " + _server.getAddress()); } finally { _mutex.unlock(); } } } /** Notifies about channel close */ public interface ChannelCloseListener { /** Notifies about channel close */ public void onChannelClose(); } /** Notifies for the result of connect attempts */ public interface ConnectResultListener { /** Notifies about connect success with the given Netty channel */ void onConnectSuccess(Channel channel); /** Notifies about connect failure with the given cause */ void onConnectFailure(Throwable cause); } /** Notifies for the result of sending requests */ public interface SendRequestResultListener { /** Notifies about success of sending the specified request */ void onSendRequestSuccess(HttpRequest req); /** Notifies about failure of sending the specified request with the given cause */ void onSendRequestFailure(HttpRequest req, Throwable cause); } static class BaseHttpResponseProcessor extends AbstractHttpResponseProcessorDecorator <ChunkedBodyReadableByteChannel> { private final ExtendedReadTimeoutHandler _readTimeOutHandler; private final AbstractNettyHttpConnection _parent; protected String _serverErrorClass; protected String _serverErrorMessage; /** * Constructor * @param parent the AbstractNettyHttpConnection object that instantiated this * response processor * @param readTimeOutHandler the ReadTimeoutHandler for the connection handled by this * response handler. */ public BaseHttpResponseProcessor(AbstractNettyHttpConnection parent, ExtendedReadTimeoutHandler readTimeOutHandler) { super(null); _readTimeOutHandler = readTimeOutHandler; _parent = parent; } /** * @see com.linkedin.databus.client.netty.AbstractHttpResponseProcessorDecorator#finishResponse() */ @Override public void finishResponse() throws Exception { try { super.finishResponse(); } finally { stopReadTimeoutTimer(); } } protected void stopReadTimeoutTimer() { if (null != _readTimeOutHandler) { _readTimeOutHandler.stop(); } } /** * @see com.linkedin.databus.client.netty.AbstractHttpResponseProcessorDecorator#startResponse(org.jboss.netty.handler.codec.http.HttpResponse) */ @Override public void startResponse(HttpResponse response) throws Exception { //check for errors in the response _serverErrorClass = response.getHeader(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_CLASS_HEADER); _serverErrorMessage = response.getHeader(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_MESSAGE_HEADER); if (null == _serverErrorClass) { _serverErrorClass = response.getHeader(DatabusHttpHeaders.DATABUS_ERROR_CLASS_HEADER); _serverErrorMessage = response.getHeader(DatabusHttpHeaders.DATABUS_ERROR_MESSAGE_HEADER); } if (null != _serverErrorClass) { if (null != _parent) { _parent.getLog().error("server error detected class=" + _serverErrorClass + " message=" + _serverErrorMessage); } } super.startResponse(response); if (null != _parent) _parent.addConnectionTracking(response); } protected AbstractNettyHttpConnection getParent() { return _parent; } /* (non-Javadoc) * @see com.linkedin.databus.client.netty.HttpResponseProcessorDecorator#addTrailer(org.jboss.netty.handler.codec.http.HttpChunkTrailer) */ @Override public void addTrailer(HttpChunkTrailer trailer) throws Exception { //check for errors in the middle of the response if (null == _serverErrorClass) { _serverErrorClass = trailer.getHeader(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_CLASS_HEADER); _serverErrorMessage = trailer.getHeader(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_MESSAGE_HEADER); if (null == _serverErrorClass) { _serverErrorClass = trailer.getHeader(DatabusHttpHeaders.DATABUS_ERROR_CLASS_HEADER); _serverErrorMessage = trailer.getHeader(DatabusHttpHeaders.DATABUS_ERROR_MESSAGE_HEADER); } if (null != _serverErrorClass) { if (null != _parent) { _parent.getLog().error("server error detected class=" + _serverErrorClass + " message=" + _serverErrorMessage); } } } super.addTrailer(trailer); } } protected String getHostHdr() { return _hostHdr; } protected String getSvcHdr() { return _svcHdr; } @Override public int getMaxEventVersion() { // return default return DbusEventFactory.DBUS_EVENT_V1; } }