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

Implement Eclipse Jetty core HTTP handler adapter #32097

Open
wants to merge 75 commits into
base: main
Choose a base branch
from

Conversation

gregw
Copy link

@gregw gregw commented Jan 24, 2024

This provides an implementation of a HTTP Handler Adaptor that is coded directly to the Eclipse Jetty core API, bypassing any servlet implementation.

Fixes #32035

@pivotal-cla
Copy link

@gregw Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 24, 2024
@gregw
Copy link
Author

gregw commented Jan 24, 2024

@pivotal-cla Working on getting a CCLA signed. Stand by....

@gregw
Copy link
Author

gregw commented Feb 1, 2024

@lachlan-roberts can you sign the individual CLA.

@gregw
Copy link
Author

gregw commented Feb 1, 2024

This PR is failing 2 tests that undertow also fails: See #25310.
Thus I'm currently suspecting bad tests or a spring bug, as both Jetty core and undertow are similar fully asynchronous integrations without servlets. We will investigate more, but any ideas that can help...

@pivotal-cla
Copy link

@gregw Thank you for signing the Contributor License Agreement!

@gregw gregw marked this pull request as ready for review February 1, 2024 01:52
Comment on lines 133 to 138
// TODO: Why does intellij warn about possible blocking call?
// Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel
SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ);
Copy link
Author

Choose a reason for hiding this comment

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

Files.newByteChannel is a blocking operation, but I see that undertow also use it and just live with the warning. The chance of it actually blocking for any significant time is vanishingly small and there is no chance of dead lock.

// this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); // TODO this copy avoids multipart bugs
this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice?
Copy link
Author

Choose a reason for hiding this comment

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

