package org.jivesoftware.messenger.net; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; import java.io.IOException; import java.net.Socket; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * A SocketSendingTracker keeps track of all the sockets that are currently sending data and * checks the health of the sockets to detect hanged connections. If a sending operation takes * too much time (i.e. exceeds a time limit) then it is assumed that the connection has been * lost and for some reason the JVM has not been notified of the dead connection. Once a dead * connection has been detected it will be closed so that the thread that was writing to the * socket can resume. Resuming locked threads is important since otherwise a complete system halt * may occur.<p> * * The time limit to wait before considering a connection dead can be configured changing the * property <b>xmpp.session.sending-limit</b>. If the property was not defined then a default * time limit of 60 seconds will be assumed. This means that by default if a sending operation * takes longer than 60 seconds then the connection will be closed and the client disconnected. * Therefore, it is important to not set a very low time limit since active clients may be * incorrectly considered as dead clients. * * @author Gaston Dombiak */ public class SocketSendingTracker { private static SocketSendingTracker instance = new SocketSendingTracker(); /** * Map that holds the sockets that are currently sending information together with the date * when the sending operation started. */ private Map<Socket, Date> sockets = new ConcurrentHashMap<Socket, Date>(); /** * Flag that indicates if the tracket should shutdown the tracking process. */ private boolean shutdown = false; /** * Thread used for checking periodically the health of the sockets involved in sending * operations. */ private Thread checkingThread; /** * Returns the unique instance of this class. * * @return the unique instance of this class. */ public static SocketSendingTracker getInstance() { return instance; } /** * Hide the constructor so that only one instance of this class can exist. */ private SocketSendingTracker() { } /** * Register that the specified socket has started sending information. The registration will * include the timestamp when the sending operation started so that if after several minutes * it hasn't finished then the socket will be closed. * * @param socket the socket that started sending data. */ public void socketStartedSending(Socket socket) { sockets.put(socket, new Date()); } /** * Register that the specified socket has finished sending information. The socket will * be removed from the tracking list. * * @param socket the socket that finished sending data. */ public void socketFinishedSending(Socket socket) { sockets.remove(socket); } /** * Start up the daemon thread that will check for the health of the sockets that are * currently sending data. */ public void start() { shutdown = false; checkingThread = new Thread("SocketSendingTracker") { public void run() { while (!shutdown) { checkHealth(); synchronized (this) { try { wait(10000); } catch (InterruptedException e) { } } } } }; checkingThread.setDaemon(true); checkingThread.start(); } /** * Indicates that the checking thread should be stoped. The thread will be waked up * so that it can be stoped. */ public void shutdown() { shutdown = true; // Use a wait/notify algorithm to ensure that the thread stops immediately if it // was waiting synchronized (checkingThread) { checkingThread.notify(); } } /** * Checks if a socket has been trying to send data for a given amount of time. If it has * exceded a limit of time then the socket will be closed.<p> * * It is expected that sending operations will not take too much time so the checking will * be very fast since very few sockets will be present in the Map and most or all of them * will not exceed the time limit. Therefore, it is expected the overhead of this class to be * quite small. */ private void checkHealth() { for (Socket socket : sockets.keySet()) { Date startDate = sockets.get(socket); if (startDate != null && System.currentTimeMillis() - startDate.getTime() > JiveGlobals.getIntProperty("xmpp.session.sending-limit", 60000)) { // Check that the sending operation is still active if (sockets.get(socket) != null) { // Close the socket try { Log.debug("Closing socket: " + socket + " that started sending data at: " + startDate); socket.close(); } catch (IOException e) { Log.error("Error closing socket", e); } finally { // Remove tracking on this socket sockets.remove(socket); } } } } } }