From 33cc607c1f4856d3912cc8a84a92fd756ba667c0 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Thu, 24 Sep 2015 02:43:25 -0700 Subject: [PATCH] AsyncStorage improvements Differential Revision: D2475604 committer: Service User --- .../storage/AsyncLocalStorageUtil.java | 6 +- .../modules/storage/AsyncStorageModule.java | 76 +++++-------- .../storage/CatalystSQLiteOpenHelper.java | 53 --------- .../storage/ReactDatabaseSupplier.java | 106 ++++++++++++++++++ 4 files changed, 139 insertions(+), 102 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java index 36340f0aa..34fc09091 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java @@ -24,9 +24,9 @@ import com.facebook.react.bridge.ReadableArray; import org.json.JSONException; import org.json.JSONObject; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; /** * Helper for database operations. diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java index 601528a0f..c5ecd8985 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java @@ -9,16 +9,12 @@ package com.facebook.react.modules.storage; -import javax.annotation.Nullable; - import java.util.HashSet; import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; 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.modules.common.ModuleDataCleaner; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; -import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST; +import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN; public final class AsyncStorageModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { - private @Nullable SQLiteDatabase mDb; + private ReactDatabaseSupplier mReactDatabaseSupplier; private boolean mShuttingDown = false; public AsyncStorageModule(ReactApplicationContext reactContext) { super(reactContext); + mReactDatabaseSupplier = new ReactDatabaseSupplier(reactContext); } @Override @@ -59,10 +56,6 @@ public final class AsyncStorageModule @Override public void onCatalystInstanceDestroy() { mShuttingDown = true; - if (mDb != null && mDb.isOpen()) { - mDb.close(); - mDb = null; - } } @Override @@ -74,10 +67,17 @@ public final class AsyncStorageModule new Callback() { @Override public void invoke(Object... args) { - if (args.length > 0) { - throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]); + if (args.length == 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}; HashSet keysRemaining = SetBuilder.newHashSet(); WritableArray data = Arguments.createArray(); - Cursor cursor = Assertions.assertNotNull(mDb).query( + Cursor cursor = mReactDatabaseSupplier.get().query( TABLE_CATALYST, columns, AsyncLocalStorageUtil.buildKeySelection(keys.size()), @@ -171,8 +171,8 @@ public final class AsyncStorageModule } String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; - SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql); - mDb.beginTransaction(); + SQLiteStatement statement = mReactDatabaseSupplier.get().compileStatement(sql); + mReactDatabaseSupplier.get().beginTransaction(); try { for (int idx=0; idx < keyValueArray.size(); idx++) { if (keyValueArray.getArray(idx).size() != 2) { @@ -193,12 +193,12 @@ public final class AsyncStorageModule statement.bindString(2, keyValueArray.getArray(idx).getString(1)); statement.execute(); } - mDb.setTransactionSuccessful(); + mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); } finally { - mDb.endTransaction(); + mReactDatabaseSupplier.get().endTransaction(); } callback.invoke(); } @@ -224,7 +224,7 @@ public final class AsyncStorageModule } try { - Assertions.assertNotNull(mDb).delete( + mReactDatabaseSupplier.get().delete( TABLE_CATALYST, AsyncLocalStorageUtil.buildKeySelection(keys.size()), AsyncLocalStorageUtil.buildKeySelectionArgs(keys)); @@ -250,7 +250,7 @@ public final class AsyncStorageModule callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } - Assertions.assertNotNull(mDb).beginTransaction(); + mReactDatabaseSupplier.get().beginTransaction(); try { for (int idx = 0; idx < keyValueArray.size(); idx++) { if (keyValueArray.getArray(idx).size() != 2) { @@ -269,19 +269,19 @@ public final class AsyncStorageModule } if (!AsyncLocalStorageUtil.mergeImpl( - mDb, + mReactDatabaseSupplier.get(), keyValueArray.getArray(idx).getString(0), keyValueArray.getArray(idx).getString(1))) { callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } } - mDb.setTransactionSuccessful(); + mReactDatabaseSupplier.get().setTransactionSuccessful(); } catch (Exception e) { FLog.w(ReactConstants.TAG, e.getMessage(), e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); } finally { - mDb.endTransaction(); + mReactDatabaseSupplier.get().endTransaction(); } callback.invoke(); } @@ -296,12 +296,12 @@ public final class AsyncStorageModule new GuardedAsyncTask(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { - if (!ensureDatabase()) { + if (!mReactDatabaseSupplier.ensureDatabase()) { callback.invoke(AsyncStorageErrorUtil.getDBError(null)); return; } try { - Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null); + mReactDatabaseSupplier.get().delete(TABLE_CATALYST, null, null); } catch (Exception e) { FLog.w(ReactConstants.TAG, "Exception in database clear ", e); callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); @@ -325,7 +325,7 @@ public final class AsyncStorageModule } WritableArray data = Arguments.createArray(); String[] columns = {KEY_COLUMN}; - Cursor cursor = Assertions.assertNotNull(mDb) + Cursor cursor = mReactDatabaseSupplier.get() .query(TABLE_CATALYST, columns, null, null, null, null, null); try { 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() { - if (mShuttingDown) { - 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(); + return !mShuttingDown && mReactDatabaseSupplier.ensureDatabase(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java deleted file mode 100644 index facf52e15..000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java +++ /dev/null @@ -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); - } - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java new file mode 100644 index 000000000..be14cc3aa --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java @@ -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); + } +}