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 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<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.
* @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}.
*/
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<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
@ -432,12 +497,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@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) {
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<CachedContent> 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<String, CachedContent> 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<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) {
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);

View File

@ -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;
}

View File

@ -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();