Skip to content

store: Replace event queue on expiry #466

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 3, 2024
Merged

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Dec 27, 2023

Fixes: #185

@gnprice
Copy link
Member Author

gnprice commented Dec 27, 2023

In order to test this manually in the app, I used the following change locally:

--- a/lib/api/route/events.dart
+++ b/lib/api/route/events.dart
@@ -10,4 +10,5 @@ part 'events.g.dart';
 Future<InitialSnapshot> registerQueue(ApiConnection connection) {
   return connection.post('registerQueue', InitialSnapshot.fromJson, 'register', {
+    'queue_lifespan_secs': 1,
     'apply_markdown': true,
     'slim_presence': true,

and pointed it at my dev server, with a handful of local commits there which can be found in my dev-queue branch. Together these changes cause the event queue to be constantly lost every few seconds.

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Glad to be fixing this really visible behavior problem in the app.

I did notice some buggy behavior when I opened the "All messages" view and watched (with your helpful manual-testing steps; thanks!) how the app responded as event queues were invalidated: (link to video; you might want to watch with increased playback speed)

Possibly related to something I noticed in the code; comments below.

Comment on lines 271 to 292
@override
void dispose() {
recentDmConversationsView.dispose();
for (final view in _messageListViews) {
view.dispose();
}
super.dispose();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also dispose unreads, right? I think that's currently the only other thing with a dispose method that needs to be called here. It seems like the kind of thing that's easy to forget.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah, I guess that's a ChangeNotifier and so does have a dispose. Good catch, thanks.

It seems like the kind of thing that's easy to forget.

Yeah.

@@ -49,13 +58,17 @@ class TestGlobalStore extends GlobalStore {
}

@override
Future<PerAccountStore> loadPerAccount(Account account) {
return Future.value(PerAccountStore.fromInitialSnapshot(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to remove the Future.value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a reason it needs to be. Sure, maybe it's clearer with the Future.value.

Comment on lines 274 to 290
for (final view in _messageListViews) {
view.dispose();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I have a message list open, I'm getting a concurrent modification error here, because this call to MessageListView.dispose—which is in a loop over _messageListViews—causes a call to _messageListViews.remove.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm thanks. I guess I'll need to untangle that.

@@ -186,7 +186,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
}

@override
void onNewStore() {
void onNewStore() { // TODO(#464) try to keep using old model until new one gets messages
model?.dispose();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. model, here, refers to the message-list view on the old per-account store. In the case of refreshing a dead event queue, those things (the old message-list view and the old per-account store) will have already been disposed, by the UpdateMachine, and so I think it's not appropriate to do anything with them, including this dispose call.

I was alerted to this by the following assert in PerAccountStore.unregisterMessageList, which was throwing in my manual testing:

  void unregisterMessageList(MessageListView view) {
    final removed = _messageListViews.remove(view);
    assert(removed);
  }

One maybe-naïve fix is to just remove the model?.dispose(); line in this onNewStore. But the TODO(#464) seems fundamentally impossible to accomplish as long as the old model is disposed by the UpdateMachine before _MessageListState even learns (by a call to its onNewStore) that it has to refetch messages.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was alerted to this by the following assert in PerAccountStore.unregisterMessageList, which was throwing in my manual testing:

Hmm, good catch, thanks.

will have already been disposed, by the UpdateMachine, and so I think it's not appropriate to do anything with them, including this dispose call.

In general dispose should be idempotent — the usual spec for it is "it's no longer valid to do anything with this object, except call dispose again".

… Or maybe not. Looking around for examples, both ChangeNotifier.dispose and State.dispose have asserts that will fail if you dispose twice.

One maybe-naïve fix is to just remove the model?.dispose(); line in this onNewStore. But the TODO(#464) seems fundamentally impossible to accomplish as long as the old model is disposed by the UpdateMachine before _MessageListState even learns (by a call to its onNewStore) that it has to refetch messages.

Yeah, #464 will definitely call for the PerAccountStore.dispose to get delayed. One thing that'll be a bit tricky in implementing that issue will be to ensure that it does eventually get disposed — something like, when it's already stale and then its last MessageListView goes away.

But in the meantime, this code can be written for the way that PerAccountStore.dispose currently works — so it can just drop this dispose call.

@gnprice
Copy link
Member Author

gnprice commented Dec 28, 2023

I did notice some buggy behavior when I opened the "All messages" view and watched (with your helpful manual-testing steps; thanks!) how the app responded as event queues were invalidated: (link to video;

Wacky, yeah, thanks. Even with that assert throwing, I'm not sure quite how it ends up with that symptom. I guess the exception propagates up into the framework where it's trying to update the tree, and it ends up with the old MessageList hanging around while it adds a new one as a sibling. Anyway, presumably fixing that assert will fix it.

It seems like I must have neglected to repeat the manual testing after I added the dispose methods. Glad you caught these, thanks.

@gnprice
Copy link
Member Author

gnprice commented Dec 29, 2023

Thanks for the review! Revision pushed.

I found there were also some TODO(#185) comments, about updating docs around PerAccountStoreAwareStateMixin. I acted on those, and also revised those docs to say to ignore the old store rather than to remove listeners on it — in other words to reflect the revision that was needed in that _MessageListState.onNewStore method.

@chrisbobbe chrisbobbe merged commit 30693a8 into zulip:main Jan 3, 2024
@chrisbobbe
Copy link
Collaborator

Thanks, LGTM! Merged, after manually testing the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Start new event queue when old one has expired
2 participants