-
Notifications
You must be signed in to change notification settings - Fork 274
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
Deadlocks in tests after upgrading rxdart #587
Comments
I'm taking a look at this now. Interestingly both tests pass if you cancel the first subscription before the second run: final sub = b.listen(log.add);
await b.first
.then(log.add)
.timeout(timeout, onTimeout: () => fail('1st should complete'));
expect(log, [2, 2]);
await sub.cancel(); |
My take is that RxDart is doing some understandable but questionable things here. First, it looks like the root of the failure is from rxdart/lib/src/utils/forwarding_stream.dart Lines 68 to 74 in 3975769
This function is used by most transformers from RxDart, including The reason RxDart subjects don't suffer from the same behavior is because When one only uses the stream api from final outer = BehaviorSubject<int>();
final tc = testCase(
createOuter: () => _SyncProxyStream(outer), // Test fails because of this
createInner: () {
final inner = BehaviorSubject<int>();
Future.delayed(Duration(milliseconds: 10))
.then((_) => inner.add(2));
return inner;
});
await Future.delayed(Duration(milliseconds: 10));
outer.add(1);
await tc; This means that RxDart's transformers don't reliably work on every My suggestion would be to make |
To understand the issue correctly, Because, that is not the case, for example:
Here, the print invokes only once, "listen to me! 1". As I recall, this was actually wrong in earlier rxdart versions, where If you cancel them all, and then do a new subscription, then |
In general, yes! I know that the For instance, consider this example: class DoOnSubscribeStream<T> extends Stream<T> {
final Stream<T> inner;
final void Function() onSubscribe;
DoOnSubscribeStream(this.inner, this.onSubscribe);
@override
bool get isBroadcast => inner.isBroadcast;
@override
StreamSubscription<T> listen(void Function(T event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
onSubscribe();
return inner.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
}
void main() {
final controller = StreamController.broadcast();
final stream = DoOnSubscribeStream(controller.stream, () {
print('new subscription');
});
stream.listen(null); // prints!
stream.listen(null); // prints!
stream.listen(null); // prints!
final switched = stream.switchMap((_) => Stream.empty());
switched.listen(null); // prints!
switched.listen(null); // does not print -- why?
} I think it's perfectly reasonable to expect the last line to print In fact, there are less-contrived examples of this as well. For instance, let's say someone writes their own broadcast stream that emits a cached value for each new listener if one is available (moor is doing pretty much that). Those streams depend on new listens not being dropped by some transformer down the line. In my opinion, the special case for subjects in |
We only use But these events fire just like they would on "normal" Dart streams, and we follow the normal Stream behavior with rxdart. |
Ok I now get what you mean, You invoke on the actual Will see how breaking that would be |
See here for a PR which would fix your issue: #588 |
Thank you both for the quick responses! Glad to see it's a simple fix. I'll try to give that branch a try tomorrow. |
@Mike278 Sorry I was a bit too fast there, the fix would be a little more complex unfortunately, I'll keep you posted :/ |
So that PR branch probably works for your listen issue, but we need a better solution of course, it's a bit hacky atm. Do feel free to try it out in the meantime of course. |
|
Yes, but the issue here is that they expect the listen handler to be invoked on each listen: import 'dart:async';
import 'package:rxdart/rxdart.dart';
class DoOnSubscribeStream<T> extends Stream<T> {
final Stream<T> inner;
final void Function() onSubscribe;
DoOnSubscribeStream(this.inner, this.onSubscribe);
@override
bool get isBroadcast => inner.isBroadcast;
@override
StreamSubscription<T> listen(void Function(T event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
onSubscribe();
return inner.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
}
void main() {
final controller =
StreamController<void>.broadcast(onListen: () => print('I start!'));
final stream = DoOnSubscribeStream(controller.stream, () {
print('new subscription');
});
var switched = stream.switchMap((event) => Stream.value(event));
switched.listen(null); // prints!
switched.listen(null); // does not print!
var mapped = stream.map((event) => event);
mapped.listen(null); // prints!
mapped.listen(null); // prints!
} it prints "I start!" once, as expected, but if you override the |
IMO DoOnSubscribeStream means |
Only built-in operators cause the inconsistent, but |
I agree.
Isn't the whole point of broadcast streams that they can be listened to multiple times? :D I wonder if |
Hmm the repro still fails for me on that branch: dependencies:
# ...
rxdart:
# ...
dependency_overrides:
rxdart:
git:
url: https://github.com/ReactiveX/rxdart.git
ref: d6a7761cb74761f4f8b6e3663c445f28a145a75b
|
It looks like #588 fixes at least some of the problem. I tried to combine all the different examples from this issue into one runnable test: https://gist.github.com/Mike278/f21c92e562428af26af58128d0209b00
|
Is it an option to move the code in the listen override into an onListen handler? |
My understanding is that would mean the code is only invoked each time the listener count goes from 0 to 1, but the goal is to invoke the code each time the listener count is incremented. I think I can reduce the remaining failures to this case: import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:test/test.dart';
class WrappedStream<T> extends Stream<T> {
final Stream<T> inner;
WrappedStream(this.inner);
@override
bool get isBroadcast => inner.isBroadcast;
@override
StreamSubscription<T> listen(void Function(T event) onData,
{Function onError, void Function() onDone, bool cancelOnError}) {
return inner.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
}
void main() {
test('rxdart upgrade', () async {
final controller = BehaviorSubject.seeded('controller');
final stream = WrappedStream(controller.stream);
final switched = stream.switchMap((_) {
return BehaviorSubject.seeded('switched');
});
final timeout = Duration(milliseconds: 100);
switched.listen(null); // note: commenting this out makes the test pass on 0.27
final value1 = await switched.first
.timeout(timeout, onTimeout: () => fail('1st should complete'));
expect(value1, 'switched');
final value2 = await switched.first
.timeout(timeout, onTimeout: () => fail('2nd should complete')); // timeout here with rxdart 0.27 and PR#588
expect(value2, 'switched');
});
} |
@frankpepermans any idea what's up with that test failure above? Other than that it looks like the PR is really close! |
It's not really a good PR though, and will never be merged in the current state, but it was an effort to see if it could resolve your problem (which it apparently almost does then). Can you maybe explain in more detail why you'd need every new subscription to invoke listen on all upstream targets? ...maybe we can also think of a different solution then? |
In case of moor, it comes from three requirements basically:
Regarding the third point, there's also a philosophical argument to be made that RxDart should extend Dart's streams so I think it's unfortunate if moor has to use RxDart's subjects to be compatible with it. |
Ok I think I understand the issue now, so we use ForwardStream to add some hooks that we need for some transformers, and indeed, we have special cases for our own Subjects in there. We do that indeed to maintain the behavior, i.e. to not suddenly switch from, say a BehaviorSubject, to a plain StreamController, because that would lose the "emit last event on subscribe" behavior. Correct? |
Yes exactly. I don't think it's bad to keep that behavior (it's essentially an optimization when the source stream is known to avoid duplicate computations of transformers). I would prefer new subscriptions to go through for non-subject broadcast streams though. Lasse from the Dart team suggested using |
Thank you both again for your collaboration here! @frankpepermans Do you think an approach based on |
@Mike278 not sure, but I'll try to make some time to investigate |
I did a few attempts to get Also, a bit of an annoyance is that subscribing to a Stream.multi always yields a StreamSubscription, even if the underlying Stream is not a broadcast Stream for example, since the description is deferred internally and the StateError that we expect, throws at a different point in time. |
...actually, bit more tinkering, might be able to get it up without breaking too much, |
I haven't tried it out yet, but having a quick look at the code I think there might be a few issues. For example
I think the idea for
|
maybeListen is being called, if onListen does nothing, then we can remove it. Ignore that comment I made before, all 700+ tests now pass, with using Stream.multi |
I had a chance to try this out. The moor test from the repro in the OP no longer times out (woo! 🥳), but now both tests fail because an extra test('duplicate events', () async {
final source = BehaviorSubject.seeded('source');
final switched = source.switchMap((value) => BehaviorSubject.seeded('switched'));
int i = 0;
switched.listen((_) => i++);
expect(await switched.first, 'switched');
expect(i, 1);
expect(await switched.first, 'switched');
expect(i, 1); // fails because i==2
});
|
Looks like that works, all our tests pass with #605 - no more deadlocks! |
Took another stab at upgrading off rxdart 0.23.1, but unfortunately still getting some failing tests. Here's a repro/comparison (could probably be minimized further but not sure which parts are relevant).
Since this began after upgrading rxdart I've started by creating an issue here, but @simolus3 might be able to help determine if this is a moor bug.
#477 and #500 might have some context from the last time we tried to upgrade off rxdart 0.23.1.
I looked around for similar issues since last time and came #511 looks similar. The 1st repro in that issue still times out with rxdart 0.27.0, even though the 2nd repro looks fixed.
The text was updated successfully, but these errors were encountered: