Skip to content
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

Issue #6167 - Review failure cases of HttpOutput.Interceptor #6226

Closed

Conversation

lachlan-roberts
Copy link
Contributor

Issue #6167

This PR adds extra testing around what happens if an HttpOutput.Interceptor throws or fails the callback.

Some changes were made to HttpOutput so that in most cases we will respond with a 500 response instead of just aborting the connection.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
gregw
gregw previously approved these changes Apr 30, 2021
Copy link
Contributor

@gregw gregw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I like the approach.... other than my niggles about test, name and javadoc.
But I'd still like to hear from @sbordet regarding throwing from async close/write/flush.

…xception.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
// Otherwise the async call to onWriteComplete must abort if there is a failure as onError may not be called.
_abortOnCloseError = true;
if (_onError != null)
IO.throwCheckedIOException(_onError);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This piece of code after channelWrite() is in a race with the WriteCompleteCB, so I think you should only do the work in WriteCompleteCB.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is about getting the close() method to throw if the WriteCompleteCB has been failed before we get to this (like if the interceptor failed it). We do this here to report the exception back to the application without aborting the channel so we can send a 500 response.

@Test
public void testInterceptorCallbackFailed() throws Exception
{
addInterceptor(nextInterceptor -> new HttpOutput.Interceptor()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addInterceptor() seems unnecessary. Why don't you just add the interceptor from the AbstractHandler that you have in every test? In this way you also won't need _handlerCollection.

Something like:

@Test
public void testInterceptorCallbackFailed() 
{
    start(new AbstractHandler()
    {
        @Override
        public void handle(...) 
        {
            HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput();
            httpOutput.setInterceptor(...);
            ...
        }
    });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all the interceptors added by the tests do the same thing, some fail the callback, some throw, some start a new thread and wait on a CountDownLatch before failing the callback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lachlan-roberts you can do what you need with the interceptor on the second line of the handler, no?

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Comment on lines +623 to +624
if (_onError != null)
IO.throwCheckedIOException(_onError);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be more explicit that this code is in a known race, even though it will work the same:

Suggested change
if (_onError != null)
IO.throwCheckedIOException(_onError);
Throwable onError = _onError; // Was an exception reported already
if (onError != null)
IO.throwCheckedIOException(onError);

@sbordet I told @lachlan-roberts that you wouldn't like this code... yes it is a race for a general type of async exception, but there is a class of exception that will be thrown during the processing of the close, and it is those that we should throw here so the application can know all was not OK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregw @lachlan-roberts IIUC this if block is the case where we do an async close, which is actually our educated interpretation of what we should do in case of async I/O, because it's underspecified.
I'm not sure it's worth reporting it with an exception, because we already report it with onError() (and if we don't we probably should).

I am worried about the case where we throw from close, but also fail the callback, and we will have one thread receiving the exception trying to write the error page, and to do that modify the channel and output states, and another thread failing the callback also modifying the channel and output states, and calling onError().
I feel that we are just asking for troubles, garbled responses, etc.

If there is an exception in an async close, we call onError() and that's enough notifications to the application.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that we don't call onError() if there is a failure with the async close.
If there is the following code:

httpOutput.close();
asyncContext.complete();
return;

then you can't call onError() because asyncContext.complete() was called and there was no call to isReady() which returned false.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover, we can't rely on calling onError for issues at the end of the stream, because when we have an exception or are at end of stream, then isReady() returns true, so the app will loop and not return from onWritePossible, so we will never be able to call onError even if we wanted to!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't call onError(), which is the usual path for errors in async I/O, why do we even bother?

The exception may or may not be thrown depending on thread scheduling etc, so it's not reliable behavior, we have a race in our code and we don't provide any reliable benefit to user code, just that sometimes, under certain conditions, if the thread scheduling aligns, maybe we throw.

This is the kind of code that in 6 months we are going to have an issue reported and ask ourselves "who put that racy code in there?"

I don't think it's worth it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if we don't throw, then we end up aborting the connection rather than sending a 500

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregw some times we send a 500; some other times we don't.
If it was possible to always send a 500, would be good -- even if it was racy.
But if we can't always send a 500, then I don't think it's worth it, also considering that calling close() explicitly is not that common.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we also throw from write or flush, so it's just making it the same as those operations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in write() and flush() there is no race.

Comment on lines +623 to +624
if (_onError != null)
IO.throwCheckedIOException(_onError);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregw @lachlan-roberts IIUC this if block is the case where we do an async close, which is actually our educated interpretation of what we should do in case of async I/O, because it's underspecified.
I'm not sure it's worth reporting it with an exception, because we already report it with onError() (and if we don't we probably should).

I am worried about the case where we throw from close, but also fail the callback, and we will have one thread receiving the exception trying to write the error page, and to do that modify the channel and output states, and another thread failing the callback also modifying the channel and output states, and calling onError().
I feel that we are just asking for troubles, garbled responses, etc.

If there is an exception in an async close, we call onError() and that's enough notifications to the application.

@gregw
Copy link
Contributor

gregw commented May 15, 2021

Considering @lachlan-roberts research regarding this, I am switching this PR back to draft and Lachlan should work on a version that returns false from isReady if there is an exception that will be passed to onError.

@lachlan-roberts can you write up your findings either here on in the actual issue.

@gregw gregw marked this pull request as draft May 15, 2021 22:52
@gregw gregw added this to In progress in Jetty 10.0.4/11.0.4 FROZEN via automation May 15, 2021
Jetty 10.0.4/11.0.4 FROZEN automation moved this from In progress to Review in progress May 15, 2021
Copy link
Contributor

@gregw gregw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try changing so isReady() returns false if an exception has occurred

@sbordet sbordet removed this from Review in progress in Jetty 10.0.4/11.0.4 FROZEN Jun 4, 2021
@sbordet sbordet added this to In progress in Jetty 10.0.5/11.0.5 FROZEN via automation Jun 4, 2021
@sbordet sbordet removed this from In progress in Jetty 10.0.5/11.0.5 FROZEN Jun 10, 2021
@sbordet sbordet added this to In progress in Jetty 10.0.6/11.0.6 FROZEN via automation Jun 10, 2021
@gregw gregw removed this from In progress in Jetty 10.0.6/11.0.6 FROZEN Jun 21, 2021
@gregw gregw added this to In progress in Jetty 10.0.7/11.0.7 FROZEN via automation Jun 21, 2021
@stale
Copy link

stale bot commented Jul 9, 2021

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the Stale For auto-closed stale issues and pull requests label Jul 9, 2021
@lachlan-roberts lachlan-roberts removed the Stale For auto-closed stale issues and pull requests label Jul 14, 2021
@joakime joakime added this to the 10.0.x milestone Sep 15, 2021
@lachlan-roberts lachlan-roberts removed this from In progress in Jetty 10.0.7/11.0.7 FROZEN Sep 22, 2021
@github-actions
Copy link

This pull request has been automatically marked as stale because it has been a
full year without activity. It will be closed if no further activity occurs.
Thank you for your contributions.

@github-actions github-actions bot added the Stale For auto-closed stale issues and pull requests label Sep 16, 2022
@github-actions
Copy link

This pull request has been closed due to it having no activity.

@github-actions github-actions bot closed this Oct 16, 2022
@joakime joakime deleted the jetty-10.0.x-6167-HttpOutputInterceptor branch November 21, 2022 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Stale For auto-closed stale issues and pull requests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants