-
Notifications
You must be signed in to change notification settings - Fork 2k
Call completeStream only once on failure. #13814
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
base: jetty-12.0.x
Are you sure you want to change the base?
Conversation
a8cf31d to
bee65a6
Compare
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback.
jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java
Outdated
Show resolved
Hide resolved
|
The issue is fundamentally that For this to happen, we need a failure to be detected in the Lines 1573 to 1580 in 8dbd986
This means that We can see that there is a failure detected in
These are all plausible application errors, especially with something like server sent events. So once thread 258 has called which is called from this code after the handler has been invoked: Lines 691 to 713 in 8dbd986
Note that in order for this code to actually call Thus I believe the core fix is to not call Unfortunately I have been unable to produce a unit test for this, as I believe it needs precisely unlucky timing and an application error. |
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback.
|
@sbordet @lorban Can you review the diagnosis that @janbartel and I have come up with. I'm 90% sure this is it, but I cannot reproduce (any thoughts how we might be able to do that?). |
|
Note that we added this |
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.
ChannelCallback.failed() seems not entirely correct either.
Can we write test cases for this scenario?
jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java
Outdated
Show resolved
Hide resolved
| // We are committed and still handling, so let the HandlerInvoker complete, ignoring any pending reads/writes. | ||
| httpChannelState._streamSendState = StreamSendState.LAST_COMPLETE; |
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.
Again, not sure we should ignore pending writes.
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 pending reads.... it is a difficult one. I'll at least change the code to enumerate the possible states
| else | ||
| else if (httpChannelState._handling == null) | ||
| // We are committed, but no longer handling, so will complete here, ignoring any pending reads/writes | ||
| completeStream = true; |
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 don't think this is right.
If there is a write pending, we cannot complete the stream, or we bypass one leg.
I think we should call lockedIsLastStreamSendCompleted() here to figure out whether that leg is done.
@sbordet I will look....
Very hard, because unless there is another thread racing the second completeStream is not called. I'm open to suggestions. |
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback. refactor success and failure into single method.
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback. refactor success and failure into single method. Improved EventSourceServlet
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback. refactor success and failure into single method. Improved EventSourceServlet
|
@sbordet @lorban I'm getting concerned at the number of tests this PR is breaking in its current state. So I propose that this PR should simply be 83c1718 for 12.0.x (perhaps with the EventSourceServlet cleanups), and then we can do a wider cleanup and refactor in 12.1.x next month. |
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback. refactor success and failure into single method. Improved EventSourceServlet
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback. refactor success and failure into single method. Improved EventSourceServlet
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback. refactor success and failure into single method. Improved EventSourceServlet
|
Regarding the minimal 12.0 fix, I think |
| Throwable unconsumed = stream.consumeAvailable(); | ||
| if (failure != null) | ||
| ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed); |
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.
Just ExceptionUtil.combine(failure, stream.consumeAvailable()); as the line above?
But then, this else block is identical to the else-if above?
| * @param throwable The type of {@link Throwable} class to check association against. Can be null. | ||
| * @return true if the {@link Throwable} is associated with the provided type; otherwise, false. | ||
| */ | ||
| public static boolean isAssociated(Throwable failure, Class<? extends Throwable> throwable) |
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.
Reads better hasAssociated() to me.
| else if (response.lockedIsWriting()) | ||
| { | ||
| // We are currently writing, so let the completion of that write handle the failure | ||
| httpChannelState._callbackFailure = failure; |
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.
Unnecessary line.
| else | ||
| { | ||
| // There has been no last write, but we will just fail the stream instead. | ||
| completeStream = true; |
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.
Feels like it should be this, so the third leg is complete:
| completeStream = true; | |
| httpChannelState._streamSendState = StreamSendState.FAILED; | |
| completeStream = true; |
| else if (!httpChannelState.lockedIsLastStreamSendCompleted() && !response.lockedIsWriting()) | ||
| { | ||
| // last write is not going to happen after failure, so we can just fail anyway | ||
| httpChannelState._streamSendState = StreamSendState.LAST_COMPLETE; | ||
| } |
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.
If lockedIsWriting() I should do the same logic as above: steal the write callback, etc.
I would prefer this else-if to be written as just else, because the lack of a catch-all else branch feels like we're forgetting something: I prefer to have an explicit branch that says "here we should really do nothing".
| private class HandlerInvoker implements Invocable.Task, Callback | ||
| // HandlerInvoker is used as the Response's _writeCallback when ChannelCallback is succeeded and the last send still | ||
| // needs to be done, i.e.: _streamSendState set to LAST_SENDING by lockedLastStreamSend(). | ||
| private class HandlerInvoker implements Task, Callback |
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 think we should split the functionality of this class.
Leave run() in HandlerInvoker, but move the Callback functionality into a LastStreamSendCallback class.
| failedCallback = response._writeCallback; | ||
| response._writeCallback = httpChannelState._handlerInvoker; |
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.
In this way, we may wait forever for the write to complete and invoke the _handlerInvoker.
How about this:
| failedCallback = response._writeCallback; | |
| response._writeCallback = httpChannelState._handlerInvoker; | |
| Runnable task = response.lockedFailWrite(failure); | |
| failedCallback = Callback.from(task, httpChannelState._handlerInvoker); |
In 12.1.x we will leverage the new cancelSend() feature automatically.
| LAST_COMPLETE, | ||
|
|
||
| /** Failing, so last send will never happen */ | ||
| FAILED |
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 state is only read, never assigned.
| { | ||
| assert _callbackCompleted; | ||
| _streamSendState = StreamSendState.LAST_COMPLETE; | ||
| completeStream = _handling == null; |
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.
| completeStream = _handled; |
|
|
||
| if (writeFailure == NOTHING_TO_SEND) | ||
| { | ||
| httpChannelState._writeInvoker.run(callback::succeeded); |
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.
Retain the InvocationType.
Fix #13470 by calling completeStream only once even when there is a failure in the channel callback.