Search code examples
androidsqliteandroid-contentprovider

SQLite DB with Content Provider race conditions


I implemented Content Provider that uses a SQLiteDatabase as its backing data source.

One activity writing to the DB by calling getContentResolver().applyBatch(operations), which should be atomic.

protected void onPause() {
    new Thread(){
        @Override
        public void run() {
            ArrayList<ContentProviderOperation> ops = new ArrayList<>();
            ContentProviderOperation.Builder builder;
            for (Tag tag: mTopicAdapter.getTags()) {
                builder = ContentProviderOperation.newUpdate(QuizProvider.TAG_URI);
                builder.withValue(Tag.Table.SELECTED, tag.getSelectionStatus());
                builder.withSelection(Tag.Table._ID + " = " + tag.getId(), null);
                ops.add(builder.build());
            }
            try {
                ContentProviderResult[] res = getContentResolver().applyBatch(QuizProvider.AUTHORITY, ops);
                Timber.d("Update result: %d", res.length);
                getContentResolver().notifyChange(QuizProvider.TAG_URI, null);
                getContentResolver().notifyChange(QuizProvider.QUESTION_URI, null);
            } catch (RemoteException e) {
                e.printStackTrace();
            } catch (OperationApplicationException e) {
                e.printStackTrace();
            }
        }
    }.start();
    super.onPause();
}

Second activity reading from the DB with the help of Cursor Loader, and sometimes gets old data (race condition).

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    Uri randQuestionUri = QuizProvider.QUESTION_URI
            .buildUpon()
            .appendPath("rand").appendPath(Integer.toString(QUIZ_SIZE))
            .build();
    return new CursorLoader(this, randQuestionUri, null, null, null, null);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    if (DEBUG) Timber.d("load finished: %d", data.hashCode());
    mPagerAdapter.swapCursor(data);
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mPagerAdapter.swapCursor(null);
}

Full project here.

Log output:

