Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 Bug Report: RealtimeSubscription fails to work when document is being listened to multiple times by the same client #6515

Open
2 tasks done
arrowxpr opened this issue Oct 6, 2023 · 9 comments
Assignees
Labels
api / realtime Fixes and upgrades for the Appwrite Realtime API. bug Something isn't working

Comments

@arrowxpr
Copy link

arrowxpr commented Oct 6, 2023

馃憻 Reproduction steps

  • Appwrite Console 1.4.3 (Self-hosted)
  • Flutter 3.13.6
  • Dart 3.1.3

Plugins:

  • appwrite: ^11.0.0
  • dart_appwrite: ^10.0.0

Let's get some stuff done first

1. Create a database with the ID itemListDatabaseId

2. Create a collection inside it with the ID itemListCollectionId

3. Add some random attributes (e.g. name)

4. Create a new document with the ID myNewDocument

5. Prepare an Appwrite server client, I'm using Dart, and add the following code:

*The point here is to trigger some automatic document update while we are busy checking if realtime is doing its job in our Flutter app or not. (We will run this code when the Flutter app is ready to show our documents)

  for (int i = 0; i < 1000; i++) {
    await databases.updateDocument(
      databaseId: itemListDatabaseId,
      collectionId: itemListCollectionId,
      documentId: 'myNewDocument',
      data: {
        'name': 'My Document was updated $i times!',
      },
    );
    await Future.delayed(const Duration(seconds: 1));
  }

6. Copy paste the following code into your Flutter project and implement the remaining TODO


class ItemListViewScreen extends StatefulWidget {
  const ItemListViewScreen({super.key});

  @override
  State<ItemListViewScreen> createState() => _ItemListViewScreenState();
}

class _ItemListViewScreenState extends State<ItemListViewScreen> {
  RealtimeSubscription? realtimeSubscription;

  @override
  void initState() {
    super.initState();
    realtimeSubscription = realtime.subscribe([
      'databases.[itemListDatabaseId].collections.[itemListCollectionId].documents'
    ]);
    realtimeSubscription?.stream.listen(
      (event) {
        // print('event: $event');
      },
    );
  }

  @override
  void dispose() {
    realtimeSubscription?.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: 1. implement showing the items
    // TODO: 2. add interactivity to open [ItemViewScreen] when an item is tapped
    return const Placeholder();
  }
}

class ItemViewScreen extends StatefulWidget {
  final String documentId;

  const ItemViewScreen({super.key, required this.documentId});

  @override
  State<ItemViewScreen> createState() => _ItemViewScreenState();
}

class _ItemViewScreenState extends State<ItemViewScreen> {
  RealtimeSubscription? realtimeSubscription;

  @override
  void initState() {
    super.initState();
    realtimeSubscription = realtime.subscribe([
      'databases.[itemListDatabaseId].collections.[itemListCollectionId].documents.${widget.documentId}'
    ]);
    realtimeSubscription?.stream.listen(
      (event) {
        print('event: $event');
      },
    );
  }

