/*
* Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
* Distributed under the terms of either:
* - the common development and distribution license (CDDL), v1.0; or
* - the GNU Lesser General Public License, v2.1 or later
*/
package winstone.cluster;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import winstone.Cluster;
import winstone.HostConfiguration;
import winstone.HostGroup;
import winstone.Logger;
import winstone.WebAppConfiguration;
import winstone.WinstoneResourceBundle;
import winstone.WinstoneSession;
/**
* Represents a cluster of winstone containers.
*
* @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
* @version $Id: SimpleCluster.java,v 1.8 2006/08/10 06:38:31 rickknowles Exp $
*/
public class SimpleCluster implements Runnable, Cluster {
final int SESSION_CHECK_TIMEOUT = 100;
final int HEARTBEAT_PERIOD = 5000;
final int MAX_NO_OF_MISSING_HEARTBEATS = 3;
final byte NODELIST_DOWNLOAD_TYPE = (byte) '2';
final byte NODE_HEARTBEAT_TYPE = (byte) '3';
public static final WinstoneResourceBundle CLUSTER_RESOURCES = new WinstoneResourceBundle("winstone.cluster.LocalStrings");
private int controlPort;
private String initialClusterNodes;
private Map clusterAddresses;
private boolean interrupted;
/**
* Builds a cluster instance
*/
public SimpleCluster(Map args, Integer controlPort) {
this.interrupted = false;
this.clusterAddresses = new Hashtable();
if (controlPort != null)
this.controlPort = controlPort.intValue();
// Start cluster init thread
this.initialClusterNodes = (String) args.get("clusterNodes");
Thread thread = new Thread(this, CLUSTER_RESOURCES
.getString("SimpleCluster.ThreadName"));
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
public void destroy() {
this.interrupted = true;
}
/**
* Send a heartbeat every now and then, and remove any nodes that haven't
* responded in 3 heartbeats.
*/
public void run() {
// Ask each of the known addresses for their cluster lists, and build a
// set
if (this.initialClusterNodes != null) {
StringTokenizer st = new StringTokenizer(this.initialClusterNodes,
",");
while (st.hasMoreTokens() && !interrupted)
askClusterNodeForNodeList(st.nextToken());
}
Logger.log(Logger.DEBUG, CLUSTER_RESOURCES, "SimpleCluster.InitNodes", ""
+ this.clusterAddresses.size());
while (!interrupted) {
try {
Set addresses = new HashSet(this.clusterAddresses.keySet());
Date noHeartbeatDate = new Date(System.currentTimeMillis()
- (MAX_NO_OF_MISSING_HEARTBEATS * HEARTBEAT_PERIOD));
for (Iterator i = addresses.iterator(); i.hasNext();) {
String ipPort = (String) i.next();
Date lastHeartBeat = (Date) this.clusterAddresses
.get(ipPort);
if (lastHeartBeat.before(noHeartbeatDate)) {
this.clusterAddresses.remove(ipPort);
Logger.log(Logger.FULL_DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.RemovingNode", ipPort);
}
// Send heartbeat
else
sendHeartbeat(ipPort);
}
Thread.sleep(HEARTBEAT_PERIOD);
} catch (Throwable err) {
Logger.log(Logger.ERROR, CLUSTER_RESOURCES,
"SimpleCluster.ErrorMonitorThread", err);
}
}
Logger.log(Logger.FULL_DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.FinishedMonitorThread");
}
/**
* Check if the other nodes in this cluster have a session for this
* sessionId.
*
* @param sessionId The id of the session to check for
* @return A valid session instance
*/
public WinstoneSession askClusterForSession(String sessionId,
WebAppConfiguration webAppConfig) {
// Iterate through the cluster members
Collection addresses = new ArrayList(clusterAddresses.keySet());
Collection searchThreads = new ArrayList();
for (Iterator i = addresses.iterator(); i.hasNext();) {
String ipPort = (String) i.next();
ClusterSessionSearch search = new ClusterSessionSearch(
webAppConfig.getContextPath(), webAppConfig.getOwnerHostname(),
sessionId, ipPort, this.controlPort);
searchThreads.add(search);
}
// Wait until we get an answer
WinstoneSession answer = null;
String senderThread = null;
boolean finished = false;
while (!finished) {
// Loop through all search threads. If finished, exit, otherwise
// sleep
List finishedThreads = new ArrayList();
for (Iterator i = searchThreads.iterator(); i.hasNext();) {
ClusterSessionSearch searchThread = (ClusterSessionSearch) i
.next();
if (!searchThread.isFinished())
continue;
else if (searchThread.getResult() == null)
finishedThreads.add(searchThread);
else {
answer = searchThread.getResult();
senderThread = searchThread.getAddressPort();
}
}
// Remove finished threads
for (Iterator i = finishedThreads.iterator(); i.hasNext();)
searchThreads.remove(i.next());
if (searchThreads.isEmpty() || (answer != null))
finished = true;
else
try {
Thread.sleep(100);
} catch (InterruptedException err) {
}
}
// Once we have an answer, terminate all search threads
for (Iterator i = searchThreads.iterator(); i.hasNext();) {
ClusterSessionSearch searchThread = (ClusterSessionSearch) i.next();
searchThread.destroy();
}
if (answer != null) {
answer.activate(webAppConfig);
Logger.log(Logger.DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.SessionTransferredFrom", senderThread);
}
return answer;
}
/**
* Given an address, retrieve the list of cluster nodes and initialise dates
*
* @param address The address to request a node list from
*/
private void askClusterNodeForNodeList(String address) {
try {
int colonPos = address.indexOf(':');
String ipAddress = address.substring(0, colonPos);
String port = address.substring(colonPos + 1);
Socket clusterListSocket = new Socket(ipAddress,
Integer.parseInt(port));
this.clusterAddresses.put(clusterListSocket.getInetAddress()
.getHostAddress() + ":" + port, new Date());
InputStream in = clusterListSocket.getInputStream();
OutputStream out = clusterListSocket.getOutputStream();
out.write(NODELIST_DOWNLOAD_TYPE);
out.flush();
// Write out the control port
ObjectOutputStream outControl = new ObjectOutputStream(out);
outControl.writeInt(this.controlPort);
outControl.flush();
// For each node, add an entry to cluster nodes
ObjectInputStream inData = new ObjectInputStream(in);
int nodeCount = inData.readInt();
for (int n = 0; n < nodeCount; n++)
this.clusterAddresses.put(inData.readUTF(), new Date());
inData.close();
outControl.close();
out.close();
in.close();
clusterListSocket.close();
} catch (ConnectException err) {
Logger.log(Logger.DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.NoNodeListResponse", address);
} catch (Throwable err) {
Logger.log(Logger.ERROR, CLUSTER_RESOURCES,
"SimpleCluster.ErrorGetNodeList", address, err);
}
}
/**
* Given an address, send a heartbeat
*
* @param address The address to request a node list from
*/
private void sendHeartbeat(String address) {
try {
int colonPos = address.indexOf(':');
String ipAddress = address.substring(0, colonPos);
String port = address.substring(colonPos + 1);
Socket heartbeatSocket = new Socket(ipAddress,
Integer.parseInt(port));
OutputStream out = heartbeatSocket.getOutputStream();
out.write(NODE_HEARTBEAT_TYPE);
out.flush();
ObjectOutputStream outData = new ObjectOutputStream(out);
outData.writeInt(this.controlPort);
outData.close();
heartbeatSocket.close();
Logger.log(Logger.FULL_DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.HeartbeatSent", address);
} catch (ConnectException err) {/* ignore - 3 fails, and we remove */
} catch (Throwable err) {
Logger.log(Logger.ERROR, CLUSTER_RESOURCES,
"SimpleCluster.HeartbeatError", address, err);
}
}
/**
* Accept a control socket request related to the cluster functions and
* process the request.
*
* @param requestType A byte indicating the request type
* @param in Socket input stream
* @param outSocket output stream
* @param webAppConfig Instance of the web app
* @throws IOException
*/
public void clusterRequest(byte requestType, InputStream in,
OutputStream out, Socket socket, HostGroup hostGroup)
throws IOException {
if (requestType == ClusterSessionSearch.SESSION_CHECK_TYPE)
handleClusterSessionRequest(socket, in, out, hostGroup);
else if (requestType == NODELIST_DOWNLOAD_TYPE)
handleNodeListDownloadRequest(socket, in, out);
else if (requestType == NODE_HEARTBEAT_TYPE)
handleNodeHeartBeatRequest(socket, in);
else
Logger.log(Logger.ERROR, CLUSTER_RESOURCES,
"SimpleCluster.UnknownRequest", "" + (char) requestType);
}
/**
* Handles incoming socket requests for session search
*/
public void handleClusterSessionRequest(Socket socket, InputStream in,
OutputStream out, HostGroup hostGroup)
throws IOException {
// Read in a string for the sessionId
ObjectInputStream inControl = new ObjectInputStream(in);
int port = inControl.readInt();
String ipPortSender = socket.getInetAddress().getHostAddress() + ":" + port;
String sessionId = inControl.readUTF();
String hostname = inControl.readUTF();
HostConfiguration hostConfig = hostGroup.getHostByName(hostname);
String webAppPrefix = inControl.readUTF();
WebAppConfiguration webAppConfig = hostConfig.getWebAppByURI(webAppPrefix);
ObjectOutputStream outData = new ObjectOutputStream(out);
if (webAppConfig == null) {
outData.writeUTF(ClusterSessionSearch.SESSION_NOT_FOUND);
} else {
WinstoneSession session = webAppConfig.getSessionById(sessionId, true);
if (session != null) {
outData.writeUTF(ClusterSessionSearch.SESSION_FOUND);
outData.writeObject(session);
outData.flush();
if (inControl.readUTF().equals(
ClusterSessionSearch.SESSION_RECEIVED))
session.passivate();
Logger.log(Logger.DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.SessionTransferredTo", ipPortSender);
} else {
outData.writeUTF(ClusterSessionSearch.SESSION_NOT_FOUND);
}
}
outData.close();
inControl.close();
}
/**
* Handles incoming socket requests for cluster node lists.
*/
public void handleNodeListDownloadRequest(Socket socket, InputStream in,
OutputStream out) throws IOException {
// Get the ip and port of the requester, and make sure we don't send
// that
ObjectInputStream inControl = new ObjectInputStream(in);
int port = inControl.readInt();
String ipPortSender = socket.getInetAddress().getHostAddress() + ":"
+ port;
List allClusterNodes = new ArrayList(this.clusterAddresses.keySet());
List relevantClusterNodes = new ArrayList();
for (Iterator i = allClusterNodes.iterator(); i.hasNext();) {
String node = (String) i.next();
if (!node.equals(ipPortSender))
relevantClusterNodes.add(node);
}
ObjectOutputStream outData = new ObjectOutputStream(out);
outData.writeInt(relevantClusterNodes.size());
outData.flush();
for (Iterator i = relevantClusterNodes.iterator(); i.hasNext();) {
String ipPort = (String) i.next();
if (!ipPort.equals(ipPortSender))
outData.writeUTF(ipPort);
outData.flush();
}
outData.close();
inControl.close();
}
/**
* Handles heartbeats. Just updates the date of this node's last heartbeat
*/
public void handleNodeHeartBeatRequest(Socket socket, InputStream in)
throws IOException {
ObjectInputStream inData = new ObjectInputStream(in);
int remoteControlPort = inData.readInt();
inData.close();
String ipPort = socket.getInetAddress().getHostAddress() + ":"
+ remoteControlPort;
this.clusterAddresses.put(ipPort, new Date());
Logger.log(Logger.FULL_DEBUG, CLUSTER_RESOURCES,
"SimpleCluster.HeartbeatReceived", ipPort);
}
}