This is the work around for the failing multipart tests (see #25310)

@jhoeller jhoeller added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Feb 5, 2024
lachlan-roberts and others added 2 commits April 9, 2024 15:09
Signed-off-by: Lachlan Roberts <lachlan.p.roberts@gmail.com>
@poutsma poutsma self-assigned this Apr 16, 2024
@poutsma
Copy link
Contributor

poutsma commented Apr 16, 2024

@gregw Hi Greg, sorry for the recent lack of feedback. I plan to take a look at this PR soon, and hopefully we can merge it in 6.2 M2 or M3.

@gregw
Copy link
Author

gregw commented May 23, 2024

@poutsma Would be good to get at least an initial review, as I'm sure there is some rough edges that need to be knocked off. Giving us a few comments will get us working on this and updating... plus we want to flow the work through springboot as well.

@poutsma
Copy link
Contributor

poutsma commented May 24, 2024

@gregw Fair point. I had to take some unforeseen medical leave, but plan to focus on this PR this coming week.

@poutsma poutsma modified the milestones: 6.2.x, 6.2.0-M4 May 24, 2024
@poutsma
Copy link
Contributor

poutsma commented May 28, 2024

@gregw First off, thank you for submitting this PR, as well as your patience with regards to us handling it. The PR seems to be quite usable, and I am sure it will make it into 6.2.

Before getting into any specific details, my main question is why do JettyCoreServerHttpRequest and JettyCoreServerHttpResponse implement the ServerHttpRequest and ServerHttpResponse interfaces, and not to extend the abstract base classes? It appears that a lot of changes in the PR derive from this decision (for instance getNativeRequest is moved from the abstract base class to the interface). While it can be argued that this refactoring is desirable, it seems unrelated and makes this PR harder to review.

How should we proceed? If you prefer, I can take the PR from here, and get back you when I have questions. Or I can provide a more thorough review.

@gregw
Copy link
Author

gregw commented May 28, 2024

@gregw First off, thank you for submitting this PR, as well as your patience with regards to us handling it. The PR seems to be quite usable, and I am sure it will make it into 6.2.

Awesome!

Before getting into any specific details, my main question is why do JettyCoreServerHttpRequest and JettyCoreServerHttpResponse implement the ServerHttpRequest and ServerHttpResponse interfaces, and not to extend the abstract base classes? It appears that a lot of changes in the PR derive from this decision (for instance getNativeRequest is moved from the abstract base class to the interface). While it can be argued that this refactoring is desirable, it seems unrelated and makes this PR harder to review.

Very good question. I cannot recall and looking at the code now, I see no particular reason, at least not for the request.

How should we proceed? If you prefer, I can take the PR from here, and get back you when I have questions. Or I can provide a more thorough review.

I'm flexible. I think it would be good to get you closely involved as I'm sure there a spring things that I'm doing wrong and working together would be a good way to communicate knowledge. So how about this as an approach:

  1. I'll have a go at reacting to your first feedback: i.e. try to use the abstract base classes. That will refresh my brain on this branch (it has been a while since I looked).
  2. You can then take over it for a while, changing anything you don't like or that is wrong
  3. I can stay involved, helping out as you require and reviewing your changes with a Jetty eye.

gregw and others added 3 commits May 29, 2024 14:26
Signed-off-by: Lachlan Roberts <lachlan.p.roberts@gmail.com>
Signed-off-by: Lachlan Roberts <lachlan.p.roberts@gmail.com>
@poutsma
Copy link
Contributor

poutsma commented May 29, 2024

I'm flexible. I think it would be good to get you closely involved as I'm sure there a spring things that I'm doing wrong and working together would be a good way to communicate knowledge. So how about this as an approach:

  1. I'll have a go at reacting to your first feedback: i.e. try to use the abstract base classes. That will refresh my brain on this branch (it has been a while since I looked).
  2. You can then take over it for a while, changing anything you don't like or that is wrong
  3. I can stay involved, helping out as you require and reviewing your changes with a Jetty eye.

Sounds good to me. It looks like you committed step 1 (d6986ee), so I will get started with step 2. Unless there are more changes forthcoming?

@gregw
Copy link
Author

gregw commented May 29, 2024

@poutsma my step 1 is pretty broken. I think it need more work. stand by....

@gregw
Copy link
Author

gregw commented May 29, 2024

@poutsma I've backed out the abstract response changes for now, as they broke a lot of tests. So I need to understand the difference and try again. Probably tomorrow now...

gregw and others added 3 commits May 30, 2024 06:48
…dapter

# Conflicts:
#	framework-platform/framework-platform.gradle
Signed-off-by: Lachlan Roberts <lachlan.p.roberts@gmail.com>
@poutsma
Copy link
Contributor

poutsma commented May 30, 2024

@poutsma I've backed out the abstract response changes for now, as they broke a lot of tests. So I need to understand the difference and try again. Probably tomorrow now...

@gregw No worries, just let me know when you want me to take a look.

@gregw
Copy link
Author

gregw commented May 31, 2024

@poutsma OK I think I've cracked the abstract response now as well. There is one test that fails occasionally that I need to look into more, but I think the PR is good for you to look at now. If you want to start making changes, go for it.. or direct me.

@poutsma
Copy link
Contributor

poutsma commented May 31, 2024

@gregw I have made a first pass at reviewing the PR, and so far have made two changes:

  • I have reverted all unnecessary changes to ServerHttpRequest, ServerHttpRequest and subtypes, and simplified JettyCoreHttpHandlerAdapter, JettyCoreServerHttpRequest and JettyCoreServerHttpResponse, see here.
  • I have replaced flatMap(mapper, 1) with concatMap(mapper) in JettyCoreServerHttpResponse as these are semantically equivalent, and we use concatMap elsewhere, see here.

Regardless of whether the changes above have been applied, the main test failure I see is MultipartWebClientIntegrationTests. Next week I will take a look at why these fail. I will also take a look at the websocket changes, as I haven't done so yet.

@poutsma
Copy link
Contributor

poutsma commented Jun 6, 2024

I have committed several other changes, see https://github.com/poutsma/spring-framework/commits/gh-32097/.

The only test that fails is the MultipartWebClientIntegrationTests. It does not fail in the IDE, making it a bit harder to reproduce. This is the server-side stack trace I get:

java.lang.IllegalArgumentException: demand pending
    at org.eclipse.jetty.server.internal.HttpChannelState$ChannelRequest.demand(HttpChannelState.java:955) ~[jetty-server-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.io.content.ContentSourcePublisher$SubscriptionImpl.process(ContentSourcePublisher.java:124) ~[jetty-io-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.util.thread.SerializedInvoker$Link.run(SerializedInvoker.java:191) [jetty-util-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.server.internal.HttpConnection$DemandContentCallback.succeeded(HttpConnection.java:680) [jetty-server-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99) [jetty-io-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53) [jetty-io-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979) [jetty-util-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209) [jetty-util-12.0.10.jar:12.0.10]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164) [jetty-util-12.0.10.jar:12.0.10]
    at java.base/java.lang.Thread.run(Thread.java:840) [?:?]

@gregw Does that ring any bell with you, or are we doing something wrong in the Spring code?

I have also asked @rstoyanchev and @simonbasle to review (my branch of) the PR, so comments from them might be forthcoming.

The next Spring Framework milestone (6.2.0-M4) is on June 13th. I am not sure if we will have resolved the test above by that point, so this PR might not make that milestone. The milestone after that is scheduled for July 11th.


private final Flux<WebSocketMessage> flux;
private final Sinks.One<CloseStatus> closeStatusSink = Sinks.one();
private final Lock lock = new ReentrantLock();
Copy link
Contributor

Choose a reason for hiding this comment

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

This Lock got my attention because pessimistic locking is usually not the first go-to method in Reactive Streams. Instead it's usually some sort of optimistic locking with a CAS loop.

That said, the lock protects 2 critical sections that represent quick state checks and updates. There is precedent for that sort of things, e.g. in Reactor, so in that case I think it is fine (although care will need to be exercised if the code is modified or new usage of the lock is introduced).

boolean demand = false;
this.lock.lock();
try {
this.requested += n;
Copy link
Contributor

@simonbasle simonbasle Jun 6, 2024

Choose a reason for hiding this comment

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

this is more problematic, as n can be an "unbounded request" (Integer.MAX_VALUE) and this.requested can overflow to negative.

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
this.requested += n;
this.requested += n;
if (this.requested < 0L) {
this.requested = Long.MAX_VALUE;
}

try {
this.requested += n;
if (!this.awaitingMessage && this.requested > 0) {
this.requested--;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not entirely sure how it translates in terms of getDelegate().demand() calls, but if the requested amount is unbounded, this decrement shouldn't happen.

with the above suggested change, unbounded request amount (n == Integer.MAXVALUE at any point) leads to this.requested == Long.MAX_VALUE so if that's the case the this.requested-- decrement should be skipped.

}
this.awaitingMessage = false;
if (this.requested > 0) {
this.requested--;
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above, this should take the "unbounded request" scenario (in the Reactive Streams sense) into consideration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add an Eclipse Jetty Core HttpHandlerAdaptor
8 participants