aws-amplify / amplify-flutter

A declarative library with an easy-to-use interface for building Flutter applications on AWS.
https://docs.amplify.aws
Apache License 2.0
1.31k stars 247 forks source link

Api GraphQL subscription stops working properly in release mode #3865

Closed jamilsaadeh97 closed 7 months ago

jamilsaadeh97 commented 1 year ago

Description

Hi everyone!

My users from my app in production are complaining from an issue and it's been one week I'm trying to debug it to know if it's a problem in my code or a bug in Amplify.

I'm subscribing to a stream and the issue happens when I swipe the app away, switch the phone off, turn back on again and go back to the app. The subscription is still connected (I'm also listening to the Api Hub Channel) but stops receiving any event whatsoever.

I found #2918 mentioning that "the underlying websocket connection is not able to stay alive" when the app goes to the background. To deal with that I'm using AppLifecycleState to stop the subscription on paused and re-listen on resumed.

I gathered some info:

  1. The issue is only happening in release mode in iOS on a physical device (prod users, ad hoc or TestFlight)
  2. The issue is not consistently happening from the first try. Out of 20 times, I was able to reproduce it 17 times on the first try(background-phone off-phone on).
  3. I noticed it happens fairly regularly when I switch the phone off, send an event (in my test, the subscription is onCreate), turn the phone back on and go back to the app.
  4. No need to wait an amount of time in the background for the issue to arise.
  5. If it didn't happen on the first try, it will on the second or third.
  6. Now the most important thing to note: I noticed, while debugging it in debug mode (and using AmplifyLogger.verbose), that when I do the steps to reproduce it, the underlying WebSocketBloc receives a "UnsubscribeEvent" but it doesn't go all the way to "Closed subscription...." and I think this is where something is going wrong. Let's say I switched the phone off and on 6 times. When I filter the logs for "subscription event", I get 6 events(expected). When I filter the logs for "closed subscription", I get 4(unexpected). It should be 5. A subscription was beginning to unsubscribe but stopped somehow.

Please find bellow a minimum reproducible example.

Minimum reproducible example ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); AmplifyLogger().logLevel = LogLevel.verbose; await configureAmplify(); runApp(child: const DebugApp()); } Future _configureAmplify() async { final apiPlugin = AmplifyAPI(modelProvider: ModelProvider.instance); final authPlugin = AmplifyAuthCognito(); await Amplify.addPlugins([ apiPlugin, authPlugin, ]); try { await Amplify.configure(amplifyconfig); } on AmplifyAlreadyConfiguredException { safePrint( 'Tried to reconfigure Amplify; this can occur when your app restarts on Android.'); } on Exception catch (e) { safePrint("Exception when configuring amplify: $e"); } } class DebugApp extends StatelessWidget { const DebugApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( scaffoldMessengerKey: scaffoldMessengerKey, debugShowCheckedModeBanner: false, title: 'Debug App', theme: ThemeData.dark(), home: const MyList(), ); } } class MyList extends StatefulWidget { const MyList({super.key}); @override State createState() => _MyListState(); } class _MyListState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); subscribe(); listenToApiHub(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: subscribe(); case AppLifecycleState.paused: unsubscribe(); default: break; } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); unsubscribe(); super.dispose(); } StreamSubscription>? _itemsSubscription; List itemsList = []; String _hubConnectionStatus = ""; void subscribe() { final subscriptionRequest = ModelSubscriptions.onCreate(Item.classType); final Stream> operation = Amplify.API.subscribe( subscriptionRequest, onEstablished: () => safePrint('Subscription established'), ); _itemsSubscription ??= operation.listen( (event) { safePrint('Subscription event data received: ${event.data}'); if (event.data != null) { if (mounted) { setState(() { itemsList.insert(0, event.data!); }); } } }, onError: (Object e) => scaffoldMessengerKey.currentState?.showSnackBar(SnackBar( content: Text( "Error in stream: ${e.toString()}", style: const TextStyle(color: Colors.white), ), backgroundColor: Colors.red, )), ); } void listenToApiHub() { Amplify.Hub.listen(HubChannel.Api, (event) { if (event is SubscriptionHubEvent) { if (mounted) { setState(() { _hubConnectionStatus = event.status.name; }); } } }); } void unsubscribe() { _itemsSubscription?.cancel(); _itemsSubscription = null; } @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( body: Column( children: [ Text( "Connection Status: $_hubConnectionStatus", style: const TextStyle(color: Colors.white), ), const SizedBox( height: 20, ), Expanded( child: ListView.builder( itemBuilder: (context, index) => ListTile( title: Text( itemsList[index].id, style: const TextStyle(color: Colors.white), ), ), itemCount: itemsList.length, )) ], ), ), ); } } ```

What's fascinating is that when I do the steps to reproduce, if I see the Connection Status in the example above, whenever I open the app back, go from "connecting" to "connected", I can receive new events. If I go back and it's directly "connected", no events are received anymore (hence my theory that a stream subscription was starting to cancel in the WebSocketBloc but stopped midway). Also if it stops receiving events and I stay in the app, I get the Timeout exception after some time from the stream and only after this exception that the status goes from "connected" to "disconnected".

Finally, in debug mode, I still see the WebSocketBloc not cancelling one of the subscriptions all the way but I still receive events no matter how many times I close the app.

This issue is affecting the most critical and core part of my app (this is the only place I'm using subscriptions) and more and more users are complaining from it since it will require them to close the app completely and reopen it again (a very bad user experience).

I tried to give as much details of my findings as possible. If something is not clear or not explained properly please let me know.

Thank you!

Categories

Steps to Reproduce

  1. Have a stream subscription listening to onCreate or onUpdate or both.
  2. Listen to the AppLifecycle changes. (subscribe on resumed, unsubscribe on paused)
  3. Swipe the app away to go back to the Home Screen of iOS.
  4. Switch the phone off.
  5. OPTIONAL (I felt that the issue happens more often with this step): Send an event depending on the stream subscribed to
  6. Switch the phone on and go back to the app
  7. No more new events are received after this point. (If events are received, repeat from step 3 to 6 until the issue arises)

Screenshots

No response

Platforms

Flutter Version

3.13

Amplify Flutter Version

1.4.2

Deployment Method

Amplify CLI

Schema

type Item @model{
  id: ID!
}
Equartey commented 7 months ago

Hi all, I'm going to close this issue for the lack of activity. If you're still experiencing this issue, please refer to the previous comment and we will reopen this given new feedback.