realm / realm-dart

Realm is a mobile database: a replacement for SQLite & ORMs.
Apache License 2.0
766 stars 87 forks source link

[Bug]: Unable to open realm on background isolate #1055

Closed geosebas closed 1 year ago

geosebas commented 1 year ago

What happened?

Hello,

I'm trying to follow the doc from here : https://www.mongodb.com/docs/realm/sdk/flutter/sync/sync-multiple-processes/

But as soon as I open the disconnected realm, I got this error :

 RealmException: Error opening realm at path /data/user/0/com.example.padel_app/app_flutter/mongodb-realm/flex.realm.
 Error code: 18 . Message: Realm at path '/data/user/0/com.example.padel_app/app_flutter/mongodb-realm/flex.realm'
 already opened with different sync configurations.

My goal is to create complex data from multiple collection, so I need to do this in another Isolate so I don't freeze the UI. To do that I use the compute function from flutter foundation.

Here some of my code :

Synced realm :

Future<Realm> initRealm({String? path}) async {
    final currentUser = _app.currentUser;
    if (currentUser == null) {
      throw Exception('Trying to access realm without logged user');
    }
    this.path = path ?? await absolutePath('flex.realm');
    final config = Configuration.flexibleSync(
        currentUser,
        [
          Account.schema,
          Club.schema,
          Court.schema,
          CourtSlot.schema,
          Match.schema,
          MatchSystem.schema,
          PlayerLevelBySport.schema,
          Player.schema,
          Sport.schema,
          SubscriptionOffer.schema,
          PlayerCredit.schema,
          PlayerSubscription.schema
        ],
        path: this.path);
    final realm = Realm(
      config,
    );
    final customDataSub = realm.subscriptions.findByName('getUser');
    if (customDataSub == null) {
      final identity = currentUser.identities.firstWhere((identity) => identity.provider == AuthProviderType.function);
      realm.subscriptions.update((mutableSubscriptions) {
        mutableSubscriptions.add(realm.query<Player>('_id == "${identity.id}" AND accountId == "$accountId"'),
            name: 'getUser', update: true);
        mutableSubscriptions.add(realm.query<Account>('_id == "$accountId"'), name: 'getAccount', update: true);
      });
      await realm.subscriptions.waitForSynchronization();
    }
    _realm = realm;
    return updateSubscriptions();
  }

DisconnectedRealm :

  Future<Realm> initRealmNoSync({String? path}) async {
    this.path = path ?? await absolutePath('flex.realm');
    final config = Configuration.disconnectedSync([
      Account.schema,
      Club.schema,
      Court.schema,
      CourtSlot.schema,
      Match.schema,
      MatchSystem.schema,
      PlayerLevelBySport.schema,
      Player.schema,
      Sport.schema,
      SubscriptionOffer.schema,
      PlayerCredit.schema,
      PlayerSubscription.schema
    ], path: this.path!);
    final realm = Realm(
      config,
    );
    _realm = realm;
    return _realm!;
  }

Flutter doctor :

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.3.9, on Microsoft Windows [version 10.0.22621.819], locale fr-FR)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop for Windows (Visual Studio Enterprise 2022 17.2.4)
[√] Android Studio (version 2021.3)
[√] VS Code (version 1.73.1)
[√] Connected device (4 available)
[√] HTTP Host Availability

• No issues found!

Repro steps

  1. Create a synced realm
  2. Access the synced realm with disconnected configuration.

Version

0.8.0rc

What Realm SDK flavor are you using?

MongoDB Atlas (i.e. Sync, auth, functions)

What type of application is this?

Flutter Application

Client OS and version

Android 33.0

Code snippets

No response

Stacktrace of the exception/crash you're getting

I/flutter (20742): RealmException: Error opening realm at path /data/user/0/com.example.padel_app/app_flutter/mongodb-realm/flex.realm. Error code: 18 . Message: Realm at path '/data/user/0/com.example.padel_app/app_flutter/mongodb-realm/flex.realm' already opened with different sync configurations.
I/flutter (20742): #0      _RealmCore.throwLastError.<anonymous closure> (package:realm/src/native/realm_core.dart:112:7)
I/flutter (20742): #1      using (package:ffi/src/arena.dart:124:31)
I/flutter (20742): #2      _RealmCore.throwLastError (package:realm/src/native/realm_core.dart:106:5)
I/flutter (20742): #3      _RealmLibraryEx.invokeGetPointer (package:realm/src/native/realm_core.dart:2564:17)
I/flutter (20742): #4      _RealmCore.openRealm (package:realm/src/native/realm_core.dart:584:32)
I/flutter (20742): #5      Realm._openRealm (package:realm/src/realm_class.dart:180:22)
I/flutter (20742): #6      new Realm._ (package:realm/src/realm_class.dart:135:98)
I/flutter (20742): #7      new Realm (package:realm/src/realm_class.dart:133:38)
I/flutter (20742): #8      RealmService.initRealmNoSync (package:padel_app/core/services/realm_service.dart:84:19)
I/flutter (20742): #9      PlayerSearchHelper.initData (package:padel_app/ui/shared/player_search_helper.dart:142:40)
I/flutter (20742): #10     _IsolateConfiguration.applyAndTime.<anonymous closure> (package:flutter/src/foundation/_isolates_io.dart:108:21)
I/flutter (20742): #11     Timeline.timeSync (dart:developer/timeline.dart:160:22)
I/flutter (20742): #12     _IsolateConfiguration.applyAndTim
I/flutter (20742): RealmException: Error opening realm at path /data/user/0/com.example.padel_app/app_flutter/mongodb-realm/flex.realm. Error code: 18 . Message: Realm at path '/data/user/0/com.example.padel_app/app_flutter/mongodb-realm/flex.realm' already opened with different sync configurations.

