Skip to content

Commit 9c18390

Browse files
author
Stephen Colebourne
committed
IO-99 - FileCleaner.exitWhenFinished, to allow the thread to be terminated
includes some code from Jochen Wiedmann git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/io/trunk@482437 13f79535-47bb-0310-9956-ffa450edef68
1 parent f732c9a commit 9c18390

4 files changed

Lines changed: 206 additions & 36 deletions

File tree

RELEASE-NOTES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ Enhancements from 1.2
110110
- This can be used as a calback in FileCleaner
111111
- Together these allow FileCleaner to do a forceDelete to kill directories
112112

113+
- FileCleaner.exitWhenFinished [IO-99]
114+
- A new method that allows the internal cleaner thread to be cleanly terminated
115+
113116
- WildcardFileFilter
114117
- Replacement for WildcardFilter
115118
- Accepts both files and directories

project.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@
223223
<contributor>
224224
<name>James Urie</name>
225225
</contributor>
226+
<contributor>
227+
<name>Jochen Wiedmann</name>
228+
</contributor>
226229
<contributor>
227230
<name>Frank W. Zammetti</name>
228231
</contributor>

src/java/org/apache/commons/io/FileCleaner.java

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
* This utility creates a background thread to handle file deletion.
3030
* Each file to be deleted is registered with a handler object.
3131
* When the handler object is garbage collected, the file is deleted.
32+
* <p>
33+
* In an environment with multiple class loaders (a servlet container, for
34+
* example), you should consider stopping the background thread if it is no
35+
* longer needed. This is done by invoking the method
36+
* {@link {@link #exitWhenFinished}, typically in
37+
* {@link javax.servlet.ServletContextListener#contextDestroyed} or similar.
3238
*
3339
* @author Noel Bergman
3440
* @author Martin Cooper
@@ -39,47 +45,19 @@ public class FileCleaner {
3945
/**
4046
* Queue of <code>Tracker</code> instances being watched.
4147
*/
42-
private static ReferenceQueue /* Tracker */ q = new ReferenceQueue();
43-
48+
static ReferenceQueue /* Tracker */ q = new ReferenceQueue();
4449
/**
4550
* Collection of <code>Tracker</code> instances in existence.
4651
*/
47-
private static Collection /* Tracker */ trackers = new Vector();
48-
52+
static Collection /* Tracker */ trackers = new Vector();
4953
/**
50-
* The thread that will clean up registered files.
54+
* Whether to terminate the thread when the tracking is complete.
5155
*/
52-
private static Thread reaper = new Thread("File Reaper") {
53-
54-
/**
55-
* Run the reaper thread that will delete files as their associated
56-
* marker objects are reclaimed by the garbage collector.
57-
*/
58-
public void run() {
59-
for (;;) {
60-
Tracker tracker = null;
61-
try {
62-
// Wait for a tracker to remove.
63-
tracker = (Tracker) q.remove();
64-
} catch (Exception e) {
65-
continue;
66-
}
67-
68-
tracker.delete();
69-
tracker.clear();
70-
trackers.remove(tracker);
71-
}
72-
}
73-
};
74-
56+
static volatile boolean exitWhenFinished = false;
7557
/**
76-
* The static initializer that starts the reaper thread.
58+
* The thread that will clean up registered files.
7759
*/
78-
static {
79-
reaper.setPriority(Thread.MAX_PRIORITY);
80-
reaper.setDaemon(true);
81-
reaper.start();
82-
}
60+
static Thread reaper;
8361

8462
//-----------------------------------------------------------------------
8563
/**
@@ -109,7 +87,7 @@ public static void track(File file, Object marker, FileDeleteStrategy deleteStra
10987
if (file == null) {
11088
throw new NullPointerException("The file must not be null");
11189
}
112-
trackers.add(new Tracker(file.getPath(), deleteStrategy, marker, q));
90+
addTracker(file.getPath(), marker, deleteStrategy);
11391
}
11492

11593
/**
@@ -139,9 +117,28 @@ public static void track(String path, Object marker, FileDeleteStrategy deleteSt
139117
if (path == null) {
140118
throw new NullPointerException("The path must not be null");
141119
}
120+
addTracker(path, marker, deleteStrategy);
121+
}
122+
123+
/**
124+
* Adds a tracker to the list of trackers.
125+
*
126+
* @param path the full path to the file to be tracked, not null
127+
* @param marker the marker object used to track the file, not null
128+
* @param deleteStrategy the strategy to delete the file, null means normal
129+
*/
130+
private static synchronized void addTracker(String path, Object marker, FileDeleteStrategy deleteStrategy) {
131+
if (exitWhenFinished) {
132+
throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
133+
}
134+
if (reaper == null) {
135+
reaper = new Reaper();
136+
reaper.start();
137+
}
142138
trackers.add(new Tracker(path, deleteStrategy, marker, q));
143139
}
144140

141+
//-----------------------------------------------------------------------
145142
/**
146143
* Retrieve the number of files currently being tracked, and therefore
147144
* awaiting deletion.
@@ -152,11 +149,75 @@ public static int getTrackCount() {
152149
return trackers.size();
153150
}
154151

152+
/**
153+
* Call this method to cause the file cleaner thread to terminate when
154+
* there are no more objects being tracked for deletion.
155+
* <p>
156+
* In a simple environment, you don't need this method as the file cleaner
157+
* thread will simply exit when the JVM exits. In a more complex environment,
158+
* with multiple class loaders (such as an application server), you should be
159+
* aware that the file cleaner thread will continue running even if the class
160+
* loader it was started from terminates. This can consitute a memory leak.
161+
* <p>
162+
* For example, suppose that you have developed a web application, which
163+
* contains the commons-io jar file in your WEB-INF/lib directory. In other
164+
* words, the FileCleaner class is loaded through the class loader of your
165+
* web application. If the web application is terminated, but the servlet
166+
* container is still running, then the file cleaner thread will still exist,
167+
* posing a memory leak.
168+
* <p>
169+
* This method allows the thread to be terminated. Simply call this method
170+
* in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}.
171+
* One called, no new objects can be tracked by the file cleaner.
172+
*/
173+
public static synchronized void exitWhenFinished() {
174+
exitWhenFinished = true;
175+
if (reaper != null) {
176+
synchronized (reaper) {
177+
reaper.interrupt();
178+
}
179+
}
180+
}
181+
182+
// -----------------------------------------------------------------------
183+
/**
184+
* The reaper thread.
185+
*/
186+
static final class Reaper extends Thread {
187+
Reaper() {
188+
super("File Reaper");
189+
setPriority(Thread.MAX_PRIORITY);
190+
setDaemon(true);
191+
}
192+
193+
/**
194+
* Run the reaper thread that will delete files as their associated
195+
* marker objects are reclaimed by the garbage collector.
196+
*/
197+
public void run() {
198+
// thread exits when exitWhenFinished is true and there are no more tracked objects
199+
while (exitWhenFinished == false || trackers.size() > 0) {
200+
Tracker tracker = null;
201+
try {
202+
// Wait for a tracker to remove.
203+
tracker = (Tracker) q.remove();
204+
} catch (Exception e) {
205+
continue;
206+
}
207+
if (tracker != null) {
208+
tracker.delete();
209+
tracker.clear();
210+
trackers.remove(tracker);
211+
}
212+
}
213+
}
214+
}
215+
155216
//-----------------------------------------------------------------------
156217
/**
157218
* Inner class which acts as the reference for a file pending deletion.
158219
*/
159-
static class Tracker extends PhantomReference {
220+
static final class Tracker extends PhantomReference {
160221

161222
/**
162223
* The full path to the file being tracked.

src/test/org/apache/commons/io/FileCleanerTestCase.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.RandomAccessFile;
22+
import java.lang.ref.ReferenceQueue;
23+
import java.util.Vector;
2224

2325
import junit.framework.Test;
2426
import junit.framework.TestSuite;
@@ -62,6 +64,12 @@ protected void setUp() throws Exception {
6264
/** @see junit.framework.TestCase#tearDown() */
6365
protected void tearDown() throws Exception {
6466
FileUtils.deleteDirectory(getTestDirectory());
67+
68+
// reset file cleaner class, so as not to break other tests
69+
FileCleaner.q = new ReferenceQueue();
70+
FileCleaner.trackers = new Vector();
71+
FileCleaner.exitWhenFinished = false;
72+
FileCleaner.reaper = null;
6573
}
6674

6775
//-----------------------------------------------------------------------
@@ -170,6 +178,101 @@ public void testFileCleanerNull() throws Exception {
170178
}
171179
}
172180

181+
public void testFileCleanerExitWhenFinishedFirst() throws Exception {
182+
assertEquals(false, FileCleaner.exitWhenFinished);
183+
FileCleaner.exitWhenFinished();
184+
assertEquals(true, FileCleaner.exitWhenFinished);
185+
assertEquals(null, FileCleaner.reaper);
186+
187+
waitUntilTrackCount();
188+
189+
assertEquals(0, FileCleaner.getTrackCount());
190+
assertEquals(true, FileCleaner.exitWhenFinished);
191+
assertEquals(null, FileCleaner.reaper);
192+
}
193+
194+
public void testFileCleanerExitWhenFinished_NoTrackAfter() throws Exception {
195+
assertEquals(false, FileCleaner.exitWhenFinished);
196+
FileCleaner.exitWhenFinished();
197+
assertEquals(true, FileCleaner.exitWhenFinished);
198+
assertEquals(null, FileCleaner.reaper);
199+
200+
String path = testFile.getPath();
201+
Object marker = new Object();
202+
try {
203+
FileCleaner.track(path, marker);
204+
fail();
205+
} catch (IllegalStateException ex) {
206+
// expected
207+
}
208+
assertEquals(true, FileCleaner.exitWhenFinished);
209+
assertEquals(null, FileCleaner.reaper);
210+
}
211+
212+
public void testFileCleanerExitWhenFinished1() throws Exception {
213+
String path = testFile.getPath();
214+
215+
assertEquals(false, testFile.exists());
216+
RandomAccessFile r = new RandomAccessFile(testFile, "rw");
217+
assertEquals(true, testFile.exists());
218+
219+
assertEquals(0, FileCleaner.getTrackCount());
220+
FileCleaner.track(path, r);
221+
assertEquals(1, FileCleaner.getTrackCount());
222+
assertEquals(false, FileCleaner.exitWhenFinished);
223+
assertEquals(true, FileCleaner.reaper.isAlive());
224+
225+
assertEquals(false, FileCleaner.exitWhenFinished);
226+
FileCleaner.exitWhenFinished();
227+
assertEquals(true, FileCleaner.exitWhenFinished);
228+
assertEquals(true, FileCleaner.reaper.isAlive());
229+
230+
r.close();
231+
testFile = null;
232+
r = null;
233+
234+
waitUntilTrackCount();
235+
236+
assertEquals(0, FileCleaner.getTrackCount());
237+
assertEquals(false, new File(path).exists());
238+
assertEquals(true, FileCleaner.exitWhenFinished);
239+
assertEquals(false, FileCleaner.reaper.isAlive());
240+
}
241+
242+
public void testFileCleanerExitWhenFinished2() throws Exception {
243+
String path = testFile.getPath();
244+
245+
assertEquals(false, testFile.exists());
246+
RandomAccessFile r = new RandomAccessFile(testFile, "rw");
247+
assertEquals(true, testFile.exists());
248+
249+
assertEquals(0, FileCleaner.getTrackCount());
250+
FileCleaner.track(path, r);
251+
assertEquals(1, FileCleaner.getTrackCount());
252+
assertEquals(false, FileCleaner.exitWhenFinished);
253+
assertEquals(true, FileCleaner.reaper.isAlive());
254+
255+
r.close();
256+
testFile = null;
257+
r = null;
258+
259+
waitUntilTrackCount();
260+
261+
assertEquals(0, FileCleaner.getTrackCount());
262+
assertEquals(false, new File(path).exists());
263+
assertEquals(false, FileCleaner.exitWhenFinished);
264+
assertEquals(true, FileCleaner.reaper.isAlive());
265+
266+
assertEquals(false, FileCleaner.exitWhenFinished);
267+
FileCleaner.exitWhenFinished();
268+
for (int i = 0; i < 20 && FileCleaner.reaper.isAlive(); i++) {
269+
Thread.sleep(500L); // allow reaper thread to die
270+
}
271+
assertEquals(true, FileCleaner.exitWhenFinished);
272+
assertEquals(false, FileCleaner.reaper.isAlive());
273+
}
274+
275+
//-----------------------------------------------------------------------
173276
private void waitUntilTrackCount() {
174277
while (FileCleaner.getTrackCount() != 0) {
175278
int total = 0;

0 commit comments

Comments
 (0)