package jade.imtp.leap.nio; //#J2ME_EXCLUDE_FILE import jade.core.BackEnd; import jade.core.BackEndContainer; import jade.core.BEConnectionManager; import jade.core.FrontEnd; import jade.core.IMTPException; import jade.core.Profile; import jade.core.ProfileException; import jade.core.Runtime; import jade.core.Timer; import jade.core.TimerListener; import jade.imtp.leap.BackEndSkel; import jade.imtp.leap.FrontEndStub; import jade.imtp.leap.ICPException; import jade.imtp.leap.MicroSkeleton; import jade.imtp.leap.Dispatcher; import jade.imtp.leap.JICP.Connection; import jade.imtp.leap.JICP.JICPMediatorManager; import jade.imtp.leap.JICP.JICPProtocol; import jade.imtp.leap.JICP.JICPPacket; import jade.util.Logger; import jade.util.leap.Properties; import java.io.IOException; import java.net.InetAddress; /** * * @author Eduard Drenth: Logica, 11-jul-2009 * */ public class NIOHTTPBEDispatcher implements NIOMediator, Dispatcher, BEConnectionManager { // Local statuses private static final int ACTIVE = 0; private static final int NOT_ACTIVE = 1; // Front-end statuses private static final int CONNECTED = 0; private static final int CONNECTING = 1; private static final int DISCONNECTED = 2; private static final int TERMINATED = 3; private static final long OUTGOING_COMMANDS_CONNECTION_TIMEOUT = 30000; // 30 sec private static final long RESPONSE_TIMEOUT = 30000; // 30 sec private static final long RESPONSE_TIMEOUT_INCREMENT = 100; // 100 msec private static final int MAX_SID = 0x0f; private JICPMediatorManager myMediatorManager; private String myID; private MicroSkeleton mySkel = null; private FrontEndStub myStub = null; private BackEndContainer myContainer = null; private int status = ACTIVE; private int frontEndStatus = CONNECTING; private long maxDisconnectionTime; private Timer maxDisconnectionTimer = null; private long keepAliveTime; private Timer keepAliveTimer = null; private JICPPacket lastResponse = null; private byte lastIncomingCommandSid; private boolean waitingForFlush = false; private Connection outgoingCommandsConnection = null; private Object outgoingCommandsConnectionLock = new Object(); private int nextOutgoingCommandSid; private JICPPacket responseToLastOutgoingCommand = null; private Object responseToLastOutgoingCommandLock = new Object(); private Logger myLogger = Logger.getMyLogger(getClass().getName()); ////////////////////////////////////////// // NIOMediator interface implementation ////////////////////////////////////////// public String getID() { return myID; } /** * Initialize parameters and activate the BackEndContainer */ public void init(JICPMediatorManager mgr, String id, Properties props) throws ICPException { myMediatorManager = mgr; myID = id; // Read parameters // Max disconnection time maxDisconnectionTime = JICPProtocol.DEFAULT_MAX_DISCONNECTION_TIME; try { maxDisconnectionTime = Long.parseLong(props.getProperty(JICPProtocol.MAX_DISCONNECTION_TIME_KEY)); } catch (Exception e) { // Keep default } // Max disconnection time keepAliveTime = JICPProtocol.DEFAULT_KEEP_ALIVE_TIME; try { keepAliveTime = Long.parseLong(props.getProperty(JICPProtocol.KEEP_ALIVE_TIME_KEY)); } catch (Exception e) { // Keep default } // Counter to assign the SID to the next command to be delivered to the FE (only present if this is a back-end re-creation) nextOutgoingCommandSid = 0; try { // The FE indicates the SID of the last command it received --> Increment it by 1 nextOutgoingCommandSid = increment(Integer.parseInt(props.getProperty("lastsid"))); } catch (Exception e) { // Keep default } // SID of last command received from the FE (only present if this is a back-end re-creation) lastIncomingCommandSid = 0x10; try { // The FE indicates the SID of the next command it will send us --> Decrement it by 1 lastIncomingCommandSid = (byte) decrement(Integer.parseInt(props.getProperty("outcnt"))); } catch (Exception e) { // Keep default } myLogger.log(Logger.INFO, "Created NIOHTTPBEDispatcher V1.0. ID = " + myID + "\n- Max-disconnection-time = " + maxDisconnectionTime+ "\n- Keep-alive-time = " + keepAliveTime); myLogger.log(Logger.CONFIG, myID+" - Next outgoing command SID = " + nextOutgoingCommandSid + ", Last incoming command SID = " + lastIncomingCommandSid); myStub = new FrontEndStub(this); mySkel = startBackEndContainer(props); } private final BackEndSkel startBackEndContainer(Properties props) throws ICPException { try { String nodeName = myID.replace(':', '_'); props.setProperty(Profile.CONTAINER_NAME, nodeName); myContainer = new BackEndContainer(props, this); if (!myContainer.connect()) { throw new ICPException("BackEnd container failed to join the platform"); } //Possibly the node name was re-assigned by the main myID = myContainer.here().getName(); myLogger.log(Logger.CONFIG, myID+" - BackEndContainer " + myID + " successfully joined the platform."); return new BackEndSkel(myContainer); } catch (ProfileException pe) { // should never happen pe.printStackTrace(); throw new ICPException("Error creating profile"); } } /** * Termination self initiated or forced by the MediatorManager we are attached to. */ public void kill() { status = NOT_ACTIVE; // Force the BackEndContainer to terminate. // This will also cause this NIOHTTPBEDispatcher to terminate and deregister from the MediatorManager myContainer.shutDown(); } public void tick(long time) { // Not used: just do nothing } /** * Handle an incoming connection. This is called by the MediatorManager * when a CREATE or CONNECT_MEDIATOR request is received. * In both cases the Front-end is connecting --> * Set the front-end status to CONNECTING, but don't use this connection: to allow us * sending commands to the front-end an initial dummy response will be received soon. */ public synchronized boolean handleIncomingConnection(Connection c, JICPPacket pkt, InetAddress addr, int port) { myLogger.log(Logger.INFO, myID+" - Front-end connecting ["+addr+":"+port+"]"); setFrontEndConnecting(); // Returning false will make the MediatorManager close the connection return false; } /** * Handle an incoming JICP packet received by the MediatorManager */ public JICPPacket handleJICPPacket(Connection c, JICPPacket pkt, InetAddress addr, int port) throws ICPException { JICPPacket response = null; if ((status == ACTIVE) && (frontEndStatus != TERMINATED)) { if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+" - Incoming packet. Type = "+pkt.getType()+", SID = "+pkt.getSessionID()+", terminated-info = "+((pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0)); } String from = " [" + addr + ":" + port + "]"; if (pkt.getType() == JICPProtocol.COMMAND_TYPE) { // COMMAND if ((pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0) { // PEER TERMINATION NOTIFICATION // The remote FrontEnd terminated spontaneously --> Terminate and notify up. myLogger.log(Logger.INFO, myID+" - Peer termination notification received"); handlePeerSelfTermination(); // Since we return null the MediatorManager would keep the connection open --> close it explicitly return createTerminationNotificationAck(); } else { // NORMAL COMMAND // Serve the incoming command and send back the response byte sid = pkt.getSessionID(); if (sid == lastIncomingCommandSid) { myLogger.log(Logger.WARNING, myID+" - Duplicated command received. SID = " + sid); response = lastResponse; } else { if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+" - Incoming command received. SID = " + sid); } byte[] rspData = mySkel.handleCommand(pkt.getData()); if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+" - Incoming command served. SID = " + sid); } response = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, rspData); response.setSessionID(sid); lastIncomingCommandSid = sid; lastResponse = response; } } } else { // RESPONSE. handleResponse(c, pkt, from); if ((pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0) { // PEER TERMINATION NOTIFICATION. // The remote FrontEnd terminated as a consequence of an EXIT command setFrontEndTerminated(); shutdown(); // Since we return null the MediatorManager would keep the connection open --> close it explicitly return createTerminationNotificationAck(); } } } else { myLogger.log(Logger.FINE, "Unexpected packet received after termination. Type = "+pkt.getType()+", SID = "+pkt.getSessionID()+", terminated-info = "+((pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0)); } return response; } public JICPPacket handleJICPPacket(JICPPacket pkt, InetAddress addr, int port) throws ICPException { throw new ICPException("Unexpected call"); } public void handleConnectionError(Connection c, Exception e) { // The MediatorManager got an exception reading from connection c // FIXME: What should we do? For the moment just print a warning if ((status == ACTIVE) && (frontEndStatus != TERMINATED)) { myLogger.log(Logger.WARNING, myID+" - Exception reading from the connection", e); } } public Properties getProperties() { return new Properties(); } private void handlePeerSelfTermination() { // The FrontEnd exited --> Set its new status and then suicide! setFrontEndTerminated(); kill(); } //////////////////////////////////////////////// // BEConnectionManager interface implementation //////////////////////////////////////////////// /** * Return a stub of the remote FrontEnd that is connected to the local BackEnd. * @param be The local BackEnd * @param props Additional (implementation dependent) connection configuration properties. * @return A stub of the remote FrontEnd. */ public FrontEnd getFrontEnd(BackEnd be, Properties props) throws IMTPException { return myStub; } /** * Termination initiated by the BackEndContainer (i.e. by the platform). * When the BackEndContainer exits */ public void shutdown() { myLogger.log(Logger.INFO, myID+" - Initiate NIOHTTPBEDispatcher shutdown"); status = NOT_ACTIVE; // Deregister from the MediatorManager if (myID != null) { myMediatorManager.deregisterMediator(myID); myID = null; } // Clean everything clean(); } ////////////////////////////////////////// // Dispatcher interface implementation ////////////////////////////////////////// /** * This is called by the Stub using this Dispatcher to dispatch a serialized command to the FrontEnd. * Mutual exclusion with itself to ensure one command at a time is dispatched. */ public synchronized byte[] dispatch(byte[] payload, boolean flush) throws ICPException { if (status == ACTIVE) { if (frontEndStatus == CONNECTED) { // The following check preserves dispatching order when the // front-end has just reconnected but flushing of postponed commands has not started yet if (waitingForFlush && !flush) { throw new ICPException("Upsetting dispatching order"); } waitingForFlush = false; // Wait for the connection to deliver outgoing commands to be ready Connection c = getOutgoingCommandsConnection(); // Send the command to the front-end JICPPacket cmd = new JICPPacket(JICPProtocol.COMMAND_TYPE, JICPProtocol.DEFAULT_INFO, payload); int sid = nextOutgoingCommandSid; nextOutgoingCommandSid = increment(nextOutgoingCommandSid); if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+" - Delivering outgoing command to front-end. SID = " + sid); } cmd.setSessionID((byte) sid); boolean deliverOK = false; try { c.writePacket(cmd); close(c); // Wait for the response JICPPacket response = getResponse(RESPONSE_TIMEOUT + RESPONSE_TIMEOUT_INCREMENT * (cmd.getLength() / 1024)); deliverOK = true; if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+" - Response got. SID = " + sid); } if (response.getType() == JICPProtocol.ERROR_TYPE) { // Communication OK, but there was a JICP error on the peer throw new ICPException(new String(response.getData())); } return response.getData(); } catch (IOException ioe) { setFrontEndDisconnected(); throw new ICPException("Error delivering outgoing command to front-end. ", ioe); } catch (ICPException icpe) { // Note that in this case setFrontEndDisconnected() is already called within getResponse() or getOutgoingCommandsConnection() throw icpe; } finally { if (!deliverOK) { nextOutgoingCommandSid = decrement(nextOutgoingCommandSid); } } } else { throw new ICPException("Front-end not connected"); } } else { throw new ICPException("Not-active"); } } private synchronized void dispatchKeepAlive() { if (status == ACTIVE) { if (frontEndStatus == CONNECTED) { try { // Wait for the connection to deliver outgoing commands to be ready Connection c = getOutgoingCommandsConnection(); // Send the command to the front-end if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, myID+" - Delivering keep-alive to front-end"); } JICPPacket cmd = new JICPPacket(JICPProtocol.KEEP_ALIVE_TYPE, JICPProtocol.DEFAULT_INFO, null); c.writePacket(cmd); close(c); // Wait for the response JICPPacket response = getResponse(RESPONSE_TIMEOUT + RESPONSE_TIMEOUT_INCREMENT * (cmd.getLength() / 1024)); if (isKeepAliveResponse(response)) { if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, myID+" - Keep-alive response got"); } } else { // Should never happen myLogger.log(Logger.WARNING, "Unexpected response received while waiting for Keep-alive response"); } } catch (IOException ioe) { myLogger.log(Logger.WARNING, myID+" - Error delivering keep-alive packet to the front-end", ioe); setFrontEndDisconnected(); } catch (ICPException icpe) { // Note that in this case setFrontEndDisconnected() is already called within getResponse() or getOutgoingCommandsConnection() if (frontEndStatus != TERMINATED) { myLogger.log(Logger.WARNING, myID+" - Keep-alive error. "+icpe.getMessage()); } } } } } /** * Wait until a connection to deliver commands to the FrontEnd is ready * @see handleResponse() */ private Connection getOutgoingCommandsConnection() throws ICPException { try { synchronized (outgoingCommandsConnectionLock) { while (outgoingCommandsConnection == null) { outgoingCommandsConnectionLock.wait(OUTGOING_COMMANDS_CONNECTION_TIMEOUT); if (outgoingCommandsConnection == null) { if (frontEndStatus == TERMINATED) { // We terminated in the meanwhile throw new ICPException("Terminated"); } else { // Timeout expired setFrontEndDisconnected(); throw new ICPException("Response timeout"); } } } Connection c = outgoingCommandsConnection; outgoingCommandsConnection = null; return c; } } catch (InterruptedException ie) { throw new ICPException("Interrupted while waiting for outgoing-commands-connection"); } } /** * Wait until the response to the last outgoing command is received * @see handleResponse() */ private JICPPacket getResponse(long timeout) throws ICPException { try { synchronized (responseToLastOutgoingCommandLock) { while (responseToLastOutgoingCommand == null) { responseToLastOutgoingCommandLock.wait(timeout); if (responseToLastOutgoingCommand == null) { if (frontEndStatus == TERMINATED) { // We terminated in the meanwhile throw new ICPException("Terminated"); } else { // Timeout expired setFrontEndDisconnected(); throw new ICPException("Response timeout"); } } } JICPPacket response = responseToLastOutgoingCommand; responseToLastOutgoingCommand = null; return response; } } catch (InterruptedException ie) { throw new ICPException("Interrupted while waiting for response to outgoing command"); } } private void handleResponse(Connection c, JICPPacket response, String from) { if (frontEndStatus != CONNECTED) { if (frontEndStatus == CONNECTING) { // Initial dummy response myLogger.log(Logger.INFO, myID+" - Initial dummy response received."); } else { // Unexpected response myLogger.log(Logger.WARNING, myID+" - Unexpected (likely out of time) response received.", new Exception("DUMMY!!!!")); } // In any case now we are connected setFrontEndConnected(); } else { // Normal response: pass it to the thread that dispatched the command if (isKeepAliveResponse(response)) { if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, myID+" - Keep-alive response received"); } } else { if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+" - Response received. SID = " + response.getSessionID()); } } synchronized (responseToLastOutgoingCommandLock) { responseToLastOutgoingCommand = response; responseToLastOutgoingCommandLock.notifyAll(); } } // Store the connection: it will be used to deliver the next outgoing command synchronized (outgoingCommandsConnectionLock) { outgoingCommandsConnection = c; outgoingCommandsConnectionLock.notifyAll(); } updateKeepAliveTimer(); } private void close(Connection c) { try { c.close(); } catch (Exception e) { e.printStackTrace(); } } private int increment(int val) { return (val + 1) & MAX_SID; } private int decrement(int val) { val--; if (val < 0) { val = MAX_SID; } return val; } private boolean isKeepAliveResponse(JICPPacket response) { // The OK_INFO bit is set only on KEEP-ALIVE responses return (response.getInfo() & JICPProtocol.OK_INFO) != 0; } private void setFrontEndConnecting() { frontEndStatus = CONNECTING; resetMaxDisconnectionTimer(); outgoingCommandsConnection = null; responseToLastOutgoingCommand = null; } private void setFrontEndConnected() { frontEndStatus = CONNECTED; resetMaxDisconnectionTimer(); waitingForFlush = myStub.flush(); } private void setFrontEndDisconnected() { frontEndStatus = DISCONNECTED; activateMaxDisconnectionTimer(); } private void setFrontEndTerminated() { frontEndStatus = TERMINATED; } private synchronized void updateKeepAliveTimer() { if (keepAliveTime > 0) { // Update the timer that triggers the delivery of a KEEP-ALIVE packet if (keepAliveTimer != null) { Runtime.instance().getTimerDispatcher().remove(keepAliveTimer); } long now = System.currentTimeMillis(); keepAliveTimer = new Timer(now + keepAliveTime, new TimerListener() { public void doTimeOut(Timer t) { dispatchKeepAlive(); } }); keepAliveTimer = Runtime.instance().getTimerDispatcher().add(keepAliveTimer); if (myLogger.isLoggable(Logger.FINEST)) { myLogger.log(Logger.FINEST, myID+" - Keep-alive timer activated."); } } } // No need for synchronization as this is always executed within a synchronized block private void activateMaxDisconnectionTimer() { // Set the disconnection timer long now = System.currentTimeMillis(); maxDisconnectionTimer = new Timer(now + maxDisconnectionTime, new TimerListener() { public void doTimeOut(Timer t) { synchronized (NIOHTTPBEDispatcher.this) { if (frontEndStatus != CONNECTED) { myLogger.log(Logger.WARNING, myID+" - Max disconnection timeout expired."); // The remote FrontEnd is probably down --> notify up. handlePeerSelfTermination(); } } } }); maxDisconnectionTimer = Runtime.instance().getTimerDispatcher().add(maxDisconnectionTimer); myLogger.log(Logger.INFO, myID+" - Max-disconnection-timer activated."); } private void resetMaxDisconnectionTimer() { if (maxDisconnectionTimer != null) { Runtime.instance().getTimerDispatcher().remove(maxDisconnectionTimer); maxDisconnectionTimer = null; } } private void clean() { // Be sure not to leave any pending timer resetMaxDisconnectionTimer(); // If there is some thread waiting for something, make it exit synchronized (responseToLastOutgoingCommandLock) { responseToLastOutgoingCommand = null; responseToLastOutgoingCommandLock.notifyAll(); } synchronized (outgoingCommandsConnectionLock) { outgoingCommandsConnection = null; outgoingCommandsConnectionLock.notifyAll(); } } private JICPPacket createTerminationNotificationAck() { return new JICPPacket(JICPProtocol.COMMAND_TYPE, JICPProtocol.TERMINATED_INFO, null); } }