/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.server; import java.io.IOException; import java.io.InterruptedIOException; import java.net.Socket; import java.util.ArrayList; import java.util.Date; import com.slamd.asn1.ASN1Element; import com.slamd.asn1.ASN1Reader; import com.slamd.asn1.ASN1Writer; import com.slamd.common.Constants; import com.slamd.common.SLAMDException; import com.slamd.job.JobClass; import com.slamd.message.ClientManagerHelloMessage; import com.slamd.message.HelloResponseMessage; import com.slamd.message.KeepAliveMessage; import com.slamd.message.Message; import com.slamd.message.ServerShutdownMessage; import com.slamd.message.StartClientRequestMessage; import com.slamd.message.StartClientResponseMessage; import com.slamd.message.StatusResponseMessage; import com.slamd.message.StopClientRequestMessage; import com.slamd.message.StopClientResponseMessage; /** * This class encapsulates a connection to a client manager, and it is used to * keep track of information about them. * * * @author Neil A. Wilson */ public class ClientManagerConnection extends Thread implements Comparable { // The queue that will be used to hold messages received from the client // manager. private ArrayList<Message> messageQueue; // The ASN.1 reader used to read data from the client manager. private ASN1Reader asn1Reader; // The ASN.1 writer used to write data to the client manager. private ASN1Writer asn1Writer; // Indicates whether this thread should continue listening for communication // from the client manager. private boolean keepListening; // The listener that accepted this client manager connection. private ClientManagerListener listener; // The time that this connection was established. private Date establishedTime; // The maximum number of clients that may be created by this client manager. private int maxClients; // The next message ID that should be used for sending a request to the client // manager. private int nextMessageID; // The number of clients that have been started by this client manager. private int startedClients; // A mutex used to provide threadsafe access to the message queue. private final Object messageQueueMutex; // The SLAMD server with which this client manager connection is associated. private SLAMDServer slamdServer; // The socket that provides communication with the client manager. private Socket clientManagerSocket; // The client ID of the client associated with this connection. private String clientID; // The IP address for this client manager connection. private String clientIPAddress; // The version of the software on the client associated with this connection. private String clientVersion; /** * Creates a new client manager connection using the provided socket. * * @param slamdServer The SLAMD server with which this client * manager connection is associated. * @param listener The client manager listener associated with * this connection. * @param clientManagerSocket The socket used to communicate with the client * manager. * * @throws SLAMDException If a problem occurs while creating the client * manager connection. */ public ClientManagerConnection(SLAMDServer slamdServer, ClientManagerListener listener, Socket clientManagerSocket) throws SLAMDException { this.slamdServer = slamdServer; this.listener = listener; this.clientManagerSocket = clientManagerSocket; messageQueue = new ArrayList<Message>(); messageQueueMutex = new Object(); startedClients = 0; nextMessageID = 2; establishedTime = new Date(); // Get the IP address of the client manager and create the ASN.1 reader and // writer. try { clientIPAddress = clientManagerSocket.getInetAddress().getHostAddress(); asn1Reader = new ASN1Reader(clientManagerSocket); asn1Writer = new ASN1Writer(clientManagerSocket); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to establish the reader and/or writer " + "to the client manager.", ioe); } // Read the hello request from the client. ClientManagerHelloMessage helloMessage = null; try { ASN1Element element = asn1Reader.readElement(Constants.MAX_BLOCKING_READ_TIME); helloMessage = (ClientManagerHelloMessage) Message.decode(element); } catch (Exception e) { disconnect(false); slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(e)); throw new SLAMDException("Unable to read or parse the hello message " + "from the client manager: " + e, e); } // Extract the information contained in the hello request. clientID = helloMessage.getClientID(); clientVersion = helloMessage.getClientVersion(); maxClients = helloMessage.getMaxClients(); // If we should use keepalive messages, then add a socket timeout for this // connection. int keepAliveInterval = slamdServer.getClientListener().getKeepAliveInterval(); if (keepAliveInterval > 0) { try { clientManagerSocket.setSoTimeout(keepAliveInterval*1000); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Unable to set socket timeout for connection " + "to client manager " + clientID + " -- keepalive messages will not be used."); } } // See if the client ID for this client conflicts with the client ID of // a client manager that has already been connected. ClientManagerConnection[] cmConns = listener.getClientManagers(); for (int i=0; i < cmConns.length; i++) { if (cmConns[i].getClientID().equalsIgnoreCase(clientID)) { try { HelloResponseMessage helloResponse = new HelloResponseMessage(helloMessage.getMessageID(), Constants.MESSAGE_RESPONSE_CLIENT_REJECTED, "A client manager connection has already been " + "established with client ID \"" + clientID + "\".", -1); asn1Writer.writeElement(helloResponse.encode()); disconnect(true); } catch (IOException ioe) { disconnect(false); slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to send the hello response to the " + "client manager: " + ioe, ioe); } throw new SLAMDException("Rejected client manager connection due to " + "duplicate client ID -- " + clientID + '.'); } } // Send the hello response to the client manager. try { HelloResponseMessage helloResponse = new HelloResponseMessage(helloMessage.getMessageID(), Constants.MESSAGE_RESPONSE_SUCCESS, -1); asn1Writer.writeElement(helloResponse.encode()); } catch (IOException ioe) { disconnect(false); slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to send the hello response to the " + "client manager: " + ioe, ioe); } setName("Client Manager Connection " + clientID); } /** * Retrieves the ID of the client associated with this client manager * connection. * * @return The ID of the client associated with this client manager * connection. */ public String getClientID() { return clientID; } /** * Retrieves the IP address of the client manager system. * * @return The IP address of the client manager system. */ public String getClientIPAddress() { return clientIPAddress; } /** * Retrieves the software version of the client associated with this client * manager connection. * * @return The software version of the client associated with this client * manager connection. */ public String getClientVersion() { return clientVersion; } /** * Retrieves the time at which this connection was established. * * @return The time at which this connection was established. */ public Date getEstablishedTime() { return establishedTime; } /** * Retrieves the maximum number of clients that may be started for this client * manager. * * @return The maximum number of clients that may be started for this client * manager. */ public int getMaxClients() { return maxClients; } /** * Retrieves the number of clients associated with this client manager that * are currently running. * * @return The number of clients associated with this client manager that are * currently running. */ public int getStartedClients() { return startedClients; } /** * Requests that the client manager start the specified number of clients. * * @param numClients The number of clients to be started by this client * manager. * * @throws SLAMDException If the number of clients requested would cause the * client manager to start more than the maximum * allowed number of clients, or if there is a * problem starting any of the clients. */ public void startClients(int numClients) throws SLAMDException { // First, see if we should allow this based on what we believe is running. if ((maxClients > 0) && ((startedClients + numClients) > maxClients)) { throw new SLAMDException("Requested number of clients (" + numClients + ") would create more than the maximum number " + "of allowed connections (" + maxClients + ')'); } // Create the start client request and send it to the client manager. int messageID = nextMessageID(); try { StartClientRequestMessage startMessage = new StartClientRequestMessage(messageID, numClients, slamdServer.getClientListener().listenPort); asn1Writer.writeElement(startMessage.encode()); } catch (IOException ioe) { disconnect(false); slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to send start client request to " + "client manager " + clientID + " -- closing the connection.", ioe); } // Read the response from the client manager. try { StartClientResponseMessage startResponseMessage = (StartClientResponseMessage) getMessage(messageID); if (startResponseMessage == null) { disconnect(false); throw new SLAMDException("Unable to read response message from the " + "client manager -- closing the connection."); } else { if (startResponseMessage.getResponseCode() == Constants.MESSAGE_RESPONSE_SUCCESS) { startedClients += numClients; return; } else { throw new SLAMDException("Unable to start requested clients: " + startResponseMessage.getResponseMessage() + " (response code " + startResponseMessage.getResponseCode() + ')'); } } } catch (Exception e) { slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(e)); throw new SLAMDException("Unable to read response message from the " + "client manager -- " + e, e); } } /** * Requests that the client manager start the specified number of clients. * * @param numClients The number of clients to be stopped by this client * manager. A negative value will indicate that all * clients should be stopped. * * @throws SLAMDException If the requested number of clients is greater * than the number of clients actually running, or if * a problem occurs while trying to stop the clients. */ public void stopClients(int numClients) throws SLAMDException { // See if there was a specific number of clients specified. If so, then see // if we think there are that many running. if ((numClients > 0) && (numClients > startedClients)) { throw new SLAMDException("Request to stop " + numClients + " clients for client manager " + clientID + " rejected -- only " + startedClients + " clients have been started"); } // Create the stop client request and send it to the client manager. int messageID = nextMessageID(); try { StopClientRequestMessage stopMessage = new StopClientRequestMessage(messageID, numClients); asn1Writer.writeElement(stopMessage.encode()); } catch (IOException ioe) { disconnect(false); slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to send stop client request to " + "client manager " + clientID + " -- closing the connection.", ioe); } // Read the response from the client manager. try { StopClientResponseMessage stopResponseMessage = (StopClientResponseMessage) getMessage(messageID); if (stopResponseMessage == null) { disconnect(false); throw new SLAMDException("Unable to read response message from the " + "client manager -- closing the connection."); } else { if (stopResponseMessage.getResponseCode() == Constants.MESSAGE_RESPONSE_SUCCESS) { if (numClients > 0) { startedClients -= numClients; } else { startedClients = 0; } return; } else { throw new SLAMDException("Unable to stop requested clients: " + stopResponseMessage.getResponseMessage() + " (response code " + stopResponseMessage.getResponseCode() + ')'); } } } catch (Exception e) { slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(e)); throw new SLAMDException("Unable to read response message from the " + "client manager -- " + e, e); } } /** * Closes the connection to the client manager. The process of closing the * connection may optionally include sending a shutdown message to the client * manager before the actual disconnect. * * @param sendShutdownMessage Indicates whether a server shutdown message * should be sent to the client manager before * the connection is closed. */ public void disconnect(boolean sendShutdownMessage) { // Create the shutdown message, if appropriate. if (sendShutdownMessage) { ServerShutdownMessage shutdownMessage = new ServerShutdownMessage(nextMessageID()); try { asn1Writer.writeElement(shutdownMessage.encode()); } catch (Exception e) {} } // Close the connection to the client manager. try { clientManagerSocket.close(); } catch (Exception e) {} } /** * Indicates that a client believed to have been started by this client * manager has been lost and that the list of active connections should be * updated accordingly. */ public void clientConnectionLost() { if (startedClients > 0) { startedClients--; } } /** * Retrieves the message ID that should be used for the next request sent to * the client manager. * * @return The message ID that should be used for the next request sent to * the client manager. */ public synchronized int nextMessageID() { int returnID = nextMessageID; nextMessageID += 2; return returnID; } /** * Retrieves the message with the specified message ID from the receive queue. * * @param messageID The message ID of the message to retrieve. * * @return The requested message, or <CODE>null</CODE> if an appropriate * response does not arrive within an appropriate timeout period. */ public Message getMessage(int messageID) { synchronized (messageQueueMutex) { for (int i=0; i < messageQueue.size(); i++) { Message message = messageQueue.get(i); if (message.getMessageID() == messageID) { messageQueue.remove(i); return message; } } // If we have gotten here, then the requested message wasn't in the // queue. Wait for it to arrive. try { messageQueueMutex.wait(1000 * listener.getMaxResponseWaitTime()); } catch (InterruptedException ie) {} for (int i=0; i < messageQueue.size(); i++) { Message message = messageQueue.get(i); if (message.getMessageID() == messageID) { messageQueue.remove(i); return message; } } } // If we have gotten here, then the message didn't arrive. Return null. return null; } /** * Create a loop that waits for new communication to arrive from the client * manager. If it is a solicited response (i.e, has a message ID that is an * even number), then put it in the message queue to be picked up by an * appropriate listener. If it is an unsolicited message (i.e., has an odd * number), then try to handle it directly. */ @Override() public void run() { keepListening = true; while (keepListening) { try { ASN1Element element = asn1Reader.readElement(); if (element == null) { // This should only happen if the client has closed the connection. slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Detected connection closure from client " + "manager " + clientID); try { clientManagerSocket.close(); } catch (IOException ioe2) {} listener.connectionLost(this); return; } Message message = Message.decode(element); int messageID = message.getMessageID(); if ((messageID % 2) == 0) { synchronized (messageQueueMutex) { messageQueue.add(message); messageQueueMutex.notify(); } } else { // The only only unsolicited message type that we allow is a // status response message that indicates the client manager is // shutting down. if (message instanceof StatusResponseMessage) { StatusResponseMessage statusResponse = (StatusResponseMessage) message; if (statusResponse.getClientStatusCode() == Constants.CLIENT_STATE_SHUTTING_DOWN) { keepListening = false; break; } else { // This was not an expected response -- that's a protocol error. slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Unexpected status response message " + "received from client manager " + clientID + ": response code " + statusResponse.getClientStatusCode()); disconnect(true); keepListening = false; break; } } else { // This was not an allowed message -- that's a protocol error and // we should close the connection to the client manager. slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Unexpected unsolicited message " + "received from client manager " + clientID + ": message type " + message.getMessageType()); disconnect(true); keepListening = false; break; } } } catch (InterruptedIOException iioe) { // This means that a socket timeout occurred and that we should send a // keepalive message to the client manager. KeepAliveMessage keepAliveMessage = new KeepAliveMessage(nextMessageID()); try { asn1Writer.writeElement(keepAliveMessage.encode()); } catch (IOException ioe) { // This should not happen. But if it does, close the connection to // the client. slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Unable to send keepalive message to client " + clientID + " (" + ioe + ") -- disconnecting"); keepListening = false; break; } } catch (Exception e) { // This should be either an ASN.1 exception or an I/O exception. If // either occurs, then that almost certainly means that the connection // is unusable, so drop it. keepListening = false; } } synchronized (listener.clientManagerMutex) { listener.connectionLost(this); } try { clientManagerSocket.close(); } catch (Exception e) {} } /** * Compares this client manager connection with the provided object. The * given object must be a client manager connection object, and the comparison * will be made based on the lexicographic ordering of the associated client * IDs. * * @param o The client manager connection object to compare against this * client manager connection. * * @return A negative value if this client manager connection should come * before the provided client manager connection in a sorted list, a * positive value if this client manager connection should come after * the provided client manager connection in a sorted list, or zero * if there is no difference in their ordering as far as this method * is concerned. * * @throws ClassCastException If the provided object is not a client manager * connection object. */ public int compareTo(Object o) throws ClassCastException { ClientManagerConnection c = (ClientManagerConnection) o; return clientID.compareTo(c.clientID); } }