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
This commit is contained in:
olly 2019-02-04 16:48:34 +00:00 committed by Oliver Woodman
parent f1ded9c3c2
commit fb99c26426
3 changed files with 142 additions and 25 deletions

View File

@ -94,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*/ */
private final SparseBooleanArray removedIds; 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 * 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<>(); idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray(); removedIds = new SparseBooleanArray();
Random random = new Random(); Random random = new Random();
storage = Storage atomicFileStorage =
new AtomicFileStorage( new AtomicFileStorage(
new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec); new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec);
// storage = // Storage sqliteStorage =
// new SQLiteStorage( // new SQLiteStorage(
// new File(cacheDir, FILE_NAME_DATABASE), // new File(cacheDir, FILE_NAME_DATABASE), random, encrypt, cipher, secretKeySpec);
// random, storage = atomicFileStorage;
// encrypt, previousStorage = null;
// cipher,
// secretKeySpec);
} }
/** Loads the index file. */ /** Loads the index file. */
public void load() { public void load() {
if (!storage.load(keyToContent, idToKey)) { if (!storage.exists() && previousStorage != null && previousStorage.exists()) {
keyToContent.clear(); // Copy from previous storage into current storage.
idToKey.clear(); 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. */ /** Stores the index data to index file if there is a change. */
public void store() throws CacheException { 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. // Make ids that were removed since the index was last stored eligible for re-use.
int removedIdCount = removedIds.size(); int removedIdCount = removedIds.size();
for (int i = 0; i < removedIdCount; i++) { for (int i = 0; i < removedIdCount; i++) {
@ -181,6 +195,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
removedIds.clear(); 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. * 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; 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) { private CachedContent addNew(String key) {
int id = getNewId(idToKey); int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key); CachedContent cachedContent = new CachedContent(id, key);
@ -363,8 +393,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Interface for the persistent index. */ /** Interface for the persistent index. */
private interface Storage { 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 content The key to content map to populate with persisted data.
* @param idToKey The id to key 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<String, CachedContent> content, SparseArray<@NullableType String> idToKey); boolean load(HashMap<String, CachedContent> 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. * @param content The key to content map to persist.
* @throws CacheException If an error occurs persisting the index. * @throws CacheException If an error occurs persisting the index.
*/ */
void store(HashMap<String, CachedContent> content) throws CacheException; void storeFully(HashMap<String, CachedContent> 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<String, CachedContent> content) throws CacheException;
/**
* Called when a {@link CachedContent} is added or updated.
* *
* @param cachedContent The updated {@link CachedContent}. * @param cachedContent The updated {@link CachedContent}.
*/ */
void onUpdate(CachedContent 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}. * @param cachedContent The removed {@link CachedContent}.
*/ */
@ -420,6 +473,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
atomicFile = new AtomicFile(file); atomicFile = new AtomicFile(file);
} }
@Override
public boolean exists() {
return atomicFile.exists();
}
@Override
public void release(boolean delete) {
if (delete) {
atomicFile.delete();
}
}
@Override @Override
public boolean load( public boolean load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) { HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
@ -432,12 +497,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
@Override @Override
public void store(HashMap<String, CachedContent> content) throws CacheException { public void storeFully(HashMap<String, CachedContent> content) throws CacheException {
writeFile(content);
changed = false;
}
@Override
public void storeIncremental(HashMap<String, CachedContent> content) throws CacheException {
if (!changed) { if (!changed) {
return; return;
} }
writeFile(content); storeFully(content);
changed = false;
} }
@Override @Override
@ -637,11 +707,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final int FLAG_ENCRYPTED = 1; private static final int FLAG_ENCRYPTED = 1;
private final File file;
private final Random random; private final Random random;
private final boolean encrypt; private final boolean encrypt;
@Nullable private final Cipher cipher; @Nullable private final Cipher cipher;
@Nullable private final SecretKeySpec secretKeySpec; @Nullable private final SecretKeySpec secretKeySpec;
private final DatabaseProvider databaseProvider; private final ExoDatabaseProvider databaseProvider;
private final SparseArray<CachedContent> pendingUpdates; private final SparseArray<CachedContent> pendingUpdates;
@Nullable private ReusableBufferedOutputStream bufferedOutputStream; @Nullable private ReusableBufferedOutputStream bufferedOutputStream;
@ -652,6 +723,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
boolean encrypt, boolean encrypt,
@Nullable Cipher cipher, @Nullable Cipher cipher,
@Nullable SecretKeySpec secretKeySpec) { @Nullable SecretKeySpec secretKeySpec) {
this.file = file;
this.random = random; this.random = random;
this.encrypt = encrypt; this.encrypt = encrypt;
this.cipher = cipher; this.cipher = cipher;
@ -660,9 +732,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
pendingUpdates = new SparseArray<>(); 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 @Override
public boolean load( public boolean load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) { HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
Assertions.checkState(pendingUpdates.size() == 0);
try { try {
int version = int version =
VersionTable.getVersion( VersionTable.getVersion(
@ -671,9 +760,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction(); writableDatabase.beginTransaction();
try { try {
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); initializeTable(writableDatabase);
writableDatabase.execSQL(SQL_CREATE_TABLE);
VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION);
writableDatabase.setTransactionSuccessful(); writableDatabase.setTransactionSuccessful();
} finally { } finally {
writableDatabase.endTransaction(); writableDatabase.endTransaction();
@ -717,7 +804,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
@Override @Override
public void store(HashMap<String, CachedContent> content) throws CacheException { public void storeFully(HashMap<String, CachedContent> 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<String, CachedContent> content) throws CacheException {
if (pendingUpdates.size() == 0) { if (pendingUpdates.size() == 0) {
return; return;
} }
@ -764,6 +869,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* orderBy= */ null); /* 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) { private void deleteRow(SQLiteDatabase writableDatabase, int key) {
String[] selectionArgs = {Integer.toString(key)}; String[] selectionArgs = {Integer.toString(key)};
writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs);

View File

@ -157,6 +157,7 @@ public final class SimpleCache implements Cache {
} catch (CacheException e) { } catch (CacheException e) {
Log.e(TAG, "Storing index file failed", e); Log.e(TAG, "Storing index file failed", e);
} finally { } finally {
index.release();
unlockFolder(cacheDir); unlockFolder(cacheDir);
released = true; released = true;
} }

View File

@ -52,6 +52,11 @@ public final class AtomicFile {
backupName = new File(baseName.getPath() + ".bak"); 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. */ /** Delete the atomic file. This deletes both the base and backup files. */
public void delete() { public void delete() {
baseName.delete(); baseName.delete();