realm / realm-java

Realm is a mobile database: a replacement for SQLite & ORMs
http://realm.io
Apache License 2.0
11.46k stars 1.75k forks source link

Realm is not Closed but Illegal State: The Realm has been closed and is no longer accessible. #2953

Closed andhie closed 2 years ago

andhie commented 8 years ago

Goal

my app needs to get configuration from server to create a Table/Schema after API call. since the schema is a job specific, another job will have a slight different schema. so i need to delete Schema once user is done with a job.

e.g. Product class schema. (not all product have expiry, may not always appear)

[
  {
    "Id":"1",
    "Name":"ProductName"
  },
  { 
    "Id":"12",
    "Name":"Expiry"
  }
]

Expected Results

Able to dynamically create a table/schema, perform CRUD and delete Schema when user is done.

Actual Results

E.g. full stack trace with exception

com.example.realm I/tag: John
com.example.realm I/tag: isClosed = false
com.example.realm I/tag: schema removed, isClosed = false
com.example.realm D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true

   [ 06-07 18:55:00.921 26948:26948 D/         ]
   HostConnection::get() New Host Connection established 0x7fe9ecc0e600, tid 26948
com.example.realm D/REALM: jni: ThrowingException 8, The Realm has been closed and is no longer accessible., .
com.example.realm D/REALM: Exception has been throw: Illegal State: The Realm has been closed and is no longer accessible.
com.example.realm D/AndroidRuntime: Shutting down VM

   --------- beginning of crash
06-07 18:55:00.961 26948-26948/com.example.realm E/AndroidRuntime: FATAL EXCEPTION: main
      Process: com.example.realm, PID: 26948
      java.lang.IllegalStateException: Illegal State: The Realm has been closed and is no longer accessible.
       at io.realm.internal.TableView.nativeSyncIfNeeded(Native Method)
       at io.realm.internal.TableView.syncIfNeeded(TableView.java:831)
       at io.realm.RealmResults.syncIfNeeded(RealmResults.java:618)
       at io.realm.RealmResults.notifyChangeListeners(RealmResults.java:1001)
       at io.realm.RealmResults.notifyChangeListeners(RealmResults.java:996)
       at io.realm.HandlerController.notifyRealmResultsCallbacks(HandlerController.java:303)
       at io.realm.HandlerController.notifySyncRealmResultsCallbacks(HandlerController.java:284)
       at io.realm.HandlerController.notifyTypeBasedListeners(HandlerController.java:275)
       at io.realm.HandlerController.notifyAllListeners(HandlerController.java:262)
       at io.realm.HandlerController.realmChanged(HandlerController.java:385)
       at io.realm.HandlerController.handleMessage(HandlerController.java:116)
       at android.os.Handler.dispatchMessage(Handler.java:98)
       at android.os.Looper.loop(Looper.java:148)
       at android.app.ActivityThread.main(ActivityThread.java:5417)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

Steps & Code to Reproduce

Run the code, it crashes immediately. but it successfully log all action.

Code Sample

public class MainActivity extends AppCompatActivity {

    DynamicRealm realm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RealmConfiguration realmConfig = new RealmConfiguration.Builder(this).build();
        realm = DynamicRealm.getInstance(realmConfig);

        realm.executeTransaction(new DynamicRealm.Transaction() {
            @Override
            public void execute(DynamicRealm realm) {
                RealmSchema schema = realm.getSchema();
                // remove old schema, or we get exception for testing
                if (schema.contains("Person")) schema.remove("Person");

                // Create Person class with two fields: name and age
                schema.create("Person")
                        .addField("name", String.class)
                        .addField("age", int.class);

                // Fields with special properties
                schema.get("Person")
                        .addField("id", long.class, FieldAttribute.PRIMARY_KEY);

            }
        });

        realm.executeTransaction(new DynamicRealm.Transaction() {
            @Override
            public void execute(DynamicRealm realm) {
                // Creating new data during migrations
                DynamicRealmObject person = realm.createObject("Person");
                person.setString("name", "John");
            }
        });

        RealmResults<DynamicRealmObject> persons = realm.where("Person")
                .findAll();

        for (DynamicRealmObject ppl : persons) {
            Log.i("tag", ppl.getString("name"));
        }

