package jade.imtp.leap.nio; //#J2ME_EXCLUDE_FILE import jade.core.FrontEnd; import jade.core.BackEnd; import jade.core.BackEndContainer; import jade.core.BEConnectionManager; import jade.core.Profile; import jade.core.ProfileException; import jade.core.IMTPException; import jade.imtp.leap.BackEndSkel; import jade.imtp.leap.FrontEndStub; import jade.imtp.leap.Dispatcher; import jade.imtp.leap.ICPException; import jade.imtp.leap.JICP.JICPProtocol; import jade.imtp.leap.JICP.JICPMediatorManager; import jade.imtp.leap.JICP.JICPPacket; import jade.imtp.leap.JICP.Connection; import jade.util.leap.Properties; import jade.util.Logger; import java.io.IOException; import java.net.InetAddress; import java.util.logging.Level; /** This class implements the BIFEDispatcher related BackEnd dispatcher managable by an asynchronous JICPMediatorManager @author Giovanni Caire - Telecom Italia LAB S.p.A. */ public class NIOBEDispatcher implements NIOMediator, BEConnectionManager, Dispatcher { private static final long RESPONSE_TIMEOUT = 60000; private long keepAliveTime; private boolean enableServerKeepAlive; private long lastReceivedTime; private boolean active = true; private boolean peerActive = true; private boolean connectionDropped = false; private JICPMediatorManager myMediatorManager; private String myID; private Properties myProperties; private BackEndContainer myContainer = null; protected InputManager inpManager; protected OutputManager outManager; private Logger myLogger = Logger.getMyLogger(getClass().getName()); /** Retrieve the ID of this mediator. Returns null if this mediator is not active */ public String getID() { return (active ? myID : null); } /** Retrieve the startup Properties for this NIOBEDispatcher. */ public Properties getProperties() { return myProperties; } /** Initialize this JICPMediator */ public void init(JICPMediatorManager mgr, String id, Properties props) throws ICPException { myMediatorManager = mgr; myID = id; myProperties = props; // Max disconnection time long maxDisconnectionTime = JICPProtocol.DEFAULT_MAX_DISCONNECTION_TIME; try { maxDisconnectionTime = Long.parseLong(props.getProperty(JICPProtocol.MAX_DISCONNECTION_TIME_KEY)); } catch (Exception e) { // Keep default } // Keep-alive time keepAliveTime = JICPProtocol.DEFAULT_KEEP_ALIVE_TIME; try { keepAliveTime = Long.parseLong(props.getProperty(JICPProtocol.KEEP_ALIVE_TIME_KEY)); } catch (Exception e) { // Keep default } // Server-keep-alive time enableServerKeepAlive = false; try { enableServerKeepAlive = Boolean.valueOf(props.getProperty("enable-server-keep-alive")).booleanValue(); } catch (Exception e) { // Keep default } // inpCnt int inpCnt = 0; try { inpCnt = (Integer.parseInt(props.getProperty("lastsid")) + 1) & 0x0f; } catch (Exception e) { // Keep default } System.out.println("Next command for FE will have sessionID " + inpCnt); // lastSid int lastSid = 0x0f; try { lastSid = (byte) (Integer.parseInt(props.getProperty("outcnt")) - 1); if (lastSid < 0) { lastSid = 0x0f; } } catch (Exception e) { // Keep default } FrontEndStub st = new FrontEndStub(this); inpManager = new InputManager(inpCnt, st); BackEndSkel sk = startBackEndContainer(props); outManager = new OutputManager(lastSid, sk, maxDisconnectionTime); } protected 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(); if (myLogger.isLoggable(Logger.CONFIG)) { myLogger.log(Logger.CONFIG, "BackEndContainer " + myID + " successfully joined the platform"); } return new BackEndSkel(myContainer); } catch (ProfileException pe) { // should never happen pe.printStackTrace(); throw new ICPException("Error creating profile"); } } // Local variable only used in the kill() method private Object shutdownLock = new Object(); /** Kill the above container. This may be called by the JICPMediatorManager or when a peer termination notification is received. */ public void kill() { // Avoid killing the above container two times synchronized (shutdownLock) { if (active) { active = false; myContainer.shutDown(); } } } /** * Passes to this JICPMediator the connection opened by the mediated * entity. * This is called by the JICPMediatorManager this Mediator is attached to * as soon as the mediated entity (re)connects. * @param c the connection to the mediated entity * @param pkt the packet that was sent by the mediated entity when * opening this connection * @param addr the address of the mediated entity * @param port the local port used by the mediated entity * @return an indication to the JICPMediatorManager to keep the * connection open. */ public boolean handleIncomingConnection(Connection c, JICPPacket pkt, InetAddress addr, int port) { checkTerminatedInfo(pkt); if (peerActive) { if (connectionDropped) { droppedToDisconnected(); } // Update keep-alive info lastReceivedTime = System.currentTimeMillis(); boolean inp = false; byte[] data = pkt.getData(); if (data.length == 1) { inp = (data[0] == 1); } if (inp) { inpManager.setConnection((NIOJICPConnection) c); if (myLogger.isLoggable(Logger.CONFIG)) { myLogger.log(Logger.CONFIG, myID + ": New INP Connection establishd"); } } else { outManager.setConnection(c); if (myLogger.isLoggable(Logger.CONFIG)) { myLogger.log(Logger.CONFIG, myID + ": New OUT Connection establishd"); } } return true; } else { // The remote FrontEnd has terminated spontaneously --> // Kill the above container (this will also kill this NIOBEDispatcher). kill(); return false; } } /** Notify this NIOMediator that an error occurred on one of the Connections it was using. This information is important since, unlike normal mediators, a NIOMediator typically does not read packets from connections on its own (the JICPMediatorManager does that in general). */ public void handleConnectionError(Connection c, Exception e) { myLogger.log(Level.WARNING, "connection error", e); if (active && peerActive) { // Try assuming it is the input connection try { inpManager.checkConnection(c); myLogger.log(Logger.WARNING, myID + ": IC Disconnection detected"); inpManager.resetConnection(); } catch (ICPException icpe) { // Then try assuming it is the output connection try { outManager.checkConnection(c); myLogger.log(Logger.WARNING, myID + ": OC Disconnection detected"); outManager.resetConnection(); } catch (ICPException icpe2) { // Ignore it } } } } /** Passes to this mediator a JICPPacket received by the JICPMediatorManager this mediator is attached to. In a NIOMediator this should never be called. */ public JICPPacket handleJICPPacket(JICPPacket p, InetAddress addr, int port) throws ICPException { throw new ICPException("Unexpected call"); } /** Overloaded version of the handleJICPPacket() method including the <code>Connection</code> the incoming JICPPacket was received from. This information is important since, unlike normal mediators, a NIOMediator may not read packets from connections on its own (the JICPMediatorManager does that in general). */ public JICPPacket handleJICPPacket(Connection c, JICPPacket pkt, InetAddress addr, int port) throws ICPException { if (pkt.getType() == JICPProtocol.DROP_DOWN_TYPE) { // Note that the return packet is written inside the handleDropDown() // method since the connection must be closed after the response has // been sent back. handleDropDown(c, pkt, addr, port); return null; } checkTerminatedInfo(pkt); // Update keep-alive info lastReceivedTime = System.currentTimeMillis(); byte type = pkt.getType(); if (type == JICPProtocol.COMMAND_TYPE) { if (peerActive) { return outManager.handleCommand(c, pkt); } else { // The remote FrontEnd has terminated spontaneously --> // Kill the above container (this will also kill this NIOBEDispatcher). kill(); return null; } } else if (type == JICPProtocol.KEEP_ALIVE_TYPE) { if (enableServerKeepAlive) { inpManager.sendServerKeepAlive(); } return outManager.handleKeepAlive(c, pkt); } /* Asynch-reply else if (type == JICPProtocol.RESPONSE_TYPE || type == JICPProtocol.ERROR_TYPE) { inpManager.handleResponse(c, pkt); return null; }*/ else { throw new ICPException("Unexpected packet type " + type); } } /** This is periodically called by the JICPMediatorManager and is used by this NIOMediator to evaluate the elapsed time without the need of a dedicated thread or timer. */ public final void tick(long currentTime) { if (active && !connectionDropped) { // 1) If there is a blocking read operation in place check the // response timeout inpManager.checkResponseTime(currentTime); // 2) Evaluate the keep alive if (keepAliveTime > 0) { if ((currentTime - lastReceivedTime) > (keepAliveTime + RESPONSE_TIMEOUT)) { // Missing keep-alive. // The OUT connection is no longer valid if (outManager.isConnected()) { myLogger.log(Logger.WARNING, myID + ": Missing keep-alive"); outManager.resetConnection(); } // Check the INP connection. Since this method must return // asap, does it in a separated Thread if (inpManager.isConnected()) { Thread t = new Thread() { public void run() { try { //JICPPacket pkt = new JICPPacket(JICPProtocol.KEEP_ALIVE_TYPE, JICPProtocol.DEFAULT_INFO, null); //inpManager.dispatch(pkt, false); inpManager.sendServerKeepAlive(); if (myLogger.isLoggable(Logger.CONFIG)) { myLogger.log(Logger.CONFIG, myID + ": IC valid"); } } catch (Exception e) { // Just do nothing: the INP connection has been reset } } }; t.start(); } } } // 3) Evaluate the max disconnection time if (outManager.checkMaxDisconnectionTime(currentTime)) { myLogger.log(Logger.SEVERE, myID + ": Max disconnection time expired."); // Consider as if the FrontEnd has terminated spontaneously --> // Kill the above container (this will also kill this NIOBEDispatcher). 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 inpManager.getStub(); } /** Make this NIOBEDispatcher terminate. */ public void shutdown() { active = false; if (myLogger.isLoggable(Logger.INFO)) { myLogger.log(Logger.INFO, myID + ": shutting down"); } // Deregister from the JICPServer if (myID != null) { myMediatorManager.deregisterMediator(myID); } inpManager.shutdown(); outManager.shutdown(); } ////////////////////////////////////////// // Dispatcher interface implementation ////////////////////////////////////////// public byte[] dispatch(byte[] payload, boolean flush) throws ICPException { if (connectionDropped) { // Move from DROPPED state to DISCONNECTED state and wait // for the FE to reconnect droppedToDisconnected(); requestRefresh(); throw new ICPException("Connection dropped"); } else { // Normal dispatch JICPPacket pkt = new JICPPacket(JICPProtocol.COMMAND_TYPE, JICPProtocol.DEFAULT_INFO, payload); pkt = inpManager.dispatch(pkt, flush); return pkt.getData(); } } ////////////////////////////////////////////////////// // Methods related to connection drop-down management ////////////////////////////////////////////////////// /** Handle a connection DROP_DOWN request from the FE. */ protected void handleDropDown(Connection c, JICPPacket pkt, InetAddress addr, int port) { if (myLogger.isLoggable(Logger.INFO)) { myLogger.log(Logger.INFO, myID + ": DROP_DOWN request received."); } try { // If the INP connection is down or we have some postponed command // to flush, refuse dropping the connection if (inpManager.isConnected() && inpManager.isEmpty()) { JICPPacket rsp = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, null); c.writePacket(rsp); inpManager.resetConnection(); outManager.resetConnection(); connectionDropped = true; } else { myLogger.log(Logger.WARNING, myID + ": DROP_DOWN request refused."); JICPPacket rsp = new JICPPacket(JICPProtocol.ERROR_TYPE, getReconnectInfo(), null); c.writePacket(rsp); } } catch (Exception e) { myLogger.log(Logger.WARNING, myID + ": Error writing DROP_DOWN response. " + e); } } /** Move from the connectionDropped state to the Disconnected state. This may happen when - a packet must be dispatched to the FE. - an incoming connection is detected */ private void droppedToDisconnected() { connectionDropped = false; outManager.setExpirationDeadline(); } /** Request the FE to refresh the connection. */ protected void requestRefresh() { } public boolean isConnected() { return inpManager.isConnected() && outManager.isConnected(); } private void updateConnectedState() { myProperties.put(BEManagementHelper.CONNECTED, (isConnected() ? "true" : "false")); } /** Inner class InputManager. This class manages the delivery of commands to the FrontEnd */ protected class InputManager { private NIOJICPConnection myConnection; private boolean dispatching = false; private boolean connectionRefreshed; private boolean waitingForFlush; private long readStartTime = -1; private JICPPacket currentReply; private Object dispatchLock = new Object(); private int inpCnt; private FrontEndStub myStub; InputManager(int c, FrontEndStub s) { inpCnt = c; myStub = s; } FrontEndStub getStub() { return myStub; } synchronized void setConnection(NIOJICPConnection c) { // Reset the old connection resetConnection(); // Set the new connection myConnection = c; connectionRefreshed = true; waitingForFlush = myStub.flush(); //myContainer.notifyInputConnectionReady(); updateConnectedState(); } synchronized void resetConnection() { // Close the connection if it was in place if (myConnection != null) { close(myConnection); myConnection = null; } // Asynch-reply // If there was someone waiting for a response on the // connection notify it. //notifyAll(); updateConnectedState(); } final void checkConnection(Connection c) throws ICPException { if (c != myConnection) { throw new ICPException("Wrong connection"); } } final boolean isConnected() { return myConnection != null; } final boolean isEmpty() { // We are empty if we are not dispatching a JICPPacket and our stub // has no postponed commands waiting to be delivered. return (!dispatching) && myStub.isEmpty(); } void shutdown() { resetConnection(); } /** Dispatch a JICP command to the FE and get back a reply. This method must NOT be executed in mutual exclusion with setConnection() and resetConnection() since it performs a blocking read operation --> It can't just be declared synchronized. */ final JICPPacket dispatch(JICPPacket pkt, boolean flush) throws ICPException { synchronized (dispatchLock) { dispatching = true; try { synchronized (this) { if ((!active) || (myConnection == null) || (waitingForFlush && (!flush))) { // If we are waiting for flushed packets and the current packet // is a normal (i.e. non-flushed) one, then throw an exception --> // The packet will be put in the queue of packets to be flushed throw new ICPException("Unreachable"); } waitingForFlush = false; connectionRefreshed = false; } try { pkt.setSessionID((byte) inpCnt); if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID + ": Sending command " + inpCnt + " to FE"); } long start = System.currentTimeMillis(); myConnection.writePacket(pkt); // Asynch-reply: JICPPacket reply = waitForReply(RESPONSE_TIMEOUT); readStartTime = System.currentTimeMillis(); JICPPacket reply = myConnection.readPacket(); readStartTime = -1; checkTerminatedInfo(reply); lastReceivedTime = System.currentTimeMillis(); long end = lastReceivedTime; System.out.println("INP Session " + inpCnt + ". Dispatching time = " + (end - start)); if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, myID + ": Received response " + inpCnt + " from FE"); } if (reply.getType() == JICPProtocol.ERROR_TYPE) { // Communication OK, but there was a JICP error on the peer throw new ICPException(new String(pkt.getData())); } if (!peerActive) { // This is the response to an exit command --> Suicide, without // killing the above container since it is already dying. NIOBEDispatcher.this.shutdown(); } inpCnt = (inpCnt + 1) & 0x0f; return reply; } catch (NullPointerException npe) { // This can happen if a resetConnection() occurs just before // myConnection.writePacket()/readPacket() is called. throw new ICPException("Connection reset."); } catch (IOException ioe) { synchronized (this) { if (myConnection != null && !connectionRefreshed) { // There was an IO exception writing data to the connection // --> reset the connection. myLogger.log(Logger.WARNING, myID + ": IOException IC. " + ioe); resetConnection(); } } readStartTime = -1; throw new ICPException("Dispatching error.", ioe); } } finally { dispatching = false; } } } public final void checkResponseTime(long currentTime) { if (readStartTime > 0 && (currentTime - readStartTime) > RESPONSE_TIMEOUT) { myLogger.log(Logger.WARNING, myID + ": Response timeout expired."); resetConnection(); } } public void sendServerKeepAlive() throws ICPException { JICPPacket pkt = new JICPPacket(JICPProtocol.KEEP_ALIVE_TYPE, JICPProtocol.DEFAULT_INFO, null); dispatch(pkt, false); } /* Asynch-reply final synchronized void handleResponse(Connection c, JICPPacket reply) throws ICPException { checkConnection(c); currentReply = reply; notifyAll(); } private synchronized JICPPacket waitForReply(long timeout) throws ICPException, IOException { try { if (currentReply == null) { wait(timeout); if (currentReply == null) { if (isConnected()) { if (connectionRefreshed) { throw new ICPException("Connection refreshed"); } else { throw new IOException("Response timeout expired"); } } else { throw new ICPException("Connection reset"); } } } JICPPacket tmp = currentReply; currentReply = null; return tmp; } catch (InterruptedException ie) { throw new ICPException("Interrupted"); } }*/ } // END of inner class InputManager /** Inner class OutputManager This class manages the reception of commands and keep-alive packets from the FrontEnd. This class also manages the maxDisconnectionTime, i.e. the remote FrontEnd is considered dead if it cannot re-establish the OUT connection within the maxDisconnectionTime. */ protected class OutputManager { private Connection myConnection; private JICPPacket lastResponse; private int lastSid; private BackEndSkel mySkel; private long maxDisconnectionTime, expirationDeadline; OutputManager(int n, BackEndSkel s, long t) { lastSid = n; mySkel = s; maxDisconnectionTime = t; } synchronized void setConnection(Connection c) { // Close the old connection if any if (myConnection != null) { close(myConnection); } // Set the new connection myConnection = c; updateConnectedState(); } synchronized void resetConnection() { if (myConnection != null) { setExpirationDeadline(); close(myConnection); } myConnection = null; updateConnectedState(); } synchronized void setExpirationDeadline() { expirationDeadline = System.currentTimeMillis() + maxDisconnectionTime; } final void checkConnection(Connection c) throws ICPException { if (c != myConnection) { throw new ICPException("Wrong connection"); } } final boolean isConnected() { return (myConnection != null); } void shutdown() { resetConnection(); } final synchronized JICPPacket handleCommand(Connection c, JICPPacket cmd) throws ICPException { checkConnection(c); JICPPacket reply = null; byte sid = cmd.getSessionID(); if (sid == lastSid) { myLogger.log(Logger.WARNING, myID + ": Duplicated command from FE " + sid); reply = lastResponse; } else { if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID + ": Received command " + sid + " from FE"); } byte[] rspData = mySkel.handleCommand(cmd.getData()); if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, myID + ": Command " + sid + " from FE served "); } reply = new JICPPacket(JICPProtocol.RESPONSE_TYPE, getReconnectInfo(), rspData); reply.setSessionID(sid); lastSid = sid; lastResponse = reply; } return reply; } synchronized JICPPacket handleKeepAlive(Connection c, JICPPacket command) throws ICPException { checkConnection(c); if (myLogger.isLoggable(Logger.FINEST)) { myLogger.log(Logger.FINEST, myID + ": Keep-alive received"); } return new JICPPacket(JICPProtocol.RESPONSE_TYPE, getReconnectInfo(), null); } final synchronized boolean checkMaxDisconnectionTime(long currentTime) { return (!isConnected()) && (currentTime > expirationDeadline); } } // END of inner class OutputManager private final byte getReconnectInfo() { byte info = JICPProtocol.DEFAULT_INFO; // If the inpConnection is null request the FrontEnd to reconnect if (!inpManager.isConnected()) { info |= JICPProtocol.RECONNECT_INFO; } return info; } private final void checkTerminatedInfo(JICPPacket pkt) { if ((pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0) { peerActive = false; if (myLogger.isLoggable(Logger.INFO)) { myLogger.log(Logger.INFO, myID + ": Peer termination notification received"); } } } private void close(Connection c) { try { c.close(); } catch (IOException ioe) { } } }