-
Notifications
You must be signed in to change notification settings - Fork 534
Define semantics of subscribers erroring in onNext(). #106
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
Comments
There was a lengthy discussion of this which led to rule 2.13, my understanding was that |
…o have onNext throw an exception.
I've addressed this in: #110 |
IllegalStateException for onNext() ? I'm not a fan for wrapping up the exception in this specific case, it should just call onError with the source exception innit ? |
@smaldini I think it is important to signal that it is not acceptable behavior, if we just pass the exception back nobody will ever know that it's not well-behaved spec-wise. Thoughts? |
I agree it should just pass the |
For history, related discussions exist in #52 (comment) and #68 Particularly:
My stance on this discussion depends on the nuance of what is being restricted. If it is an operator implementation, then I'm okay with saying it shouldn't throw (except for the non-recoverable errors as discussed elsewhere). If user provided functions are trying to be prevented from throwing, then I disagree. Consider this: stream.map(t -> {
... do stuff ...
return someComputation();
}) That user provided function should not need to be written like this: stream.map(t -> {
try {
... do stuff ...
return someComputation();
}catch(Exception e) {
// now what do I do?
return what???;
}
}) In particular, there is no mechanism for returning an Exception as there is in stream.map(t -> {
... do stuff that might fail ...
return someComputation();
}).handleError(t -> {
log(t);
return defaultValue;
}) I also don't think we should force wrapping into an |
@benjchristensen I think we’re all in full agreement here: it is the obligation of the |
@rkuhn yes, this is exactly what I meant. Thanks for elaborating! |
Glad we agree on the user function failure scenario and its impact on an
Since operators/processors are generally going to be composed, wouldn't compliance with this rule mean that every processor would need to have a try/catch that wraps in as an IllegalStateException? Also due to composition, the code that catches the exception would not be aware of what caused it. So I don't feel like I'm understanding what you want code to do. Take this example: @Override
public void onNext(T t) {
try {
o.onNext(userFunction.call(t));
} catch (Throwable e) {
onError(e);
}
} Here the try/catch is intended to capture problems from the Are you saying you want all code that calls @Override
public void onNext(T t) {
R r = null;
try {
r = transformer.call(t);
} catch (Throwable e) {
onError(e);
}
try {
o.onNext(r);
} catch(Throwable e) {
onError(new IllegalStateException("Rule 2.13", e));
}
} |
@benjchristensen my interpretation is that your latter example is the intention, and is most desirable in my opinion (with the additional provisor that the upstream subscription should also be cancelled). |
Yes, the last sample is what I meant, and it will predominantly library code that implements RS in this fashion (though mostly not directly from |
I don't know that I'm a fan of requiring every single processor/operator having to treat If An async processor/operator will generally want to be overly cautious and wrap everything, but in the dozens of operators implementations of RxJava we only need a handful to In other words, it feels like too strong a statement to require every single invocation of |
If we wanted to "force" implementors to handle an |
Agreed. And I really don't like checked exceptions so would prefer we just don't force this. If a processor/operator is doing something that could result in an exception being thrown, code defensively and deal with it. Others shouldn't all have to be defensive when the contract says |
@jbrisbin I'm not sure I understand, your argument seems unsound to me—it is proposed to be illegal to throw exceptions from |
@benjchristensen Just to confirm that I understand the implications of your position: It is OK if the caller of the onNext that fails does not call onError? |
@viktorklang the point is that if you want to force the caller to handle Exceptions, there are tools in the Java language to do that. Burdening the developer with try/catch around every onNext call just doesn't strike me as all that valuable. There are plenty of times within Reactor where we have to catch |
@jbrisbin My point is that there is no way of doing that without encouraging the callee to throw exceptions. (Because then -they- can ignore doing try-catch themselves.) |
@viktorklang Unexpected Exception? Take this: |
@jbrisbin you forgot |
Why does it need to specify anything? It has already specified that If we do specify anything, it should be something along the lines of: an implementation MAY choose to catch errors thrown by Since the spec says Even 2.13 about
If These seem to be conflating "user functions" failing versus the processor/operator failing and I think we need to differentiate. I think 2.13 is trying to say that if a user provided Similarly, if a user provided function in If however an operator has a bug and blows up and throws from If someone wants to be defensive many of these can be handled, but a processor/operator a developer implements should be able to trust the spec and not try/catch the |
The question is how you as the implementor will know that an exception was thrown and that you violated the spec so you can fix it.
I'd -strongly- advise against, and vote against that as it is then easy to start relying on specific behavior exhibited by some implementations, which then fails for other implementations.
The question is how you as the implementor will know that an exception was thrown and that you violated the spec so you can fix it.
Because the alternative is worse: not calling onError == potential runtime resource leak. Not wrapping in an ISE and referring to the spec == not knowing that this is a specification violation that needs to be addressed.
I see no such conflation (but would be grateful if this was elaborated upon). This is all about specification enforcement, so that implementors can find out if they get something wrong while minimizing adverse effects.
See my previous answer on this question.
Same as above.
Could you elaborate here? What does "cause things to fail" mean in practice and how does one reason about those failures?
This is not about trusting the spec, this is about the implementations of the spec and whether the implementor will find out whether they broke it or not. |
So you're going to force every single implementor of a If that's the case then make it |
I mentioned this earlier, so let me elaborate: "def onNext(e: E) throws X" encourages the implementor of the method not to deal with their exceptions (because the effect is that they don't have to do try-catch for checked excpetions that may happen in their onNext code), and we want them to deal with their exceptions and, (as you know,) the omission of a throws clause would not deal with (read: give any warning for) RuntimeExcepions. In addition, Java is the only language on the JVM (in the world?) that has checked exceptions so this is not a solution in any case (not for Clojure, not for Scala, not for Groovy…) So, my concerns are as follows:
Passing the exception raised by onNext into onError deals with concern number 1, as it allows for cleanup to be performed and as such minimize resource leakage, stalls etc. Passing the exception raised by onNext wrapped in another exception that explains why this is not a good thing deals with concern number 2 by adding important information to the implementor of onNext. It would be great to get some answers for the questions I asked in my previous response. |
An exception gets thrown and causes a system to fail with a stacktrace. I can now reason about what happened. I broke the contract and an exception was thrown.
See above. Exception thrown and system crashing is pretty fast and easily comprehensible. If the spec says An exception being thrown resulting in stacktraces being printed somewhere in a failed app solves both of the expressed concerns. An operator/processor implementation is of course capable of wrapping a try/catch around them, and anything scheduling work on a new thread should have a catch-all for anything that goes wrong, but that is separate from this spec, that's just good coding practices for an implementation. There are many, many ways I can break this spec. The whole point of having a spec is to define how things will behave so when I use it and invoke methods I have some level of trust that they will indeed work as the spec says. If they don't that is a bug and the system may fail. I could also call The spec does not need to specify how to police itself. An implementation can do that if it wishes to. The spec should state how things should behave. The TCK and implementations can then choose how they wish to assert or audit compliance and deal with bad implementations. |
You make the assumption that the exception was both created at the site it was thrown (rethrowing and/or throwing of cached exception instances is not black swan events) and that it has a stack trace (suppression of stacktraces is not unheard of as filling them in tends to involve a lot of overhead)
See above.
I think I'm missing something, why would the system crash just because a single stream has a bug for perhaps a specific input?
Do we want the entire system to fail if one single Subscriber implementation has a buggy onNext?
True in the general sense, but not always, see discussion earlier on both cached exception instances, rethrows and exceptions without stacktraces.
That is all true. My point is not about Byzantine implementations, its about the implementor understanding that they have violated the spec and need to fix it, which separates it from other onErrors which may be natural or occuring due to the fact that Tcp connections get broken etc.
That is a point I can definitely sympathize with. The most obvious difference is the runtime performance cost between trapping an exception and 2 * LOCK XADD.
I think that could be a fair point, if so then we shouldn't mandate that |
Although the outcome of this discussion interests me I'm afraid I don't have the bandwidth for such a long back and forth for something so microscopic. If a single exception can crash a system then you get what you deserve when it happens. Otherwise we should NOT burden the developer without exceptionally just cause. This smells to me more like us imposing patterns on others that we don't even follow ourselves consistently. THE END |
@jbrisbin So to conserve your bandwidth :) Simplest possible solution: you violate the spec, if you're lucky the caller will log it! |
Why even suggest that restriction? If an implementation wants to catch them and emit to onError then it can. It could also choose to just log or it could just do nothing and let exceptions be thrown. = |
@benjchristensen Because otherwise you'll see different behavior when a broken Subscriber subscribes to different Publisher implementations. The publisher could also decide to call System.exit(1), but what I'm talking about is what the spec says and whether it will behave the same for the implementations that follow the spec. |
I don't want to debate this any further. We are over specifying this if the spec says "don't throw" but then also specifies what to do if something throws (breaks the spec). It's turtles all the way down. |
@benjchristensen Just for clarification, your proposal is to drop 2:13 and 2:14 from the spec? |
Effectively yes. 1.9 is then almost clear enough. If the rule in 1.9 was applied to everything instead of just 'subscribe' that should be sufficient for specifying 'error throwing behavior'. 2.13 and 2.14 could be done by an implementation if it chooses and can do so in a way that recovers gracefully, but it's all out of spec at that point. = |
I propose to replace 2.13 and 2.14 with the following:
|
I like this, though not sure about this: "The only legal way for a Subscriber to signal failure is by cancelling the Subscription" That isn't clear to me. The legal way to signal failure is to call onError. If onError fails then all bets are off. A Subscription is cancelled by the consumer of it, not the producer. A producer terminates a Subscription with onError or onComplete. So if the producer also has a parent Subscriber it is consuming then if onError down fails it can cancel the subscription to its parent, but that seems like complicated implementation details we shouldn't specify. I suggest we just state the only legal way to signal error is calling onError. |
The subscriber is of course allowed to call methods on itself it it wants to, but what I meant was upwards signals. Perhaps what is missing is: "The only legal way for a Subscriber to signal failure _to the Publisher_ is by cancelling the Subscription"
This does not make sense (to me) in the context we are talking about, here's why: We are talking about when a _Subscribers_ signal methods throw to the caller/upstream. And we've already been through it before? It cannot be allowed to call signal methods once signal methods throw exceptions if signal methods are not allowed to throw exceptions. It's circular and dangerous. |
A reminder of one of my initial meta-comments: It is rare for an API spec to say anything about what a component must NOT do. As seen here by counter-example, usually the best tactic is to state only the positives/MUSTs. |
@DougLea Ad antiquitatem? ;-) Since the goal is interoperability, my position is that we need to both outline the positive space and where interoperability might suffer due to omission, the negative space, with of course, a focus on the positive space. Do you have an alternate wording in mind for the rule in question? |
The ad-antiquitatem-ism stems from experience: Usually, when tempted to state negatively, there's a better way lurking. In some cases, you can rework as preconditions: Instead of" NOT pred", say "if pred". When dealing with exceptions, there's often some sort of layering assumption going on: If you want to ensure that any exception in a call to a triggered method is caught, you can say this directly rather than saying that the outer method does not throw it. This makes it clearer that any throw from the outer method is due to its own faults. |
@DougLea I like that! |
@DougLea So something along the lines of:
|
@viktorklang Yes! Including the term "non-revoverable" that I was about to mention. |
@DougLea Regarding "non-recoverable", I believe we can omit it for this rule and have it fly under the "UST raise this error condition in a fashion that is adequate for the runtime environment."-part of the rule, do you agree? |
@viktorklang Yes. Or maybe better, place a top-level disclaimer that the term "Exception" excludes Throwables (such as OOME and SOE) when they result in termination of the program/service. (The secondary "when..." disclaimer allows but doesn't require recovery even from OOME). |
@DougLea That's a very nice solution, I'm going to open an Issue for it. |
Fixed by #117 |
I can't find this defined in the spec. Apologies if I've just missed it.
Equivalent in Rx semantics would be the Publisher must catch anything thrown by onNext() and terminate with an onError(). For processors, the upstream subscription would also have to be cancelled.
The text was updated successfully, but these errors were encountered: