AsyncStorage improvements

Differential Revision: D2475604

committer: Service User <svcscm@fb.com>
This commit is contained in:
Andrei Coman
2015-09-24 02:43:25 -07:00
committed by facebook-github-bot-9
parent 7615d74d14
commit 33cc607c1f
4 changed files with 139 additions and 102 deletions

View File

@@ -24,9 +24,9 @@ import com.facebook.react.bridge.ReadableArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN;
/** /**
* Helper for database operations. * Helper for database operations.

View File

@@ -9,16 +9,12 @@
package com.facebook.react.modules.storage; package com.facebook.react.modules.storage;
import javax.annotation.Nullable;
import java.util.HashSet; import java.util.HashSet;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement; import android.database.sqlite.SQLiteStatement;
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask; import com.facebook.react.bridge.GuardedAsyncTask;
@@ -31,18 +27,19 @@ import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SetBuilder; import com.facebook.react.common.SetBuilder;
import com.facebook.react.modules.common.ModuleDataCleaner; import com.facebook.react.modules.common.ModuleDataCleaner;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN;
public final class AsyncStorageModule public final class AsyncStorageModule
extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable {
private @Nullable SQLiteDatabase mDb; private ReactDatabaseSupplier mReactDatabaseSupplier;
private boolean mShuttingDown = false; private boolean mShuttingDown = false;
public AsyncStorageModule(ReactApplicationContext reactContext) { public AsyncStorageModule(ReactApplicationContext reactContext) {
super(reactContext); super(reactContext);
mReactDatabaseSupplier = new ReactDatabaseSupplier(reactContext);
} }
@Override @Override
@@ -59,10 +56,6 @@ public final class AsyncStorageModule
@Override @Override
public void onCatalystInstanceDestroy() { public void onCatalystInstanceDestroy() {
mShuttingDown = true; mShuttingDown = true;
if (mDb != null && mDb.isOpen()) {
mDb.close();
mDb = null;
}
} }
@Override @Override
@@ -74,10 +67,17 @@ public final class AsyncStorageModule
new Callback() { new Callback() {
@Override @Override
public void invoke(Object... args) { public void invoke(Object... args) {
if (args.length > 0) { if (args.length == 0) {
throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]); FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage.");
return;
} }
FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); // Clearing the database has failed, delete it instead.
if (mReactDatabaseSupplier.deleteDatabase()) {
FLog.d(ReactConstants.TAG, "Deleted Local Database AsyncLocalStorage.");
return;
}
// Everything failed, crash the app
throw new RuntimeException("Clearing and deleting database failed: " + args[0]);
} }
}); });
} }
@@ -104,7 +104,7 @@ public final class AsyncStorageModule
String[] columns = {KEY_COLUMN, VALUE_COLUMN}; String[] columns = {KEY_COLUMN, VALUE_COLUMN};
HashSet<String> keysRemaining = SetBuilder.newHashSet(); HashSet<String> keysRemaining = SetBuilder.newHashSet();
WritableArray data = Arguments.createArray(); WritableArray data = Arguments.createArray();
Cursor cursor = Assertions.assertNotNull(mDb).query( Cursor cursor = mReactDatabaseSupplier.get().query(
TABLE_CATALYST, TABLE_CATALYST,
columns, columns,
AsyncLocalStorageUtil.buildKeySelection(keys.size()), AsyncLocalStorageUtil.buildKeySelection(keys.size()),
@@ -171,8 +171,8 @@ public final class AsyncStorageModule
} }
String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);";
SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql); SQLiteStatement statement = mReactDatabaseSupplier.get().compileStatement(sql);
mDb.beginTransaction(); mReactDatabaseSupplier.get().beginTransaction();
try { try {
for (int idx=0; idx < keyValueArray.size(); idx++) { for (int idx=0; idx < keyValueArray.size(); idx++) {
if (keyValueArray.getArray(idx).size() != 2) { if (keyValueArray.getArray(idx).size() != 2) {
@@ -193,12 +193,12 @@ public final class AsyncStorageModule
statement.bindString(2, keyValueArray.getArray(idx).getString(1)); statement.bindString(2, keyValueArray.getArray(idx).getString(1));
statement.execute(); statement.execute();
} }
mDb.setTransactionSuccessful(); mReactDatabaseSupplier.get().setTransactionSuccessful();
} catch (Exception e) { } catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e); FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
} finally { } finally {
mDb.endTransaction(); mReactDatabaseSupplier.get().endTransaction();
} }
callback.invoke(); callback.invoke();
} }
@@ -224,7 +224,7 @@ public final class AsyncStorageModule
} }
try { try {
Assertions.assertNotNull(mDb).delete( mReactDatabaseSupplier.get().delete(
TABLE_CATALYST, TABLE_CATALYST,
AsyncLocalStorageUtil.buildKeySelection(keys.size()), AsyncLocalStorageUtil.buildKeySelection(keys.size()),
AsyncLocalStorageUtil.buildKeySelectionArgs(keys)); AsyncLocalStorageUtil.buildKeySelectionArgs(keys));
@@ -250,7 +250,7 @@ public final class AsyncStorageModule
callback.invoke(AsyncStorageErrorUtil.getDBError(null)); callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return; return;
} }
Assertions.assertNotNull(mDb).beginTransaction(); mReactDatabaseSupplier.get().beginTransaction();
try { try {
for (int idx = 0; idx < keyValueArray.size(); idx++) { for (int idx = 0; idx < keyValueArray.size(); idx++) {
if (keyValueArray.getArray(idx).size() != 2) { if (keyValueArray.getArray(idx).size() != 2) {
@@ -269,19 +269,19 @@ public final class AsyncStorageModule
} }
if (!AsyncLocalStorageUtil.mergeImpl( if (!AsyncLocalStorageUtil.mergeImpl(
mDb, mReactDatabaseSupplier.get(),
keyValueArray.getArray(idx).getString(0), keyValueArray.getArray(idx).getString(0),
keyValueArray.getArray(idx).getString(1))) { keyValueArray.getArray(idx).getString(1))) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null)); callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return; return;
} }
} }
mDb.setTransactionSuccessful(); mReactDatabaseSupplier.get().setTransactionSuccessful();
} catch (Exception e) { } catch (Exception e) {
FLog.w(ReactConstants.TAG, e.getMessage(), e); FLog.w(ReactConstants.TAG, e.getMessage(), e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
} finally { } finally {
mDb.endTransaction(); mReactDatabaseSupplier.get().endTransaction();
} }
callback.invoke(); callback.invoke();
} }
@@ -296,12 +296,12 @@ public final class AsyncStorageModule
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override @Override
protected void doInBackgroundGuarded(Void... params) { protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) { if (!mReactDatabaseSupplier.ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null)); callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return; return;
} }
try { try {
Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null); mReactDatabaseSupplier.get().delete(TABLE_CATALYST, null, null);
} catch (Exception e) { } catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database clear ", e); FLog.w(ReactConstants.TAG, "Exception in database clear ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
@@ -325,7 +325,7 @@ public final class AsyncStorageModule
} }
WritableArray data = Arguments.createArray(); WritableArray data = Arguments.createArray();
String[] columns = {KEY_COLUMN}; String[] columns = {KEY_COLUMN};
Cursor cursor = Assertions.assertNotNull(mDb) Cursor cursor = mReactDatabaseSupplier.get()
.query(TABLE_CATALYST, columns, null, null, null, null, null); .query(TABLE_CATALYST, columns, null, null, null, null, null);
try { try {
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
@@ -345,25 +345,9 @@ public final class AsyncStorageModule
} }
/** /**
* Verify the database exists and is open. * Verify the database is open for reads and writes.
*/ */
private boolean ensureDatabase() { private boolean ensureDatabase() {
if (mShuttingDown) { return !mShuttingDown && mReactDatabaseSupplier.ensureDatabase();
return false;
}
if (mDb != null && mDb.isOpen()) {
return true;
}
mDb = initializeDatabase();
return true;
}
/**
* Create and/or open the database.
*/
private SQLiteDatabase initializeDatabase() {
CatalystSQLiteOpenHelper helperForDb =
new CatalystSQLiteOpenHelper(getReactApplicationContext());
return helperForDb.getWritableDatabase();
} }
} }

