View | Details | Raw Unified | Return to bug 50306
Collapse All | Expand All

(-)java/org/apache/catalina/valves/LocalStrings.properties (+4 lines)
Lines 41-46 Link Here
41
# Remote IP valve
41
# Remote IP valve
42
remoteIpValve.syntax=Invalid regular expressions [{0}] provided.
42
remoteIpValve.syntax=Invalid regular expressions [{0}] provided.
43
43
44
#Stuck thread detection Valve
45
stuckThreadDetectionValve.notifyStuckThreadDetected=Thread "{0}" has been active for {1} milliseconds (started {2}) and may be stuck. There is/are {3} thread(s) in total that are monitored by this Valve and may be stuck.
46
stuckThreadDetectionValve.notifyStuckThreadCompleted=Thread "{0}" was previously reported to be stuck but has completed. It was active for approximately {1} milliseconds. There is/are {2} thread(s) in total that are monitored by this Valve and still may be stuck.
47
44
# HTTP status reports
48
# HTTP status reports
45
http.100=The client may continue ({0}).
49
http.100=The client may continue ({0}).
46
http.101=The server is switching protocols according to the "Upgrade" header ({0}).
50
http.101=The server is switching protocols according to the "Upgrade" header ({0}).
(-)java/org/apache/catalina/valves/StuckThreadDetectionValve.java (+300 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
package org.apache.catalina.valves;
18
19
import java.io.IOException;
20
import java.util.Date;
21
import java.util.Queue;
22
import java.util.concurrent.ConcurrentHashMap;
23
import java.util.concurrent.ConcurrentLinkedQueue;
24
import java.util.concurrent.atomic.AtomicInteger;
25
26
import javax.servlet.ServletException;
27
import org.apache.catalina.Container;
28
import org.apache.catalina.Engine;
29
import org.apache.catalina.LifecycleException;
30
31
import org.apache.catalina.connector.Request;
32
import org.apache.catalina.connector.Response;
33
import org.apache.juli.logging.Log;
34
import org.apache.juli.logging.LogFactory;
35
import org.apache.tomcat.util.res.StringManager;
36
37
public class StuckThreadDetectionValve extends ValveBase {
38
39
    /**
40
     * The descriptive information related to this implementation.
41
     */
42
    private static final String info =
43
            "org.apache.catalina.valves.StuckThreadDetectionValve/1.0";
44
    /**
45
     * Logger
46
     */
47
    private static final Log log = LogFactory.getLog(StuckThreadDetectionValve.class);
48
    /**
49
     * Keeps count of the number of stuck threads detected
50
     */
51
    private final AtomicInteger stuckCount = new AtomicInteger(0);
52
    /**
53
     * In seconds. Default 600 (10 minutes).
54
     */
55
    private int threshold = 600;
56
    /**
57
     * 'SO_TIMEOUT' is 20 seconds in Tomcat.
58
     * The threshold must at least be larger than that.
59
     */
60
    private static final int THRESHOLD_MIN_VALUE = 120;
61
    /**
62
     * The only references we keep to actual running Thread objects are in
63
     * this Map (which is automatically cleaned in invoke()s finally clause).
64
     * That way, Threads can be GC'ed, eventhough the Valve still thinks they
65
     * are stuck (caused by a long monitor interval)
66
     */
67
    private ConcurrentHashMap<Long, MonitoredThread> activeThreads =
68
            new ConcurrentHashMap<Long, MonitoredThread>();
69
    /**
70
     *
71
     */
72
    private Queue<CompletedStuckThread> completedStuckThreadsQueue =
73
            new ConcurrentLinkedQueue<CompletedStuckThread>();
74
    /**
75
     * The string manager for this package.
76
     */
77
    private static final StringManager sm =
78
            StringManager.getManager(Constants.Package);
79
80
    /**
81
     * Specify the threshold (in seconds) used when checking for stuck threads.
82
     * Minimum value is 120 seconds. The default is 600 seconds.
83
     *
84
     * @param threshold   The new threshold in seconds
85
     */
86
    public void setThreshold(int threshold) {
87
        if (threshold >= THRESHOLD_MIN_VALUE) {
88
            this.threshold = threshold;
89
            if (log.isDebugEnabled()) {
90
                log.debug("Threshold set to " + threshold + " seconds");
91
            }
92
        } else {
93
            log.warn("Threshold too low. Must be "
94
                    + THRESHOLD_MIN_VALUE
95
                    + " or larger. Actual: " + threshold);
96
        }
97
    }
98
99
    /**
100
     * @see #setThreshold(int)
101
     * @return  The current threshold in seconds
102
     */
103
    public int getThreshold() {
104
        return threshold;
105
    }
106
107
    @Override
108
    protected void initInternal() throws LifecycleException {
109
        super.initInternal();
110
111
        if (log.isDebugEnabled()) {
112
            log.debug("Monitoring stuck threads with threshold = "
113
                    + threshold
114
                    + " sec");
115
        }
116
    }
117
118
    /**
119
     * Return descriptive information about this Valve implementation.
120
     */
121
    @Override
122
    public String getInfo() {
123
        return info;
124
    }
125
126
    private void notifyStuckThreadDetected(String threadName,
127
            StackTraceElement[] trace, long activeTime,
128
            Date startTime, int numStuckThreads) {
129
130
        String msg = sm.getString(
131
                "stuckThreadDetectionValve.notifyStuckThreadDetected",
132
                threadName, activeTime, startTime, numStuckThreads);
133
        msg += "\n" + getStackTraceAsString(trace);
134
        log.warn(msg);
135
    }
136
137
    private void notifyStuckThreadCompleted(String threadName,
138
            long activeTime, int numStuckThreads) {
139
140
        String msg = sm.getString(
141
                "stuckThreadDetectionValve.notifyStuckThreadCompleted",
142
                threadName, activeTime, numStuckThreads);
143
        //Since the "stuck thread notification" is warn, this should also be warn
144
        log.warn(msg);
145
    }
146
147
    //TODO Utility method, should be moved to utility class
148
    public static String getStackTraceAsString(StackTraceElement[] trace) {
149
        StringBuilder buf = new StringBuilder();
150
        for (int i = 0; i < trace.length; i++) {
151
            buf.append("\tat ").append(trace[i]).append("\n");
152
        }
153
        return buf.toString();
154
    }
155
156
    //FIXME where does it make sense to add this Valve?
157
    /**
158
     * Set the Container to which this Valve is attached.
159
     *
160
     * @param container The container to which we are attached
161
     */
162
    @Override
163
    public void setContainer(Container container) {
164
165
        if (container == null){
166
            throw new IllegalArgumentException("Configuration error:  "
167
                    + "Container cannot be null. Valve must be attached to an Engine");
168
        }
169
        if(!(container instanceof Engine)){
170
            throw new IllegalArgumentException("Configuration error:  "
171
                    + "Valve must be attached to an Engine. Actual: "
172
                    +container.getClass().getName());
173
        }
174
175
        super.setContainer(container);
176
//        this.engine = (Engine) container;
177
    }
178
179
    /**
180
     * {@inheritDoc}
181
     */
182
    @Override
183
    public void invoke(Request request, Response response)
184
            throws IOException, ServletException {
185
        //Save the thread/runnable
186
        //Keeping a reference to the thread object here does not prevent GC'ing,
187
        //as the reference is removed from the Map in the finally clause
188
189
        Long key = new Long(Thread.currentThread().getId());
190
191
        MonitoredThread previous = activeThreads.put(key,
192
                new MonitoredThread(Thread.currentThread()));
193
194
        //In theory we might get collisions when using "new Long(thread.getId()).hashCode()".
195
        //We can prevent this by using new Integer(AtomicInt.getAndIncrement()) for uniqueness
196
        if (previous != null) {
197
            log.error("Key already added to map. Previous value has been removed ("
198
                    + previous.getThread().getName() + ")");
199
        }
200
201
        if (log.isTraceEnabled()) {
202
            log.trace("Monitoring execution time for Request "
203
                    + request.getRequestURI() + " with originalRemoteAddr '"
204
                    + request.getRemoteAddr() + ":" + request.getRemotePort()
205
                    + "'");
206
        }
207
208
        try {
209
            getNext().invoke(request, response);
210
        } finally {
211
            MonitoredThread monitoredThread = activeThreads.remove(key);
212
            if (monitoredThread.isMarkedAsStuck()) {
213
                completedStuckThreadsQueue.add(
214
                        new CompletedStuckThread(monitoredThread.getThread().getName(),
215
                        monitoredThread.getActiveTimeInMillis()));
216
            }
217
        }
218
    }
219
220
    @Override
221
    public void backgroundProcess() {
222
        super.backgroundProcess();
223
224
        long thresholdInMillis = threshold * 1000;
225
226
        //Check monitored threads
227
        for (MonitoredThread monitoredThread : activeThreads.values()) {
228
            long activeTime = monitoredThread.getActiveTimeInMillis();
229
230
            if (!monitoredThread.isMarkedAsStuck() && activeTime >= thresholdInMillis) {
231
                int numStuckThreads = stuckCount.incrementAndGet();
232
                monitoredThread.markAsStuck();
233
                notifyStuckThreadDetected(monitoredThread.getThread().getName(),
234
                        monitoredThread.getThread().getStackTrace(),
235
                        activeTime, monitoredThread.getStartTime(), numStuckThreads);
236
            }
237
        }
238
        //Check if any threads previously reported as stuck, have finished.
239
        CompletedStuckThread completedStuckThread = completedStuckThreadsQueue.poll();
240
        while (completedStuckThread != null) {
241
            int numStuckThreads = stuckCount.decrementAndGet();
242
            notifyStuckThreadCompleted(completedStuckThread.getName(),
243
                    completedStuckThread.getTotalActiveTime(), numStuckThreads);
244
            completedStuckThread = completedStuckThreadsQueue.poll();
245
        }
246
    }
247
248
    private class MonitoredThread {
249
250
        /**
251
         * Reference to the thread to get a stack trace from background task
252
         */
253
        private Thread thread;
254
        private long start = System.currentTimeMillis();
255
        private volatile boolean isStuck = false;
256
257
        public MonitoredThread(Thread thread) {
258
            this.thread = thread;
259
        }
260
261
        public Thread getThread() {
262
            return this.thread;
263
        }
264
265
        public long getActiveTimeInMillis() {
266
            return System.currentTimeMillis() - start;
267
        }
268
269
        public Date getStartTime() {
270
            return new Date(start);
271
        }
272
273
        public void markAsStuck() {
274
            this.isStuck = true;
275
        }
276
277
        public boolean isMarkedAsStuck() {
278
            return this.isStuck;
279
        }
280
    }
281
282
    private class CompletedStuckThread {
283
284
        private String threadName;
285
        private long totalActiveTime;
286
287
        public CompletedStuckThread(String threadName, long totalActiveTime) {
288
            this.threadName = threadName;
289
            this.totalActiveTime = totalActiveTime;
290
        }
291
292
        public String getName() {
293
            return this.threadName;
294
        }
295
296
        public long getTotalActiveTime() {
297
            return this.totalActiveTime;
298
        }
299
    }
300
}

Return to bug 50306