/*
 * Copyright 2004-2022 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.util;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import org.h2.engine.SysProperties;
import org.h2.mvstore.db.MVTable;

/**
 * Detects deadlocks between threads. Prints out data in the same format as the
 * CTRL-BREAK handler, but includes information about table locks.
 */
public class ThreadDeadlockDetector {

    private static final String INDENT = "    ";

    private static ThreadDeadlockDetector detector;

    private final ThreadMXBean threadBean;

    private ThreadDeadlockDetector() {
        this.threadBean = ManagementFactory.getThreadMXBean();

        // a daemon thread
        // delay: 10 ms
        // period: 10000 ms (100 seconds)
        Timer threadCheck = new Timer("ThreadDeadlockDetector", true);
        threadCheck.schedule(new TimerTask() {
            @Override
            public void run() {
                checkForDeadlocks();
            }
        }, 10, 10_000);
    }

    /**
     * Initialize the detector.
     */
    public static synchronized void init() {
        if (detector == null) {
            detector = new ThreadDeadlockDetector();
        }
    }

    /**
     * Checks if any threads are deadlocked. If any, print the thread dump
     * information.
     */
    void checkForDeadlocks() {
        long[] deadlockedThreadIds = threadBean.findDeadlockedThreads();
        if (deadlockedThreadIds == null) {
            return;
        }
        dumpThreadsAndLocks("ThreadDeadlockDetector - deadlock found :",
                threadBean, deadlockedThreadIds, System.out);
    }

    /**
     * Dump all deadlocks (if any).
     *
     * @param msg the message
     */
    public static void dumpAllThreadsAndLocks(String msg) {
        dumpAllThreadsAndLocks(msg, System.out);
    }

    /**
     * Dump all deadlocks (if any).
     *
     * @param msg the message
     * @param out the output
     */
    public static void dumpAllThreadsAndLocks(String msg, PrintStream out) {
        final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        final long[] allThreadIds = threadBean.getAllThreadIds();
        dumpThreadsAndLocks(msg, threadBean, allThreadIds, out);
    }

    private static void dumpThreadsAndLocks(String msg, ThreadMXBean threadBean,
            long[] threadIds, PrintStream out) {
        final StringWriter stringWriter = new StringWriter();
        final PrintWriter print = new PrintWriter(stringWriter);

        print.println(msg);

        final HashMap<Long, String> tableWaitingForLockMap;
        final HashMap<Long, ArrayList<String>> tableExclusiveLocksMap;
        final HashMap<Long, ArrayList<String>> tableSharedLocksMap;
        if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
            tableWaitingForLockMap = MVTable.WAITING_FOR_LOCK
                    .getSnapshotOfAllThreads();
            tableExclusiveLocksMap = MVTable.EXCLUSIVE_LOCKS
                    .getSnapshotOfAllThreads();
            tableSharedLocksMap = MVTable.SHARED_LOCKS
                    .getSnapshotOfAllThreads();
        } else {
            tableWaitingForLockMap = new HashMap<>();
            tableExclusiveLocksMap = new HashMap<>();
            tableSharedLocksMap = new HashMap<>();
        }

        final ThreadInfo[] infos = threadBean.getThreadInfo(threadIds, true,
                true);
        for (ThreadInfo ti : infos) {
            printThreadInfo(print, ti);
            printLockInfo(print, ti.getLockedSynchronizers(),
                    tableWaitingForLockMap.get(ti.getThreadId()),
                    tableExclusiveLocksMap.get(ti.getThreadId()),
                    tableSharedLocksMap.get(ti.getThreadId()));
        }

        print.flush();
        // Dump it to system.out in one block, so it doesn't get mixed up with
        // other stuff when we're using a logging subsystem.
        out.println(stringWriter.getBuffer());
        out.flush();
    }

    private static void printThreadInfo(PrintWriter print, ThreadInfo ti) {
        // print thread information
        printThread(print, ti);

        // print stack trace with locks
        StackTraceElement[] stackTrace = ti.getStackTrace();
        MonitorInfo[] monitors = ti.getLockedMonitors();
        for (int i = 0; i < stackTrace.length; i++) {
            StackTraceElement e = stackTrace[i];
            print.println(INDENT + "at " + e.toString());
            for (MonitorInfo mi : monitors) {
                if (mi.getLockedStackDepth() == i) {
                    print.println(INDENT + "  - locked " + mi);
                }
            }
        }
        print.println();
    }

    private static void printThread(PrintWriter print, ThreadInfo ti) {
        print.print("\"" + ti.getThreadName() + "\"" + " Id="
                + ti.getThreadId() + " in " + ti.getThreadState());
        if (ti.getLockName() != null) {
            print.append(" on lock=").append(ti.getLockName());
        }
        if (ti.isSuspended()) {
            print.append(" (suspended)");
        }
        if (ti.isInNative()) {
            print.append(" (running in native)");
        }
        print.println();
        if (ti.getLockOwnerName() != null) {
            print.println(INDENT + " owned by " + ti.getLockOwnerName() + " Id="
                    + ti.getLockOwnerId());
        }
    }

    private static void printLockInfo(PrintWriter print, LockInfo[] locks,
            String tableWaitingForLock,
            ArrayList<String> tableExclusiveLocks,
            ArrayList<String> tableSharedLocksMap) {
        print.println(INDENT + "Locked synchronizers: count = " + locks.length);
        for (LockInfo li : locks) {
            print.println(INDENT + "  - " + li);
        }
        if (tableWaitingForLock != null) {
            print.println(INDENT + "Waiting for table: " + tableWaitingForLock);
        }
        if (tableExclusiveLocks != null) {
            print.println(INDENT + "Exclusive table locks: count = " + tableExclusiveLocks.size());
            for (String name : tableExclusiveLocks) {
                print.println(INDENT + "  - " + name);
            }
        }
        if (tableSharedLocksMap != null) {
            print.println(INDENT + "Shared table locks: count = " + tableSharedLocksMap.size());
            for (String name : tableSharedLocksMap) {
                print.println(INDENT + "  - " + name);
            }
        }
        print.println();
    }

}