  @override
  void dispose() {
    realtimeSubscription?.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

7. Now run the loop to update the document and do the following in the Flutter app

In the ItemListViewScreen, Tap the item to move to ItemViewScreen... And Go back (exit)
Repeat this a few times, and each time you open the new window notice your console logs before exiting. If the events were being printed out then exit the screen.

Doing this a few times will result in the realtime sub to not deliver any events, it's random and not clear exactly how frequently this process will produce the mentioned bug.

8. If you found the bug in step 7, then do the following to confirm the bug does exist.

In your ItemListView comment the code where we started the realtime subscription (this will require you to add a fetching functionality to fetch the document, or just hard code the myNewDocument's ID into a UI item so that you can click on it and move to ItemViewScreen and supplying the document's ID.

If you did not reproduce the behavior of step 7 in step 8 then we can confirm that the bug does exist.

馃憤 Expected behavior

This is a case where we want to use multiple streams on a single document. The streams should be consistent on delivering the events.

馃憥 Actual Behavior

As mentioned above, the Realtime Subscription stops working at times when the document is being listened to multiple times by the same client.

馃幉 Appwrite version

Version 1.4.x

馃捇 Operating system

Linux

馃П Your Environment

No response

馃憖 Have you spent some time to check if this issue has been raised before?

  • I checked and didn't find similar issue

馃彚 Have you read the Code of Conduct?

@arrowxpr arrowxpr added the bug Something isn't working label Oct 6, 2023
@stnguyen90
Copy link
Contributor

Where and how is realtime defined?

@arrowxpr
Copy link
Author

arrowxpr commented Oct 6, 2023

Where and how is realtime defined?

In a separate dart file

Client _client = Client().setEndpoint(_endpoint).setProject(_projectID);

Realtime get realtime => Realtime(_client);

@stnguyen90
Copy link
Contributor

@arrowxpr would you please provide more details about that separate file?

@stnguyen90 stnguyen90 self-assigned this Oct 9, 2023
@stnguyen90 stnguyen90 added the api / realtime Fixes and upgrades for the Appwrite Realtime API. label Oct 9, 2023
@arrowxpr
Copy link
Author

arrowxpr commented Oct 9, 2023

@arrowxpr would you please provide more details about that separate file?

Absolutely. Here is the content of mentioned dart file.

import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';

const _endpoint = 'https://mydomain.com/v1';
const _projectID = 'myProjectId';

Client _client = Client().setEndpoint(_endpoint).setProject(_projectID);

Client client() => _client;

Future<User> getCurrentUser() async =>
    Account(_client).get().then((value) => value);

Future<Preferences> getAccountPrefs() async =>
    getCurrentUser().then((value) => value.prefs);

Account getAccountService() => Account(_client);

Databases get databases => Databases(_client);

Realtime get realtime => Realtime(_client); // <---

Storage get storage => Storage(_client);

Functions get functions => Functions(_client);

@stnguyen90
Copy link
Contributor

@arrowxpr:

This doesn't quite make sense...I don't think you can have getters and whatnot like this in a file by itself 馃

@arrowxpr
Copy link
Author

arrowxpr commented Oct 9, 2023

@arrowxpr:

This doesn't quite make sense...I don't think you can have getters and whatnot like this in a file by itself 馃

I originally noticed the issue while using those getters, and then I even instantiated new Realtime objects because I suspected something, and the issue persisted. And I wonder why it does not make sense? Please do enlighten my knowledge.

@stnguyen90
Copy link
Contributor

@arrowxpr, unfortunately, I have not been able to reproduce your problem. In my test, i have a Map<int, RealtimeSubscription> to hold all my subscriptions and this method to subscribe:

  _subscribe(List<String> channels) {
    final realtime = Realtime(widget.client);
    final subscription = realtime.subscribe(channels);
    subscription!.stream.listen((data) {
      print(data);
      setState(() {
        realtimeEvent = jsonEncode(data.toMap());
      });
    });

    setState(() {
      final hashCode = _getHashCode(channels);
      subscriptions[hashCode] = subscription;
    });
  }

Using this, I subscribed to 'databases.default.collections.usernames.documents' and the individual documents returned by listDocuments(). After updating one of the documents, I received 2 print messages indicating 2 events were received.

@arrowxpr
Copy link
Author

@arrowxpr, unfortunately, I have not been able to reproduce your problem. In my test, i have a Map<int, RealtimeSubscription> to hold all my subscriptions and this method to subscribe:

  _subscribe(List<String> channels) {
    final realtime = Realtime(widget.client);
    final subscription = realtime.subscribe(channels);
    subscription!.stream.listen((data) {
      print(data);
      setState(() {
        realtimeEvent = jsonEncode(data.toMap());
      });
    });

    setState(() {
      final hashCode = _getHashCode(channels);
      subscriptions[hashCode] = subscription;
    });
  }

Using this, I subscribed to 'databases.default.collections.usernames.documents' and the individual documents returned by listDocuments(). After updating one of the documents, I received 2 print messages indicating 2 events were received.

I am not sure if you tried cancelling and then starting those subscriptions again within 5 seconds. I keep reproducing this bug in my project.

@pomarec
Copy link
Contributor

pomarec commented Nov 14, 2023

It seems related to
src/Appwrite/Messaging/Adapter/Realtime.php:198 :

                             /**
                             * To prevent duplicates, we save the connections as array keys.
                             */
                            $receivers[$id] = 0;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api / realtime Fixes and upgrades for the Appwrite Realtime API. bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants