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

HttpInput refactoring #4556

Merged
merged 6 commits into from
Nov 3, 2020
Merged

HttpInput refactoring #4556

merged 6 commits into from
Nov 3, 2020

Conversation

lorban
Copy link
Contributor

@lorban lorban commented Feb 7, 2020

This is a work in progress, the PR is only about exercising the code against the full test suite under Jenkins. As a reminder, this work was triggered by #4318.

Old status:

  • HTTP 1 and FCGI have been reimplemented and nearly pass the full test suite (Milestone 1)
  • HTTP 2 has been reimplemented and nearly passes the full test suite (Milestone 2)
  • All HttpInput* implementations pass the full test suite (Milestone 3)
  • HTTP 1, 2 and FCGI implementations share 90% of their code, don't queue and pass the full test suite (Milestone 4)
  • addContent() doesn't return a boolean that is used for scheduling. (Milestone 5)
  • HTTP 1, 2 and FCGI use the exact same implementation, all differences having been pushed to HttpChannel (Milestone 6).

Current status:

  • Got rid of addContent() by making produceContent() return Content instead.
  • Make EOF and errors be special content.
  • Transition to a much simplified FSM by using the needContent() / produceContent() model.
  • Implement blocking on top of async, this way there is only one FSM.

Next steps:

  • Code review.

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.

just some thoughts from a light review in passing...

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.

review in progress...

HttpInput.Content content = takeContent(endStreamResult);
if (content != null)
return content;
getStream().demand(0);
Copy link
Contributor

@sbordet sbordet Apr 13, 2020

Choose a reason for hiding this comment

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

I'm not particularly fond of this - I'm sure it can be done otherwise.
The demand mechanism is well established now (even present in the JDK) so I would not change its semantic.

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 reason why I introduced this mechanism is to make HTTP 1 and 2 more similar and avoid triggering data demands until we reach a safe spot in the state machine, much like HTTP 1 makes the distinction between parseAndFill and setting fillInterested.

This drastically helps controlling the paths that could lead to race conditions as the latter can only happen wherever we decide they can happen instead of virtually everywhere, making debugging a lot more manageable.

I tried really hard without touching this mechanism and the fact that you can't read data without triggering an async call that adds content makes the code, IMHO, virtually unmaintainable.

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to discuss this more, as we have certainly figured out more complex cases, and I don't feel this one is impossible to solve without breaking the demand semantic - which would potentially introduce a much larger problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I agree.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about just adding an available() method to the interface. All that is necessary is something that can be used to query to decide if we should continue iterating on content or exit and let HttpChannel.unhandle do the demand(1) to setup to receive more content. So long as we don't have content arriving between those two events, then things become simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the idea of adding an available() method a lot: it's a cleaner interface, trivial to implement and provides the necessary semantic.

@gregw
Copy link
Contributor

gregw commented Jun 13, 2020

Just a note on status of my much interrupted redux review of this PR.
I have a branch that has a few non consequential changes (cleanups, renames etc.)... will push that soonish

I am working through the interaction diagrams of the main flows for h1|h2 x blocking| async. I have still some more work to do there to get a good overall picture before deciding how I think it should change.... but I'll note down my current thinking here & now, just incase I get interrupted again and forget.

The issue for me is how in some scenarios the callbacks are doing the content processing, while in others they don't. Thecrux for my is h2 callbacks with call addContent, which in turn calls available (to see if that raw content actually produced real content) and then decides if handle should be called or not. This is done outside of the normal handle cycle, so it is racey and needs protection etc.

So I think we can greatly improve this by not having addContent call available. The h2 callbacks will still add the raw content, but it will not be immediately processed, instead they will return true if a wakeup is needed and then we will call HttpChannel.handle, which should be in the REGISTER (or may PRODUCING???) state and it can call available() (or something similar) to process raw content to see if there is real content and then decide to call onReadPossible or not. The processing is then done in the better protected handling loop, the same as for h1..... at least that is my bubble of an idea.. Lot's more detailed analysis still needed for the exact state transitions.

@gregw
Copy link
Contributor

gregw commented Aug 18, 2020

