-
Notifications
You must be signed in to change notification settings - Fork 58
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
HY-4957 Alert devs about batchedRedraw race condition, and log if it happens in prod. ⚠️ #72
HY-4957 Alert devs about batchedRedraw race condition, and log if it happens in prod. ⚠️ #72
Conversation
RavenNumber of Findings: 0 |
@@ -88,6 +89,9 @@ abstract class _FluxComponentMixin<TProps extends FluxUiProps> implements Batche | |||
/// | |||
/// These subscriptions are canceled when the component is unmounted. | |||
List<StreamSubscription> _subscriptions = []; | |||
@visibleForTesting | |||
List<StreamSubscription> get subscriptions =>_subscriptions; |
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.
I'm open to better ways to check the subscriptions in unit tests.
Codecov Report
@@ Coverage Diff @@
## master #72 +/- ##
==========================================
- Coverage 97.7% 97.63% -0.06%
==========================================
Files 28 28
Lines 1389 1392 +3
==========================================
+ Hits 1357 1359 +2
- Misses 32 33 +1 |
await store.dispose(); | ||
renderedInstance = render(TestDefault()..store = store); | ||
component = getDartComponent(renderedInstance); | ||
expect(component.subscriptions.length, 0); |
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.
Alternatively (or supplementarily), this could test that rendering does not cause an exception (when the store is listened to while disposing). Technically, that tests the failure case that we're addressing, but it also asserts the expected behavior in a weird way that is dependent on behavior from another repo. But it also wouldn't require the subscriptions list to be exposed.
🤷♂️
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.
Good point; that could be done like so:
test('does not subscribe to a store that is disposed or disposing', () async {
await store.dispose();
expect(() {
render(TestDefault()..store = store);
}, returnsNormally);
});
@tomconnell-wf Thanks for the PR! Question, though; if the store is disposed or disposing, can the newly-mounted component even recover? Or is this a case where the component will, in most cases, be unmounted by whatever's disposing the store immediately after mounting? I'm also a little fuzzy on |
My unit test was disposing a store, before the next animation frame, where the BatchedRedraws were happening. As a result, that listen call that is guarded would fail on a close stream. I could have awaited the next animation frame, in that test, but this seemed like something that consumers shouldn't have to worry about. I wanted to fix it somewhere that didn't take others hours to track down. The batched redraw mixin doesn't know anything about the store, so the fluxComponent seemed like the correct place to change. That being said, I'm a little fuzzy on how disposing ties in to the react lifecycle. I'm unclear on what should happen, because as it was, I think the components never would have rendered, because of the uncaught exception. |
It's seeming to me like in this case, the component should throw an error if mounted with a disposing/disposed store, as opposed to failing to register silently—in a production app, that would definitely be a problem we'd want to know about.
What about something like this, instead? handlers.forEach((store, handler) {
+ assert(!store.isDisposedOrDisposing,
+ 'Cannot listen to a disposed/disposing Store. '
+ 'This can be caused by BatchedRedraws mounting the component asynchronously after the store has been disposed. '
+ 'If you're in a test environment, try adding an `await window.animationFrame;` before disposing your store.');
+
StreamSubscription subscription = store.listen(handler);
_subscriptions.add(subscription);
}); But there's still the possibility that this race condition could happen in a production environment... I'm having a hard time thinking of how we could effectively guard against this... @evanweible-wf @trentgrover-wf, ideas? |
I would be happy to compromise with an assert, although I think I would prefer a log, so we could track if it is happening in prod. Maybe a hybrid of both for better dev ergonomics. |
@greglittlefield-wf How does this seem? |
@@ -15,6 +15,7 @@ | |||
library over_react.component_declaration.flux_component; | |||
|
|||
import 'dart:async'; | |||
import 'package:logging/logging.dart'; |
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.
Could you please add logging
to pubspec.yaml
?
+1 other than my one comment |
'in a test environment, try adding an `await window.animationFrame;` before disposing your ' | ||
'store.'); | ||
|
||
if(store.isDisposedOrDisposing) _logger.warning(message); |
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.
space after if
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.
I wasn't going to say anything since we don't use dartfmt :P
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.
😈
+1 |
@@ -82,6 +83,7 @@ abstract class FluxUiStatefulComponent<TProps extends FluxUiProps, TState extend | |||
/// | |||
/// Private so it will only get used in this file, since having lifecycle methods in a mixin is risky. | |||
abstract class _FluxComponentMixin<TProps extends FluxUiProps> implements BatchedRedraws { | |||
final Logger _logger = new Logger('_FluxComponentMixin'); |
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.
There's not clear guidance that I can find, but I think we should clearListeners on the Logger
when things are disposed.
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.
Mm, good idea; we don't want this potentially leaking for every FluxComponent instance.
@tomconnell-wf What about making this instance static / top-level instead?
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.
Or cleaning it up in componentWillUnmount (shrug)
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.
Sure
+1 CR, FWIW |
+1 |
Ah, good idea. +1 CR again as well. |
+10 CI passed :) |
@greglittlefield-wf is this enough for merge in this repo? |
@tomconnell-wf We just need a QA +1 now, and it can be merged |
I'l do that since we have a +10 :) |
QA +1
Merging. |
Ultimate problem:
If a store is disposing or disposed before batch redraws, the next componentWillMount will try to subscribe to the disposed store stream, resulting in exceptions.
How it was fixed:
Give useful information in dev, and send a log so we know if it is happening much in prod.
Testing suggestions:
CI passes
Potential areas of regression:
Triggering