objectbox / objectbox-java

Android Database - first and fast, lightweight on-device vector database
https://objectbox.io
Apache License 2.0
4.38k stars 302 forks source link

io.objectbox.exception.FileCorruptException: Could not get from cursor (corrupted) (error code -30796) #926

Open tomwassink opened 3 years ago

tomwassink commented 3 years ago

In my Pre-launch report details I see continuously crashes on Android 8.1 phones. I cannot reproduce it on a Emulator with SDK 27. Stacktrace error (see for log details below): io.objectbox.exception.FileCorruptException: Could not get from cursor (corrupted) (error code -30796)

Is this a issue with pre-launch testing or might this occur on real devices as well? After searching I found multiple issues with Android 8.1. Any clue?

I'm using a read-only box from a fragment (write box is in a remote service).

Code Initialization done in MainActivity class after being connected to Service (remote process). The method initMainActivity is called.

protected void initMainActivity() {
        if (!mInitMainActivity) {
            JepsterManager jepsterManager = (JepsterManager) getApplication();
            jepsterManager.initReadOnlyBox();
            //Check if version is updated and update information has to be shown
            JepsterVersionUtilities.checkVersion(this, mService, mPreferences);
            //Check version to verify if preference migration is needed
            PrefsMigrationUtilities.checkVersion(this, mPreferences);

            setNavigationHeader();

            mInitMainActivity = true;

            //ObjectBox need to be available, JepsterService has to be started first to be able to update schema of Box.
            if (mOnPostResume){
                selectDrawerItem(mMenuItemId);
            }
        }
    }
public class ObjectBox {
    private static BoxStore boxStore;
    private static int messageId = 0;

    public static void init(Context context) {
        if (boxStore == null) {
            boxStore = MyObjectBox.builder()
                    .androidContext(context.getApplicationContext())
                    .androidReLinker(ReLinker.log(new ReLinker.Logger() {
                        @Override
                        public void log(String message) {
                            Log.d(Constants.TAG, message);
                        }
                    }))
                    .build();

            if (BuildConfig.DEBUG) {
                boolean started = new AndroidObjectBrowser(boxStore).start(context);
            }
        }
    }

    public static void initReadOnly(Context context) {
        if (boxStore == null) {
            boxStore = MyObjectBox.builder()
                    .androidContext(context.getApplicationContext())
                    .androidReLinker(ReLinker.log(new ReLinker.Logger() {
                        @Override
                        public void log(String message) {
                            Log.d(Constants.TAG, message);
                        }
                    }))
                    .readOnly()
                    .build();

            if (BuildConfig.DEBUG) {
                boolean started = new AndroidObjectBrowser(boxStore).start(context);
            }
        }
    }

    public static BoxStore get() {
        return boxStore;
    }

    public static int getMessageId(){
        messageId ++;
        return messageId;
    }
}
public int getDataFieldValue(int bike, int page, int dataField){
        Box<BikeDataFieldEntity> bikeDataFieldBox = getBikeDataFieldBox();

        //Search data field ID
        QueryBuilder<BikeDataFieldEntity> builder = bikeDataFieldBox.query();
        builder.equal(BikeDataFieldEntity_.bike, bike)
                .equal(BikeDataFieldEntity_.page, page)
                .equal(BikeDataFieldEntity_.dataField, dataField);
        Query<BikeDataFieldEntity> query = builder.build();
        BikeDataFieldEntity bikeDataField = query.findFirst(); (**Line 321**)

        return bikeDataField.getDataFieldValue();
    }
public Box<BikeDataFieldEntity> getBikeDataFieldBox(){
        if (mBikeDataFieldBox == null){
            mBikeDataFieldBox = ObjectBox.get().boxFor(BikeDataFieldEntity.class);
        }
        return mBikeDataFieldBox;
    }

Logs, stack traces

io.objectbox.exception.FileCorruptException: Could not get from cursor (corrupted) (error code -30796)
    at io.objectbox.query.Query.nativeFindFirst(Native Method)
    at io.objectbox.query.Query.lambda$findFirst$0$Query(Query.java:153)
    at io.objectbox.query.-$$Lambda$Query$U3Pw2NlOHcqDE3imeFDwfgwkeJw.call(Unknown Source:2)
    at io.objectbox.BoxStore.callInReadTx(BoxStore.java:813)
    at io.objectbox.BoxStore.callInReadTxWithRetry(BoxStore.java:759)
    at io.objectbox.query.Query.callInReadTx(Query.java:275)
    at io.objectbox.query.Query.findFirst(Query.java:151)
    at com.twom.bico.pages.FragmentPageDataFields.getDataFieldValue(FragmentPageDataFields.java:321)
    at com.twom.bico.pages.FragmentPageDataFields.initViews(FragmentPageDataFields.java:111)
    at com.twom.bico.pages.FragmentPage9DataFields.onCreateView(FragmentPage9DataFields.java:31)
    at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2698)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:320)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1356)
    at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1434)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1497)
    at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:447)
    at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2169)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1992)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1947)
    at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1818)
    at androidx.fragment.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:303)
    at androidx.fragment.app.FragmentStatePagerAdapter.finishUpdate(FragmentStatePagerAdapter.java:270)
