From fb99c26426b2f5b82b8528f52f6890c7ed2ef23b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Feb 2019 16:48:34 +0000 Subject: [PATCH] Implement CacheContentIndex storage switching This change enables transitioning to/from different Storage implementations, to allow experimentally enabling (and if necessary, disabling) SQLiteStorage. All that's left to do is the final wiring to turn it on PiperOrigin-RevId: 232304458 --- .../upstream/cache/CachedContentIndex.java | 161 +++++++++++++++--- .../upstream/cache/SimpleCache.java | 1 + .../android/exoplayer2/util/AtomicFile.java | 5 + 3 files changed, 142 insertions(+), 25 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index e31c60e752..0888290e1b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -94,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private final SparseBooleanArray removedIds; - private final Storage storage; + private Storage storage; + @Nullable private Storage previousStorage; /** * Returns whether the file is an index file, or an auxiliary file associated with an index file @@ -150,29 +151,42 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); Random random = new Random(); - storage = + Storage atomicFileStorage = new AtomicFileStorage( new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec); - // storage = + // Storage sqliteStorage = // new SQLiteStorage( - // new File(cacheDir, FILE_NAME_DATABASE), - // random, - // encrypt, - // cipher, - // secretKeySpec); + // new File(cacheDir, FILE_NAME_DATABASE), random, encrypt, cipher, secretKeySpec); + storage = atomicFileStorage; + previousStorage = null; } /** Loads the index file. */ public void load() { - if (!storage.load(keyToContent, idToKey)) { - keyToContent.clear(); - idToKey.clear(); + if (!storage.exists() && previousStorage != null && previousStorage.exists()) { + // Copy from previous storage into current storage. + loadFrom(previousStorage); + try { + storage.storeFully(keyToContent); + } catch (CacheException e) { + // We failed to copy into current storage, so keep using previous storage. + storage.release(); + storage = previousStorage; + previousStorage = null; + } + } else { + // Load from the current storage. + loadFrom(storage); + } + if (previousStorage != null) { + previousStorage.release(/* delete= */ true); + previousStorage = null; } } /** Stores the index data to index file if there is a change. */ public void store() throws CacheException { - storage.store(keyToContent); + storage.storeIncremental(keyToContent); // Make ids that were removed since the index was last stored eligible for re-use. int removedIdCount = removedIds.size(); for (int i = 0; i < removedIdCount; i++) { @@ -181,6 +195,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; removedIds.clear(); } + /** Releases any underlying resources. */ + public void release() { + storage.release(); + if (previousStorage != null) { + previousStorage.release(); + } + } + /** * Adds the given key to the index if it isn't there already. * @@ -267,6 +289,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; } + /** Loads the index from the specified storage. */ + private void loadFrom(Storage storage) { + if (!storage.load(keyToContent, idToKey)) { + keyToContent.clear(); + idToKey.clear(); + } + } + private CachedContent addNew(String key) { int id = getNewId(idToKey); CachedContent cachedContent = new CachedContent(id, key); @@ -363,8 +393,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Interface for the persistent index. */ private interface Storage { + /** Returns whether the persisted index exists. */ + boolean exists(); + + /** Releases any held resources. */ + default void release() { + release(/* delete= */ false); + } + + /** Releases and held resources and optionally deletes the persisted index. */ + void release(boolean delete); + /** - * Loads the persisted index into {@code content} and {@code idToKey}. + * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't + * already exist. * * @param content The key to content map to populate with persisted data. * @param idToKey The id to key map to populate with persisted data. @@ -373,22 +415,33 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; boolean load(HashMap content, SparseArray<@NullableType String> idToKey); /** - * Ensures all changes in the in-memory table are persisted. + * Writes the persisted index, creating it if it doesn't already exist and replacing any + * existing content if it does. * * @param content The key to content map to persist. * @throws CacheException If an error occurs persisting the index. */ - void store(HashMap content) throws CacheException; + void storeFully(HashMap content) throws CacheException; /** - * Called when a {@link CachedContent} is added or updated in the in-memory index. + * Ensures incremental changes to the index since the last {@link #load()} or {@link + * #storeFully(HashMap)} are persisted. The storage will have been notified of all such changes + * via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}. + * + * @param content The key to content map to persist. + * @throws CacheException If an error occurs persisting the index. + */ + void storeIncremental(HashMap content) throws CacheException; + + /** + * Called when a {@link CachedContent} is added or updated. * * @param cachedContent The updated {@link CachedContent}. */ void onUpdate(CachedContent cachedContent); /** - * Called when a {@link CachedContent} is removed from the in-memory index. + * Called when a {@link CachedContent} is removed. * * @param cachedContent The removed {@link CachedContent}. */ @@ -420,6 +473,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; atomicFile = new AtomicFile(file); } + @Override + public boolean exists() { + return atomicFile.exists(); + } + + @Override + public void release(boolean delete) { + if (delete) { + atomicFile.delete(); + } + } + @Override public boolean load( HashMap content, SparseArray<@NullableType String> idToKey) { @@ -432,12 +497,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void store(HashMap content) throws CacheException { + public void storeFully(HashMap content) throws CacheException { + writeFile(content); + changed = false; + } + + @Override + public void storeIncremental(HashMap content) throws CacheException { if (!changed) { return; } - writeFile(content); - changed = false; + storeFully(content); } @Override @@ -637,11 +707,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static final int FLAG_ENCRYPTED = 1; + private final File file; private final Random random; private final boolean encrypt; @Nullable private final Cipher cipher; @Nullable private final SecretKeySpec secretKeySpec; - private final DatabaseProvider databaseProvider; + private final ExoDatabaseProvider databaseProvider; private final SparseArray pendingUpdates; @Nullable private ReusableBufferedOutputStream bufferedOutputStream; @@ -652,6 +723,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; boolean encrypt, @Nullable Cipher cipher, @Nullable SecretKeySpec secretKeySpec) { + this.file = file; this.random = random; this.encrypt = encrypt; this.cipher = cipher; @@ -660,9 +732,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; pendingUpdates = new SparseArray<>(); } + @Override + public boolean exists() { + return file.exists() + && VersionTable.getVersion( + databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE) + != VersionTable.VERSION_UNSET; + } + + @Override + public void release(boolean delete) { + release(); + if (delete) { + SQLiteDatabase.deleteDatabase(file); + } + } + @Override public boolean load( HashMap content, SparseArray<@NullableType String> idToKey) { + Assertions.checkState(pendingUpdates.size() == 0); try { int version = VersionTable.getVersion( @@ -671,9 +760,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); try { - writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); - writableDatabase.execSQL(SQL_CREATE_TABLE); - VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + initializeTable(writableDatabase); writableDatabase.setTransactionSuccessful(); } finally { writableDatabase.endTransaction(); @@ -717,7 +804,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void store(HashMap content) throws CacheException { + public void storeFully(HashMap content) throws CacheException { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + initializeTable(writableDatabase); + for (CachedContent cachedContent : content.values()) { + addOrUpdateRow(writableDatabase, cachedContent); + } + writableDatabase.setTransactionSuccessful(); + pendingUpdates.clear(); + } catch (IOException | SQLiteException e) { + throw new CacheException(e); + } finally { + writableDatabase.endTransaction(); + } + } + + @Override + public void storeIncremental(HashMap content) throws CacheException { if (pendingUpdates.size() == 0) { return; } @@ -764,6 +869,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* orderBy= */ null); } + private void initializeTable(SQLiteDatabase writableDatabase) { + VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.execSQL(SQL_CREATE_TABLE); + } + private void deleteRow(SQLiteDatabase writableDatabase, int key) { String[] selectionArgs = {Integer.toString(key)}; writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 75236ad9c7..d2091c362a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -157,6 +157,7 @@ public final class SimpleCache implements Cache { } catch (CacheException e) { Log.e(TAG, "Storing index file failed", e); } finally { + index.release(); unlockFolder(cacheDir); released = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 2466d5a049..b7b6c05d82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -52,6 +52,11 @@ public final class AtomicFile { backupName = new File(baseName.getPath() + ".bak"); } + /** Whether the file or its backup exists. */ + public boolean exists() { + return baseName.exists() || backupName.exists(); + } + /** Delete the atomic file. This deletes both the base and backup files. */ public void delete() { baseName.delete();