NSA:QuizProvider:138: update db: selected=false _id = 4
NSA:QuizProvider:138: update db: selected=false _id = 5
NSA:QuizProvider:138: update db: selected=false _id = 26
NSA:QuizProvider:138: update db: selected=false _id = 19
NSA:QuizProvider:138: update db: selected=false _id = 28
NSA:QuizProvider:138: update db: selected=false _id = 10
NSA:QuizProvider:138: update db: selected=false _id = 12
NSA:QuizProvider:138: update db: selected=false _id = 15
NSA:QuizProvider:138: update db: selected=false _id = 18
NSA:QuizProvider:138: update db: selected=false _id = 25
NSA:QuizProvider:138: update db: selected=false _id = 16
NSA:QuizProvider:138: update db: selected=false _id = 17
NSA:QuizProvider:138: update db: selected=false _id = 8
NSA:QuizProvider:138: update db: selected=false _id = 3
NSA:QuizProvider:138: update db: selected=false _id = 20
NSA:QuizProvider:138: update db: selected=false _id = 29
NSA:QuizProvider:138: update db: selected=false _id = 24
NSA:QuizProvider:138: update db: selected=false _id = 23
NSA:QuizProvider:138: update db: selected=false _id = 30
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuizProvider:103: query db: content://doit.study.droi    question/ran    280 null null
NSA:QuizProvider:138: update db: selected=false _id = 6
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuizProvider:138: update db: selected=false _id = 1
NSA:QuizProvider:138: update db: selected=false _id = 14
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuizProvider:138: update db: selected=false _id = 7
NSA:QuizProvider:138: update db: selected=false _id = 27
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuestionsActivity:84: load finished: 154982045
NSA:QuestionsPagerAdapter:66: swap cursor, cnt: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:35: instantiateItem, pos=0
NSA:QuestionsPagerAdapter:25: getItem, pos=0
NSA:QuestionsPagerAdapter:35: instantiateItem, pos=1
NSA:QuestionsPagerAdapter:25: getItem, pos=1
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuizProvider:138: update db: selected=false _id = 2
NSA:QuizProvider:138: update db: selected=false _id = 11
NSA:QuizProvider:138: update db: selected=false _id = 22
NSA:QuizProvider:138: update db: selected=false _id = 9
NSA:QuizProvider:138: update db: selected=false _id = 31
NSA:QuizProvider:138: update db: selected=false _id = 21
NSA:QuizProvider:138: update db: selected=false _id = 32
NSA:QuizProvider:138: update db: selected=false _id = 13
NSA:QuizProvider:138: update db: selected=false _id = 4
W/FragmentManager: moveToState: Fragment state for QuestionFragment{5c25e01 #0 id=0x7f0f00e5} not updated inline; expected state 3 found 2
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:52: title pos: 0, questions: tags: [User Interfaces]
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:52: title pos: 1, questions: tags: [User Interfaces]
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuizProvider:103: query db: content://doit.study.droi    tag null null
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuizProvider:138: update db: selected=false _id = 5
NSA:QuizProvider:138: update db: selected=false _id = 26
NSA:QuizProvider:103: query db: content://doit.study.droi    tag null null
NSA:QuizProvider:138: update db: selected=false _id = 19
NSA:QuizProvider:138: update db: selected=false _id = 28
NSA:QuizProvider:138: update db: selected=false _id = 10
NSA:QuizProvider:138: update db: selected=false _id = 12
NSA:QuizProvider:138: update db: selected=false _id = 15
NSA:QuizProvider:138: update db: selected=false _id = 18
NSA:QuizProvider:138: update db: selected=false _id = 25
NSA:QuizProvider:138: update db: selected=false _id = 16
NSA:QuizProvider:138: update db: selected=false _id = 17
NSA:QuizProvider:138: update db: selected=false _id = 8
NSA:QuizProvider:138: update db: selected=false _id = 3
NSA:QuizProvider:138: update db: selected=false _id = 20
NSA:QuizProvider:138: update db: selected=false _id = 29
NSA:QuizProvider:138: update db: selected=false _id = 24
NSA:QuizProvider:138: update db: selected=false _id = 23
NSA:QuizProvider:138: update db: selected=false _id = 30
NSA:QuizProvider:138: update db: selected=false _id = 6
NSA:QuizProvider:138: update db: selected=false _id = 1
NSA:QuizProvider:138: update db: selected=false _id = 14
NSA:QuizProvider:138: update db: selected=false _id = 7
NSA:QuizProvider:138: update db: selected=false _id = 27
NSA:QuizProvider:138: update db: selected=false _id = 2
NSA:QuizProvider:138: update db: selected=false _id = 11
NSA:QuizProvider:138: update db: selected=false _id = 22
NSA:QuizProvider:138: update db: selected=false _id = 9
NSA:QuizProvider:138: update db: selected=false _id = 31
NSA:QuizProvider:138: update db: selected=false _id = 21
NSA:QuizProvider:138: update db: selected=false _id = 32
NSA:QuizProvider:138: update db: selected=false _id = 13
NSA:QuizProvider:103: query db: content://doit.study.droi    question/ran    280 null null
NSA:QuestionsActivity:84: load finished: 19434496
NSA:QuestionsPagerAdapter:66: swap cursor, cnt: 0
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuestionsPagerAdapter:42: counter: 0

Log shows that applyBatch is not finished and Cursor Loader gets partially modified data (cursor counter=104, should be 0 or 280).

Some resources (sorry, cannot add more then two links):

_http://developer.android.com/guide/topics/providers/content-provider-basics.html#Batch

_http://www.androiddesignpatterns.com/2012/10/sqlite-contentprovider-thread-safety.html

_http://stackoverflow.com/questions/8104832/sqlite-simultaneous-reading-and-writing

_http://www.grokkingandroid.com/better-performance-with-contentprovideroperation/

Do you have any ideas what's wrong?


Solution

  • There are basically three things to consider here:

    • Move those notifyChange() calls to your ContentProvider. That way you can be sure that they are called whenever you make a change. Or in other words: You might forget to do so somewhere else. The client shouldn't be responsible for this - it doesn't belong here.
    • Ensure that your applyBatch() method actually uses transactions. only then do you get the desired performance benefits and only then locks are used in the way you need.
    • Ensure that while applyBatch() is running, no notifications are emitted. Otherwise your Loader would be called too often. You definitely want to avoid that.

    You can see my cpsample project for a content provider that follows these rules.