scheglov pointed out a leak in compilation units in dev mode after a refresh
caused by leaving the output stream open.  This change rotates the logfile
after each compile.
This change also attempts to handle a failure to open the cache file more
gracefully.

Review at http://gwt-code-reviews.appspot.com/1490801

Review by: cromwellian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10915 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/javac/CachedCompilationUnit.java b/dev/core/src/com/google/gwt/dev/javac/CachedCompilationUnit.java
index 48b1f15..e4875f5 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CachedCompilationUnit.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CachedCompilationUnit.java
@@ -87,8 +87,6 @@
    * {@link CompilationUnit}.
    *
    * @param unit A unit to copy
-   * @param sourceToken A valid {@DiskCache} token for this unit's
-   *          source code.
    * @param astToken A valid {@DiskCache} token for this unit's
    *          serialized AST types.
    */
diff --git a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
index 183b500..a3c70f9 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
 import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
@@ -35,11 +36,14 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A class that manages a persistent cache of {@link CompilationUnit} instances.
@@ -90,345 +94,43 @@
  * 
  */
 class PersistentUnitCache extends MemoryUnitCache {
-  /**
-   * A thread used when the cache is instantiated to load up cached units from
-   * the persistent store in the background. The
-   * {@link UnitCacheFactory#addUnit(CompilationUnit)} and
-   * {@link UnitCacheFactory#findUnit(String)} methods block if invoked before
-   * this thread finishes.
-   */
-  private class UnitCacheMapLoader extends Thread {
-    private final CountDownLatch loadCompleteLatch = new CountDownLatch(1);
-    private final TreeLogger logger;
-
-    public UnitCacheMapLoader(TreeLogger logger) {
-      this.logger = logger;
-      setDaemon(true);
-      setName("UnitCacheLoader");
-      setPriority(Thread.NORM_PRIORITY);
-    }
-
-    public void await() {
-      try {
-        loadCompleteLatch.await();
-      } catch (InterruptedException ex) {
-        logger.log(TreeLogger.ERROR, "Interrupted waiting for PersistentUnitCache to load.", ex);
-      }
-    }
-
-    @Override
-    public void run() {
-      try {
-        loadUnitMap(logger);
-      } finally {
-        loadCompleteLatch.countDown();
-        if (logger.isLoggable(TreeLogger.TRACE)) {
-          logger
-              .log(TreeLogger.TRACE, "Loaded " + unitMap.size() + " units from persistent store.");
-        }
-      }
-    }
-  }
-
-  /**
-   * Used to pass messages to the unitWriteThread.
-   */
-  private static class UnitWriteMessage {
-    private static final UnitWriteMessage DELETE_OLD_CACHE_FILES = new UnitWriteMessage();
-    private static final UnitWriteMessage SHUTDOWN_THREAD = new UnitWriteMessage();
-    private final UnitCacheEntry unitCacheEntry;
-
-    public UnitWriteMessage() {
-      unitCacheEntry = null;
-    }
-
-    public UnitWriteMessage(UnitCacheEntry unitCacheEntry) {
-      this.unitCacheEntry = unitCacheEntry;
-    }
-  }
-
-  /**
-   * Thread that reads units from a queue and writes out to a cache file for
-   * this session.
-   */
-  private class UnitWriter extends Thread {
-    private final CountDownLatch shutDownLatch = new CountDownLatch(1);
-    private final TreeLogger logger;
-    private boolean errorLogged = false;
-    private Thread shutdownHook = new Thread() {
-      @Override
-      public void run() {
-        try {
-          doShutdown();
-        } catch (InterruptedException ex) {
-          // ignore
-        }
-      }
-    };
-
-    public UnitWriter(TreeLogger logger) {
-      this.logger = logger;
-      setDaemon(true);
-      setName("UnitWriteThread");
-      setPriority(Thread.MIN_PRIORITY);
-
-      Runtime.getRuntime().addShutdownHook(shutdownHook);
-    }
-
-    @Override
-    public void run() {
-      logger.log(TreeLogger.TRACE, "Starting UnitWriteThread.");
-
-      FileOutputStream fstream = null;
-      BufferedOutputStream bstream = null;
-      ObjectOutputStream stream = null;
-      try {
-        fstream = new FileOutputStream(currentCacheFile);
-        bstream = new BufferedOutputStream(fstream);
-        stream = new ObjectOutputStream(bstream);
-      } catch (IOException ex) {
-        logger.log(TreeLogger.ERROR, "Error creating cache " + currentCacheFile
-            + ". Disabling cache.", ex);
-      }
-      int recentUnitsWritten = 0;
-      int totalUnitsWritten = 0;
-      try {
-        while (true) {
-          UnitWriteMessage msg = null;
-          try {
-            msg = unitWriteQueue.take();
-          } catch (InterruptedException e) {
-            // Allow shutdown to interrupt
-            break;
-          }
-          if (stream == null) {
-            // if there is no output stream, just ignore the unit
-            continue;
-          }
-
-          try {
-            if (msg != null) {
-              if (msg == UnitWriteMessage.DELETE_OLD_CACHE_FILES) {
-                if (logger.isLoggable(TreeLogger.TRACE)) {
-                  logger.log(TreeLogger.TRACE, "Wrote " + recentUnitsWritten
-                      + " units to persistent cache.");
-                }
-                recentUnitsWritten = 0;
-                deleteOldCacheFiles(logger, currentCacheFile);
-              } else if (msg == UnitWriteMessage.SHUTDOWN_THREAD) {
-                stream.flush();
-                assert unitWriteQueue.size() == 0;
-                break;
-              } else {
-                assert msg.unitCacheEntry.getOrigin() != UnitOrigin.ARCHIVE;
-                CompilationUnit unit = msg.unitCacheEntry.getUnit();
-                assert unit != null;
-                stream.writeObject(unit);
-                recentUnitsWritten++;
-                totalUnitsWritten++;
-              }
-            }
-
-            if (unitWriteQueue.isEmpty()) {
-              stream.flush();
-            }
-          } catch (IOException ex) {
-            if (!errorLogged) {
-              errorLogged = true;
-              if (logger.isLoggable(TreeLogger.TRACE)) {
-                logger.log(TreeLogger.TRACE, "Error saving unit to file: "
-                    + currentCacheFile.getAbsolutePath(), ex);
-              }
-            }
-          }
-        }
-      } finally {
-        Utility.close(stream);
-        // Paranoia - close all streams
-        Utility.close(bstream);
-        Utility.close(fstream);
-        if (totalUnitsWritten == 0) {
-          // Remove useless empty output.
-          currentCacheFile.delete();
-        }
-        shutDownLatch.countDown();
-        logger.log(TreeLogger.TRACE, "Shutting down PersistentUnitCache thread");
-      }
-    }
-
-    /**
-     * Shutdown the thread and wait for it.
-     */
-    private void doShutdown() throws InterruptedException {
-      // force the shutdown to finish after 5 seconds
-      unitWriteQueue.add(UnitWriteMessage.SHUTDOWN_THREAD);
-      // wait for shutdown
-      shutDownLatch.await(5000, TimeUnit.MILLISECONDS);
-      try {
-        Runtime.getRuntime().removeShutdownHook(shutdownHook);
-      } catch (IllegalStateException ex) {
-        // ignore.
-      }
-    }
-  }
-
-  /**
-   * Common prefix for creating directories and cache files.
-   */
-  static final String UNIT_CACHE_PREFIX = "gwt-unitCache";
-
-  static final String CACHE_FILE_PREFIX = UNIT_CACHE_PREFIX + "-";
 
   /**
    * If there are more than this many files in the cache, clean up the old
    * files.
    */
-  static final int CACHE_FILE_THRESHOLD = 10;
+  static final int CACHE_FILE_THRESHOLD = 40;
+  
+  /**
+   * Common prefix for creating directories and cache files.
+   */
+  static final String UNIT_CACHE_PREFIX = "gwt-unitCache";
+  static final String CACHE_FILE_PREFIX = UNIT_CACHE_PREFIX + "-";
 
   /**
-   * Used for communication to the unit write thread.
+   * Creates a new file with a name based on the current system time.
    */
-  private final BlockingQueue<UnitWriteMessage> unitWriteQueue =
-      new LinkedBlockingQueue<UnitWriteMessage>();
-  private final AtomicInteger addCount = new AtomicInteger(0);
-  private final UnitCacheMapLoader unitCacheMapLoader;
-  private final UnitWriter unitWriter;
-  private boolean cleanupHasRun = false;
-
-  /**
-   * A directory that ideally persists between invocations.
-   */
-  private final File cacheDirectory;
-
-  /**
-   * Cache log file currently being written to.
-   */
-  private File currentCacheFile;
-
-  PersistentUnitCache(TreeLogger logger, File cacheDir) throws UnableToCompleteException {
-    assert cacheDir != null;
-
-    this.cacheDirectory = new File(cacheDir, UNIT_CACHE_PREFIX);
-    if (logger.isLoggable(TreeLogger.TRACE)) {
-      logger.log(TreeLogger.TRACE, "Persistent unit cache dir set to: "
-          + this.cacheDirectory.getAbsolutePath());
-    }
-
-    if (!cacheDirectory.isDirectory() && !cacheDirectory.mkdirs()) {
-      logger.log(TreeLogger.WARN, "Unable to initialize cache. Couldn't create directory "
-          + cacheDirectory.getAbsolutePath() + ".");
-      throw new UnableToCompleteException();
-    }
-
+  private static File createCacheFile(TreeLogger logger, File cacheDirectory)
+      throws UnableToCompleteException {
+    File newFile = null;
     long timestamp = System.currentTimeMillis();
-    do {
-      currentCacheFile =
-          new File(cacheDirectory, CACHE_FILE_PREFIX + String.format("%016X", timestamp++));
-    } while (currentCacheFile.exists());
-
-    // This isn't 100% reliable if multiple processes are in contention
     try {
-      currentCacheFile.createNewFile();
+      do {
+        newFile = new File(cacheDirectory, CACHE_FILE_PREFIX + String.format("%016X", timestamp++));
+      } while (!newFile.createNewFile());
     } catch (IOException ex) {
       logger.log(TreeLogger.WARN, "Unable to create new cache log file "
-          + currentCacheFile.getAbsolutePath() + ".", ex);
+          + (newFile == null ? "<not created>" : newFile.getAbsolutePath()) + ".", ex);
       throw new UnableToCompleteException();
     }
 
-    unitCacheMapLoader = new UnitCacheMapLoader(logger);
-    unitCacheMapLoader.start();
-    unitWriter = new UnitWriter(logger);
-    unitWriter.start();
-  }
-
-  /**
-   * Enqueue a unit to be written by the background thread.
-   */
-  @Override
-  public void add(CompilationUnit newUnit) {
-    unitCacheMapLoader.await();
-    super.add(newUnit);
-    UnitCacheEntry entry = unitMap.get(newUnit.getResourcePath());
-    addCount.getAndIncrement();
-    unitWriteQueue.add(new UnitWriteMessage(entry));
-  }
-
-  /**
-   * Cleans up old cache files in the directory, migrating everything previously
-   * loaded in them to the current cache file.
-   * 
-   * Normally, only newly compiled units are written to the current log, but
-   * when it is time to cleanup, valid units from older log files need to be
-   * re-written.
-   */
-  @Override
-  public void cleanup(TreeLogger logger) {
-    if (logger.isLoggable(TreeLogger.TRACE)) {
-      logger.log(TreeLogger.TRACE, "Added " + addCount.intValue() + " units to persistent cache.");
-    }
-    addCount.set(0);
-
-    if (cleanupHasRun) {
-      return;
+    if (!newFile.canWrite()) {
+      logger.log(TreeLogger.WARN, "Unable to write to new cache log file "
+          + newFile.getAbsolutePath() + ".");
+      throw new UnableToCompleteException();
     }
 
-    cleanupHasRun = true;
-    unitCacheMapLoader.await();
-    File[] cacheFiles = getCacheFiles();
-
-    if (cacheFiles.length < CACHE_FILE_THRESHOLD) {
-      return;
-    }
-
-    /*
-     * Resend all units read in from the in-memory cache to the writer thread.
-     * They will be re-written out and the old cache files removed.
-     */
-    synchronized (unitMap) {
-      for (UnitCacheEntry unitCacheEntry : unitMap.values()) {
-        if (unitCacheEntry.getOrigin() == UnitOrigin.PERSISTENT) {
-          unitWriteQueue.add(new UnitWriteMessage(unitCacheEntry));
-        }
-      }
-    }
-    unitWriteQueue.add(UnitWriteMessage.DELETE_OLD_CACHE_FILES);
-  }
-
-  @Override
-  public CompilationUnit find(ContentId contentId) {
-    unitCacheMapLoader.await();
-    return super.find(contentId);
-  }
-
-  @Override
-  public CompilationUnit find(String resourcePath) {
-    unitCacheMapLoader.await();
-    return super.find(resourcePath);
-  }
-
-  /**
-   * Delete all cache files in the directory except for the currently open file.
-   * 
-   * @param current Specifies the currently open cache file which will not be
-   *          deleted.
-   */
-  void deleteOldCacheFiles(TreeLogger logger, File current) {
-    assert current != null;
-
-    SpeedTracerLogger.Event deleteEvent = SpeedTracerLogger.start(DevModeEventType.DELETE_CACHE);
-    File[] filesToDelete = getCacheFiles();
-    if (filesToDelete == null) {
-      return;
-    }
-    if (logger.isLoggable(TreeLogger.INFO)) {
-      logger.log(TreeLogger.TRACE, "Purging cache files from " + cacheDirectory);
-    }
-    for (File toDelete : filesToDelete) {
-      if (!current.equals(toDelete)) {
-        toDelete.delete();
-      }
-    }
-    deleteEvent.end();
+    return newFile;
   }
 
   /**
@@ -437,7 +139,7 @@
    * @return an array of sorted filenames. The file name pattern is such that
    *         sorting them alphabetically also sorts the files by age.
    */
-  File[] getCacheFiles() {
+  private static File[] getCacheFiles(File cacheDirectory) {
     if (cacheDirectory.isDirectory()) {
       File[] files = cacheDirectory.listFiles();
       List<File> cacheFiles = new ArrayList<File>();
@@ -454,16 +156,338 @@
   }
 
   /**
-   * For Unit testing - shutdown the persistent cache.
+   * There is no significance in the return value, we just want to be able
+   * to tell if the purgeOldCacheFilesTask has completed.
    */
-  void shutdown() throws InterruptedException {
-    unitWriter.doShutdown();
+  private Future<Boolean> purgeTaskStatus;
+  private AtomicBoolean purgeInProgress = new AtomicBoolean(false);
+  
+  private final Runnable purgeOldCacheFilesTask = new Runnable() {
+    @Override
+    public void run() {
+      try {
+        // Delete all cache files in the directory except for the currently open
+        // file.
+        SpeedTracerLogger.Event deleteEvent = SpeedTracerLogger.start(DevModeEventType.DELETE_CACHE);
+        File[] filesToDelete = getCacheFiles(cacheDirectory);
+        logger.log(TreeLogger.TRACE, "Purging cache files from " + cacheDirectory);
+        for (File toDelete : filesToDelete) {
+          if (!currentCacheFile.equals(toDelete)) {
+            if (!toDelete.delete()) {
+              logger.log(TreeLogger.WARN, "Couldn't delete file: " + toDelete);
+            }
+          }
+        }
+        deleteEvent.end();
+        
+        rotateCurrentCacheFile();
+      } catch (UnableToCompleteException e) {
+        backgroundService.shutdownNow();
+      } finally {
+        purgeInProgress.set(false);
+      }
+    }      
+  };
+
+  private final Runnable rotateCacheFilesTask = new Runnable() {
+    @Override
+    public void run() {
+      try {
+        rotateCurrentCacheFile();
+      } catch (UnableToCompleteException e) {
+        backgroundService.shutdownNow();
+      }
+      assert (currentCacheFile != null);
+    }
+  };
+
+  private final Runnable shutdownThreadTask = new Runnable() {
+    @Override
+    public void run() {
+      assert (currentCacheFile != null);
+      closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
+      logger.log(TreeLogger.TRACE, "Shutting down PersistentUnitCache thread");
+      backgroundService.shutdownNow();
+    }
+  };
+
+  /**
+   * Saved to be able to wait for UNIT_MAP_LOAD_TASK to complete.
+   */
+  private Future<Boolean> unitMapLoadStatus;
+  
+  private final Runnable unitMapLoadTask = new Runnable() {
+    @Override
+    public void run() {
+      loadUnitMap(logger, currentCacheFile);
+    }
+  };
+
+  /**
+   * Used to execute the above Runnables in a background thread.
+   */
+  private final ExecutorService backgroundService;
+  
+  private int unitsWritten = 0;
+
+  private int addedSinceLastCleanup = 0;
+
+  /**
+   * A directory to store the cache files that should persist between
+   * invocations.
+   */
+  private final File cacheDirectory;
+
+  /**
+   * Current file and stream being written to.
+   */
+  private File currentCacheFile;
+  private ObjectOutputStream currentCacheFileStream;
+
+  private final TreeLogger logger;
+
+  PersistentUnitCache(final TreeLogger logger, File cacheDir) throws UnableToCompleteException {
+    assert cacheDir != null;
+    this.logger = logger;
+    this.cacheDirectory = new File(cacheDir, UNIT_CACHE_PREFIX);
+    if (logger.isLoggable(TreeLogger.TRACE)) {
+      logger.log(TreeLogger.TRACE, "Persistent unit cache dir set to: "
+          + this.cacheDirectory.getAbsolutePath());
+    }
+
+    if (!cacheDirectory.isDirectory() && !cacheDirectory.mkdirs()) {
+      logger.log(TreeLogger.WARN, "Unable to initialize cache. Couldn't create directory "
+          + cacheDirectory.getAbsolutePath() + ".");
+      throw new UnableToCompleteException();
+    }
+
+    currentCacheFile = createCacheFile(logger, cacheDirectory);
+
+    backgroundService = Executors.newSingleThreadExecutor();
+    Runtime.getRuntime().addShutdownHook(new Thread() {
+      @Override
+      public void run() {
+        try {
+          Future<Boolean> status = backgroundService.submit(shutdownThreadTask, Boolean.TRUE);
+          // Don't let the shutdown hang more than 5 seconds
+          status.get(5, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+          // ignore
+        } catch (RejectedExecutionException e) {
+          // already shutdown, ignore
+        } catch (ExecutionException e) {
+          logger.log(TreeLogger.ERROR, "Error during shutdown", e);
+        } catch (TimeoutException e) {
+          // ignore
+        } finally {
+          backgroundService.shutdownNow();
+        }
+      }
+    });
+    
+    /**
+     * Load up cached units from the persistent store in the background. The
+     * {@link #add(CompilationUnit)} and {@link #find(String)} methods block if
+     * invoked before this thread finishes.
+     */
+    unitMapLoadStatus = backgroundService.submit(unitMapLoadTask, Boolean.TRUE);
+
+    FileOutputStream fstream = null;
+    BufferedOutputStream bstream = null;
+
+    try {
+      fstream = new FileOutputStream(currentCacheFile);
+      bstream = new BufferedOutputStream(fstream);
+      currentCacheFileStream = new ObjectOutputStream(bstream);
+    } catch (IOException ex) {
+      closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
+      logger.log(TreeLogger.ERROR, "Error creating cache " + currentCacheFile
+          + ". Disabling cache.", ex);
+      backgroundService.shutdownNow();
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Enqueue a unit to be written by the background thread.
+   */
+  @Override
+  public void add(CompilationUnit newUnit) {
+    awaitUnitCacheMapLoad();
+    addedSinceLastCleanup++;
+    super.add(newUnit);
+    addImpl(unitMap.get(newUnit.getResourcePath()));
+  }
+
+  /**
+   * Cleans up old cache files in the directory, migrating everything previously
+   * loaded in them to the current cache file.
+   * 
+   * Normally, only newly compiled units are written to the current log, but
+   * when it is time to cleanup, valid units from older log files need to be
+   * re-written.
+   */
+  @Override
+  public void cleanup(TreeLogger logger) {
+    awaitUnitCacheMapLoad();
+
+    if (backgroundService.isShutdown()) {
+      return;
+    }
+    boolean shouldRotate = addedSinceLastCleanup > 0;
+    logger.log(TreeLogger.TRACE, "Added " + addedSinceLastCleanup + " units to cache since last cleanup.");
+    addedSinceLastCleanup = 0;
+    try {
+      File[] cacheFiles = getCacheFiles(cacheDirectory);
+      if (cacheFiles.length < CACHE_FILE_THRESHOLD) {
+        if (shouldRotate) {
+          backgroundService.execute(rotateCacheFilesTask);
+        }
+        return;
+      }
+
+      // Check to see if the previous purge task finished.
+      boolean inProgress = purgeInProgress.getAndSet(true);
+      if (inProgress) {
+        try {
+          purgeTaskStatus.get(0, TimeUnit.NANOSECONDS);
+        } catch (InterruptedException ex) {
+          Thread.currentThread().interrupt();
+        } catch (TimeoutException ex) {
+          // purge is currently in progress.
+          return;
+        }
+      }
+      
+      /*
+       * Resend all units read in from the in-memory cache to the background
+       * thread. They will be re-written out and the old cache files removed.
+       */
+      synchronized (unitMap) {
+        for (UnitCacheEntry unitCacheEntry : unitMap.values()) {
+          if (unitCacheEntry.getOrigin() == UnitOrigin.PERSISTENT) {
+            addImpl(unitCacheEntry);
+          }
+        }
+      }
+
+      purgeTaskStatus = backgroundService.submit(purgeOldCacheFilesTask, Boolean.TRUE);
+
+    } catch (ExecutionException ex) {
+      throw new InternalCompilerException("Error purging cache", ex);
+    } catch (RejectedExecutionException ex) {
+      // Cache background thread is not running - ignore
+    }
+  }
+
+  @Override
+  public CompilationUnit find(ContentId contentId) {
+    awaitUnitCacheMapLoad();
+    return super.find(contentId);
+  }
+
+  @Override
+  public CompilationUnit find(String resourcePath) {
+    awaitUnitCacheMapLoad();
+    return super.find(resourcePath);
+  }
+
+  public void rotateCurrentCacheFile() throws UnableToCompleteException {
+    if (logger.isLoggable(TreeLogger.TRACE)) {
+      logger.log(TreeLogger.TRACE, "Wrote " + unitsWritten + " units to persistent cache.");
+    }
+
+    // Close and re-open a new log file to drop object references kept
+    // alive in the existing file by Java serialization.
+    closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
+    unitsWritten = 0;
+    currentCacheFile = createCacheFile(logger, cacheDirectory);
+    FileOutputStream fstream = null;
+    BufferedOutputStream bstream = null;
+    try {
+      fstream = new FileOutputStream(currentCacheFile);
+      bstream = new BufferedOutputStream(fstream);
+      currentCacheFileStream = new ObjectOutputStream(bstream);
+    } catch (IOException ex) {
+      // Close all 3 streams, not sure where the exception occurred.
+      Utility.close(bstream);
+      Utility.close(fstream);
+      closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
+      logger.log(TreeLogger.ERROR, "Error rotating file.  Shutting down cache thread.", ex);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * For Unit testing - shutdown the persistent cache.
+   * 
+   * @throws ExecutionException
+   * @throws InterruptedException
+   */
+  void shutdown() throws InterruptedException, ExecutionException {
+    try {
+      Future<Runnable> future = backgroundService.submit(shutdownThreadTask, shutdownThreadTask);
+      backgroundService.shutdown();
+      future.get();
+    } catch (RejectedExecutionException ex) {
+      // background thread is not running - ignore
+    }
+  }
+
+  private void addImpl(final UnitCacheEntry entry) {
+    try {
+      backgroundService.execute(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            assert entry.getOrigin() != UnitOrigin.ARCHIVE;
+            CompilationUnit unit = entry.getUnit();
+            assert unit != null;
+            currentCacheFileStream.writeObject(unit);
+            unitsWritten++;
+          } catch (IOException ex) {
+            backgroundService.shutdownNow();
+            if (logger.isLoggable(TreeLogger.TRACE)) {
+              logger.log(TreeLogger.TRACE, "Error saving unit to cache in: "
+                  + cacheDirectory.getAbsolutePath(), ex);
+            }
+          }
+        }
+      });
+    } catch (RejectedExecutionException ex) {
+      // background thread is not running, ignore
+    }
+  }
+
+  private synchronized void awaitUnitCacheMapLoad() {
+    // wait on initial load of unit map to complete.
+    try {
+      if (unitMapLoadStatus != null) {
+        unitMapLoadStatus.get();
+        // no need to check any more.
+        unitMapLoadStatus = null;
+      }
+    } catch (InterruptedException e) {
+      throw new InternalCompilerException("Interrupted waiting for unit cache map to load.", e);
+    } catch (ExecutionException e) {
+      logger.log(TreeLogger.ERROR, "Failure in unit cache map load.", e);
+      // keep going
+      unitMapLoadStatus = null;
+    }
+  }
+
+  private void closeCurrentCacheFile(File openFile, ObjectOutputStream stream) {
+    Utility.close(stream);
+    if (unitsWritten == 0) {
+      // Remove useless empty file.
+      openFile.delete();
+    }
   }
 
   /**
    * Load everything cached on disk into memory.
    */
-  private void loadUnitMap(TreeLogger logger) {
+  private void loadUnitMap(TreeLogger logger, File currentCacheFile) {
     Event loadPersistentUnitEvent =
         SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE);
     if (logger.isLoggable(TreeLogger.TRACE)) {
@@ -472,7 +496,7 @@
     }
     try {
       if (cacheDirectory.isDirectory() && cacheDirectory.canRead()) {
-        File[] files = getCacheFiles();
+        File[] files = getCacheFiles(cacheDirectory);
         for (File cacheFile : files) {
           FileInputStream fis = null;
           BufferedInputStream bis = null;
@@ -507,7 +531,8 @@
                * out to be stale, it will be recompiled and the updated unit
                * will win this test the next time the session starts.
                */
-              if (existingEntry != null && unit.getLastModified() >= existingEntry.getUnit().getLastModified()) {
+              if (existingEntry != null
+                  && unit.getLastModified() >= existingEntry.getUnit().getLastModified()) {
                 super.remove(existingEntry.getUnit());
                 unitMap.put(unit.getResourcePath(), entry);
                 unitMapByContentId.put(unit.getContentId(), entry);
diff --git a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
index 0f032d8..d8ce8ef 100644
--- a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
@@ -28,6 +28,7 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.concurrent.ExecutionException;
 
 /**
  * Unit test for {@link PersistentUnitCache}.
@@ -50,6 +51,7 @@
 
   File lastCacheDir = null;
 
+  @Override
   public void tearDown() {
     if (lastCacheDir != null) {
       Util.recursiveDelete(lastCacheDir, false);
@@ -62,7 +64,7 @@
    * the cache log is stale and remove it.
    */
   public void testClassNotFoundException() throws IOException, UnableToCompleteException,
-      InterruptedException {
+      InterruptedException, ExecutionException {
     checkInvalidObjectInCache(new ThrowsClassNotFoundException());
   }
 
@@ -88,7 +90,8 @@
    * stale cache file), then the exception should be ignored and the cache file
    * removed.
    */
-  public void testIOException() throws IOException, UnableToCompleteException, InterruptedException {
+  public void testIOException() throws IOException, UnableToCompleteException,
+      InterruptedException, ExecutionException {
     checkInvalidObjectInCache(new ThrowsIOException());
   }
 
@@ -107,7 +110,7 @@
   }
 
   public void testPersistentCache() throws IOException, InterruptedException,
-      UnableToCompleteException {
+      UnableToCompleteException, ExecutionException {
     TreeLogger logger = TreeLogger.NULL;
 
     File cacheDir = lastCacheDir = File.createTempFile("persistentCacheTest", "");
@@ -190,7 +193,7 @@
     // keep making more files
     MockCompilationUnit lastUnit = null;
     assertTrue(PersistentUnitCache.CACHE_FILE_THRESHOLD > 3);
-    for (int i = 2; i < PersistentUnitCache.CACHE_FILE_THRESHOLD; ++i) {
+    for (int i = 2; i <= PersistentUnitCache.CACHE_FILE_THRESHOLD - 1; i++) {
       cache = new PersistentUnitCache(logger, cacheDir);
       lastUnit = new MockCompilationUnit("com.example.Foo", "Foo Source" + i);
       cache.add(lastUnit);
@@ -236,7 +239,7 @@
   }
 
   private void checkInvalidObjectInCache(Object toSerialize) throws IOException,
-      FileNotFoundException, UnableToCompleteException, InterruptedException {
+      FileNotFoundException, UnableToCompleteException, InterruptedException, ExecutionException {
     TreeLogger logger = TreeLogger.NULL;
     File cacheDir = lastCacheDir = File.createTempFile("PersistentUnitTest-CNF", "");
     File unitCacheDir = mkCacheDir(cacheDir);