/* * Copyright 2009 Red Hat, Inc. * * Red Hat licenses this file to you 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.io.IOException; import java.net.ConnectException; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.util.concurrent.RejectedExecutionException; import org.apache.log4j.Logger; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.channel.WriteCompletionEvent; import org.jboss.netty.handler.codec.http.HttpChunk; import org.jboss.netty.handler.codec.http.HttpChunkTrailer; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import com.linkedin.databus.client.netty.AbstractNettyHttpConnection.ChannelCloseListener; import com.linkedin.databus.client.netty.AbstractNettyHttpConnection.ConnectResultListener; import com.linkedin.databus.client.netty.AbstractNettyHttpConnection.SendRequestResultListener; import com.linkedin.databus.core.DbusPrettyLogUtils; import com.linkedin.databus2.core.DatabusException; public class GenericHttpResponseHandler extends SimpleChannelHandler { public static final String MODULE = GenericHttpResponseHandler.class.getName(); public static enum KeepAliveType { KEEP_ALIVE, NO_KEEP_ALIVE } public static enum MessageState { INIT, CONNECTED,// on successful connection CONNECT_FAIL, REQUEST_WAIT, // waiting for new requests REQUEST_START, REQUEST_SENT, REQUEST_FAILURE, RESPONSE_START, RESPONSE_FINISH, RESPONSE_FAILURE, WAIT_FOR_CHUNK, CLOSED; public boolean hasSentRequest() { if (this.equals(INIT) || this.equals(CONNECTED) || this.equals(REQUEST_WAIT) || this.equals(REQUEST_START) || this.equals(REQUEST_FAILURE) ) return false; return true; } public boolean waitForResponse() { if(this.equals(REQUEST_SENT) || this.equals(RESPONSE_START) || this.equals(WAIT_FOR_CHUNK)) return true; return false; } }; /** is used primary for status printing */ public static enum ChannelState { CHANNEL_ACTIVE, CHANNEL_EXCEPTION, CHANNEL_CLOSED }; private HttpResponseProcessor _responseProcessor; private final KeepAliveType _keepAlive; MessageState _messageState; private ChannelState _channelState; private HttpResponse _httpResponse; private HttpChunkTrailer _httpTrailer; private HttpRequest _httpRequest; private ConnectResultListener _connectListener = null; private SendRequestResultListener _requestListener = null; private ChannelCloseListener _closeListener = null; private Throwable _lastError; private final StateLogger _log; // TODO - remove this constructor (used in tests only) GenericHttpResponseHandler(HttpResponseProcessor responseProcessor, KeepAliveType keepAlive) { this(keepAlive, null); _responseProcessor = responseProcessor; } public GenericHttpResponseHandler(KeepAliveType keepAlive) { this(keepAlive, null); } public GenericHttpResponseHandler(KeepAliveType keepAlive, Logger log) { super(); _keepAlive = keepAlive; if(log == null) { log = Logger.getLogger(MODULE); } _log = new StateLogger(log); //wrapper for extra dynamic info in the log _messageState = MessageState.INIT; reset(); _log.info("Created new Handler"); } /** set listener for channelConnected event */ public synchronized void setConnectionListener(ConnectResultListener listener) { _connectListener = listener; } /** set listener for request submitted event */ public synchronized void setRequestListener(SendRequestResultListener listener) { _requestListener = listener; } /** set listerner for channelClosed event */ public synchronized void setCloseListener(ChannelCloseListener listener) { _closeListener = listener; } public Throwable getLastError() { return _lastError; } /** * replace response processor for this Handler * @param responseProcessor * @throws DatabusException */ public synchronized void setResponseProcessor(HttpResponseProcessor responseProcessor) throws DatabusException { if(responseProcessor == null) { throw new RuntimeException("GenericHttpResponseHandler cannot have null responseProcessor"); } if(_messageState != MessageState.REQUEST_WAIT) { String msg = "replacing responseProcessor while in state=" + _messageState; _log.error(msg); _messageState = MessageState.CLOSED; // do we need to find the channel and close it? throw new DatabusException(new IllegalStateException(msg)); } else { _log.debug("setting processor " + responseProcessor); _responseProcessor = responseProcessor; } } /** * validate current state and call listener with failures if it is not as expected * @param channel * @param expectedState * @return true if valid */ private boolean validateCurrentState(Channel c, MessageState expectedState) { if(_messageState != expectedState) { String msg = "unexpected state: expectedState=" + expectedState + "; actual State" + _messageState; Throwable cause = new IllegalStateException(msg); _log.error(msg, cause); if(_messageState == MessageState.INIT) informConnectListener(c, cause); else if(_messageState == MessageState.REQUEST_START || _messageState == MessageState.REQUEST_WAIT) { informRequestListener(_httpRequest, cause); } else if(_messageState.waitForResponse()) { if(_responseProcessor != null) { _responseProcessor.channelException(cause); } else { _log.error("waiting for response but responseProcessor is null", cause); } } // since we failing the request we need to close the channel if(c != null) { _log.debug("closing the channel because state validate failed"); c.close(); } _messageState = MessageState.CLOSED; return false; // validation failed } return true; } /** * this is used mostly in test (except in the constructor) * state of the Handler shouldn't be changed from outside * if the handler failed - create a new connection (and a new handler) */ synchronized void reset() { if(_messageState == MessageState.INIT || _messageState == MessageState.CLOSED) { _messageState = MessageState.INIT; _channelState = ChannelState.CHANNEL_ACTIVE; _requestListener = null; _connectListener = null; _closeListener = null; } else { String msg = "calling reset in wrong state " + _messageState; _log.error(msg); throw new IllegalStateException(msg); } } private void informConnectListener(Channel channel, Throwable cause) { boolean success = (cause == null); _log.debug("informRequestListener: success=" + success + ";ch=" + channel, cause); // listener is nullified (under sync) to guarantee it is called only once ConnectResultListener tempListener = null; synchronized(this) { if(cause != null) _lastError = cause; if(_connectListener != null) { tempListener = _connectListener; _connectListener = null; } } if(tempListener != null) { _log.info("Notify about connection completed. success=" + success); if(success) tempListener.onConnectSuccess(channel); else tempListener.onConnectFailure(cause); } else { _log.warn("informConnectListener called with listener==null; ch=" + channel, cause); } } private void informRequestListener(HttpRequest req, Throwable cause) { boolean success = (cause == null); boolean debug = _log.isDebugEnabled(); // listener is nullified (under sync) to guarantee it is called only once SendRequestResultListener tempListener = null; synchronized(this) { if(cause != null) _lastError = cause; if(_requestListener != null) { tempListener = _requestListener; _requestListener = null; } } if(debug) _log.debug("informRequestListener: success=" + success + ";req=" + req, cause); if(tempListener != null) { _log.debug("Notify about requestSent completed. success=" + success); if(success) tempListener.onSendRequestSuccess(req); else tempListener.onSendRequestFailure(req, cause); } else { _log.warn("informRequestListener called with listener==null; req=" + req, cause); } } private void informCloseListener() { _log.info("Calling channelCloseListener"); // listener is nullified (under sync) to guarantee it is called only once ChannelCloseListener tempListener = null; synchronized (this) { if(_closeListener != null) { tempListener = _closeListener; _closeListener = null; } } if(tempListener != null) tempListener.onChannelClose(); } @Override public void channelBound(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { _log.info("channel to peer bound: " + e.getChannel().getRemoteAddress()); super.channelBound(ctx, e); } @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { _log.debug("channelConnected"); super.channelConnected(ctx, e); synchronized(this) { if(! validateCurrentState(e.getChannel(), MessageState.INIT)) return; _messageState = MessageState.CONNECTED; _log.info("channel to peer connected: " + e.getChannel().getRemoteAddress()); _messageState = MessageState.REQUEST_WAIT; } informConnectListener(e.getChannel(), null); //success } MessageState getMessageState() { return _messageState; } @Override public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception { boolean debugEnabled = _log.isDebugEnabled(); if(debugEnabled) _log.debug("WriteRequested: chConnected=" + e.getChannel().isConnected() + "; msg=" + e.getMessage()); synchronized (this){ if ( e.getMessage() instanceof HttpRequest) { _httpRequest = (HttpRequest)e.getMessage(); if(! validateCurrentState(e.getChannel(), MessageState.REQUEST_WAIT)) return; _messageState = MessageState.REQUEST_START; if (debugEnabled) _log.debug("Write Requested :" + e); } } super.writeRequested(ctx, e); } @Override public void writeComplete(ChannelHandlerContext ctx, WriteCompletionEvent e) throws Exception { Throwable cause = null; if(_httpRequest == null) super.writeComplete(ctx, e); synchronized(this) { _log.debug("WriteComplete"); if(! validateCurrentState(e.getChannel(), MessageState.REQUEST_START)) { _httpRequest = null; return; } // Future should be done by this time ChannelFuture future = e.getFuture(); boolean success = future.isSuccess(); if (!success) { String msg = "Write request failed with cause :" + future.getCause(); _log.error(msg); _messageState = MessageState.REQUEST_FAILURE; cause = new IllegalStateException(msg); _messageState = MessageState.CLOSED; } else { _messageState = MessageState.REQUEST_SENT; _log.debug("Write Completed successfully :" + e); _messageState = MessageState.RESPONSE_START; } _httpRequest = null; } informRequestListener(_httpRequest, cause); super.writeComplete(ctx, e); if(cause != null) e.getChannel().close(); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { boolean debug = _log.isDebugEnabled(); Object message = e.getMessage(); if(! //not ( message instanceof HttpResponse || message instanceof HttpChunkTrailer || message instanceof HttpChunk)) { _log.debug("Uknown object type:" + message.getClass().getName()); super.messageReceived(ctx, e); return; } if( null == _responseProcessor) { _log.error("No response processor set"); _messageState = MessageState.CLOSED; e.getChannel().close(); throw new RuntimeException("No response processor set in messageReceived.state="+_messageState + ";msg=" + message); } synchronized (this) { if (message instanceof HttpResponse) { if(! validateCurrentState(e.getChannel(), MessageState.RESPONSE_START)) { _log.error("MessageReceived(HttpResponse) failed for message: " + message); return; } if(debug) _log.debug("msgRecived. HttpResponse"); _httpResponse = (HttpResponse) message; _responseProcessor.startResponse(_httpResponse); if (!_httpResponse.isChunked()) { finishResponse(e); // done with this request/response } else { _messageState = MessageState.WAIT_FOR_CHUNK; } } else if (message instanceof HttpChunkTrailer) { if(! validateCurrentState(e.getChannel(), MessageState.WAIT_FOR_CHUNK)) { _log.error("MessageReceived(HttpChunkTrailer) failed for message: " + message); return; } if(debug) _log.debug("msgRecived. HttpChunkTrailer"); _httpTrailer = (HttpChunkTrailer) message; _responseProcessor.addTrailer(_httpTrailer); finishResponse(e); // done with this request/response } else if (message instanceof HttpChunk) { if(! validateCurrentState(e.getChannel(), MessageState.WAIT_FOR_CHUNK)) { _log.error("MessageReceived(HttpChunk) failed for message: " + message); return; } if(debug) _log.debug("msgRecived. HttpChunk"); _messageState = MessageState.WAIT_FOR_CHUNK; _responseProcessor.addChunk((HttpChunk) message); } } } private void finishResponse(MessageEvent e) throws Exception { _messageState = MessageState.RESPONSE_FINISH; _log.debug("FINISH_RESPONSE"); _responseProcessor.finishResponse(); _responseProcessor = null; if (_keepAlive == KeepAliveType.NO_KEEP_ALIVE) { e.getChannel().close(); } _messageState = MessageState.REQUEST_WAIT; } private void logExceptionMessage(Throwable cause) { if (!_messageState.hasSentRequest()) { if (!(cause instanceof ConnectException)) { _log.info("Skipping exception message even before request has been sent. State=" + _messageState, cause); } else { _log.info("Got connection Exception", cause); } } else { if (cause instanceof RejectedExecutionException) { _log.info("shutdown in progress"); } else if (cause instanceof IOException && null != cause.getMessage() && cause.getMessage().contains("Connection reset by peer")) { _log.warn("connection reset by peer"); } else if (!(cause instanceof ClosedChannelException)) { _log.error( "http client exception(" + cause.getClass().getSimpleName() + "):" + cause.getMessage(), cause); } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { boolean debug = _log.isDebugEnabled(); Throwable cause = e.getCause(); if(cause == null) { // may it happen? (may be in test) cause = new RuntimeException("exceptionCaught is invoked with empty exception"); } logExceptionMessage(cause); if(debug) { _log.debug("exceptionCaught.rp=" + _responseProcessor, cause); } synchronized (this) { switch(_messageState) { case INIT: _messageState = MessageState.CONNECT_FAIL; informConnectListener(e.getChannel(), cause); break; case REQUEST_START: _messageState = MessageState.REQUEST_FAILURE; informRequestListener(_httpRequest, cause); break; case REQUEST_SENT: case RESPONSE_START: case WAIT_FOR_CHUNK: _messageState = MessageState.RESPONSE_FAILURE; if (null != _responseProcessor) { _responseProcessor.channelException(cause); } break; default: _log.warn("exceptionCaught is called", cause); } _messageState = MessageState.CLOSED; _channelState = ChannelState.CHANNEL_EXCEPTION; } super.exceptionCaught(ctx, e); e.getChannel().close(); } @Override public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { Channel channel = e.getChannel(); SocketAddress a = (null != channel) ? channel.getRemoteAddress() : null; _log.info("channel to peer closed: " + a); synchronized (this){ _channelState = ChannelState.CHANNEL_CLOSED; switch(_messageState) { case INIT: _log.warn("got closed channel before connecting"); _messageState = MessageState.CONNECT_FAIL; informConnectListener(e.getChannel(), new ClosedChannelException()); break; case REQUEST_WAIT: _messageState = MessageState.CLOSED;//normal case break; case REQUEST_START: _log.warn("got closed channel before sending request"); _messageState = MessageState.REQUEST_FAILURE; informRequestListener(_httpRequest, new ClosedChannelException()); break; case REQUEST_SENT: case RESPONSE_START: case WAIT_FOR_CHUNK: _log.error("got closed channel while waiting for response"); _messageState = MessageState.RESPONSE_FAILURE; if(_responseProcessor != null) _responseProcessor.channelException(new ClosedChannelException()); break; default: _log.warn("closeChannel is called in unexpected state:" + _messageState); } _messageState = MessageState.CLOSED; _channelState = ChannelState.CHANNEL_EXCEPTION; } informCloseListener(); super.channelClosed(ctx, e); } @Override public String toString() { return "GenericHttpResponseHandler [" + "_keepAlive=" + _keepAlive + ", _messageState=" + _messageState + ", _channelState=" + _channelState + "]"; } public synchronized String getHeader(String headerName) { String result = null; if (null != _httpResponse) { result = _httpResponse.getHeader(headerName); if (null == result && null != _httpTrailer) { result = _httpTrailer.getHeader(headerName); } } return result; } public StateLogger getLog() { return _log; } public class StateLogger { private final Logger _log; public StateLogger (Logger l) { _log = l; } protected StringBuilder setPrefix() { StringBuilder sb = new StringBuilder(); sb.append("<").append(GenericHttpResponseHandler.this.hashCode()); sb.append("_").append(_messageState).append(">"); return sb; } public void info(String msg) { info(msg, null); } public void debug(String msg) { debug(msg, null); } public void warn(String msg) { warn(msg, null); } public void error(String msg) { error(msg, null); } public void info(String msg, Throwable e) { if(isDebugEnabled()) msg = setPrefix().append(msg).toString(); DbusPrettyLogUtils.logExceptionAtInfo(msg, e, _log); } public void debug(String msg, Throwable e) { msg = setPrefix().append(msg).toString(); DbusPrettyLogUtils.logExceptionAtDebug(msg, e, _log); } public void warn(String msg, Throwable e) { msg = setPrefix().append(msg).toString(); DbusPrettyLogUtils.logExceptionAtWarn(msg, e, _log); } public void error(String msg, Throwable e) { msg = setPrefix().append(msg).toString(); DbusPrettyLogUtils.logExceptionAtError(msg, e, _log); } public boolean isDebugEnabled() { return _log.isDebugEnabled(); } public void setLevel(org.apache.log4j.Level l) { _log.setLevel(l); } } }