Relevant log output

No response

nielsenko commented 1 year ago

@geosebas You don't need to use a different config when you are in the same process. Configuration.disconnectedSync is for opening a synchronized realm without connecting to the server and start syncing. A benefit is that you can open the same synchronized realm in multiple processes, but if you have just one process you should just use Configuration.flexibleSync for all realms. Isolates are not true processes and realm-core will share the sync session across isolates at the native layer.

In general DisconnectedSyncConfiguration is mostly intended for less common use-cases, such as writing your own version of Realm Studio, working with multiprocess systems such as Electron, or as in the time_track sample, where the different cli commands use Configuration.disconnectedSync to quickly open, change, and close a realm, that is synced in the background by a deamon process.

nielsenko commented 1 year ago

To be a bit more precise

geosebas commented 1 year ago

Hi @nielsenko.

Thanks for the answer, but I'm not sure to understand something. You first say :

but if you have just one process you should just use Configuration.flexibleSync for all realms.

and then :

When opening a synchronized realm, only one can open it with a FlexibleSyncConfiguration at a time,

Of course I directly tried to use Configuration.flexibleSync just before writing this message but I got the same error.

So the big question is, how can I access my realm from different isolate ?

nielsenko commented 1 year ago

I forgot a process in there, have edited the answer.

You should be able to open the realm again in another isolate in the same process, as long as you use the same config type, ie. FlexibleSyncConfiguration . That said, I tried to create an example of how to do it, and I hit another issue:

..b91b8fd9d77bf/default.realm' already opened with different sync user.)

despite using the exact same user 🤔

I think you have hit a bug here. Will get back when I know more.

geosebas commented 1 year ago

I forgot a process in there, have edited the answer.

Much clearer now :D

That said, I tried to create an example of how to do it, and I hit another issue:

I got this error too ! This is weird because when I print this in both isolate, I got the same result :

    debugPrint(currentUser.id);
    debugPrint(currentUser.identities[0].id);
nielsenko commented 1 year ago

@geosebas I believe this is an issue in realm-core caused by a comparison of std::shared_ptr<SyncUser>s instead of SyncUsers. I have opened a PR https://github.com/realm/realm-core/pull/6087. Let us see what they come back with.

Unlike most SDK we don't use native caching of app instances in realm-dart. This is because they cause trouble when used between different isolates (you may remember the hot-restart crash of 0.3.2+beta). I think that is what causes the current check to be defeated.

The PR fixes it for me, in my test, and passes all existing tests in realm-dart, but let us see how it fares in core.

geosebas commented 1 year ago

Nice I will wait for the fix :)

Thanks for your help and the quick answer !

geosebas commented 1 year ago

Hi @nielsenko ,

Sorry to bother you but do you have any news about this ? Do you know if the fix will be included in the next release ?

On another point, do you know how the SDK handle the call to the manual reset handler when the realm is opened in more than one isolate ? What will happen if more than one clientResetError.resetRealm(); is called ?

Have a nice day :)

nielsenko commented 1 year ago

I got a bit side-tracked, I will return to this now.

geosebas commented 1 year ago

Sorry I pressed 'enter' to soon so I edited my previous message ^^

nielsenko commented 1 year ago

I expect the fix to be included in the next release, but no promises. The second call to clientResetError.resetRealm() will do nothing.

nielsenko commented 1 year ago

I have opened a PR https://github.com/realm/realm-core/pull/6117 to surface the result. Once that land I will change resetRealm to return bool indicating if resetRealm ran or not.

geosebas commented 1 year ago

Nice thanks a lot :)

geosebas commented 1 year ago

Hello there, just wanted to know if you have an idea about when will we have the next release and if this fix will be in the next release.

Sorry to bother you just for that, but I will release soon to production my app, and I don't like the dirty workarround I did for this, so if I can have the fix before going in production, it will be perfect for me :)

And not to bother you again with this type of question, do you have any channels where we can have theses type of info ? Like a discord or slack where we will know when you release, or if you're hold back or anything ? That would be great !

nielsenko commented 1 year ago

@geosebas This is dependent on a new release of realm-core. We have been hesitant to roll a release, since there was a number of issues with the latest realm-core version (13+), after a major refactoring of how frozen objects affects version pinning. These are slowly and steadily being worked through.

I cannot give dates, but we are getting close.

nielsenko commented 1 year ago

@geosebas This is fixed in 0.10.0+rc

nielsenko commented 1 year ago
import 'dart:async';
import 'dart:isolate';

import 'package:realm_dart/realm.dart';

part 'isolate_test.g.dart';

@RealmModel()
class _Stuff {
  late int no;

  @override
  String toString() => 'Stuff($no)';
}

// Note this is per isolate!
final realm = Realm(Configuration.inMemory([Stuff.schema]));

StreamSubscription monitorChanges(String isolateTag) {
  return realm.all<Stuff>().changes.listen((event) {
    print('$isolateTag: ${event.inserted}');
  });
}

extension on StreamSubscription {
  Future<void> cancelIn(int seconds) async {
    await Future.delayed(Duration(seconds: seconds));
    await cancel();
  }
}

Future<void> main(List<String> arguments) async {
  final s = monitorChanges('root');

  await Isolate.spawn((message) async {
    final s = monitorChanges('spawned');

    realm.write(() => realm.addAll([
          Stuff(1),
          Stuff(2),
        ]));

    await s.cancelIn(1);
  }, null);

  await s.cancelIn(1);

  for (final stuff in realm.all<Stuff>()) {
    print(stuff);
  }

  Realm.shutdown();
}

Outputs:

root: []
spawned: []
root: [0, 1]
spawned: [0, 1]
Stuff(1)
Stuff(2)

Exited.