/** * Verify that multiple threads can write to the cache at the same time. */ @Test public void testConcurrency() throws Exception { final CyclicBarrier barrier = new CyclicBarrier(3); WriterCallback writerCallback = new WriterCallback() { @Override public void write(OutputStream os) throws IOException { try { // Both threads will need to hit this barrier. If writing is serialized, // the second thread will never reach here as the first will hold // the write lock forever. barrier.await(10, TimeUnit.SECONDS); } catch (Exception e) { throw new RuntimeException(e); } } }; CacheKey key1 = new SimpleCacheKey("concurrent1"); CacheKey key2 = new SimpleCacheKey("concurrent2"); Thread t1 = runInsertionInSeparateThread(key1, writerCallback); Thread t2 = runInsertionInSeparateThread(key2, writerCallback); barrier.await(10, TimeUnit.SECONDS); t1.join(1000); t2.join(1000); }
private Thread runInsertionInSeparateThread(final CacheKey key, final WriterCallback callback) { Runnable runnable = new Runnable() { @Override public void run() { try { mCache.insert(key, callback); } catch (IOException e) { fail(); } } }; Thread thread = new Thread(runnable); thread.setDaemon(true); thread.start(); return thread; }
@Test public void testSizeEvictionClearsIndex() throws Exception { when(mClock.now()).thenReturn(TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS)); CacheKey key1 = putOneThingInCache(); CacheKey key2 = new SimpleCacheKey("bar"); CacheKey key3 = new SimpleCacheKey("duck"); byte[] value2 = new byte[(int) FILE_CACHE_MAX_SIZE_HIGH_LIMIT]; value2[80] = 'c'; WriterCallback callback = WriterCallbacks.from(value2); when(mClock.now()).thenReturn(TimeUnit.MILLISECONDS.convert(2, TimeUnit.DAYS)); mCache.insert(key2, callback); // now over limit. Next write will evict key1 when(mClock.now()).thenReturn(TimeUnit.MILLISECONDS.convert(3, TimeUnit.DAYS)); mCache.insert(key3, callback); assertFalse(mCache.hasKeySync(key1)); assertFalse(mCache.hasKey(key1)); assertTrue(mCache.hasKeySync(key3)); assertTrue(mCache.hasKey(key3)); }
/** * Writes to disk cache * @throws IOException */ private void writeToDiskCache( final CacheKey key, final EncodedImage encodedImage) { FLog.v(TAG, "About to write to disk-cache for key %s", key.getUriString()); try { mFileCache.insert( key, new WriterCallback() { @Override public void write(OutputStream os) throws IOException { mPooledByteStreams.copy(encodedImage.getInputStream(), os); } } ); FLog.v(TAG, "Successful disk-cache write for key %s", key.getUriString()); } catch (IOException ioe) { // Log failure // TODO: 3697790 FLog.w(TAG, ioe, "Failed to write to disk-cache for key %s", key.getUriString()); } }
@Test public void testWritesToDiskCache() throws Exception { mBufferedDiskCache.put(mCacheKey, mEncodedImage); reset(mPooledByteBuffer); when(mPooledByteBuffer.size()).thenReturn(0); final ArgumentCaptor<WriterCallback> wcCapture = ArgumentCaptor.forClass(WriterCallback.class); when(mFileCache.insert( eq(mCacheKey), wcCapture.capture())).thenReturn(null); mWritePriorityExecutor.runUntilIdle(); OutputStream os = mock(OutputStream.class); wcCapture.getValue().write(os); // Ref count should be equal to 2 ('owned' by the mCloseableReference and other 'owned' by // mEncodedImage) assertEquals(2, mCloseableReference.getUnderlyingReferenceTestOnly().getRefCountTestOnly()); }
@Override public BinaryResource insert(CacheKey key, WriterCallback callback) throws IOException { // Write to a temp file, then move it into place. This allows more parallelism // when writing files. SettableCacheEvent cacheEvent = SettableCacheEvent.obtain() .setCacheKey(key); mCacheEventListener.onWriteAttempt(cacheEvent); String resourceId; synchronized (mLock) { // for multiple resource ids associated with the same image, we only write one file resourceId = CacheKeyUtil.getFirstResourceId(key); } cacheEvent.setResourceId(resourceId); try { // getting the file is synchronized DiskStorage.Inserter inserter = startInsert(resourceId, key); try { inserter.writeData(callback, key); // Committing the file is synchronized BinaryResource resource = endInsert(inserter, key, resourceId); cacheEvent.setItemSize(resource.size()) .setCacheSize(mCacheStats.getSize()); mCacheEventListener.onWriteSuccess(cacheEvent); return resource; } finally { if (!inserter.cleanUp()) { FLog.e(TAG, "Failed to delete temp file"); } } } catch (IOException ioe) { cacheEvent.setException(ioe); mCacheEventListener.onWriteException(cacheEvent); FLog.e(TAG, "Failed inserting a file into the cache", ioe); throw ioe; } finally { cacheEvent.recycle(); } }
private static void writeToResource( DiskStorage.Inserter inserter, final byte[] content) throws IOException { inserter.writeData( new WriterCallback() { @Override public void write(OutputStream os) throws IOException { os.write(content); } }, null); }
@Override public void writeData(WriterCallback callback, Object debugInfo) throws IOException { FileOutputStream fileStream; try { fileStream = new FileOutputStream(mTemporaryFile); } catch (FileNotFoundException fne) { mCacheErrorLogger.logError( CacheErrorLogger.CacheErrorCategory.WRITE_UPDATE_FILE_NOT_FOUND, TAG, "updateResource", fne); throw fne; } long length; try { CountingOutputStream countingStream = new CountingOutputStream(fileStream); callback.write(countingStream); // just in case underlying stream's close method doesn't flush: // we flush it manually and inside the try/catch countingStream.flush(); length = countingStream.getCount(); } finally { // if it fails to close (or write the last piece) we really want to know // Normally we would want this to be quiet because a closing exception would hide one // inside the try, but now we really want to know if something fails at flush or close fileStream.close(); } // this code should never throw, but if filesystem doesn't fail on a failing/uncomplete close // we want to know and manually fail if (mTemporaryFile.length() != length) { throw new IncompleteFileException(length, mTemporaryFile.length()); } }
@Test public void testCacheFileWithIOException() throws IOException { CacheKey key1 = new SimpleCacheKey("aaa"); // Before inserting, make sure files not exist. final BinaryResource resource1 = getResource(key1); assertNull(resource1); // 1. Should not create cache files if IOException happens in the middle. final IOException writeException = new IOException(); try { mCache.insert( key1, new WriterCallback() { @Override public void write(OutputStream os) throws IOException { throw writeException; } }); fail(); } catch (IOException e) { assertNull(getResource(key1)); } verifyListenerOnWriteAttempt(key1); verifyListenerOnWriteException(key1, writeException); // 2. Test a read failure from DiskStorage CacheKey key2 = new SimpleCacheKey("bbb"); int value2Size = 42; byte[] value2 = new byte[value2Size]; value2[25] = 'b'; mCache.insert(key2, WriterCallbacks.from(value2)); verifyListenerOnWriteAttempt(key2); String resourceId2 = verifyListenerOnWriteSuccessAndGetResourceId(key2, value2Size); ((DiskStorageWithReadFailures) mStorage).setPoisonResourceId(resourceId2); assertNull(mCache.getResource(key2)); verifyListenerOnReadException(key2, DiskStorageWithReadFailures.POISON_EXCEPTION); assertFalse(mCache.probe(key2)); verifyListenerOnReadException(key2, DiskStorageWithReadFailures.POISON_EXCEPTION); verifyNoMoreInteractions(mCacheEventListener); }
@Override public BinaryResource insert(CacheKey cacheKey, WriterCallback writerCallback) throws IOException { return null; }
/** * Inserts resource into file with key * @param key cache key * @param writer Callback that writes to an output stream * @return a sequence of bytes * @throws IOException */ BinaryResource insert(CacheKey key, WriterCallback writer) throws IOException;
/** * Update the contents of the resource to be inserted. Executes outside the session lock. * The writer callback will be provided with an OutputStream to write to. * For high efficiency client should make sure that data is written in big chunks * (for example by employing BufferedInputStream or writing all data at once). * @param callback the write callback * @param debugInfo helper object for debugging * @throws IOException */ void writeData(WriterCallback callback, Object debugInfo) throws IOException;