greenrobot commented 3 years ago

Android 8.1 - it's not the first time that this version makes trouble. Also the devices you mentioned seem familiar - I think those corrupted files from time to time... All we can do is to try to work around a likely broken file system.

Please try the following:

https://github.com/objectbox/objectbox-examples/blob/5898d12d658866235a165468373bd63708f6afe7/android-app/src/main/java/io/objectbox/example/ObjectBox.java#L17-L28

Also, are you ensuring that the writing process is setting up the database completely, before the read-only process accesses it? If that doesn't happen, the read-only process can not initialize the database (it's read-only after all...).

tomwassink commented 3 years ago

Thanks for your fast reply. I tried your workaround, but the issues stayed the same. I published the app also in the Play store (small roll out) but got (relatively) quite a lot of crash reports.

I just published another update with a normal box for the read only activity (so called just ObjectBox.init() instead of ObjectBox.initReadOnly(). Write actions are only done by the remote process. The remote process will also first build a BoxStore as well.

This seems to work (no issues in the pre-launch report anymore). Reported crashes is just only one up to now. But it seems to be another issue (or not?).

java.util.concurrent.TimeoutException: 
  at io.objectbox.BoxStore.close (BoxStore.java:554)
  at io.objectbox.BoxStore.finalize (BoxStore.java:403)
  at java.lang.Daemons$FinalizerDaemon.doFinalize (Daemons.java:289)
  at java.lang.Daemons$FinalizerDaemon.runInternal (Daemons.java:276)
  at java.lang.Daemons$Daemon.run (Daemons.java:137)
  at java.lang.Thread.run (Thread.java:919)

I will post an update within a couple of days to see if there are still issues.

greenrobot commented 3 years ago

Are you saying the read-only store might potentially cause a higher frequency of FileCorruptException? And thanks for keeping us updated.

TimeoutException: yeah likely a separate issue

tomwassink commented 3 years ago

Update: up to 2020-11-18 11:40h no new crashes reported with FileCorruptionException. So it seems that it is somehow related to readOnly vs. "write" box.

Only one DbException:

io.objectbox.exception.DbException: 
  at io.objectbox.BoxStore.verifyNotAlreadyOpen (BoxStore.java:332)
  at io.objectbox.BoxStore.<init> (BoxStore.java:261)
  at io.objectbox.BoxStoreBuilder.build (BoxStoreBuilder.java:498)
  at com.twom.bico.helper.ObjectBox.init (ObjectBox.java:34)
tomwassink commented 3 years ago

Update 2020-11-22 10:40h: still no new FileCorruptoinExceptions. I only had 4 users with a crash with a DbException within the verifyNotAlreadyOpen method and 2 users with a crash after a TimeOutExceptoin within the close method.

greenrobot-team commented 3 years ago

As

it sounds like a concurrency issue.

As @greenrobot mentioned, do you ensure that ObjectBox.init() completes before ObjectBox.initReadOnly() is called? (Your code examples only show the read-only part, no?)

But yeah, if in pre-launch test it only affects Android 8.1 (they also run on Android 8.0 and 9.0 AFAIK) it might be an Android 8.1 file system specific bug we are not aware of, yet.

Edit: given the verifyNotAlreadyOpen exception I'm wondering if your code closes and re-builds the BoxStore at some point?

tomwassink commented 3 years ago

What I did:

I also tried to execute the same query as in the getDataFieldValue method just before returning the messenger from the service:

@Override
    public IBinder onBind(Intent intent){
        //Make sure database is ready before read only box is initiated
        //ObjectBoxUtilities.isDatabaseReady();
        return mMessenger.getBinder();
    }

But I had still the issues when creating the readOnly BoxStore.

// verifyNotAlreadyOpen exception log:

io.objectbox.exception.DbException: 
  at io.objectbox.BoxStore.verifyNotAlreadyOpen (BoxStore.java:332)
  at io.objectbox.BoxStore.<init> (BoxStore.java:261)
  at io.objectbox.BoxStoreBuilder.build (BoxStoreBuilder.java:498)
  at com.twom.bico.helper.ObjectBox.init (ObjectBox.java:34)
  at com.twom.bico.JepsterManager.initReadOnlyBox (JepsterManager.java:102)
  at com.twom.bico.MainActivity.initMainActivity (MainActivity.java:373)
  at com.twom.bico.AbstractInterProcessCommunicationActivity$1.onServiceConnected (AbstractInterProcessCommunicationActivity.java:122)
  at android.app.LoadedApk$ServiceDispatcher.doConnected (LoadedApk.java:1962)
  at android.app.LoadedApk$ServiceDispatcher$RunConnection.run (LoadedApk.java:1994)
  at android.os.Handler.handleCallback (Handler.java:883)
  at android.os.Handler.dispatchMessage (Handler.java:100)
  at android.os.Looper.loop (Looper.java:241)
  at android.app.ActivityThread.main (ActivityThread.java:7582)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:941)

I do not close the box in my app. I only create a BoxStore in the ObjectBox class as posted. There is always a check if boxStore is null.