/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.test.dunit;

import static org.apache.geode.test.dunit.Jitter.jitterInterval;
import static org.junit.Assert.fail;

import org.apache.logging.log4j.Logger;

import org.apache.geode.logging.internal.OSProcess;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * <code>ThreadUtils</code> provides static utility methods to perform thread related actions such
 * as dumping thread stacks.
 *
 * These methods can be used directly: <code>ThreadUtils.dumpAllStacks()</code>, however, they are
 * intended to be referenced through static import:
 *
 * <pre>
 * import static org.apache.geode.test.dunit.ThreadUtils.*;
 *    ...
 *    dumpAllStacks();
 * </pre>
 *
 * Extracted from DistributedTestCase.
 */
public class ThreadUtils {

  private static final Logger logger = LogService.getLogger();

  protected ThreadUtils() {}

  /**
   * Print stack dumps for all vms.
   *
   * @since GemFire 5.0
   */
  public static void dumpAllStacks() {
    for (int h = 0; h < Host.getHostCount(); h++) {
      dumpStack(Host.getHost(h));
    }
  }

  /**
   * Dump all thread stacks
   */
  public static void dumpMyThreads() {
    OSProcess.printStacks(0, false);
  }

  /**
   * Print a stack dump for this vm.
   *
   * @since GemFire 5.0
   */
  public static void dumpStack() {
    OSProcess.printStacks(0, false);
  }

  /**
   * Print stack dumps for all vms on the given host.
   *
   * @param host the Host for which to print stack dumps
   *
   * @since GemFire 5.0
   */
  public static void dumpStack(final Host host) {
    for (int v = 0; v < host.getVMCount(); v++) {
      host.getVM(v).invoke(org.apache.geode.test.dunit.ThreadUtils.class, "dumpStack");
    }
  }

  /**
   * Print a stack dump for the given vm.
   *
   * @param vm the VM for which to print a stack dump
   * @since GemFire 5.0
   */
  public static void dumpStack(final VM vm) {
    vm.invoke(org.apache.geode.test.dunit.ThreadUtils.class, "dumpStack");
  }

  public static void dumpStackTrace(final Thread thread, final StackTraceElement[] stackTrace) {
    StringBuilder msg = new StringBuilder();
    msg.append("Thread=<").append(thread).append("> stackDump:\n");
    for (final StackTraceElement stackTraceElement : stackTrace) {
      msg.append("\t").append(stackTraceElement).append("\n");
    }
    logger.info(msg.toString());
  }

  /**
   * Wait for a thread to join.
   *
   * @param async async invocation to wait on
   * @param timeoutMilliseconds maximum time to wait
   * @throws AssertionError if the thread does not terminate
   */
  public static void join(final AsyncInvocation<?> async, final long timeoutMilliseconds) {
    join(async.getThread(), timeoutMilliseconds);
  }

  /**
   * Wait for a thread to join.
   *
   * @param thread thread to wait on
   * @param timeoutMilliseconds maximum time to wait
   * @throws AssertionError if the thread does not terminate
   */
  public static void join(final Thread thread, final long timeoutMilliseconds) {
    final long tilt = System.currentTimeMillis() + timeoutMilliseconds;
    final long incrementalWait = jitterInterval(timeoutMilliseconds);
    final long start = System.currentTimeMillis();
    for (;;) {
      // I really do *not* understand why this check is necessary
      // but it is, at least with JDK 1.6. According to the source code
      // and the javadocs, one would think that join() would exit immediately
      // if the thread is dead. However, I can tell you from experimentation
      // that this is not the case. :-( djp 2008-12-08
      if (!thread.isAlive()) {
        break;
      }
      try {
        thread.join(incrementalWait);
      } catch (InterruptedException e) {
        fail("interrupted");
      }
      if (System.currentTimeMillis() >= tilt) {
        break;
      }
    } // for
    if (thread.isAlive()) {
      logger.info("HUNG THREAD");
      ThreadUtils.dumpStackTrace(thread, thread.getStackTrace());
      ThreadUtils.dumpMyThreads();
      thread.interrupt(); // We're in trouble!
      fail("Thread did not terminate after " + timeoutMilliseconds + " ms: " + thread);
    }
    long elapsedMs = (System.currentTimeMillis() - start);
    if (elapsedMs > 0) {
      String msg = "Thread " + thread + " took " + elapsedMs + " ms to exit.";
      logger.info(msg);
    }
  }
}