@lorban don't forget the interaction diagrams! I know you are going step by step towards your destination.... but we don't want you to get there only to discover that the usual malcontents didn't want to go there in the first place!

Please have a go of updating them to what you think they should be and check them in.

@lorban lorban marked this pull request as ready for review October 7, 2020 07:00
@lorban lorban requested review from gregw and sbordet October 7, 2020 07:00
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.

I couple of little things while I get on with deeper review...

if (content != null)
return content;

_semaphore.drainPermits();
Copy link
Contributor

Choose a reason for hiding this comment

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

I really REALLY don't like having drainPermits. This should not be necessary

boolean handleRequest = onRequestComplete();
handle |= handleContent | handleRequest;
}

boolean woken = getRequest().getHttpInput().wakeup();
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 this is wrong because it means a spurious wakeup can occur even if needContent has not been called.

A call to produceContent can do demand, so onData may be called at any time afterwards... so it is wrong for it to wakeup HttpInput if needContent has not been called.

This class needs to know if needContent has been called and only do a wakeup if it has.

Copy link
Contributor

Choose a reason for hiding this comment

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

The more I think about this, the more I'm convinced we need a dedicated onContentAvailable callback that is only called if needContent has been called.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could almost pass a Callback in needContent to make the contract clear!

boolean isError();

/**
* This call may block.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this call may block? Isn't it wrong that there is a blocking method if it can be implemented by AsyncContentProducer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of poor javadoc. What I meant is that some implementations may decide to make nextContent() a blocking call, some others may not. This is exactly what BlockingContentProducer and AsyncContentProducer do but I'd like the javadoc to avoid speaking about specific implementations.

// events like timeout: we get notified and either schedule onError or release the
// blocking semaphore.
private HttpInput.Content _content;
private HttpInput.Content _specialContent;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not true that _specialContent is only accessed by one thread. It is set by the scheduler thread during idle timeouts.

Copy link
Contributor Author

@lorban lorban Oct 15, 2020

Choose a reason for hiding this comment

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

My original analysis was that the scheduler thread only ever writes to _specialContent, then either notifies the blocked thread with the semaphore (which gives us a happens-before) or reschedules a thread that reads it. A job submission to a standard ExecutorService creates a happens-before relationship that would make this safe but I may need to check if the form of scheduling we use in EWYK does too. I suspect it does, but I'll review this in more details.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks safe to me, but I could mark _specialContent as volatile nevertheless. This would make the code a bit less brittle in wrt potential future changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need both _content and _specialContent. We only have a single content in H2 and I think similar logic could be applied here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got rid of _specialContent.


_semaphore.drainPermits();
_httpChannel.getState().onReadUnready();
if (_httpChannel.needContent())
Copy link
Contributor

Choose a reason for hiding this comment

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

It needs to be PRECISELY that only when needContent returns false will there be some callback that will release the semaphore. So we need to only ever acquire the semaphore in that case. We then don't need drain permits. I'm still thinking needContent(Callback) is the way to go.

@Override
public boolean wakeup()
{
// Calling _asyncContentProducer.wakeup() breaks AsyncIOServletTest.testAsyncWriteThrowsError H2C.
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, feels weird that we are not forwarding to _asyncContextProducer -- if really not needed, it deserves a comment.

@lorban lorban requested review from gregw and sbordet October 26, 2020 13:39
@lorban lorban requested a review from gregw October 28, 2020 09:36
Copy link
Contributor

@sbordet sbordet left a comment

Choose a reason for hiding this comment

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

LGTM, apart few niceties and a little more javadocs.

else if (c == null)
{
if (!content.isSpecial())
throw new AssertionError("Non special content without demand");
Copy link
Contributor

Choose a reason for hiding this comment

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

I insist that we cannot throw, even if it's a bug.
Throwing will cause the server to hang and require a restart, while failing the callback will fail the current request and allow for future requests.

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…CGI (Milestone 3)

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…to HttpChannel* impls (Milestone 5)

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…tead.

Make EOF and errors be special content.
Transition to a much simplified FSM by using the needContent() / produceContent() model.
Implement blocking on top of async, this way there is only one FSM.
(Milestone 6)

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants