/*
* 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.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import javax.net.ssl.SSLServerSocketFactory;
import com.slamd.asn1.ASN1Writer;
import com.slamd.common.Constants;
import com.slamd.db.SLAMDDB;
import com.slamd.job.Job;
import com.slamd.job.JobClass;
import com.slamd.message.HelloResponseMessage;
import com.slamd.message.JobCompletedMessage;
import com.slamd.message.StatusRequestMessage;
import com.slamd.parameter.IntegerParameter;
import com.slamd.parameter.Parameter;
import com.slamd.parameter.ParameterList;
import com.slamd.stat.StatTracker;
/**
* This class implements the client listener that the SLAMD server uses to
* listen for connections from resource monitor clients. If fewer than the
* maximum connections are established, then the connection will be accepted and
* a new connection thread will be spawned to handle operations on that monitor
* connection. If the maximum number of connections are already in use, then
* the new connection will be rejected.
*
*
* @author Neil A. Wilson
*/
public class ResourceMonitorClientListener
extends Thread
implements ConfigSubscriber
{
// Indicates whether this listener should continue accepting client
// connections
private boolean keepListening;
// Indicates whether this listener has actually stopped.
private boolean hasStopped;
// Indicates whether this listener requires clients to authenticate.
private boolean requireAuthentication;
// Indicates whether this listener should use SSL.
private boolean useSSL;
// The length of time in seconds that should pass between keepalive messages
private int keepaliveInterval;
// The port on which the server is listening for new connections
private int listenPort;
// The maximum number of simultaneous clients that may be connected to the
// server at once.
private int maxClients;
// The maximum length of time in seconds that the client connection will wait
// for a response from a solicited message
private int maxResponseWaitTime;
// A map of all the resource monitor client connections that have been
// established.
private LinkedHashMap<String,ResourceMonitorClientConnection> connectionHash;
// A mutex used to protect multithreaded access to the connection hash.
private final Object connectionHashMutex;
// The configuration database associated with the SLAMD server
private SLAMDDB configDB;
// The server socket used to listen for new connections
private ServerSocket serverSocket;
// The SLAMD server with which this client listener is associated
private SLAMDServer slamdServer;
/**
* Creates a new listener to accept resource monitor client connections.
*
* @param slamdServer The SLAMD server with which this listener is
* associated.
*/
public ResourceMonitorClientListener(SLAMDServer slamdServer)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"Entering ResourceMonitorClientListener " +
"constructor");
setName("Resource Monitor Client Listener");
// Initialize all the instance variables
this.slamdServer = slamdServer;
maxClients = Constants.NO_MAX_CLIENTS;
connectionHash =
new LinkedHashMap<String,ResourceMonitorClientConnection>();
connectionHashMutex = new Object();
keepListening = true;
hasStopped = true;
// Read the appropriate information from the configuration
configDB = slamdServer.getConfigDB();
configDB.registerAsSubscriber(this);
listenPort = Constants.DEFAULT_MONITOR_LISTENER_PORT_NUMBER;
String portStr =
configDB.getConfigParameter(Constants.PARAM_MONITOR_LISTENER_PORT);
if ((portStr != null) && (portStr.length() != 0))
{
try
{
listenPort = Integer.parseInt(portStr);
} catch (NumberFormatException nfe) {}
}
ClientListener clientListener = slamdServer.getClientListener();
keepaliveInterval = clientListener.getKeepAliveInterval();
maxClients = clientListener.getMaxClients();
maxResponseWaitTime = clientListener.getMaxResponseWaitTime();
requireAuthentication = clientListener.requireAuthentication();
useSSL = clientListener.useSSL();
if (useSSL)
{
String keyStore = slamdServer.getSSLKeyStore();
if ((keyStore != null) && (keyStore.length() > 0))
{
System.setProperty(Constants.SSL_KEY_STORE_PROPERTY, keyStore);
}
String keyStorePassword = slamdServer.getSSLKeyStorePassword();
if ((keyStorePassword != null) && (keyStorePassword.length() > 0))
{
System.setProperty(Constants.SSL_KEY_PASSWORD_PROPERTY,
keyStorePassword);
}
String trustStore = slamdServer.getSSLTrustStore();
if ((trustStore != null) && (trustStore.length() > 0))
{
System.setProperty(Constants.SSL_TRUST_STORE_PROPERTY, trustStore);
}
String trustStorePassword = slamdServer.getSSLTrustStorePassword();
if ((trustStorePassword != null) && (trustStorePassword.length() > 0))
{
System.setProperty(Constants.SSL_TRUST_PASSWORD_PROPERTY,
trustStorePassword);
}
}
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"Leaving ResourceMonitorClientListener constructor");
}
/**
* Indicates that the listener should start listening for client connections.
*/
public void startListening()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientListener.startListening()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In ResourceMonitorClientListener.startListening()");
keepListening = true;
start();
}
/**
* Indicates that the listener should stop listening for client connections.
* It will also notify all connected clients that the listener is shutting
* down.
*/
public void stopListening()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientListener.stopListening()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In ResourceMonitorClientListener.stopListening()");
// Set the flag that indicates the server should no longer be listening
keepListening = false;
// Close the server socket so that no more connections will be accepted
try
{
serverSocket.close();
} catch (Exception e) {}
// Iterate through all the connections and inform them that the server is
// shutting down. The sendServerShutdownMessage() method will also close
// the connections.
synchronized (connectionHashMutex)
{
Iterator iterator = connectionHash.values().iterator();
while (iterator.hasNext())
{
ResourceMonitorClientConnection clientConnection =
(ResourceMonitorClientConnection) iterator.next();
clientConnection.sendServerShutdownMessage(true);
}
connectionHash.clear();
}
}
/**
* This method will not return until the client listener has actually stopped.
* Note that it does not stop the listener -- you should first call the
* <CODE>stopListening</CODE> method to signal the listener that it needs to
* stop.
*/
public void waitForStop()
{
while (! hasStopped)
{
try
{
Thread.sleep(Constants.THREAD_BLOCK_SLEEP_TIME);
} catch (InterruptedException ie) {}
}
}
/**
* Creates the server socket and listens for new connections. If the
* connection will be accepted, then a new connection thread will be spawned
* to handle it. If the connection will not be accepted, then it will be
* rejected here.
*/
@Override()
public void run()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.run()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In ResourceMonitorClientListener.run()");
hasStopped = false;
if (useSSL)
{
try
{
SSLServerSocketFactory socketFactory =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
serverSocket = socketFactory.createServerSocket(listenPort);
slamdServer.logMessage(Constants.LOG_LEVEL_ANY,
"Listening for SSL-based resource monitor " +
"client connections on port " + listenPort);
}
catch (Exception e)
{
e.printStackTrace();
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(e));
slamdServer.logMessage(Constants.LOG_LEVEL_ANY,
"Unable to create SSL server socket: " + e);
hasStopped = true;
return;
}
}
else
{
try
{
serverSocket = new ServerSocket(listenPort);
slamdServer.logMessage(Constants.LOG_LEVEL_ANY,
"Listening for resource monitor client " +
"connections on port " + listenPort);
}
catch (IOException ioe)
{
ioe.printStackTrace();
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(ioe));
slamdServer.logMessage(Constants.LOG_LEVEL_ANY,
"Unable to create server socket: " + ioe);
hasStopped = true;
return;
}
}
while (keepListening)
{
Socket clientSocket = null;
try
{
clientSocket = serverSocket.accept();
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"New resource monitor client connection " +
"received from " +
clientSocket.getInetAddress().toString());
}
catch (IOException ioe)
{
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Exception accepting resource monitor client " +
"connection: " + ioe);
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(ioe));
continue;
}
try
{
synchronized (connectionHashMutex)
{
if ((maxClients > 0) && (connectionHash.size() >= maxClients))
{
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Refusing monitor connection from " +
clientSocket.getInetAddress().toString() +
" -- too many concurrent connections");
// Normally, messages that the server originates are odd-numbered.
// However, in this case the client should always send a hello as
// the first operation that deserves a response, so we can use
// message ID 0 for that.
HelloResponseMessage helloResp =
new HelloResponseMessage(0,
Constants.MESSAGE_RESPONSE_CONNECTION_LIMIT_REACHED,
"The maximum number of simultaneous connections has " +
"been reached", -1);
ASN1Writer writer = new ASN1Writer(clientSocket.getOutputStream());
writer.writeElement(helloResp.encode());
clientSocket.close();
}
else
{
String connectionID = getNewConnectionID();
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Assigned a monitor connection ID of " +
connectionID);
ResourceMonitorClientConnection clientConnection =
new ResourceMonitorClientConnection(slamdServer, this,
clientSocket,
connectionID);
connectionHash.put(clientConnection.getClientIPAddress(),
clientConnection);
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Added monitor connection to the " +
"connection hash");
clientConnection.start();
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Started the monitor client thread");
StatusRequestMessage statusRequest =
new StatusRequestMessage(clientConnection.getMessageID());
clientConnection.sendMessage(statusRequest);
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Sent a status request message to the " +
"monitor client");
}
}
}
catch (Exception e)
{
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Exception sending message to client: " + e);
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(e));
e.printStackTrace();
}
}
hasStopped = true;
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"Leaving ResourceMonitorClientLister.run()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"Leaving ResourceMonitorClientListener.run()");
}
/**
* Retrieves the set of resource monitor client connections for processing the
* specified job.
*
* @param job The job for which the connections are to be retrieved.
*
* @return The appropriate set of monitor clients for the specified job, or
* <CODE>null</CODE> if the requested set of monitor clients are not
* available.
*/
public ResourceMonitorClientConnection[] getMonitorClientConnections(Job job)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister." +
"getClientConnections()");
ArrayList<ResourceMonitorClientConnection> clientList =
new ArrayList<ResourceMonitorClientConnection>();
String[] requestedClients = job.getResourceMonitorClients();
synchronized (connectionHashMutex)
{
for (int i=0;
((requestedClients != null) && (i < requestedClients.length)); i++)
{
ResourceMonitorClientConnection client =
connectionHash.get(requestedClients[i]);
if (client == null)
{
return null;
}
clientList.add(client);
}
if (job.monitorClientsIfAvailable())
{
ClientConnection[] jobClients = job.getClientConnections();
for (int i=0; i < jobClients.length; i++)
{
String clientIP = jobClients[i].getClientIPAddress();
ResourceMonitorClientConnection client = connectionHash.get(clientIP);
if (client != null)
{
if (! clientList.contains(client))
{
clientList.add(client);
}
}
}
}
}
ResourceMonitorClientConnection[] clients =
new ResourceMonitorClientConnection[clientList.size()];
clientList.toArray(clients);
return clients;
}
/**
* Retrieves a list of the resource monitor clients that are currently
* connected to the SLAMD server.
*
* @return The set of resource monitor clients that are currently connected.
*/
public ResourceMonitorClientConnection[] getMonitorClientList()
{
synchronized (connectionHashMutex)
{
ResourceMonitorClientConnection[] conns =
new ResourceMonitorClientConnection[connectionHash.size()];
int i=0;
Iterator iterator = connectionHash.values().iterator();
while (iterator.hasNext())
{
conns[i++] = (ResourceMonitorClientConnection) iterator.next();
}
return conns;
}
}
/**
* Retrieves a list of the resource monitor clients that are currently
* connected to the SLAMD server, sorted by client ID.
*
* @return The set of resource monitor clients that are currently connected,
* sorted by client ID.
*/
public ResourceMonitorClientConnection[] getSortedMonitorClientList()
{
synchronized (connectionHashMutex)
{
ResourceMonitorClientConnection[] conns =
new ResourceMonitorClientConnection[connectionHash.size()];
int i=0;
Iterator iterator = connectionHash.values().iterator();
while (iterator.hasNext())
{
conns[i++] = (ResourceMonitorClientConnection) iterator.next();
}
Arrays.sort(conns);
return conns;
}
}
/**
* Retrieves a connection ID that can be used to uniquely identify each
* client connection.
*
* @return A connection ID that can be used to uniquely identify each client
* connection.
*/
public String getNewConnectionID()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister." +
"getNewConnectionID()");
// Cheat -- use the unique ID generator in the scheduler
return slamdServer.getScheduler().generateUniqueID();
}
/**
* Indicates whether this client listener requires clients to authenticate.
*
* @return <CODE>true</CODE> if clients are required to authenticate, or
* <CODE>false</CODE> if they are not.
*/
public boolean requireAuthentication()
{
return requireAuthentication;
}
/**
* Sends a message to the specified client indicating that it should
* disconnect itself from the SLAMD server. If it is currently processing
* jobs, then it will be given an opportunity to send its results to the SLAMD
* server.
*
* @param clientID The client ID of the resource monitor client to
* disconnect.
*
* @return {@code true} if a disconnect request was sent to the resource
* monitor client, or {@code false} if not.
*/
public boolean requestDisconnect(final String clientID)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.requestDisconnect(" + clientID +
')');
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Gracefully disconnecting client " + clientID);
synchronized (connectionHashMutex)
{
for (final ResourceMonitorClientConnection c : connectionHash.values())
{
if (c.getClientID().equals(clientID))
{
c.sendServerShutdownMessage(false);
return true;
}
}
}
return false;
}
/**
* Sends a message to each client indicating that it should disconnect
* itself from the SLAMD server. If any clients are currently processing
* jobs, then those clients will be given an opportunity to send their results
* to the SLAMD server.
*/
public void requestDisconnectAll()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.forcefullyDisconnectAll()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Gracefully disconnecting all client connections.");
synchronized (connectionHashMutex)
{
Iterator iterator = connectionHash.values().iterator();
while (iterator.hasNext())
{
((ResourceMonitorClientConnection)
iterator.next()).sendServerShutdownMessage(false);
}
}
}
/**
* Forcefully closes the connection to the resource monitor client with the
* specified client ID. If it is currently processing jobs, then any data
* for those jobs will be lost.
*
* @param clientID The client ID of the resource monitor client to
* disconnect.
*
* @return {@code true} if the client was disconnected, or {@code false} if
* not.
*/
public boolean forceDisconnect(final String clientID)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.forcefullyDisconnect(" + clientID +
')');
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Forcefully disconnecting resource monitor client " + clientID);
synchronized (connectionHashMutex)
{
for (final ResourceMonitorClientConnection c : connectionHash.values())
{
if (c.getClientID().equals(clientID))
{
c.sendServerShutdownMessage(true);
connectionLostUnlocked(c);
return true;
}
}
}
return false;
}
/**
* Forcefully closes the connections for all resource monitor clients
* connected to the SLAMD server. If any clients are currently processing a
* job, then no information will be available for that job from those clients.
*/
public void forceDisconnectAll()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.forcefullyDisconnectAll()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Forcefully disconnecting all resource monitor client connections.");
synchronized (connectionHashMutex)
{
ResourceMonitorClientConnection client;
Iterator iterator = connectionHash.values().iterator();
while (iterator.hasNext())
{
client = (ResourceMonitorClientConnection) iterator.next();
client.sendServerShutdownMessage(true);
connectionLostUnlocked(client);
}
connectionHash.clear();
}
}
/**
* Indicates that the specified connection is closing and all references to it
* should be removed.
*
* @param clientConnection The connection that is shutting down.
*/
public void connectionLost(ResourceMonitorClientConnection clientConnection)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.connectionLost()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Lost connection to resource monitor client " +
clientConnection.toString());
synchronized (connectionHashMutex)
{
connectionHash.remove(clientConnection.getClientIPAddress());
}
Job[] jobsInProgress = clientConnection.getJobsInProgress();
for (int i=0; i < jobsInProgress.length; i++)
{
String[] messages = new String[]
{
"The job was cancelled on resource monitor client " +
clientConnection.getClientID() +
" because the connection to the client was lost."
};
JobCompletedMessage completedMessage = new
JobCompletedMessage(clientConnection.getMessageID(),
jobsInProgress[i].getJobID(),
Constants.JOB_STATE_STOPPED_DUE_TO_ERROR,
jobsInProgress[i].getActualStartTime(),
new Date(), -1, new StatTracker[0], messages);
jobsInProgress[i].resourceClientDone(clientConnection, completedMessage);
}
}
/**
* Indicates that the specified connection is closing and all references to it
* should be removed. This method does not require a lock on the connection
* hash and therefore requires that the caller already hold it.
*
* @param clientConnection The connection that is shutting down.
*/
private void connectionLostUnlocked(ResourceMonitorClientConnection
clientConnection)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientLister.connectionLost()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Lost connection to resource monitor client " +
clientConnection.toString());
connectionHash.remove(clientConnection.getClientIPAddress());
Job[] jobsInProgress = clientConnection.getJobsInProgress();
for (int i=0; i < jobsInProgress.length; i++)
{
String[] messages = new String[]
{
"The job was cancelled on resource monitor client " +
clientConnection.getClientID() +
" because the connection to the client was lost."
};
JobCompletedMessage completedMessage = new
JobCompletedMessage(clientConnection.getMessageID(),
jobsInProgress[i].getJobID(),
Constants.JOB_STATE_STOPPED_DUE_TO_ERROR,
jobsInProgress[i].getActualStartTime(),
new Date(), -1, new StatTracker[0], messages);
jobsInProgress[i].resourceClientDone(clientConnection,
completedMessage);
}
}
/**
* Indicates whether all the resource monitor clients needed for the specified
* job are currently available.
*
* @param job The job that specifies which monitor clients are needed.
*
* @return <CODE>true</CODE> if all requested monitor clients are available,
* or <CODE>false</CODE> if not.
*/
public boolean connectionsAvailable(Job job)
{
String[] requestedClients = job.getResourceMonitorClients();
if ((requestedClients == null) || (requestedClients.length == 0))
{
return true;
}
synchronized (connectionHashMutex)
{
for (int i=0; i < requestedClients.length; i++)
{
if (connectionHash.get(requestedClients[i]) == null)
{
return false;
}
}
}
return true;
}
/**
* Retrieves the length of time that should pass between keepalive messages.
* A keepalive message will be sent if there has been no interaction with the
* client for the specified period of time.
*
* @return The length of time that should pass between keepalive messages.
*/
public int getKeepAliveInterval()
{
return keepaliveInterval;
}
/**
* Retrieves the maximum amount of time in seconds that a client connection
* should wait for a response to a solicited message before returning an
* error.
*
* @return The maximum amount of time in seconds that a client connection
* should wait for a response to a solicited message before returning
* an error.
*/
public int getMaxResponseWaitTime()
{
return maxResponseWaitTime;
}
/**
* Retrieves the name that the client listener uses to subscribe to the
* configuration handler in order to be notified of configuration changes.
*
* @return The name that the client listener uses to subscribe to the
* configuration handler in order to be notified of configuration
* changes.
*/
public String getSubscriberName()
{
return "Resource Monitor Client Listener";
}
/**
* Retrieves the set of configuration parameters associated with this
* configuration subscriber.
*
* @return The set of configuration parameters associated with this
* configuration subscriber.
*/
public ParameterList getSubscriberParameters()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In ResourceMonitorClientListener.getParameters()");
IntegerParameter portParameter =
new IntegerParameter(Constants.PARAM_MONITOR_LISTENER_PORT,
"Resource Monitor Client Listener Port",
"The port on which the SLAMD server listens " +
"for connections from resource monitor clients.",
true,
listenPort, true, 1, true, 65535);
Parameter[] params = new Parameter[]
{
portParameter
};
return new ParameterList(params);
}
/**
* Re-reads all configuration information used by the resource client
* listener. In this case, the only configuration parameter for this listener
* is the port number, and that cannot change without restarting the SLAMD
* server. Therefore, this method does nothing.
*/
public void refreshSubscriberConfiguration()
{
// No implementation required.
}
/**
* Re-reads the configuration for the specified parameter if the parameter is
* applicable to the resource monitor client listener. In this case, the only
* configuration parameter for this listener is the port number, and that
* cannot change without restarting the SLAMD server. Therefore, this method
* does nothing.
*
* @param parameterName The name of the parameter for which to reread the
* configuration.
*/
public void refreshSubscriberConfiguration(String parameterName)
{
// No implementation required.
}
}