-
Notifications
You must be signed in to change notification settings - Fork 314
Handle disposal of message store correctly. #909
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
Conversation
The CI failures reproduce the error mentioned in the issue: https://github.com/zulip/zulip-flutter/actions/runs/10517036561/job/29140404556. |
Pushed again to resolve merge conflicts. |
Thanks!
For the part before the comma, |
Yes, this is called from |
I don't understand this yet. Since you said "recursively", I'm looking for a function that, when called, causes itself to be called. Is that what you have in mind, and could you point more specifically to which function this is and where the recursive call is? I see that the message store disposes the registered message list views, but that shouldn't cause a concurrent modification error by itself. I'm looking for what else is trying to modify the collection while that's happening, and I haven't found it yet. |
I guess the correct word would be propagatively. The for loop modified in the fixup commit iterates over the registered message lists and calls dispose on them, those message lists then unregistered themselves to cause modification concurrent to the for loop. |
Ohh, I see, yeah. Part of the work of disposing a |
One commit-message clarification might be to say "removing" instead of "unregistering" in "MessageListView instances unregistering themselves from the collection". It's true that there's code involved with the name |
How about, to replace the paragraph I quoted in #909 (comment) , something like:
|
685fa62
to
8d47ec5
Compare
Updated the description and tweaked the comment. Thanks for the feedback! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! LGTM except for one nit below.
lib/model/message.dart
Outdated
// When a MessageListView is disposed, it removes itself from the set | ||
// `MessageStoreImpl._messageListViews`, which is the exact collection this | ||
// iterates on. We need to make a copy of the collection to avoid concurrent | ||
// modifications. | ||
for (final view in _messageListViews.toList()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Well, it isn't the exact collection this iterates on :) because we're avoiding doing that, for the reason this mentions. How about:
// When a MessageListView is disposed, it removes itself from the set | |
// `MessageStoreImpl._messageListViews`, which is the exact collection this | |
// iterates on. We need to make a copy of the collection to avoid concurrent | |
// modifications. | |
for (final view in _messageListViews.toList()) { | |
// When a MessageListView is disposed, it removes itself from the Set | |
// `MessageStoreImpl._messageListViews`. Instead of iterating on that Set, | |
// iterate on a copy, to avoid concurrent modifications. |
Updated the PR. Thanks! |
Thanks for the revision! In the changed comment, let's be consistent in saying "set" or "Set", and there's a comma that should be a period. |
dd47ba8
to
3f14407
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @PIG208, and thanks @chrisbobbe for the reviews!
Generally this looks good. Small comments below.
lib/model/message.dart
Outdated
@@ -37,6 +39,9 @@ class MessageStoreImpl with MessageStore { | |||
|
|||
final Set<MessageListView> _messageListViews = {}; | |||
|
|||
@visibleForTesting | |||
Set<MessageListView> get debugMessageLists => _messageListViews; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keep consistent names for the same thing:
Set<MessageListView> get debugMessageLists => _messageListViews; | |
Set<MessageListView> get debugMessageListViews => _messageListViews; |
test/model/message_test.dart
Outdated
Condition<Object?> conditionMessageListView(Narrow narrow) => | ||
(it) => it.isA<MessageListView>().narrow.equals(narrow); | ||
|
||
check(store.debugMessageStore.debugMessageLists).deepEquals([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These can be simplified:
check(store.debugMessageStore.debugMessageLists).deepEquals([ | |
check(store.debugMessageListViews).deepEquals([ |
by putting debugMessageListViews
on the MessageStore
mixin and overriding it separately on the mixin's two subclasses MessageStoreImpl
and PerAccountStore
. That's the same pattern followed by members like messages
and registerMessageList
.
I like that pattern for those members because it means that code consuming the store doesn't have to worry about which sub-area of the store we've organized a given piece of data into — it's all just methods on the store. I think the same logic works for this debug member too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's pretty neat. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This also caused me to notice that I'd introduced a deviation from that pattern over on ChannelStore; sent a fix for that as #931, along with adding some docs.)
test/model/message_test.dart
Outdated
@@ -919,4 +919,23 @@ void main() { | |||
}); | |||
}); | |||
}); | |||
|
|||
test('disposing multiple registered MessageListView instances', () async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: organize tests in same order as code under test; here dispose
comes before reconcileMessages
, near top of file
test/model/store_test.dart
Outdated
await Future<void>.delayed(Duration.zero); | ||
check(store).isLoading.isTrue(); | ||
check(store.debugMessageStore.debugMessageLists).isEmpty(); | ||
|
||
// The global store has a new store. | ||
check(globalStore.perAccountSync(store.accountId)).not((it) => it.identicalTo(store)); | ||
updateFromGlobalStore(); | ||
check(store).isLoading.isFalse(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be more focused on the aspect this particular test is about:
await Future<void>.delayed(Duration.zero); | |
check(store).isLoading.isTrue(); | |
check(store.debugMessageStore.debugMessageLists).isEmpty(); | |
// The global store has a new store. | |
check(globalStore.perAccountSync(store.accountId)).not((it) => it.identicalTo(store)); | |
updateFromGlobalStore(); | |
check(store).isLoading.isFalse(); | |
await Future<void>.delayed(Duration.zero); | |
// The old store's [MessageListView]s have been disposed. | |
// (And no exception was thrown; that was #810.) | |
check(store.debugMessageStore.debugMessageListViews).isEmpty(); |
The other checks are effectively covered by the preceding test case.
Meanwhile the explicit comment about no exception is helpful because in this test case that's the main point of the test. Every test case implicitly checks that no miscellaneous exceptions were thrown from the code under test; but usually that's not the main thing the test is about, so when it's not mentioned the reader might overlook it.
test/model/message_test.dart
Outdated
Condition<Object?> conditionMessageListView(Narrow narrow) => | ||
(it) => it.isA<MessageListView>().narrow.equals(narrow); | ||
|
||
check(store.debugMessageStore.debugMessageLists).deepEquals([ | ||
conditionMessageListView(const MentionsNarrow()), | ||
conditionMessageListView(const StarredMessagesNarrow()), | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first thought here is that this is probably clearer if conditionMessageListView
just gets inlined at its two call sites. Less abstraction to work through, and not really any longer.
But I think this can actually go a step further and just check the length of the list of [MessageListView]s, as you do in the other test. The fact that it's these two particular narrows is sort of inevitable given the setup of the test, and it's not what this test is intended to check on.
(Also conceivably it could iterate in the other order. The current implementation doesn't do that, because the set comes from a set literal {}
and that's a LinkedHashSet
, which preserves the insertion order; but that's not a guarantee that MessageStoreImpl
otherwise has any reason to make, and if it didn't then this test would sometimes break.)
// When a MessageListView is disposed, it removes itself from the set | ||
// `MessageStoreImpl._messageListViews`. Instead of iterating on that set, | ||
// iterate on a copy, to avoid concurrent modifications. | ||
for (final view in _messageListViews.toList()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit in commit message:
This fixes a bug caused by MessageListView instances unregistering themselves
from the collection containing these instances, while we are iterating the exact
same set when disposing the message store.
lines too long for prose; from https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html#formatting-guidelines :
Your commit message should be line-wrapped to about 68 characters per line, but no more than 70, so that your commit message will be easy to read in
git log
in a normal terminal.
These lines are as long as 80 columns.
See also the tip there about configuring Git and your editor.
f0579df
to
270159c
Compare
fd3e9ba
to
1f00049
Compare
This follows the pattern we generally use for other members of ChannelStore and MessageStore, and lets us eliminate the getter returning a ChannelStoreImpl. This pattern -- exposing all the data as members directly on the overall PerAccountStore, delegating to the underlying channel store and message store as needed -- is convenient because it means that code consuming the store doesn't have to worry about which sub-area of the store we've organized a given piece of data into: it's all just methods on the store. Noticed this was here while reviewing a PR that included a new similar deviation from this pattern: zulip#909 (comment)
Updated the PR, thanks for the review! Also removed the |
This follows the pattern we generally use for other members of ChannelStore and MessageStore, and lets us eliminate the getter returning a ChannelStoreImpl. This pattern -- exposing all the data as members directly on the overall PerAccountStore, delegating to the underlying channel store and message store as needed -- is convenient because it means that code consuming the store doesn't have to worry about which sub-area of the store we've organized a given piece of data into: it's all just methods on the store. Noticed this was here while reviewing a PR that included a new similar deviation from this pattern: #909 (comment)
f6ca31c
to
0b849f6
Compare
Updated test comments and the commit message. |
Part of the work of disposing a MessageListView is to remove it from a collection of MessageListViews, and we've been doing this in a loop over the same collection (MessageStoreImpl._messageListViews). This has been causing concurrent modification errors. The errors originated from this requirement of dart Iterable: > Changing a collection while it is being iterated is generally not allowed This fix avoids those errors by having the loop iterate over a clone of that collection instead. See also: https://api.flutter.dev/flutter/dart-core/Iterable-class.html Fixes: zulip#810 Signed-off-by: Zixuan James Li <[email protected]>
Thanks for all the revisions! Looks good — merging. |
Fixes: #810