/**
 * Société Générale
 */
package deai.ft.archi.common.debug;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

/**
 * ThreadWarningSystem  detects when there are too many threads, and finds thread deadlocks.
 * 
 * @version $Revision: 1.2 $
 * @author <a href="mailto:christophe.lallement@sgcib.com">Christophe Lallement </a>
 * 
 * <!-- $CVSHeader: Common/Server/src/java/deai/ft/archi/common/debug/ThreadWarningSystem.java,v 1.2 2006/08/17 13:01:29 clalleme Exp $ -->
 * 
 */
public class ThreadWarningSystem
{
    private transient final Timer                threadCheck                = new Timer("Thread Monitor", true);
    private transient final ThreadMXBean         mbean                      = ManagementFactory.getThreadMXBean();
    private transient final Collection<Listener> listeners                  = new ArrayList<Listener>();
    /**
     * The number of milliseconds between checking for deadlocks. It may be expensive to check for deadlocks, and it is not critical to know so
     * quickly.
     */
    private static final int           DEADLOCK_CHECK_PERIOD      = 500;
    /**
     * The number of milliseconds between checking number of threads. Since threads can be created very quickly, we need to check this frequently.
     */
    private static final int           THREAD_NUMBER_CHECK_PERIOD = 20;
    private static final int           MAX_STACK_DEPTH            = 30;
    private transient boolean                    threadThresholdNotified    = false;
    private transient final Set<Long>                  deadlockedThreads          = new HashSet<Long>();

    /**
     * Monitor only deadlocks.
     */
    public ThreadWarningSystem()
    {
        this.threadCheck.schedule(new TimerTask() {
            @Override
            public void run()
            {
                final long[] ids = ThreadWarningSystem.this.mbean.findMonitorDeadlockedThreads();
                if (ids != null && ids.length > 0)
                {
                    for (final Long l : ids)
                    {
                        if (!ThreadWarningSystem.this.deadlockedThreads.contains(l))
                        {
                            ThreadWarningSystem.this.deadlockedThreads.add(l);
                            final ThreadInfo ti = ThreadWarningSystem.this.mbean.getThreadInfo(l, MAX_STACK_DEPTH);
                            fireDeadlockDetected(ti);
                        }
                    }
                }
            }
        }, 10, DEADLOCK_CHECK_PERIOD);
    }

    /**
     * Monitor deadlocks and the number of threads.
     */
    public ThreadWarningSystem(final int threadThreshold)
    {
        this();
        this.threadCheck.schedule(new TimerTask() {
            @Override
            public void run()
            {
                if (ThreadWarningSystem.this.mbean.getThreadCount() > threadThreshold)
                {
                    if (!ThreadWarningSystem.this.threadThresholdNotified)
                    {
                        fireThresholdExceeded();
                        ThreadWarningSystem.this.threadThresholdNotified = true;
                    }
                }
                else
                {
                    ThreadWarningSystem.this.threadThresholdNotified = false;
                }
            }
        }, 10, THREAD_NUMBER_CHECK_PERIOD);
    }

    private void fireDeadlockDetected(final ThreadInfo thread)
    {
        // In general I avoid using synchronized. The surrounding
        // code should usually be responsible for being threadsafe.
        // However, in this case, the timer could be notifying at
        // the same time as someone is adding a listener, and there
        // is nothing the calling code can do to prevent that from
        // occurring. Another tip though is this: when I synchronize
        // I use a private field to synchronize on, instead of
        // "this".
        synchronized (this.listeners)
        {
            for (final Listener l : this.listeners)
            {
                l.deadlockDetected(thread);
            }
        }
    }

    private void fireThresholdExceeded()
    {
        final ThreadInfo[] allThreads = this.mbean.getThreadInfo(this.mbean.getAllThreadIds());
        synchronized (this.listeners)
        {
            for (final Listener l : this.listeners)
            {
                l.thresholdExceeded(allThreads);
            }
        }
    }

    public boolean addListener(final Listener l)
    {
        synchronized (this.listeners)
        {
            return this.listeners.add(l);
        }
    }

    public boolean removeListener(final Listener l)
    {
        synchronized (this.listeners)
        {
            return this.listeners.remove(l);
        }
    }

    /**
     * This is called whenever a problem with threads is detected. The two events are deadlockDetected() and thresholdExceeded().
     */
    public interface Listener
    {
        /**
         * @param deadlockedThread
         *            The deadlocked thread, with stack trace of limited depth.
         */
        void deadlockDetected(ThreadInfo deadlockedThread);

        /**
         * @param allThreads
         *            All the threads in the JVM, without stack traces.
         */
        void thresholdExceeded(ThreadInfo[] allThreads);
    }
}

