/*
* 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.Iterator;
import javax.net.ssl.SSLServerSocketFactory;
import com.slamd.common.Constants;
import com.slamd.common.SLAMDException;
import com.slamd.db.SLAMDDB;
import com.slamd.job.JobClass;
import com.slamd.parameter.IntegerParameter;
import com.slamd.parameter.Parameter;
import com.slamd.parameter.ParameterList;
/**
* This class implements a listener that can be used to accept connections from
* clients for the use of reporting in-progress statistics while a job is
* running.
*
*
* @author Neil A. Wilson
*/
public class StatListener
extends Thread
implements ConfigSubscriber
{
// The list of connections established to this listener.
private ArrayList<StatClientConnection> connectionList;
// Indicates whether we should keep listening for more connections.
private boolean keepListening;
// Indicates whether we have stopped running yet.
private boolean hasStopped;
// Indicates whether we will require clients to authenticate.
private boolean requireAuthentication;
// Indicates whether we will use SSL to communicate with the clients.
private boolean useSSL;
// The interval that we should use for keepalive messages to the clients.
private int keepAliveInterval;
// The port on which this listener will accept connections.
private int listenPort;
// The maximum length of time we should wait for a response from the client.
private int maxResponseWaitTime;
// The mutex providing threadsafe access to the connection list.
private final Object connectionListMutex;
// The server socket used to accept connections from clients.
private ServerSocket serverSocket;
// The configuration handler to use for this stat listener.
private SLAMDDB configDB;
// The SLAMD server with which this listener is associated.
private SLAMDServer slamdServer;
/**
* Creates a new instance of this stat listener.
*
* @param slamdServer The SLAMD server with which this stat listener will be
* associated.
*/
public StatListener(SLAMDServer slamdServer)
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"Entering StatListener constructor");
setName("Stat Client Listener");
// Initialize all the instance variables
this.slamdServer = slamdServer;
connectionList = new ArrayList<StatClientConnection>();
connectionListMutex = new Object();
keepListening = true;
hasStopped = true;
// Determine which port we should listen on.
listenPort = Constants.DEFAULT_STAT_LISTENER_PORT_NUMBER;
configDB = slamdServer.getConfigDB();
String portStr =
configDB.getConfigParameter(Constants.PARAM_STAT_LISTENER_PORT);
if ((portStr != null) && (portStr.length() > 0))
{
try
{
listenPort = Integer.parseInt(portStr);
} catch (NumberFormatException nfe) {}
}
// Register as a configuration subscriber.
configDB.registerAsSubscriber(this);
ClientListener clientListener = slamdServer.getClientListener();
keepAliveInterval = clientListener.getKeepAliveInterval();
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 StatListener constructor");
}
/**
* Indicates that the listener should start listening for client connections.
*/
public void startListening()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In StatListener.startListening()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In StatListener.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 StatListener.stopListening()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In StatListener.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 (connectionListMutex)
{
Iterator i = connectionList.iterator();
while (i.hasNext())
{
StatClientConnection connection = (StatClientConnection) i.next();
connection.sendServerShutdownMessage(true);
}
connectionList.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 that will be used to accept new connections, then
* listens for new connections until the SLAMD server is shut down.
*/
@Override()
public void run()
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE, "In StatListener.run()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In StatListener.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 stat 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 stat 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 stat client server " +
"socket: " + ioe);
hasStopped = true;
return;
}
}
while (keepListening)
{
Socket clientSocket = null;
try
{
clientSocket = serverSocket.accept();
StatClientConnection conn = new StatClientConnection(slamdServer, this,
clientSocket);
synchronized (connectionListMutex)
{
connectionList.add(conn);
}
conn.start();
}
catch (SLAMDException se)
{
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(se));
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT,
"Unable to create a new stat client " +
"connection -- " + se.getMessage());
try
{
clientSocket.close();
} catch (Exception e) {}
}
catch (IOException ioe)
{
ioe.printStackTrace();
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(ioe));
slamdServer.logMessage(Constants.LOG_LEVEL_ANY,
"I/O error accepting stat client connection: " +
ioe + " -- disabling the listener.");
keepListening = false;
}
}
hasStopped = true;
}
/**
* Retrieves an array of the connections currently established to this stat
* listener.
*
* @return An array of the connections currently established to this stat
* listener.
*/
public StatClientConnection[] getConnectionList()
{
synchronized (connectionListMutex)
{
StatClientConnection[] connArray =
new StatClientConnection[connectionList.size()];
connectionList.toArray(connArray);
return connArray;
}
}
/**
* Retrieves an array of the connections currently established to this stat
* listener, sorted by client ID.
*
* @return An array of the connections currently established to this stat
* listener, sorted by client ID.
*/
public StatClientConnection[] getSortedConnectionList()
{
synchronized (connectionListMutex)
{
StatClientConnection[] connArray =
new StatClientConnection[connectionList.size()];
connectionList.toArray(connArray);
Arrays.sort(connArray);
return connArray;
}
}
/**
* Sends a message to the specified client indicating that it should
* disconnect from the SLAMD server.
*
* @param clientID The client ID of the client to which the request is to be
* sent.
*
* @return <CODE>true</CODE> if a request was sent to the specified client,
* or <CODE>false</CODE> if not.
*/
public boolean requestDisconnect(String clientID)
{
StatClientConnection clientToDisconnect = null;
synchronized (connectionListMutex)
{
for (int i=0; i < connectionList.size(); i++)
{
StatClientConnection clientConnection = connectionList.get(i);
if (clientConnection.getClientID().equals(clientID))
{
clientToDisconnect = clientConnection;
break;
}
}
}
if (clientToDisconnect == null)
{
return false;
}
clientToDisconnect.sendServerShutdownMessage(false);
return true;
}
/**
* Sends a message to all clients indicating that they should disconnect from
* the SLAMD server.
*/
public void requestDisconnectAll()
{
synchronized (connectionListMutex)
{
for (int i=0; i < connectionList.size(); i++)
{
StatClientConnection clientConnection = connectionList.get(i);
clientConnection.sendServerShutdownMessage(false);
}
}
}
/**
* Forcefully closes the connection to the specified client and removes all
* references to it from the SLAMD server.
*
* @param clientID The client ID of the client to be disconnected.
*
* @return <CODE>true</CODE> if the client was disconnected, or
* <CODE>false</CODE> if not.
*/
public boolean forceDisconnect(String clientID)
{
StatClientConnection clientToDisconnect = null;
synchronized (connectionListMutex)
{
for (int i=0; i < connectionList.size(); i++)
{
StatClientConnection clientConnection = connectionList.get(i);
if (clientConnection.getClientID().equals(clientID))
{
clientToDisconnect = clientConnection;
break;
}
}
if (clientToDisconnect == null)
{
return false;
}
try
{
clientToDisconnect.clientSocket.close();
} catch (Exception e) {}
}
connectionLost(clientToDisconnect);
return true;
}
/**
* Forcefully disconnects all real-time stat clients from the SLAMD server and
* removes all references to them.
*/
public void forceDisconnectAll()
{
synchronized (connectionListMutex)
{
for (int i=0; i < connectionList.size(); i++)
{
StatClientConnection clientConnection = connectionList.get(i);
try
{
clientConnection.clientSocket.close();
} catch (Exception e) {}
}
connectionList.clear();
}
}
/**
* Indicates that the specified stat client has disconnected from the SLAMD
* server.
*
* @param connection The connection to the stat client that has been lost.
*/
public void connectionLost(StatClientConnection connection)
{
// For now, just remove the client from the list of current connections.
synchronized (connectionListMutex)
{
connectionList.remove(connection);
}
}
/**
* Retrieves the keepalive interval that should be used for new client
* connections.
*
* @return The keepalive interval that should be used for new client
* connections.
*/
public int getKeepAliveInterval()
{
return keepAliveInterval;
}
/**
* Retrieves the maximum length of time in seconds that the SLAMD server will
* wait for a response to a request issued to a stat client.
*
* @return The maximum length of time in seconds that the SLAMD server will
* wait for a response to a request issued to a stat client.
*/
public int getMaxResponseWaitTime()
{
return maxResponseWaitTime;
}
/**
* Indicates whether this listener is configured to require authentication.
*
* @return <CODE>true</CODE> if this listener is configured to require
* authentication, or <CODE>false</CODE> if not.
*/
public boolean requireAuthentication()
{
return requireAuthentication;
}
/**
* Retrieves the name that the stat client listener uses to subscribe to
* the configuration handler in order to be notified of configuration changes.
*
* @return The name that the stat client listener uses to subscribe to the
* configuration handler in order to be notified of configuration
* changes.
*/
public String getSubscriberName()
{
return "Real-Time Stat 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 StatListener.getParameters()");
IntegerParameter portParameter =
new IntegerParameter(Constants.PARAM_STAT_LISTENER_PORT,
"Stat Listener Port",
"The port on which the SLAMD server listens " +
"for connections from clients for reporting " +
"real-time statistics.",
true,
listenPort, true, 1, true, 65535);
IntegerParameter maxIterationsParameter =
new IntegerParameter(Constants.PARAM_MAX_STAT_INTERVALS,
"Max Stat Intervals",
"The maximum number of collection intervals that " +
"should be held for a job at any time. Note that" +
"this will only take effect for jobs started " +
"after the change is made", true,
slamdServer.statHandler.maxIntervals, true,
2, false, 0);
Parameter[] params = new Parameter[]
{
portParameter,
maxIterationsParameter
};
return new ParameterList(params);
}
/**
* Re-reads all configuration information used by the stat listener.
* In this case, it doesn't do anything because nothing is dynamically
* reconfigurable.
*
* @throws SLAMDServerException If there is a problem reading or applying
* the changes.
*/
public void refreshSubscriberConfiguration()
throws SLAMDServerException
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In StatListener.refreshConfiguration()");
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In StatListener.refreshConfiguration()");
String intervalsStr =
configDB.getConfigParameter(Constants.PARAM_MAX_STAT_INTERVALS);
if ((intervalsStr != null) && (intervalsStr.length() > 0))
{
try
{
slamdServer.statHandler.maxIntervals = Integer.parseInt(intervalsStr);
}
catch (Exception e)
{
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(e));
slamdServer.logMessage(Constants.LOG_LEVEL_CONFIG,
"Invalid value for config parameter " +
Constants.PARAM_MAX_STAT_INTERVALS + ": " +
intervalsStr + " -- " + e);
}
}
}
/**
* Re-reads the configuration for the specified parameter if the parameter is
* applicable to the stat listener. In this case, nothing will be applicable.
*
* @param parameterName The name of the parameter for which to reread the
* configuration.
*
* @throws SLAMDServerException If there is a problem reading or applying
* the specified change.
*/
public void refreshSubscriberConfiguration(String parameterName)
throws SLAMDServerException
{
slamdServer.logMessage(Constants.LOG_LEVEL_TRACE,
"In StatListener.refreshConfiguration(" +
parameterName + ')');
slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG,
"In StatListener.refreshConfiguration(" +
parameterName + ')');
if (parameterName.equals(Constants.PARAM_MAX_STAT_INTERVALS))
{
String intervalsStr =
configDB.getConfigParameter(Constants.PARAM_MAX_STAT_INTERVALS);
if ((intervalsStr != null) && (intervalsStr.length() > 0))
{
try
{
slamdServer.statHandler.maxIntervals = Integer.parseInt(intervalsStr);
}
catch (Exception e)
{
slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG,
JobClass.stackTraceToString(e));
slamdServer.logMessage(Constants.LOG_LEVEL_CONFIG,
"Invalid value for config parameter " +
Constants.PARAM_MAX_STAT_INTERVALS + ": " +
intervalsStr + " -- " + e);
}
}
}
}
}