--- /dev/null
+/*\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements. See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License. You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package org.apache.catalina.valves;\r
+\r
+import java.io.IOException;\r
+import java.util.ArrayList;\r
+import java.util.Date;\r
+import java.util.List;\r
+import java.util.Queue;\r
+import java.util.concurrent.ConcurrentHashMap;\r
+import java.util.concurrent.ConcurrentLinkedQueue;\r
+import java.util.concurrent.atomic.AtomicInteger;\r
+\r
+import javax.servlet.ServletException;\r
+\r
+import org.apache.catalina.LifecycleException;\r
+import org.apache.catalina.connector.Request;\r
+import org.apache.catalina.connector.Response;\r
+import org.apache.juli.logging.Log;\r
+import org.apache.juli.logging.LogFactory;\r
+import org.apache.tomcat.util.res.StringManager;\r
+\r
+/**\r
+ * This valve allows to detect requests that take a long time to process, which might\r
+ * indicate that the thread that is processing it is stuck.\r
+ * Based on code proposed by TomLu in Bugzilla entry #50306\r
+ * \r
+ * @author slaurent\r
+ *\r
+ */\r
+public class StuckThreadDetectionValve extends ValveBase {\r
+\r
+ /**\r
+ * The descriptive information related to this implementation.\r
+ */\r
+ private static final String info =\r
+ "org.apache.catalina.valves.StuckThreadDetectionValve/1.0";\r
+ /**\r
+ * Logger\r
+ */\r
+ private static final Log log = LogFactory.getLog(StuckThreadDetectionValve.class);\r
+ \r
+ /**\r
+ * The string manager for this package.\r
+ */\r
+ private static final StringManager sm =\r
+ StringManager.getManager(Constants.Package);\r
+\r
+ /**\r
+ * Keeps count of the number of stuck threads detected\r
+ */\r
+ private final AtomicInteger stuckCount = new AtomicInteger(0);\r
+ \r
+ /**\r
+ * In seconds. Default 600 (10 minutes).\r
+ */\r
+ private int threshold = 600;\r
+ \r
+ /**\r
+ * The only references we keep to actual running Thread objects are in\r
+ * this Map (which is automatically cleaned in invoke()s finally clause).\r
+ * That way, Threads can be GC'ed, eventhough the Valve still thinks they\r
+ * are stuck (caused by a long monitor interval)\r
+ */\r
+ private ConcurrentHashMap<Long, MonitoredThread> activeThreads =\r
+ new ConcurrentHashMap<Long, MonitoredThread>();\r
+ /**\r
+ *\r
+ */\r
+ private Queue<CompletedStuckThread> completedStuckThreadsQueue =\r
+ new ConcurrentLinkedQueue<CompletedStuckThread>();\r
+\r
+ /**\r
+ * Specify the threshold (in seconds) used when checking for stuck threads.\r
+ * If <=0, the detection is disabled. The default is 600 seconds.\r
+ * \r
+ * @param threshold\r
+ * The new threshold in seconds\r
+ */\r
+ public void setThreshold(int threshold) {\r
+ this.threshold = threshold;\r
+ }\r
+\r
+ /**\r
+ * @see #setThreshold(int)\r
+ * @return The current threshold in seconds\r
+ */\r
+ public int getThreshold() {\r
+ return threshold;\r
+ }\r
+\r
+ @Override\r
+ protected void initInternal() throws LifecycleException {\r
+ super.initInternal();\r
+\r
+ if (log.isDebugEnabled()) {\r
+ log.debug("Monitoring stuck threads with threshold = "\r
+ + threshold\r
+ + " sec");\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Return descriptive information about this Valve implementation.\r
+ */\r
+ @Override\r
+ public String getInfo() {\r
+ return info;\r
+ }\r
+\r
+ private void notifyStuckThreadDetected(MonitoredThread monitoredThread,\r
+ long activeTime, int numStuckThreads) {\r
+ if (log.isWarnEnabled()) {\r
+ String msg = sm.getString(\r
+ "stuckThreadDetectionValve.notifyStuckThreadDetected",\r
+ monitoredThread.getThread().getName(), activeTime,\r
+ monitoredThread.getStartTime(), numStuckThreads,\r
+ monitoredThread.getRequestUri(), threshold);\r
+ // msg += "\n" + getStackTraceAsString(trace);\r
+ Throwable th = new Throwable();\r
+ th.setStackTrace(monitoredThread.getThread().getStackTrace());\r
+ log.warn(msg, th);\r
+ }\r
+ }\r
+\r
+ private void notifyStuckThreadCompleted(String threadName,\r
+ long activeTime, int numStuckThreads) {\r
+ if (log.isWarnEnabled()) {\r
+ String msg = sm.getString(\r
+ "stuckThreadDetectionValve.notifyStuckThreadCompleted",\r
+ threadName, activeTime, numStuckThreads);\r
+ // Since the "stuck thread notification" is warn, this should also\r
+ // be warn\r
+ log.warn(msg);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * {@inheritDoc}\r
+ */\r
+ @Override\r
+ public void invoke(Request request, Response response)\r
+ throws IOException, ServletException {\r
+ \r
+ if (threshold <= 0) {\r
+ // short-circuit if not monitoring stuck threads\r
+ getNext().invoke(request, response);\r
+ return;\r
+ }\r
+\r
+ // Save the thread/runnable\r
+ // Keeping a reference to the thread object here does not prevent\r
+ // GC'ing, as the reference is removed from the Map in the finally clause\r
+\r
+ Long key = new Long(Thread.currentThread().getId());\r
+ StringBuffer requestUrl = request.getRequestURL();\r
+ if(request.getQueryString()!=null) {\r
+ requestUrl.append("?");\r
+ requestUrl.append(request.getQueryString());\r
+ }\r
+ MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), \r
+ requestUrl.toString());\r
+ activeThreads.put(key, monitoredThread);\r
+\r
+ try {\r
+ getNext().invoke(request, response);\r
+ } finally {\r
+ activeThreads.remove(key);\r
+ if (monitoredThread.markAsDone() == MonitoredThreadState.STUCK) {\r
+ completedStuckThreadsQueue.add(\r
+ new CompletedStuckThread(monitoredThread.getThread().getName(),\r
+ monitoredThread.getActiveTimeInMillis()));\r
+ }\r
+ }\r
+ }\r
+\r
+ @Override\r
+ public void backgroundProcess() {\r
+ super.backgroundProcess();\r
+\r
+ long thresholdInMillis = threshold * 1000;\r
+\r
+ // Check monitored threads, being careful that the request might have\r
+ // completed by the time we examine it\r
+ for (MonitoredThread monitoredThread : activeThreads.values()) {\r
+ long activeTime = monitoredThread.getActiveTimeInMillis();\r
+\r
+ if (activeTime >= thresholdInMillis && monitoredThread.markAsStuckIfStillRunning()) {\r
+ int numStuckThreads = stuckCount.incrementAndGet();\r
+ notifyStuckThreadDetected(monitoredThread, activeTime, numStuckThreads);\r
+ }\r
+ }\r
+ // Check if any threads previously reported as stuck, have finished.\r
+ for (CompletedStuckThread completedStuckThread = completedStuckThreadsQueue.poll(); \r
+ completedStuckThread != null; completedStuckThread = completedStuckThreadsQueue.poll()) {\r
+\r
+ int numStuckThreads = stuckCount.decrementAndGet();\r
+ notifyStuckThreadCompleted(completedStuckThread.getName(),\r
+ completedStuckThread.getTotalActiveTime(), numStuckThreads);\r
+ }\r
+ }\r
+ \r
+ public long[] getStuckThreadIds() {\r
+ List<Long> idList = new ArrayList<Long>();\r
+ for (MonitoredThread monitoredThread : activeThreads.values()) {\r
+ if (monitoredThread.isMarkedAsStuck()) {\r
+ idList.add(monitoredThread.getThread().getId());\r
+ }\r
+ }\r
+\r
+ long[] result = new long[idList.size()];\r
+ for (int i = 0; i < result.length; i++) {\r
+ result[i] = idList.get(i);\r
+ }\r
+ return result;\r
+ }\r
+\r
+ private class MonitoredThread {\r
+\r
+ /**\r
+ * Reference to the thread to get a stack trace from background task\r
+ */\r
+ private final Thread thread;\r
+ private final String requestUri;\r
+ private final long start;\r
+ private final AtomicInteger state = new AtomicInteger(\r
+ MonitoredThreadState.RUNNING.ordinal());\r
+\r
+ public MonitoredThread(Thread thread, String requestUri) {\r
+ this.thread = thread;\r
+ this.requestUri = requestUri;\r
+ this.start = System.currentTimeMillis();\r
+ }\r
+\r
+ public Thread getThread() {\r
+ return this.thread;\r
+ }\r
+\r
+ public String getRequestUri() {\r
+ return requestUri;\r
+ }\r
+\r
+ public long getActiveTimeInMillis() {\r
+ return System.currentTimeMillis() - start;\r
+ }\r
+\r
+ public Date getStartTime() {\r
+ return new Date(start);\r
+ }\r
+\r
+ public boolean markAsStuckIfStillRunning() {\r
+ return this.state.compareAndSet(MonitoredThreadState.RUNNING.ordinal(),\r
+ MonitoredThreadState.STUCK.ordinal());\r
+ }\r
+\r
+ public MonitoredThreadState markAsDone() {\r
+ int val = this.state.getAndSet(MonitoredThreadState.DONE.ordinal());\r
+ return MonitoredThreadState.values()[val];\r
+ }\r
+ \r
+ boolean isMarkedAsStuck() {\r
+ return this.state.get() == MonitoredThreadState.STUCK.ordinal();\r
+ }\r
+ }\r
+\r
+ private class CompletedStuckThread {\r
+\r
+ private String threadName;\r
+ private long totalActiveTime;\r
+\r
+ public CompletedStuckThread(String threadName, long totalActiveTime) {\r
+ this.threadName = threadName;\r
+ this.totalActiveTime = totalActiveTime;\r
+ }\r
+\r
+ public String getName() {\r
+ return this.threadName;\r
+ }\r
+\r
+ public long getTotalActiveTime() {\r
+ return this.totalActiveTime;\r
+ }\r
+ }\r
+\r
+ private enum MonitoredThreadState {\r
+ RUNNING, STUCK, DONE;\r
+ }\r
+}\r