View File

@@ -1,53 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.storage;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
// VisibleForTesting
public class CatalystSQLiteOpenHelper extends SQLiteOpenHelper {
// VisibleForTesting
public static final String DATABASE_NAME = "RKStorage";
static final int DATABASE_VERSION = 1;
static final String TABLE_CATALYST = "catalystLocalStorage";
static final String KEY_COLUMN = "key";
static final String VALUE_COLUMN = "value";
static final String VERSION_TABLE_CREATE =
"CREATE TABLE " + TABLE_CATALYST + " (" +
KEY_COLUMN + " TEXT PRIMARY KEY, " +
VALUE_COLUMN + " TEXT NOT NULL" +
")";
private Context mContext;
public CatalystSQLiteOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(VERSION_TABLE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO: t5494781 implement data migration
if (oldVersion != newVersion) {
mContext.deleteDatabase(DATABASE_NAME);
onCreate(db);
}
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.storage;
import javax.annotation.Nullable;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
// VisibleForTesting
public class ReactDatabaseSupplier extends SQLiteOpenHelper {
// VisibleForTesting
public static final String DATABASE_NAME = "RKStorage";
static final int DATABASE_VERSION = 1;
private static final int SLEEP_TIME_MS = 30;
static final String TABLE_CATALYST = "catalystLocalStorage";
static final String KEY_COLUMN = "key";
static final String VALUE_COLUMN = "value";
static final String VERSION_TABLE_CREATE =
"CREATE TABLE " + TABLE_CATALYST + " (" +
KEY_COLUMN + " TEXT PRIMARY KEY, " +
VALUE_COLUMN + " TEXT NOT NULL" +
")";
private Context mContext;
private @Nullable SQLiteDatabase mDb;
public ReactDatabaseSupplier(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(VERSION_TABLE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion != newVersion) {
deleteDatabase();
onCreate(db);
}
}
/**
* Verify the database exists and is open.
*/
/* package */ synchronized boolean ensureDatabase() {
if (mDb != null && mDb.isOpen()) {
return true;
}
// Sometimes retrieving the database fails. We do 2 retries: first without database deletion
// and then with deletion.
SQLiteException lastSQLiteException = null;
for (int tries = 0; tries < 2; tries++) {
try {
if (tries > 0) {
deleteDatabase();
}
mDb = getWritableDatabase();
break;
} catch (SQLiteException e) {
lastSQLiteException = e;
}
// Wait before retrying.
try {
Thread.sleep(SLEEP_TIME_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (mDb == null) {
throw lastSQLiteException;
}
return true;
}
/**
* Create and/or open the database.
*/
/* package */ synchronized SQLiteDatabase get() {
ensureDatabase();
return mDb;
}
/* package */ synchronized boolean deleteDatabase() {
if (mDb != null && mDb.isOpen()) {
mDb.close();
mDb = null;
}
return mContext.deleteDatabase(DATABASE_NAME);
}
}