Revert "Introduce PersistentUnitCache.BackgroundService" This reverts commit bf556abdfb0cef9d2e49e20d27bab0bd221ebfb7. Adding synchronization introduced deadlock Change-Id: Ie0d030dcb2ed683962d2ca44bc308d2b1bd8da04
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 661ec1a..8e09d02 100644 --- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java +++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
@@ -19,21 +19,29 @@ import com.google.gwt.core.ext.TreeLogger.Type; 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.StringInterningObjectInputStream; +import com.google.gwt.dev.util.log.speedtracer.DevModeEventType; +import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; +import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting; -import com.google.gwt.thirdparty.guava.common.base.Preconditions; -import com.google.gwt.thirdparty.guava.common.collect.Lists; +import com.google.gwt.util.tools.Utility; +import java.io.BufferedInputStream; +import java.io.EOFException; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; import java.util.List; 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.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicBoolean; /** * A class that manages a persistent cache of {@link CompilationUnit} instances. @@ -91,33 +99,100 @@ */ static final int CACHE_FILE_THRESHOLD = 40; - private final BackgroundService backgroundService; + /** + * There is no significance in the return value, we just want to be able + * to tell if the purgeOldCacheFilesTask has completed. + */ + private Future<?> purgeTaskStatus; + private AtomicBoolean purgeInProgress = new AtomicBoolean(false); - private Semaphore cleanupInProgress = new Semaphore(1); - private AtomicInteger newUnitsSinceLastCleanup = new AtomicInteger(); + private final Runnable shutdownThreadTask = new Runnable() { + @Override + public void run() { + logger.log(Type.TRACE, "Shutdown hook called for persistent unit cache"); + cacheDir.closeCurrentFile(); + 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<?> unitMapLoadStatus; + + /** + * Used to execute the above Runnables in a background thread. + */ + private final ExecutorService backgroundService; + + private int addedSinceLastCleanup = 0; + + /** + * A directory to store the cache files that should persist between + * invocations. + */ + private final PersistentUnitCacheDir cacheDir; + private final TreeLogger logger; PersistentUnitCache(final TreeLogger logger, File parentDir) throws UnableToCompleteException { - this.backgroundService = new BackgroundService(logger, parentDir, this); + this.logger = logger; + cacheDir = new PersistentUnitCacheDir(logger, parentDir); + + 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(new Runnable() { + @Override + public void run() { + loadUnitMap(); + } + }); } /** * Enqueue a unit to be written by the background thread. */ @Override - public synchronized void add(CompilationUnit newUnit) { + public void add(CompilationUnit newUnit) { internalAdd(newUnit); } @VisibleForTesting Future<?> internalAdd(CompilationUnit newUnit) { - Preconditions.checkNotNull(newUnit); - backgroundService.waitForCacheToLoad(); - addNewUnit(newUnit); - return backgroundService.asyncWriteUnit(newUnit); + awaitUnitCacheMapLoad(); + addedSinceLastCleanup++; + super.add(newUnit); + return addImpl(unitMap.get(newUnit.getResourcePath())); } /** - * Rotates to a new file and/or starts garbage collection if needed after a compile is finished. + * 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 @@ -125,297 +200,238 @@ */ @Override public void cleanup(TreeLogger logger) { - logger.log(Type.TRACE, "PersistentUnitCache cleanup requested"); - backgroundService.waitForCacheToLoad(); + logger.log(Type.TRACE, "Cleanup called"); + awaitUnitCacheMapLoad(); if (backgroundService.isShutdown()) { - logger.log(TreeLogger.TRACE, "Skipped PersistentUnitCache cleanup because it's shut down"); + logger.log(TreeLogger.TRACE, "Skipped cleanup"); return; } + boolean shouldRotate = addedSinceLastCleanup > 0; + logger.log(TreeLogger.TRACE, "Added " + addedSinceLastCleanup + + " units to cache since last cleanup."); + addedSinceLastCleanup = 0; + try { + List<File> cacheFiles = cacheDir.listCacheFilesToLoad(); + logger.log(TreeLogger.TRACE, cacheFiles.size() + " persistent unit files in directory"); + if (cacheFiles.size() < CACHE_FILE_THRESHOLD) { + if (shouldRotate) { + startRotating(); + } + return; + } - if (!cleanupInProgress.tryAcquire()) { - return; // some other thread is already doing this. + // 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; + } + } + + logger.log(Type.TRACE, "Cleaning up persistent unit cache files"); + /* + * 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.ARCHIVE) { + // Units from GWTAR archives should not be kept in the persistent unit cache on disk + // because they are already being kept in their original GWTAR file location. + continue; + } + addImpl(unitCacheEntry); + } + } + + purgeTaskStatus = backgroundService.submit(new Runnable() { + @Override + public void run() { + try { + cacheDir.deleteClosedCacheFiles(); + cacheDir.rotate(); + } catch (UnableToCompleteException e) { + backgroundService.shutdownNow(); + } finally { + purgeInProgress.set(false); + } + } + }, Boolean.TRUE); + + } catch (ExecutionException ex) { + throw new InternalCompilerException("Error purging cache", ex); + } catch (RejectedExecutionException ex) { + // Cache background thread is not running - ignore } - - int addCallCount = newUnitsSinceLastCleanup.getAndSet(0); - logger.log(TreeLogger.TRACE, "Added " + addCallCount + - " units to PersistentUnitCache since last cleanup"); - if (addCallCount == 0) { - // Don't clean up until we compiled something. - logger.log(TreeLogger.TRACE, "Skipped PersistentUnitCache because no units were added"); - cleanupInProgress.release(); - return; - } - - int closedCount = backgroundService.getClosedCacheFileCount(); - if (closedCount < CACHE_FILE_THRESHOLD) { - // Not enough files yet, so just rotate to a new file. - logger.log(TreeLogger.TRACE, "Rotating PersistentUnitCache file because only " + - closedCount + " files were added."); - backgroundService.asyncRotate(cleanupInProgress); - return; - } - - logger.log(Type.TRACE, "Compacting persistent unit cache files"); - backgroundService.asyncCompact(getUnitsToSaveToDisk(), cleanupInProgress); - } - - /** - * Waits for any cleanup in progress to finish. - */ - @VisibleForTesting - void waitForCleanup() throws InterruptedException { - cleanupInProgress.acquire(); - cleanupInProgress.release(); } @VisibleForTesting - void shutdown() throws InterruptedException, ExecutionException { - backgroundService.shutdown(); + Future<?> startRotating() { + return backgroundService.submit(new Runnable() { + @Override + public void run() { + try { + cacheDir.rotate(); + } catch (UnableToCompleteException e) { + backgroundService.shutdownNow(); + } + } + }); } - // Methods that read or write the in-memory cache - @Override - public synchronized CompilationUnit find(ContentId contentId) { - backgroundService.waitForCacheToLoad(); + public CompilationUnit find(ContentId contentId) { + awaitUnitCacheMapLoad(); return super.find(contentId); } @Override - public synchronized CompilationUnit find(String resourcePath) { - backgroundService.waitForCacheToLoad(); + public CompilationUnit find(String resourcePath) { + awaitUnitCacheMapLoad(); return super.find(resourcePath); } - @Override - public synchronized void remove(CompilationUnit unit) { - super.remove(unit); - } - /** - * Saves a newly compiled unit to the in-memory cache. + * For Unit testing - shutdown the persistent cache. + * + * @throws ExecutionException + * @throws InterruptedException */ - private synchronized void addNewUnit(CompilationUnit unit) { - newUnitsSinceLastCleanup.incrementAndGet(); - super.add(unit); - } - - /** - * Adds a compilation unit from disk into the in-memory cache. - * (Callback from {@link PersistentUnitCacheDir}.) - */ - synchronized void maybeAddLoadedUnit(CachedCompilationUnit unit) { - UnitCacheEntry entry = new UnitCacheEntry(unit, UnitOrigin.PERSISTENT); - UnitCacheEntry existingEntry = unitMap.get(unit.getResourcePath()); - /* - * Don't assume that an existing entry is stale - an entry might - * have been loaded already from another source like a - * CompilationUnitArchive that is more up to date. If the - * timestamps are the same, accept the latest version. If it turns - * 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()) { - super.remove(existingEntry.getUnit()); - unitMap.put(unit.getResourcePath(), entry); - unitMapByContentId.put(unit.getContentId(), entry); - } else if (existingEntry == null) { - unitMap.put(unit.getResourcePath(), entry); - unitMapByContentId.put(unit.getContentId(), entry); + void shutdown() throws InterruptedException, ExecutionException { + logger.log(Type.INFO, "shutdown called"); + try { + Future<?> future = backgroundService.submit(shutdownThreadTask); + backgroundService.shutdown(); + future.get(); + } catch (RejectedExecutionException ex) { + // background thread is not running - ignore } } - private synchronized List<CompilationUnit> getUnitsToSaveToDisk() { - List<CompilationUnit> result = Lists.newArrayList(); - for (UnitCacheEntry entry : unitMap.values()) { - // Units from GWTAR archives should not be kept in the persistent unit cache on disk - // because they are already being kept in their original GWTAR file location. - if (entry.getOrigin() != UnitOrigin.ARCHIVE) { - result.add(Preconditions.checkNotNull(entry.getUnit())); - } - } - return result; - } - - /** - * Implements async methods that run in the background. - */ - private static class BackgroundService { - - private final TreeLogger logger; - private final PersistentUnitCacheDir cacheDir; - private final ExecutorService service; - - /** - * Non-null while the unit cache is loading. - */ - private Future<?> loadingDone; - - /** - * Starts the background thread and starts loading the given unit cache in the background. - */ - BackgroundService(TreeLogger logger, File parentDir, final PersistentUnitCache cacheToLoad) - throws UnableToCompleteException { - this.logger = logger; - this.cacheDir = new PersistentUnitCacheDir(logger, parentDir); - - service = Executors.newSingleThreadExecutor(); - Runtime.getRuntime().addShutdownHook(new Thread() { + private Future<?> addImpl(final UnitCacheEntry entry) { + try { + return backgroundService.submit(new Runnable() { @Override public void run() { try { - Future<?> status = asyncShutdown(); - // 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) { - BackgroundService.this.logger.log(TreeLogger.ERROR, "Error during shutdown", e); - } catch (TimeoutException e) { - // ignore - } finally { - 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. - */ - loadingDone = service.submit(new Runnable() { - @Override - public void run() { - cacheDir.loadUnitMap(cacheToLoad); - } - }); - } - - /** - * Blocks until the background service is done loading units into the in-memory cache. - */ - synchronized void waitForCacheToLoad() { - if (loadingDone == null) { - return; // fast path - } - - try { - loadingDone.get(); - loadingDone = null; - } catch (InterruptedException e) { - throw new InternalCompilerException( - "Interrupted waiting for PersistentUnitCache to load.", e); - } catch (ExecutionException e) { - logger.log(TreeLogger.ERROR, "Failed to load PersistentUnitCache.", e); - // Keep going. We didn't load anything but will still save units to the cache. - loadingDone = null; - } - } - - boolean isShutdown() { - return service.isShutdown(); - } - - @VisibleForTesting - void shutdown() throws InterruptedException, ExecutionException { - logger.log(Type.INFO, "PersistentUnitCache shutdown requested"); - try { - asyncShutdown().get(); - } catch (RejectedExecutionException ex) { - // background thread is not running - ignore - } - } - - int getClosedCacheFileCount() { - return cacheDir.getClosedCacheFileCount(); - } - - /** - * Rotates to a new file. - * @param cleanupInProgress a semaphore to release when done. - * (The permit must already be acquired.) - */ - Future<?> asyncRotate(final Semaphore cleanupInProgress) { - return service.submit(new Runnable() { - @Override - public void run() { - try { - cacheDir.rotate(); + cacheDir.writeObject(entry); } catch (UnableToCompleteException e) { - shutdownNow(); - } finally { - cleanupInProgress.release(); + backgroundService.shutdownNow(); } } }); + } catch (RejectedExecutionException ex) { + // background thread is not running, ignore + return null; } + } - /** - * Compacts the persistent unit cache and then rotates to a new file. - * There will be one closed file and one empty, open file when done. - * @param unitsToSave all compilation units to keep - * @param cleanupInProgress a semaphore to release when done. - * (The permit must already be acquired.) - */ - Future<?> asyncCompact(final List<CompilationUnit> unitsToSave, - final Semaphore cleanupInProgress) { + 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; + } + } - return service.submit(new Runnable() { - @Override - public void run() { - try { - for (CompilationUnit unit : unitsToSave) { - cacheDir.writeUnit(unit); + /** + * Load everything cached on disk into memory. + */ + private void loadUnitMap() { + Event loadPersistentUnitEvent = + SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE); + if (logger.isLoggable(TreeLogger.TRACE)) { + logger.log(TreeLogger.TRACE, "Looking for previously cached Compilation Units in " + + cacheDir.getPath()); + } + try { + List<File> files = cacheDir.listCacheFilesToLoad(); + for (File cacheFile : files) { + FileInputStream fis = null; + BufferedInputStream bis = null; + ObjectInputStream inputStream = null; + boolean deleteCacheFile = false; + try { + fis = new FileInputStream(cacheFile); + bis = new BufferedInputStream(fis); + /* + * It is possible for the next call to throw an exception, leaving + * inputStream null and fis still live. + */ + inputStream = new StringInterningObjectInputStream(bis); + while (true) { + CachedCompilationUnit unit = (CachedCompilationUnit) inputStream.readObject(); + if (unit == null) { + break; } - cacheDir.deleteClosedCacheFiles(); - cacheDir.rotate(); // Move to a new, empty file. - } catch (UnableToCompleteException e) { - shutdownNow(); - } finally { - cleanupInProgress.release(); - } - } - }); - } - - Future<?> asyncWriteUnit(final CompilationUnit unit) { - try { - return service.submit(new Runnable() { - @Override - public void run() { - try { - cacheDir.writeUnit(unit); - } catch (UnableToCompleteException e) { - shutdownNow(); + if (unit.getTypesSerializedVersion() != GwtAstBuilder.getSerializationVersion()) { + continue; + } + UnitCacheEntry entry = new UnitCacheEntry(unit, UnitOrigin.PERSISTENT); + UnitCacheEntry existingEntry = unitMap.get(unit.getResourcePath()); + /* + * Don't assume that an existing entry is stale - an entry might + * have been loaded already from another source like a + * CompilationUnitArchive that is more up to date. If the + * timestamps are the same, accept the latest version. If it turns + * 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()) { + super.remove(existingEntry.getUnit()); + unitMap.put(unit.getResourcePath(), entry); + unitMapByContentId.put(unit.getContentId(), entry); + } else if (existingEntry == null) { + unitMap.put(unit.getResourcePath(), entry); + unitMapByContentId.put(unit.getContentId(), entry); } } - }); - } catch (RejectedExecutionException ex) { - // background thread is not running, ignore - return null; - } - } - - Future<?> asyncShutdown() { - Future<?> status = service.submit(new Runnable() { - @Override - public void run() { - cacheDir.closeCurrentFile(); - shutdownNow(); + } catch (EOFException ex) { + // Go on to the next file. + } catch (IOException ex) { + deleteCacheFile = true; + if (logger.isLoggable(TreeLogger.TRACE)) { + logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log " + + cacheFile.getAbsolutePath() + " due to read error.", ex); + } + } catch (ClassNotFoundException ex) { + deleteCacheFile = true; + if (logger.isLoggable(TreeLogger.TRACE)) { + 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); } - }); - service.shutdown(); // Don't allow more tasks to be scheduled. - return status; - } - - private void shutdownNow() { - logger.log(TreeLogger.TRACE, "Shutting down PersistentUnitCache thread"); - service.shutdownNow(); + if (deleteCacheFile) { + logger.log(Type.WARN, "Deleting " + cacheFile + " due to an exception"); + cacheDir.deleteUnlessOpen(cacheFile); + } else { + if (logger.isLoggable(TreeLogger.TRACE)) { + logger.log(TreeLogger.TRACE, cacheFile.getName() + ": Load complete"); + } + } + } + } finally { + loadPersistentUnitEvent.end(); } } }
diff --git a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java index 94a8283..e072f19 100644 --- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java +++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java
@@ -18,26 +18,21 @@ import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.dev.javac.MemoryUnitCache.UnitCacheEntry; import com.google.gwt.dev.jjs.ast.JNode; -import com.google.gwt.dev.jjs.impl.GwtAstBuilder; -import com.google.gwt.dev.util.StringInterningObjectInputStream; import com.google.gwt.dev.util.log.speedtracer.DevModeEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; -import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting; +import com.google.gwt.thirdparty.guava.common.base.Preconditions; import com.google.gwt.thirdparty.guava.common.collect.Lists; import com.google.gwt.thirdparty.guava.common.hash.Hashing; import com.google.gwt.thirdparty.guava.common.io.Files; import com.google.gwt.util.tools.Utility; -import java.io.BufferedInputStream; import java.io.BufferedOutputStream; -import java.io.EOFException; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.JarURLConnection; import java.net.URLConnection; @@ -111,33 +106,6 @@ } /** - * Returns the number of files written to the cache directory and closed. - */ - synchronized int getClosedCacheFileCount() { - return selectClosedFiles(listFiles(CURRENT_VERSION_CACHE_FILE_PREFIX)).size(); - } - - /** - * Load everything cached on disk into memory. - */ - synchronized void loadUnitMap(PersistentUnitCache destination) { - Event loadPersistentUnitEvent = - SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE); - if (logger.isLoggable(TreeLogger.TRACE)) { - logger.log(TreeLogger.TRACE, "Looking for previously cached Compilation Units in " - + getPath()); - } - try { - List<File> files = selectClosedFiles(listFiles(CURRENT_VERSION_CACHE_FILE_PREFIX)); - for (File cacheFile : files) { - loadOrDeleteCacheFile(cacheFile, destination); - } - } finally { - loadPersistentUnitEvent.end(); - } - } - - /** * Delete all cache files in the directory except for the currently open file. */ synchronized void deleteClosedCacheFiles() { @@ -170,6 +138,13 @@ } /** + * Returns the files that should be loaded at startup to refill the cache. + */ + synchronized List<File> listCacheFilesToLoad() { + return selectClosedFiles(listFiles(CURRENT_VERSION_CACHE_FILE_PREFIX)); + } + + /** * Deletes the given file unless it's currently open for writing. */ synchronized boolean deleteUnlessOpen(File cacheFile) { @@ -185,14 +160,14 @@ } /** - * Writes a compilation unit to the disk cache. + * Writes an entry to the cache. */ - synchronized void writeUnit(CompilationUnit unit) throws UnableToCompleteException { + synchronized void writeObject(UnitCacheEntry entry) throws UnableToCompleteException { if (openFile == null) { logger.log(Type.TRACE, "Skipped writing compilation unit to cache because no file is open"); return; } - openFile.writeUnit(logger, unit); + openFile.writeObject(logger, entry); } /** @@ -216,66 +191,6 @@ } /** - * Loads all the units in a cache file into the given cache. - * Delete it if unable to read it. - */ - private void loadOrDeleteCacheFile(File cacheFile, PersistentUnitCache destination) { - FileInputStream fis = null; - BufferedInputStream bis = null; - ObjectInputStream inputStream = null; - - boolean ok = false; - int unitsLoaded = 0; - try { - fis = new FileInputStream(cacheFile); - bis = new BufferedInputStream(fis); - /* - * It is possible for the next call to throw an exception, leaving - * inputStream null and fis still live. - */ - inputStream = new StringInterningObjectInputStream(bis); - - // Read objects until we get an EOF exception. - while (true) { - CachedCompilationUnit unit = (CachedCompilationUnit) inputStream.readObject(); - if (unit == null) { - // Won't normally get here. Not sure why this check was here before. - logger.log(Type.WARN, "unexpected null in cache file: " + cacheFile); - break; - } - if (unit.getTypesSerializedVersion() != GwtAstBuilder.getSerializationVersion()) { - continue; - } - destination.maybeAddLoadedUnit(unit); - unitsLoaded++; - } - - } catch (EOFException ignored) { - // This is a normal exit. Go on to the next file. - ok = true; - } catch (IOException e) { - logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log " - + cacheFile.getAbsolutePath() + " due to read error.", e); - } catch (ClassNotFoundException e) { - logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log " - + cacheFile.getAbsolutePath() + " due to deserialization error.", e); - } finally { - Utility.close(inputStream); - Utility.close(bis); - Utility.close(fis); - } - - if (ok) { - logger.log(TreeLogger.TRACE, "Loaded " + unitsLoaded + - " units from cache file: " + cacheFile.getName()); - } else { - deleteUnlessOpen(cacheFile); - logger.log(TreeLogger.TRACE, "Loaded " + unitsLoaded + - " units from invalid cache file before deleting it: " + cacheFile.getName()); - } - } - - /** * Lists files in the cache directory that start with the given prefix. * * <p>The files will be sorted according to {@link java.io.File#compareTo}, which @@ -386,12 +301,13 @@ } /** - * Writes a compilation unit to the currently open file, if any. + * Writes an entry to the currently open file, if any. * @return true if written * @throws UnableToCompleteException if the file was open but we can't append. */ - boolean writeUnit(TreeLogger logger, CompilationUnit unit) + boolean writeObject(TreeLogger logger, UnitCacheEntry entry) throws UnableToCompleteException { + CompilationUnit unit = Preconditions.checkNotNull(entry.getUnit()); try { stream.writeObject(unit); unitsWritten++;
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 59f3e81..5dadf9c 100644 --- a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java +++ b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
@@ -19,6 +19,7 @@ import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.dev.util.Util; +import com.google.gwt.dev.util.log.PrintWriterTreeLogger; import com.google.gwt.thirdparty.guava.common.util.concurrent.Futures; import junit.framework.TestCase; @@ -32,6 +33,7 @@ import java.io.Serializable; import java.util.Arrays; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; /** * Unit test for {@link PersistentUnitCache}. @@ -58,8 +60,8 @@ @Override protected void setUp() throws Exception { super.setUp(); - logger = TreeLogger.NULL; - // logger = new PrintWriterTreeLogger(); // uncomment for debugging +// logger = TreeLogger.NULL; // Change to PrintWriterTreeLogger to debug tests + logger = new PrintWriterTreeLogger(); logger.log(Type.INFO, "\n\n*** Running " + getName()); } @@ -125,7 +127,7 @@ File parentDir = lastParentDir = File.createTempFile("persistentCacheTest", ""); File unitCacheDir = mkCacheDir(parentDir); - PersistentUnitCache cache = new PersistentUnitCache(logger, parentDir); + PersistentUnitCache cache = makeUnitCache(parentDir); MockCompilationUnit foo1 = new MockCompilationUnit("com.example.Foo", "Foo: source1"); cache.add(foo1); @@ -162,7 +164,6 @@ assertEquals("com.example.Foo", result.getTypeName()); assertEquals(foo2.getContentId(), result.getContentId()); cache.cleanup(logger); - cache.waitForCleanup(); // Shutdown the cache and re -load it cache.shutdown(); @@ -172,7 +173,7 @@ // Fire up the cache again. It should be pre-populated. // Search by type name - cache = new PersistentUnitCache(logger, parentDir); + cache = makeUnitCache(parentDir); result = cache.find("com/example/Foo.java"); assertNotNull(result); assertEquals("com.example.Foo", result.getTypeName()); @@ -195,7 +196,6 @@ assertEquals(foo2.getTypeName(), result.getTypeName()); assertEquals(foo2.getContentId(), result.getContentId()); cache.cleanup(logger); - cache.waitForCleanup(); // We didn't write anything, still 1 file. cache.shutdown(); @@ -204,7 +204,7 @@ assertNumCacheFiles(unitCacheDir, 1); // Fire up the cache again. (Creates a second file in the background.) - cache = new PersistentUnitCache(logger, parentDir); + cache = makeUnitCache(parentDir); // keep making more files MockCompilationUnit lastUnit = null; @@ -216,12 +216,11 @@ // force rotation to a new file // (Normally async but we overrode it.) cache.cleanup(logger); - cache.waitForCleanup(); assertNumCacheFiles(unitCacheDir, i + 1); } // One last check, we should load the last unit added to the cache. - cache = new PersistentUnitCache(logger, parentDir); + cache = makeUnitCache(parentDir); result = cache.find(lastUnit.getContentId()); assertNotNull(result); assertEquals("com.example.Foo", result.getTypeName()); @@ -245,7 +244,6 @@ lastUnit = new MockCompilationUnit("com.example.Baz", "Baz Source"); cache.add(lastUnit); cache.cleanup(logger); - cache.waitForCleanup(); // Add one more to put us over the top. lastUnit = new MockCompilationUnit("com.example.Qux", "Qux Source"); @@ -255,14 +253,13 @@ assertNumCacheFiles(unitCacheDir, PersistentUnitCache.CACHE_FILE_THRESHOLD + 1); cache.cleanup(logger); - cache.waitForCleanup(); cache.shutdown(); // There should be a single file in the cache dir. assertNumCacheFiles(unitCacheDir, 1); // Fire up the cache on this one coalesced file. - cache = new PersistentUnitCache(logger, parentDir); + cache = makeUnitCache(parentDir); // Verify that we can still find the content that was coalesced. assertNotNull(cache.find("com/example/Foo.java")); @@ -271,6 +268,17 @@ assertNotNull(cache.find("com/example/Qux.java")); } + private PersistentUnitCache makeUnitCache(File cacheDir) throws UnableToCompleteException { + return new PersistentUnitCache(logger, cacheDir) { + @Override + Future<Void> startRotating() { + // wait for rotation to finish to avoid flakiness + Futures.getUnchecked(super.startRotating()); + return Futures.immediateFuture(null); + } + }; + } + private void assertNumCacheFiles(File unitCacheDir, int expected) { String[] actualFiles = unitCacheDir.list(); if (expected == actualFiles.length) {