Addresses ClassNotFoundException problems when the data structures serialized in
the unit cache log files no longer matches due to changes in GWT.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9977 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 dcff5e2..554e186 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
@@ -266,7 +266,7 @@
    */
   static final String UNIT_CACHE_PREFIX = "gwt-unitCache";
 
-  static final String CACHE_PREFIX = UNIT_CACHE_PREFIX + "-";
+  static final String CACHE_FILE_PREFIX = UNIT_CACHE_PREFIX + "-";
 
   /**
    * If there are more than this many files in the cache, clean up the old
@@ -310,7 +310,7 @@
     long timestamp = System.currentTimeMillis();
     do {
       currentCacheFile =
-          new File(cacheDirectory, CACHE_PREFIX + String.format("%016X", timestamp++));
+          new File(cacheDirectory, CACHE_FILE_PREFIX + String.format("%016X", timestamp++));
     } while (currentCacheFile.exists());
 
     // this isn't 100% reliable if multiple processes are in contention
@@ -424,7 +424,7 @@
       File[] files = cacheDirectory.listFiles();
       List<File> cacheFiles = new ArrayList<File>();
       for (File file : files) {
-        if (file.getName().startsWith(CACHE_PREFIX)) {
+        if (file.getName().startsWith(CACHE_FILE_PREFIX)) {
           cacheFiles.add(file);
         }
       }
@@ -460,6 +460,7 @@
           if (cacheFile.equals(currentCacheFile)) {
             continue;
           }
+          boolean deleteCacheFile = false;
           try {
             fis = new FileInputStream(cacheFile);
             bis = new BufferedInputStream(fis);
@@ -487,15 +488,21 @@
           } catch (EOFException ex) {
             // Go on to the next file.
           } catch (IOException ex) {
-            logger.log(TreeLogger.WARN, "Error reading cache file: " + cacheFile.getAbsolutePath(),
-                ex);
+            deleteCacheFile = true;
+            logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
+                + cacheFile.getAbsolutePath() + " due to read error.", ex);
           } catch (ClassNotFoundException ex) {
-            logger.log(TreeLogger.ERROR, "Error deserializing CompilationUnit in "
-                + cacheFile.getAbsolutePath(), ex);
+            deleteCacheFile = true;
+            logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
+                + cacheFile.getAbsolutePath() + " due to deserialization error.", ex);
           } finally {
             Utility.close(inputStream);
             Utility.close(bis);
             Utility.close(fis);
+          }
+          if (deleteCacheFile) {
+            cacheFile.delete();
+          } else {
             logger.log(TreeLogger.TRACE, cacheFile.getName() + ": Load complete");
           }
         }
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 ba4fefe..27908ed 100644
--- a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
@@ -22,13 +22,32 @@
 import junit.framework.TestCase;
 
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
 
 /**
  * Unit test for {@link PersistentUnitCache}.
  */
 public class PersistentUnitCacheTest extends TestCase {
 
+  private static class ThrowsClassNotFoundException implements Serializable {
+    @SuppressWarnings("unused")
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+      throw new ClassNotFoundException();
+    }
+  }
+
+  private static class ThrowsIOException implements Serializable {
+    @SuppressWarnings("unused")
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+      throw new IOException();
+    }
+  }
+
   File lastCacheDir = null;
 
   public void tearDown() {
@@ -39,6 +58,41 @@
   }
 
   /**
+   * When a cache file encounters a serialization error, the logic should assume
+   * the cache log is stale and remove it.
+   */
+  public void testClassNotFoundException() throws IOException, UnableToCompleteException,
+      InterruptedException {
+    checkInvalidObjectInCache(new ThrowsClassNotFoundException());
+  }
+
+  /**
+   * Test if a file already exists with the name we want to put the cache dir
+   * in.
+   */
+  public void testFileInTheWay() throws IOException {
+    TreeLogger logger = TreeLogger.NULL;
+    File fileInTheWay = File.createTempFile("PersistentUnitTest-inTheWay", "");
+    assertNotNull(fileInTheWay);
+    assertTrue(fileInTheWay.exists());
+    fileInTheWay.deleteOnExit();
+    try {
+      new PersistentUnitCache(logger, fileInTheWay);
+      fail("Expected an exception to be thrown");
+    } catch (UnableToCompleteException expected) {
+    }
+  }
+
+  /**
+   * If a cache file has some kind of IO exception, (this can happen with a
+   * stale cache file), then the exception should be ignored and the cache file
+   * removed.
+   */
+  public void testIOException() throws IOException, UnableToCompleteException, InterruptedException {
+    checkInvalidObjectInCache(new ThrowsIOException());
+  }
+
+  /**
    * The cache should recursively create the directories it needs.
    */
   public void testNewDir() throws IOException, UnableToCompleteException {
@@ -52,37 +106,13 @@
     assertTrue(newDir.isDirectory());
   }
 