        Log.i("tag", "isClosed = " + realm.isClosed());
        realm.executeTransaction(new DynamicRealm.Transaction() {
            @Override
            public void execute(DynamicRealm realm) {
                realm.getSchema().remove("Person");
                Log.i("tag", "schema removed, isClosed = " + realm.isClosed());
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        realm.close();
    }
}

Version of Realm and tooling

Realm version(s): classpath "io.realm:realm-gradle-plugin:1.0.0"

Android Studio version: 2.1.2

Which Android version and device: Android Emulator version 25.1.7, Version 6.0 - API 23 (Rev 1)

stk1m1 commented 8 years ago

Hi @andhie

Thanks for the detailed issue report. I'll reproduce your issue with the sample above.

One question. Why would you need to recreate the Person schema in run time? Is there any reason you don't inherit and create a Person class from RealmObject?

andhie commented 8 years ago

Why would you need to recreate the Person schema in run time?

As mentioned in the Goals, when user starts a Job, the Product varies between each Job. It would be better to clear off the older schema and rebuild with the new Product schema. Note: Person in the code are put together from Realm samples.

the app doesnt really know the Columns are available as its determined by Server. The API will tell the app what columns/fields its requires.

stk1m1 commented 8 years ago

@andhie

I have reproduce the issue with the sample you provided. This is being escalated for further investigation. Thanks again for the detailed report. 👍

andhie commented 8 years ago

There is actually another bug this sample. at least it produced a different stacktrace and failing point.

Step: Comment out the last transaction block (that deletes the schema) as below and run again

//        Log.i("tag", "isClosed = " + realm.isClosed());
//        realm.executeTransaction(new DynamicRealm.Transaction() {
//            @Override
//            public void execute(DynamicRealm realm) {
//                realm.getSchema().remove("Person");
//                Log.i("tag", "schema removed, isClosed = " + realm.isClosed());
//            }
//        });

Observation: Immediately crash also

com.example.realm D/dalvikvm: Trying to load lib /data/app-lib/com.example.realm-2/librealm-jni.so 0x41fcb348
com.example.realm D/dalvikvm: Added shared lib /data/app-lib/com.example.realm-2/librealm-jni.so 0x41fcb348
com.example.realm D/REALM: Table 0x4f1a7a58 is no longer attached!
com.example.realm D/REALM: jni: ThrowingException 8, Table is no longer valid to operate on., .
com.example.realm D/REALM: Exception has been throw: Illegal State: Table is no longer valid to operate on.
com.example.realm D/AndroidRuntime: Shutting down VM
com.example.realm W/dalvikvm: threadid=1: thread exiting with uncaught exception (group=0x418f6300)
com.example.realm E/AndroidRuntime: FATAL EXCEPTION: main
       java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.realm/com.example.realm.MainActivity}: java.lang.IllegalStateException: Illegal State: Table is no longer valid to operate on.
           at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2102)
           at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2127)
           at android.app.ActivityThread.access$600(ActivityThread.java:136)
           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1228)
           at android.os.Handler.dispatchMessage(Handler.java:99)
           at android.os.Looper.loop(Looper.java:137)
           at android.app.ActivityThread.main(ActivityThread.java:4818)
           at java.lang.reflect.Method.invokeNative(Native Method)
           at java.lang.reflect.Method.invoke(Method.java:511)
           at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
           at dalvik.system.NativeStart.main(Native Method)
        Caused by: java.lang.IllegalStateException: Illegal State: Table is no longer valid to operate on.
           at io.realm.internal.Table.nativeAddEmptyRow(Native Method)
           at io.realm.internal.Table.addEmptyRow(Table.java:379)
           at io.realm.DynamicRealm.createObject(DynamicRealm.java:80)
           at com.example.realm.MainActivity$2.execute(MainActivity.java:57)
           at io.realm.DynamicRealm.executeTransaction(DynamicRealm.java:165)
           at com.example.realm.MainActivity.onCreate(MainActivity.java:53)
           at android.app.Activity.performCreate(Activity.java:5062)
           at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1082)
           at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2066)
           at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2127) 
           at android.app.ActivityThread.access$600(ActivityThread.java:136) 
           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1228) 
           at android.os.Handler.dispatchMessage(Handler.java:99) 
           at android.os.Looper.loop(Looper.java:137) 
           at android.app.ActivityThread.main(ActivityThread.java:4818) 
           at java.lang.reflect.Method.invokeNative(Native Method) 
           at java.lang.reflect.Method.invoke(Method.java:511) 
           at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786) 
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553) 
           at dalvik.system.NativeStart.main(Native Method) 

The offending line stated in the stack trace is this code

// Creating new data during migrations
DynamicRealmObject person = realm.createObject("Person");

Thanks again for the detailed report. 👍

No problem. Filing bugs is my hobby :P

stk1m1 commented 8 years ago

@andhie That's one stone for two birds. I'll look into it too. Appreciated.

cmelchior commented 8 years ago

Hi @andhie We found the problem. You are creating a query here: RealmResults<DynamicRealmObject> persons = realm.where("Person").findAll();

But then deletes the Person schema right afterwards: realm.getSchema().remove("Person"); This means that the query no longer knows how to listen to updates and then throws an incorrect IllegalStateException. The problem isn't that the realm is closed, but that the table reference no longer exists. Even if you create the table again, the old reference doesn't know this.

I can see why you would expect this to work in the code given above, and I can think of 3 ways we can solve this:

1) Properly detect if the underlying table is no longer available and start treating the RealmResults as an empty list from that point on (we should probably log a warning in the log though).

2) Throw a proper illegalStateException with a better error message.

3) Try to dynamically detect that a Table have been removed and re-added.

The problem with 2) is that we depend on the GC to cleanup any lingering RealmResults, so even if you no longer use it, it might still be valid for being notified about changes.

I am not sure what I think about 3) as it wouldn't be easy to do for a chain of queries.

So I am leaning towards 1) ... It also makes sense IMO that you need to re-create an query after manipulating the schema, but what do you think @realm/java @stk1m1 ?

stk1m1 commented 8 years ago

1) Properly detect if the underlying table is no longer available and start treating the RealmResults as an empty list from that point on (we should probably log a warning in the log though).

My vote goes to 1.

beeender commented 8 years ago

1) Properly detect if the underlying table is no longer available and start treating the RealmResults as an empty list from that point on (we should probably log a warning in the log though).

1 is the way how we treat the results built on a deleted LinkView, so we probably should do the same.

andhie commented 8 years ago

As i know there isnt a way to "close" a RealmResults like cursor.close() right? If so i cant prevent any crashes.

1st solution should be good enough with error logs