-
Notifications
You must be signed in to change notification settings - Fork 88
/
StopThreadsCleanUp.java
389 lines (336 loc) · 18 KB
/
StopThreadsCleanUp.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
package se.jiderhamn.classloader.leak.prevention.cleanup;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessControlContext;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderPreMortemCleanUp;
import static se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT;
/**
* Check if there are threads running within the protected {@link ClassLoader}, or otherwise referencing it,
* and either warn or stop those threads depending on settings.
* @author Mattias Jiderhamn
*/
@SuppressWarnings("WeakerAccess")
public class StopThreadsCleanUp implements ClassLoaderPreMortemCleanUp {
protected static final String JURT_ASYNCHRONOUS_FINALIZER = "com.sun.star.lib.util.AsynchronousFinalizer";
/** Thread {@link Runnable} for Sun/Oracle JRE i.e. java.lang.Thread.target */
private Field oracleTarget;
/** Thread {@link Runnable} for IBM JRE i.e. java.lang.Thread.runnable */
private Field ibmRunnable;
protected boolean stopThreads;
/**
* No of milliseconds to wait for threads to finish execution, before stopping them.
*/
protected int threadWaitMs = THREAD_WAIT_MS_DEFAULT;
/** Should Timer threads tied to the protected ClassLoader classloader be forced to stop at application shutdown? */
protected boolean stopTimerThreads;
/** Default constructor with {@link #stopThreads} = true and {@link #stopTimerThreads} = true */
@SuppressWarnings("unused")
public StopThreadsCleanUp() {
this(true, true);
}
public StopThreadsCleanUp(boolean stopThreads, boolean stopTimerThreads) {
this.stopThreads = stopThreads;
this.stopTimerThreads = stopTimerThreads;
}
public void setStopThreads(boolean stopThreads) {
this.stopThreads = stopThreads;
}
public void setStopTimerThreads(boolean stopTimerThreads) {
this.stopTimerThreads = stopTimerThreads;
}
public void setThreadWaitMs(int threadWaitMs) {
this.threadWaitMs = threadWaitMs;
}
@Override
public void cleanUp(ClassLoaderLeakPreventor preventor) {
// Force the execution of the cleanup code for JURT; see https://issues.apache.org/ooo/show_bug.cgi?id=122517
forceStartOpenOfficeJurtCleanup(preventor); // (Do this before stopThreads())
////////////////////
// Fix generic leaks
stopThreads(preventor);
}
/**
* The bug detailed at https://issues.apache.org/ooo/show_bug.cgi?id=122517 is quite tricky. This is a try to
* avoid the issues by force starting the threads and it's job queue.
*/
protected void forceStartOpenOfficeJurtCleanup(ClassLoaderLeakPreventor preventor) {
if(stopThreads) {
if(preventor.isLoadedByClassLoader(preventor.findClass(JURT_ASYNCHRONOUS_FINALIZER))) {
/*
The com.sun.star.lib.util.AsynchronousFinalizer class was found and loaded, which means that in case the
static block that starts the daemon thread had not been started yet, it has been started now.
Now let's force Garbage Collection, with the hopes of having the finalize()ers that put Jobs on the
AsynchronousFinalizer queue be executed. Then just leave it, and handle the rest in {@link #stopThreads}.
*/
preventor.info("OpenOffice JURT AsynchronousFinalizer thread started - forcing garbage collection to invoke finalizers");
ClassLoaderLeakPreventor.gc();
}
}
else {
// Check for class existence without loading class and thus executing static block
if(preventor.getClassLoader().getResource("com/sun/star/lib/util/AsynchronousFinalizer.class") != null) {
preventor.warn("OpenOffice JURT AsynchronousFinalizer thread will not be stopped if started, as stopThreads is false");
/*
By forcing Garbage Collection, we'll hopefully start the thread now, in case it would have been started by
GC later, so that at least it will appear in the logs.
*/
ClassLoaderLeakPreventor.gc();
}
}
}
/**
* Partially inspired by org.apache.catalina.loader.WebappClassLoader.clearReferencesThreads()
*/
protected void stopThreads(ClassLoaderLeakPreventor preventor) {
final Class<?> workerClass = preventor.findClass("java.util.concurrent.ThreadPoolExecutor$Worker");
final boolean waitForThreads = threadWaitMs > 0;
for(Thread thread : preventor.getAllThreads()) {
final Runnable runnable = getRunnable(preventor, thread);
final boolean threadLoadedByClassLoader = preventor.isLoadedInClassLoader(thread);
final boolean threadGroupLoadedByClassLoader = preventor.isLoadedInClassLoader(thread.getThreadGroup());
final boolean runnableLoadedByClassLoader = preventor.isLoadedInClassLoader(runnable);
final boolean hasContextClassLoader = preventor.isClassLoaderOrChild(thread.getContextClassLoader());
if(thread != Thread.currentThread() && // Ignore current thread
(threadLoadedByClassLoader || threadGroupLoadedByClassLoader || hasContextClassLoader || // = preventor.isThreadInClassLoader(thread)
runnableLoadedByClassLoader)) {
if (thread.getClass().getName().startsWith(StopThreadsCleanUp.JURT_ASYNCHRONOUS_FINALIZER)) {
// Note, the thread group of this thread may be "system" if it is triggered by the Garbage Collector
// however if triggered by us in forceStartOpenOfficeJurtCleanup() it may depend on the application server
if(stopThreads) {
preventor.info("Found JURT thread " + thread.getName() + "; starting " + JURTKiller.class.getSimpleName());
new JURTKiller(preventor, thread).start();
}
else
preventor.warn("JURT thread " + thread.getName() + " is still running in protected ClassLoader");
}
else if(thread.getThreadGroup() != null &&
("system".equals(thread.getThreadGroup().getName()) || // System thread
"RMI Runtime".equals(thread.getThreadGroup().getName()))) { // RMI thread (honestly, just copied from Tomcat)
if("Keep-Alive-Timer".equals(thread.getName())) {
thread.setContextClassLoader(preventor.getLeakSafeClassLoader());
preventor.debug("Changed contextClassLoader of HTTP keep alive thread");
}
}
else if(thread.isAlive()) { // Non-system, running in protected ClassLoader
if(thread.getClass().getName().startsWith("java.util.Timer")) { // Sun/Oracle = "java.util.TimerThread"; IBM = "java.util.Timer$TimerImpl"
if(thread.getName() != null && thread.getName().startsWith("PostgreSQL-JDBC-SharedTimer-")) { // Postgresql JDBC timer thread
// Replace contextClassLoader, if needed
if(hasContextClassLoader) {
final Class<?> postgresqlDriver = preventor.findClass("org.postgresql.Driver");
final ClassLoader postgresqlCL = (postgresqlDriver != null && ! preventor.isLoadedByClassLoader(postgresqlDriver)) ?
postgresqlDriver.getClassLoader() : // Postgresql driver loaded by other classloader than we want to protect
preventor.getLeakSafeClassLoader();
thread.setContextClassLoader(postgresqlCL);
preventor.warn("Changing contextClassLoader of " + thread + " to " + postgresqlCL);
}
// Replace AccessControlContext
setThreadSafeAccessControlContext(preventor, thread);
}
else if(stopTimerThreads) {
preventor.warn("Stopping Timer thread '" + thread.getName() + "' running in protected ClassLoader. " +
preventor.getStackTrace(thread));
stopTimerThread(preventor, thread);
}
else {
preventor.info("Timer thread is running in protected ClassLoader, but will not be stopped. " +
preventor.getStackTrace(thread));
}
}
else {
final String displayString = "Thread '" + thread + "'" +
(threadLoadedByClassLoader ? " of type " + thread.getClass().getName() + " loaded by protected ClassLoader" : "") +
(runnableLoadedByClassLoader ? " with Runnable of type " + runnable.getClass().getName() + " loaded by protected ClassLoader" : "") +
(threadGroupLoadedByClassLoader ? " with ThreadGroup of type " + thread.getThreadGroup().getClass().getName() + " loaded by protected ClassLoader" : "") +
(hasContextClassLoader ? " with contextClassLoader = protected ClassLoader or child" : "");
// If threads is running an java.util.concurrent.ThreadPoolExecutor.Worker try shutting down the executor
if(workerClass != null && workerClass.isInstance(runnable)) {
try {
// java.util.concurrent.ThreadPoolExecutor, introduced in Java 1.5
final Field workerExecutor = preventor.findField(workerClass, "this$0");
final ThreadPoolExecutor executor = preventor.getFieldValue(workerExecutor, runnable);
if(executor != null) {
if("org.apache.tomcat.util.threads.ThreadPoolExecutor".equals(executor.getClass().getName())) {
// Tomcat pooled thread
preventor.debug(displayString + " is worker of " + executor.getClass().getName());
}
else if(preventor.isLoadedInClassLoader(executor) || preventor.isLoadedInClassLoader(executor.getThreadFactory())) {
if(stopThreads) {
preventor.warn("Shutting down ThreadPoolExecutor of type " + executor.getClass().getName());
executor.shutdownNow();
}
else {
preventor.warn("ThreadPoolExecutor of type " + executor.getClass().getName() +
" should be shut down.");
}
}
else {
preventor.info(displayString + " is a ThreadPoolExecutor.Worker of " + executor.getClass().getName() +
" but found no reason to shut down ThreadPoolExecutor.");
}
}
}
catch (Exception ex) {
preventor.error(ex);
}
}
if(! threadLoadedByClassLoader && ! runnableLoadedByClassLoader && ! threadGroupLoadedByClassLoader) { // Not loaded in protected ClassLoader - just running there
// This would for example be the case with org.apache.tomcat.util.threads.TaskThread
if(waitForThreads) {
preventor.warn(displayString + "; waiting " + threadWaitMs +
" ms. " + preventor.getStackTrace(thread));
preventor.waitForThread(thread, threadWaitMs, false /* No interrupt */);
}
if(thread.isAlive() && preventor.isClassLoaderOrChild(thread.getContextClassLoader())) { // Still running in ClassLoader
preventor.warn(displayString + (waitForThreads ? " still" : "") +
" alive; changing context ClassLoader to leak safe (" +
preventor.getLeakSafeClassLoader() + "). " + preventor.getStackTrace(thread));
thread.setContextClassLoader(preventor.getLeakSafeClassLoader());
// Replace AccessControlContext since we already replaced ClassLoader,
// for test/use cease @see StopThreadsClenup_ExecutorTest
setThreadSafeAccessControlContext(preventor, thread);
}
}
else if(stopThreads) { // Thread/Runnable/ThreadGroup loaded by protected ClassLoader
if(waitForThreads) {
preventor.warn("Waiting for " + displayString + " for " + threadWaitMs + " ms. " +
preventor.getStackTrace(thread));
preventor.waitForThread(thread, threadWaitMs, true /* Interrupt if needed */);
}
// Normally threads should not be stopped (method is deprecated), since it may cause an inconsistent state.
// In this case however, the alternative is a classloader leak, which may or may not be considered worse.
if(thread.isAlive()) {
preventor.warn("Stopping " + displayString + ". " + preventor.getStackTrace(thread));
//noinspection deprecation
thread.stop();
}
else {
preventor.info(displayString + " no longer alive - no action needed.");
}
}
else {
preventor.warn(displayString + " would cause leak. " + preventor.getStackTrace(thread));
}
}
}
}
}
}
/**
* Replace Thread AccessControlContext to allow for Protection Domain GC
*/
private void setThreadSafeAccessControlContext(ClassLoaderLeakPreventor preventor, Thread thread) {
// Replace AccessControlContext
final Field inheritedAccessControlContext = preventor.findField(Thread.class, "inheritedAccessControlContext");
if(inheritedAccessControlContext != null) {
try {
final AccessControlContext acc = preventor.createAccessControlContext();
inheritedAccessControlContext.set(thread, acc);
preventor.removeDomainCombiner("thread " + thread, acc);
}
catch (Exception e) {
preventor.error(e);
}
}
}
/** Get {@link Runnable} of given thread, if any */
private Runnable getRunnable(ClassLoaderLeakPreventor preventor, Thread thread) {
if(oracleTarget == null && ibmRunnable == null) { // Not yet initialized
oracleTarget = preventor.findField(Thread.class, "target"); // Sun/Oracle JRE
ibmRunnable = preventor.findField(Thread.class, "runnable"); // IBM JRE
}
return (oracleTarget != null) ? (Runnable) preventor.getFieldValue(oracleTarget, thread) : // Sun/Oracle JRE
(Runnable) preventor.getFieldValue(ibmRunnable, thread); // IBM JRE
}
protected void stopTimerThread(ClassLoaderLeakPreventor preventor, Thread thread) {
// Seems it is not possible to access Timer of TimerThread, so we need to mimic Timer.cancel()
/**
try {
Timer timer = (Timer) findField(thread.getClass(), "this$0").get(thread); // This does not work!
warn("Cancelling Timer " + timer + " / TimeThread '" + thread + "'");
timer.cancel();
}
catch (IllegalAccessException iaex) {
error(iaex);
}
*/
try {
final Field newTasksMayBeScheduled = preventor.findField(thread.getClass(), "newTasksMayBeScheduled");
final Object queue = preventor.findField(thread.getClass(), "queue").get(thread); // java.lang.TaskQueue
final Method clear = preventor.findMethod(queue.getClass(), "clear");
// Do what java.util.Timer.cancel() does
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (queue) {
newTasksMayBeScheduled.set(thread, Boolean.FALSE);
clear.invoke(queue);
queue.notify(); // "In case queue was already empty."
}
// We shouldn't need to join() here, thread will finish soon enough
}
catch (Exception ex) {
preventor.error(ex);
}
}
/**
* Inner class with the sole task of killing JURT finalizer thread after it is done processing jobs.
* We need to postpone the stopping of this thread, since more Jobs may in theory be add()ed when the protected
* ClassLoader is closing down and being garbage collected.
* See https://issues.apache.org/ooo/show_bug.cgi?id=122517
*/
protected class JURTKiller extends Thread {
private final ClassLoaderLeakPreventor preventor;
private final Thread jurtThread;
private final List<?> jurtQueue;
public JURTKiller(ClassLoaderLeakPreventor preventor, Thread jurtThread) {
super("JURTKiller");
this.preventor = preventor;
this.jurtThread = jurtThread;
jurtQueue = preventor.getStaticFieldValue(StopThreadsCleanUp.JURT_ASYNCHRONOUS_FINALIZER, "queue");
// Make sure all classes are loaded from the current app classloader before it executes,
// as it may use them *after* the classloader has been "shutdown" by the container (if any).
State state = State.RUNNABLE;
}
@Override
public void run() {
try {
if(jurtQueue == null || jurtThread == null) {
preventor.error(getName() + ": No queue or thread!?");
return;
}
if(! jurtThread.isAlive()) {
preventor.warn(getName() + ": " + jurtThread.getName() + " is already dead?");
}
boolean queueIsEmpty = false;
while(! queueIsEmpty) {
try {
preventor.debug(getName() + " goes to sleep for " + ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT + " ms");
Thread.sleep(ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT);
}
catch (InterruptedException e) {
// Do nothing
}
if(State.RUNNABLE != jurtThread.getState()) { // Unless thread is currently executing a Job
preventor.debug(getName() + " about to force Garbage Collection");
ClassLoaderLeakPreventor.gc(); // Force garbage collection, which may put new items on queue
synchronized (jurtQueue) {
queueIsEmpty = jurtQueue.isEmpty();
preventor.debug(getName() + ": JURT queue is empty? " + queueIsEmpty);
}
}
else
preventor.debug(getName() + ": JURT thread " + jurtThread.getName() + " is executing Job");
}
preventor.info(getName() + " about to kill " + jurtThread);
if(jurtThread.isAlive()) {
//noinspection deprecation
jurtThread.stop();
}
}
catch (Throwable t) {
preventor.error(t);
}
}
}
}