/*
 * KeepAliveThread.java
 */
package org.activemq.transport.reliable;

import java.util.Iterator;

import javax.jms.JMSException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.message.KeepAlive;

import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArraySet;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;


/**
 * KeepAliveDaemon keeps channels alive by sending KeepAlive packets on a
 * specified interval. If the packets aren't don't get a receipt within a
 * specified time, the channel will be forcefully disconnected.
 */
public class KeepAliveDaemon implements Runnable {

	private static final Log log = LogFactory.getLog(KeepAliveDaemon.class);

	private static KeepAliveDaemon instance = null;

	private long checkInterval = 15000L;
	private long lastCheck = 0;
	private Object lock = new Object();

	private SynchronizedBoolean started = new SynchronizedBoolean(false);
	private SynchronizedBoolean stopped = new SynchronizedBoolean(false);
	private CopyOnWriteArraySet monitoredChannels = new CopyOnWriteArraySet();
	private CopyOnWriteArraySet zombieChannelSuspects = new CopyOnWriteArraySet();


	/**
	 * Constructs a new KeepAliveDaemon which will send KeepAlive packets
	 * throught the wrapped channel.
	 */
	protected KeepAliveDaemon() {

	}

	/**
	 * Gets the current instance. Singletons implemented this way aren't popular
	 * these days, but it might be good here. :)
	 * 
	 * @return the daemon
	 */
	public static synchronized KeepAliveDaemon getInstance() {
		if (instance == null)
			instance = new KeepAliveDaemon();
		return instance;
	}

	public void addMonitoredChannel(ReliableTransportChannel channel) {
		if (channel.getKeepAliveTimeout() <= 0)
			return;
		log.debug("Adding channel " + channel);
		// Check that the timeout isn't lower than our check interval as
		// this would cause the channel to constantly be disconnected.
		// This check should perhaps be done whenever the channel changes it's
		// interval, but in practice, this will probably never happen.
		if (channel.getKeepAliveTimeout() / 2 < checkInterval) {
			setCheckInterval(channel.getKeepAliveTimeout() / 2);
			log.info("Adjusting check interval to " + checkInterval + " as channel " + channel.toString()
					+ " has lower timeout time than the current check interval.");
		}
		monitoredChannels.add(channel);
	}

	public void removeMonitoredChannel(ReliableTransportChannel channel) {
		log.debug("Removing channel " + channel);
		monitoredChannels.remove(channel);
	}

	/**
	 * Sets the number of milliseconds between keep-alive checks are done.
	 * 
	 * @param interval
	 *            the interval
	 */
	public void setCheckInterval(long interval) {
		this.checkInterval = interval;
		if (started.and(!stopped.get())) {
			restart();
		}
	}

	public long getCheckInterval() {
		return checkInterval;
	}

	public long getLastCheckTime() {
		return lastCheck;
	}

	public void start() {
		if (started.commit(false, true)) {
			log.debug("Scheduling keep-alive every " + checkInterval + " millisecond.");
			Thread t = new Thread(this);
			t.setName("KeepAliveDaemon");
			t.setDaemon(true);
			t.start();
		}
	}

	public void stop() {
		if (stopped.commit(false, true)) {
			synchronized (lock) {
				lock.notifyAll();
			}
			log.debug("Stopping keep-alive.");
		}
	}

	public void restart() {
		log.debug("Restarting keep-alive.");
		stop();
		start();
	}

	public void run() {
		lastCheck = System.currentTimeMillis() - checkInterval;
		while (!stopped.get()) {
			for (Iterator i = zombieChannelSuspects.iterator(); i.hasNext();) {
				ReliableTransportChannel channel = (ReliableTransportChannel) i.next();
				examineZombieSuspect(channel);
			}

			for (Iterator i = monitoredChannels.iterator(); i.hasNext();) {
				ReliableTransportChannel channel = (ReliableTransportChannel) i.next();
				if (!zombieChannelSuspects.contains(channel))
					examineChannel(channel);
			}
			lastCheck = System.currentTimeMillis();
			synchronized (lock) {
				try {
					lock.wait(checkInterval);
				} catch (InterruptedException e) {

				}
			}
		}
	}

	private void examineZombieSuspect(ReliableTransportChannel channel) {
		if ((channel.getLastReceiptTimestamp() + channel.getKeepAliveTimeout() * 2) < System.currentTimeMillis()) {
			// Timed out
			log.info("Forcing channel "
					+ channel
					+ " to disconnect since it "
					+ (channel.getLastReceiptTimestamp() == 0 ? "never has responded " : "hasn't responded since "
							+ new java.util.Date(channel.getLastReceiptTimestamp())) + " and has a timeout of "
					+ channel.getKeepAliveTimeout());
			channel.forceDisconnect();
			// Remove it and wait for a reconnect
			zombieChannelSuspects.remove(channel);
		} else if ((channel.getLastReceiptTimestamp() + channel.getKeepAliveTimeout()) < System.currentTimeMillis()) {
			// Still a zombie suspect, but has not timed out
			log.debug("Still waiting for response from channel " + channel);
		} else {
			// It's alive again
			log.debug("Channel " + channel + " responded in time.");
			zombieChannelSuspects.remove(channel);
		}
	}

	private void examineChannel(ReliableTransportChannel channel) {
        // Is the channel stopping?
        if (channel.isPendingStop()) {
            removeMonitoredChannel(channel);
        } else {
            // Then it should be active 
            // Keep pinging the channel periodically
            if ((channel.getLastReceiptTimestamp() + channel.getKeepAliveTimeout()) < System.currentTimeMillis()) {
                log.debug("Sending keep-alive on channel " + channel.toString());
                KeepAlive packet = new KeepAlive();
                packet.setReceiptRequired(true);
                boolean wasConnected = channel.isTransportConnected();
                try {
                    channel.asyncSendWithReceipt(packet);
                    zombieChannelSuspects.add(channel);
                } catch (JMSException e) {
                    // only report an error if the channel was connetected.
                    if (wasConnected) {
                        log.error("Error sending keep-alive to channel " + channel.toString()
                                + ". Treating as temporary problem.", e);
                    } 
                }
            }
        }
    }
}
