Author: scolebourne Date: Mon Dec 4 17:13:05 2006 New Revision: 482437 URL: http://svn.apache.org/viewvc?view=rev&rev=482437 Log: IO-99 - FileCleaner.exitWhenFinished, to allow the thread to be terminated includes some code from Jochen Wiedmann
Modified: jakarta/commons/proper/io/trunk/RELEASE-NOTES.txt jakarta/commons/proper/io/trunk/project.xml jakarta/commons/proper/io/trunk/src/java/org/apache/commons/io/FileCleaner.java jakarta/commons/proper/io/trunk/src/test/org/apache/commons/io/FileCleanerTestCase.java Modified: jakarta/commons/proper/io/trunk/RELEASE-NOTES.txt URL: http://svn.apache.org/viewvc/jakarta/commons/proper/io/trunk/RELEASE-NOTES.txt?view=diff&rev=482437&r1=482436&r2=482437 ============================================================================== --- jakarta/commons/proper/io/trunk/RELEASE-NOTES.txt (original) +++ jakarta/commons/proper/io/trunk/RELEASE-NOTES.txt Mon Dec 4 17:13:05 2006 @@ -110,6 +110,9 @@ - This can be used as a calback in FileCleaner - Together these allow FileCleaner to do a forceDelete to kill directories +- FileCleaner.exitWhenFinished [IO-99] + - A new method that allows the internal cleaner thread to be cleanly terminated + - WildcardFileFilter - Replacement for WildcardFilter - Accepts both files and directories Modified: jakarta/commons/proper/io/trunk/project.xml URL: http://svn.apache.org/viewvc/jakarta/commons/proper/io/trunk/project.xml?view=diff&rev=482437&r1=482436&r2=482437 ============================================================================== --- jakarta/commons/proper/io/trunk/project.xml (original) +++ jakarta/commons/proper/io/trunk/project.xml Mon Dec 4 17:13:05 2006 @@ -224,6 +224,9 @@ <name>James Urie</name> </contributor> <contributor> + <name>Jochen Wiedmann</name> + </contributor> + <contributor> <name>Frank W. Zammetti</name> </contributor> </contributors> Modified: jakarta/commons/proper/io/trunk/src/java/org/apache/commons/io/FileCleaner.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/io/trunk/src/java/org/apache/commons/io/FileCleaner.java?view=diff&rev=482437&r1=482436&r2=482437 ============================================================================== --- jakarta/commons/proper/io/trunk/src/java/org/apache/commons/io/FileCleaner.java (original) +++ jakarta/commons/proper/io/trunk/src/java/org/apache/commons/io/FileCleaner.java Mon Dec 4 17:13:05 2006 @@ -29,6 +29,12 @@ * This utility creates a background thread to handle file deletion. * Each file to be deleted is registered with a handler object. * When the handler object is garbage collected, the file is deleted. + * <p> + * In an environment with multiple class loaders (a servlet container, for + * example), you should consider stopping the background thread if it is no + * longer needed. This is done by invoking the method + * [EMAIL PROTECTED] [EMAIL PROTECTED] #exitWhenFinished}, typically in + * [EMAIL PROTECTED] javax.servlet.ServletContextListener#contextDestroyed} or similar. * * @author Noel Bergman * @author Martin Cooper @@ -39,47 +45,19 @@ /** * Queue of <code>Tracker</code> instances being watched. */ - private static ReferenceQueue /* Tracker */ q = new ReferenceQueue(); - + static ReferenceQueue /* Tracker */ q = new ReferenceQueue(); /** * Collection of <code>Tracker</code> instances in existence. */ - private static Collection /* Tracker */ trackers = new Vector(); - + static Collection /* Tracker */ trackers = new Vector(); /** - * The thread that will clean up registered files. + * Whether to terminate the thread when the tracking is complete. */ - private static Thread reaper = new Thread("File Reaper") { - - /** - * Run the reaper thread that will delete files as their associated - * marker objects are reclaimed by the garbage collector. - */ - public void run() { - for (;;) { - Tracker tracker = null; - try { - // Wait for a tracker to remove. - tracker = (Tracker) q.remove(); - } catch (Exception e) { - continue; - } - - tracker.delete(); - tracker.clear(); - trackers.remove(tracker); - } - } - }; - + static volatile boolean exitWhenFinished = false; /** - * The static initializer that starts the reaper thread. + * The thread that will clean up registered files. */ - static { - reaper.setPriority(Thread.MAX_PRIORITY); - reaper.setDaemon(true); - reaper.start(); - } + static Thread reaper; //----------------------------------------------------------------------- /** @@ -109,7 +87,7 @@ if (file == null) { throw new NullPointerException("The file must not be null"); } - trackers.add(new Tracker(file.getPath(), deleteStrategy, marker, q)); + addTracker(file.getPath(), marker, deleteStrategy); } /** @@ -139,9 +117,28 @@ if (path == null) { throw new NullPointerException("The path must not be null"); } + addTracker(path, marker, deleteStrategy); + } + + /** + * Adds a tracker to the list of trackers. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + */ + private static synchronized void addTracker(String path, Object marker, FileDeleteStrategy deleteStrategy) { + if (exitWhenFinished) { + throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called"); + } + if (reaper == null) { + reaper = new Reaper(); + reaper.start(); + } trackers.add(new Tracker(path, deleteStrategy, marker, q)); } + //----------------------------------------------------------------------- /** * Retrieve the number of files currently being tracked, and therefore * awaiting deletion. @@ -152,11 +149,75 @@ return trackers.size(); } + /** + * Call this method to cause the file cleaner thread to terminate when + * there are no more objects being tracked for deletion. + * <p> + * In a simple environment, you don't need this method as the file cleaner + * thread will simply exit when the JVM exits. In a more complex environment, + * with multiple class loaders (such as an application server), you should be + * aware that the file cleaner thread will continue running even if the class + * loader it was started from terminates. This can consitute a memory leak. + * <p> + * For example, suppose that you have developed a web application, which + * contains the commons-io jar file in your WEB-INF/lib directory. In other + * words, the FileCleaner class is loaded through the class loader of your + * web application. If the web application is terminated, but the servlet + * container is still running, then the file cleaner thread will still exist, + * posing a memory leak. + * <p> + * This method allows the thread to be terminated. Simply call this method + * in the resource cleanup code, such as [EMAIL PROTECTED] javax.servlet.ServletContextListener#contextDestroyed}. + * One called, no new objects can be tracked by the file cleaner. + */ + public static synchronized void exitWhenFinished() { + exitWhenFinished = true; + if (reaper != null) { + synchronized (reaper) { + reaper.interrupt(); + } + } + } + + // ----------------------------------------------------------------------- + /** + * The reaper thread. + */ + static final class Reaper extends Thread { + Reaper() { + super("File Reaper"); + setPriority(Thread.MAX_PRIORITY); + setDaemon(true); + } + + /** + * Run the reaper thread that will delete files as their associated + * marker objects are reclaimed by the garbage collector. + */ + public void run() { + // thread exits when exitWhenFinished is true and there are no more tracked objects + while (exitWhenFinished == false || trackers.size() > 0) { + Tracker tracker = null; + try { + // Wait for a tracker to remove. + tracker = (Tracker) q.remove(); + } catch (Exception e) { + continue; + } + if (tracker != null) { + tracker.delete(); + tracker.clear(); + trackers.remove(tracker); + } + } + } + } + //----------------------------------------------------------------------- /** * Inner class which acts as the reference for a file pending deletion. */ - static class Tracker extends PhantomReference { + static final class Tracker extends PhantomReference { /** * The full path to the file being tracked. Modified: jakarta/commons/proper/io/trunk/src/test/org/apache/commons/io/FileCleanerTestCase.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/io/trunk/src/test/org/apache/commons/io/FileCleanerTestCase.java?view=diff&rev=482437&r1=482436&r2=482437 ============================================================================== --- jakarta/commons/proper/io/trunk/src/test/org/apache/commons/io/FileCleanerTestCase.java (original) +++ jakarta/commons/proper/io/trunk/src/test/org/apache/commons/io/FileCleanerTestCase.java Mon Dec 4 17:13:05 2006 @@ -19,6 +19,8 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import java.lang.ref.ReferenceQueue; +import java.util.Vector; import junit.framework.Test; import junit.framework.TestSuite; @@ -62,6 +64,12 @@ /** @see junit.framework.TestCase#tearDown() */ protected void tearDown() throws Exception { FileUtils.deleteDirectory(getTestDirectory()); + + // reset file cleaner class, so as not to break other tests + FileCleaner.q = new ReferenceQueue(); + FileCleaner.trackers = new Vector(); + FileCleaner.exitWhenFinished = false; + FileCleaner.reaper = null; } //----------------------------------------------------------------------- @@ -170,6 +178,101 @@ } } + public void testFileCleanerExitWhenFinishedFirst() throws Exception { + assertEquals(false, FileCleaner.exitWhenFinished); + FileCleaner.exitWhenFinished(); + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(null, FileCleaner.reaper); + + waitUntilTrackCount(); + + assertEquals(0, FileCleaner.getTrackCount()); + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(null, FileCleaner.reaper); + } + + public void testFileCleanerExitWhenFinished_NoTrackAfter() throws Exception { + assertEquals(false, FileCleaner.exitWhenFinished); + FileCleaner.exitWhenFinished(); + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(null, FileCleaner.reaper); + + String path = testFile.getPath(); + Object marker = new Object(); + try { + FileCleaner.track(path, marker); + fail(); + } catch (IllegalStateException ex) { + // expected + } + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(null, FileCleaner.reaper); + } + + public void testFileCleanerExitWhenFinished1() throws Exception { + String path = testFile.getPath(); + + assertEquals(false, testFile.exists()); + RandomAccessFile r = new RandomAccessFile(testFile, "rw"); + assertEquals(true, testFile.exists()); + + assertEquals(0, FileCleaner.getTrackCount()); + FileCleaner.track(path, r); + assertEquals(1, FileCleaner.getTrackCount()); + assertEquals(false, FileCleaner.exitWhenFinished); + assertEquals(true, FileCleaner.reaper.isAlive()); + + assertEquals(false, FileCleaner.exitWhenFinished); + FileCleaner.exitWhenFinished(); + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(true, FileCleaner.reaper.isAlive()); + + r.close(); + testFile = null; + r = null; + + waitUntilTrackCount(); + + assertEquals(0, FileCleaner.getTrackCount()); + assertEquals(false, new File(path).exists()); + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(false, FileCleaner.reaper.isAlive()); + } + + public void testFileCleanerExitWhenFinished2() throws Exception { + String path = testFile.getPath(); + + assertEquals(false, testFile.exists()); + RandomAccessFile r = new RandomAccessFile(testFile, "rw"); + assertEquals(true, testFile.exists()); + + assertEquals(0, FileCleaner.getTrackCount()); + FileCleaner.track(path, r); + assertEquals(1, FileCleaner.getTrackCount()); + assertEquals(false, FileCleaner.exitWhenFinished); + assertEquals(true, FileCleaner.reaper.isAlive()); + + r.close(); + testFile = null; + r = null; + + waitUntilTrackCount(); + + assertEquals(0, FileCleaner.getTrackCount()); + assertEquals(false, new File(path).exists()); + assertEquals(false, FileCleaner.exitWhenFinished); + assertEquals(true, FileCleaner.reaper.isAlive()); + + assertEquals(false, FileCleaner.exitWhenFinished); + FileCleaner.exitWhenFinished(); + for (int i = 0; i < 20 && FileCleaner.reaper.isAlive(); i++) { + Thread.sleep(500L); // allow reaper thread to die + } + assertEquals(true, FileCleaner.exitWhenFinished); + assertEquals(false, FileCleaner.reaper.isAlive()); + } + + //----------------------------------------------------------------------- private void waitUntilTrackCount() { while (FileCleaner.getTrackCount() != 0) { int total = 0; --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]