-  /**
-   * Test if a file already exists with the name we want to put the
-   * cache dir in.
-   */
-  public void testFileInTheWay() throws IOException {
-    TreeLogger logger = TreeLogger.NULL;
-    File fileInTheWay = File.createTempFile("PersistentUnitTest-inTheWay", "");
-    assertNotNull(fileInTheWay);
-    assertTrue(fileInTheWay.exists());
-    fileInTheWay.deleteOnExit();
-    try {
-      new PersistentUnitCache(logger, fileInTheWay);
-      fail("Expected an exception to be thrown");
-    } catch (UnableToCompleteException expected) {
-    }
-  }
-
   public void testPersistentCache() throws IOException, InterruptedException,
       UnableToCompleteException {
     TreeLogger logger = TreeLogger.NULL;
 
-    File cacheDir = null;
-    lastCacheDir = cacheDir = File.createTempFile("persistentCacheTest", "");
-    assertNotNull(cacheDir);
-    // Wait, this needs to be a directory, not a file.
-    assertTrue(cacheDir.delete());
-    // directory will get cleaned up in tearDown()
-    assertTrue(cacheDir.mkdir());
+    File cacheDir = lastCacheDir = File.createTempFile("persistentCacheTest", "");
+    File unitCacheDir = mkCacheDir(cacheDir);
 
-    File unitCacheDir = new File(cacheDir, PersistentUnitCache.UNIT_CACHE_PREFIX);
-    assertNull(unitCacheDir.list());
     PersistentUnitCache cache = new PersistentUnitCache(logger, cacheDir);
 
     MockCompilationUnit foo1 = new MockCompilationUnit("com.example.Foo", "Foo: source1");
@@ -204,4 +234,38 @@
   private void assertNumCacheFiles(File unitCacheDir, int expected) {
     assertEquals(expected, unitCacheDir.list().length);
   }
+
+  private void checkInvalidObjectInCache(Object toSerialize) throws IOException,
+      FileNotFoundException, UnableToCompleteException, InterruptedException {
+    TreeLogger logger = TreeLogger.NULL;
+    File cacheDir = lastCacheDir = File.createTempFile("PersistentUnitTest-CNF", "");
+    File unitCacheDir = mkCacheDir(cacheDir);
+
+    /*
+     * Create a cache file that has the right filename, but the wrong kind of
+     * object in it.
+     */
+    File errorFile = new File(unitCacheDir, PersistentUnitCache.CACHE_FILE_PREFIX + "12345");
+    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(errorFile));
+    os.writeObject(toSerialize);
+    os.close();
+
+    assertNumCacheFiles(unitCacheDir, 1);
+
+    PersistentUnitCache cache = new PersistentUnitCache(logger, cacheDir);
+    cache.cleanup(logger);
+    cache.shutdown();
+
+    // The bogus file should have been removed.
+    assertNumCacheFiles(unitCacheDir, 0);
+  }
+
+  private File mkCacheDir(File cacheDir) {
+    assertNotNull(cacheDir);
+    assertTrue(cacheDir.exists());
+    cacheDir.delete();
+    File unitCacheDir = new File(cacheDir, PersistentUnitCache.UNIT_CACHE_PREFIX);
+    unitCacheDir.mkdirs();
+    return unitCacheDir;
+  }
 }