Skip to content

8304065: HttpServer.stop should terminate immediately if no exchanges are in progress#25333

Closed
myankelev wants to merge 7 commits intoopenjdk:masterfrom
myankelev:JDK-8304065
Closed

8304065: HttpServer.stop should terminate immediately if no exchanges are in progress#25333
myankelev wants to merge 7 commits intoopenjdk:masterfrom
myankelev:JDK-8304065

Conversation

@myankelev
Copy link
Member

@myankelev myankelev commented May 20, 2025

HttpServer::stop will terminate the server immidiately after all exhcnages are complete.
If the exchanges take longer then the specified delay it will terminate straight after the delay, the same as the previous behaviour.

Used to wait until the delay is complete at all times, regardless of the number of active exchanges.

Tests based on @eirbjo work, so adding Eirik as a contributor.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8304065: HttpServer.stop should terminate immediately if no exchanges are in progress (Bug - P4)

Reviewers

Contributors

  • Eirik Bjørsnøs <eirbjo@openjdk.org>

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/25333/head:pull/25333
$ git checkout pull/25333

Update a local copy of the PR:
$ git checkout pull/25333
$ git pull https://git.openjdk.org/jdk.git pull/25333/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 25333

View PR using the GUI difftool:
$ git pr show -t 25333

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/25333.diff

Using Webrev

Link to Webrev Comment

myankelev added 2 commits May 20, 2025 11:30
…nges are in progress

HttpServer::stop will terminate the server immidiately after all exhcnages are complete.
If the exchanges take longer then the specified delay it will terminate straight after the delay.

Used to wait until the delay is complete at all times, regardless of the number of active exchanges.
@bridgekeeper
Copy link

bridgekeeper bot commented May 20, 2025

👋 Welcome back myankelevich! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented May 20, 2025

@myankelev This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8304065: HttpServer.stop should terminate immediately if no exchanges are in progress

Co-authored-by: Eirik Bjørsnøs <eirbjo@openjdk.org>
Reviewed-by: dfuchs, michaelm

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 256 new commits pushed to the master branch:

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

As you do not have Committer status in this project an existing Committer must agree to sponsor your change. Possible candidates are the reviewers of this PR (@dfuch, @Michael-Mc-Mahon) but any other Committer may sponsor as well.

➡️ To flag this PR as ready for integration with the above commit message, type /integrate in a new comment. (Afterwards, your sponsor types /sponsor in a new comment to perform the integration).

@myankelev
Copy link
Member Author

/contributor add @eirbjo

@openjdk openjdk bot added the rfr Pull request is ready for review label May 20, 2025
@openjdk
Copy link

openjdk bot commented May 20, 2025

@myankelev
Contributor Eirik Bjørsnøs <eirbjo@openjdk.org> successfully added.

@openjdk
Copy link

openjdk bot commented May 20, 2025

@myankelev The following label will be automatically applied to this pull request:

  • net

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the net net-dev@openjdk.org label May 20, 2025
@mlbridge
Copy link

mlbridge bot commented May 20, 2025

Webrevs

Copy link
Member

@dfuch dfuch left a comment

Choose a reason for hiding this comment

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

Some early comments. I will review the test next.

Comment on lines +203 to +207
final Duration delayDuration = Duration.ofSeconds(1);
log("Shutting down the server the first time");
timeShutdown(delayDuration);
log("Shutting down the server the second time");
timeShutdown(delayDuration);
Copy link
Member

Choose a reason for hiding this comment

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

Should we check the time it took the server to shutdown here too?

Copy link
Member Author

Choose a reason for hiding this comment

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

This test just validates, that there is no error thrown. The other tests cover the timing scenarios

@dfuch
Copy link
Member

dfuch commented May 20, 2025

Looking at Dispatcher::run - there are a couple of additional places where we should take into account the finished flag.
I wonder if we should stop processing events as soon as the finished has been set by one event. That would mean testing for finished right after calling handleEvent - or change handleEvent to return a boolean, and exit out of the while loop and close the selector if we have set finished.
Arguably - maybe there should not be any event in the queue after finished has been set - but there doesn't seem any valid reason to reRegister connections or call select(1000) again at that point.

@openjdk
Copy link

openjdk bot commented May 21, 2025

@myankelev Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. See OpenJDK Developers’ Guide for more information.

Co-authored-by: Daniel Fuchs <67001856+dfuch@users.noreply.github.com>
@msheppar
Copy link

If I have read the original code correctly there is a flaw in it, that is the startExchange is never called.
Thus the code in handleEvent won’t executed as anticipated

391 int exchanges = endExchange();
392 if (terminating && exchanges == 0) {
393 finished = true;
394 }

Exchanges will always be negative. This is inherited in the new update

WRT current PR, I don't think it is good that the state of the server is encapsulated in the state of the CountDownLatch.
The use of the CountDownLatch to synchronise the stop and the Dispatcher shutdown is fine, but having the server state encapsulated in the CountDownLatch value is a bit oblique or obfuscating. I think the original volatile boolean variable is a clearer semantics

I would make stop synchronised to have only one thread executing at time. First check on entry then becomes test on the server's current state. This means that this state management needs some attention.

The addition of Stop event is a neat idea, but draws attention to the semantics of the stop. What is the desired semantics of stop, to not accept any new connections, i.e. stop the listener immediately, not accept new requests on existing connections. To allow existing Exchanges to complete and then shutdown all extant connections.

@@ -223,44 +225,69 @@ public HttpsConfigurator getHttpsConfigurator () {
}

public final boolean isFinishing() {
Copy link

@msheppar msheppar May 23, 2025

Choose a reason for hiding this comment

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

encapsulating the server state in a CountDownLatch is obfuscated semantics

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but this is an existing function and it is used in other classes. I didn't change the functionality

Choose a reason for hiding this comment

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

the internal state representation of the server could do with a rethink and overhaul, and representing part of that state in a synchronisation primitive obscures/obfuscates the server state's semantics. I don't think it's "clean code"

Copy link
Member

Choose a reason for hiding this comment

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

I agree that the semantic of finished finishing is a bit obfuscated. On the other hand I don't think we should go so far as overhauling / rethinking the states in this PR. If you recall there have been lots of fixes in this area (we had some assert firing at some point, and it took some time to find the root cause and fix).
That said having both a boolean and a countdown latch would be duplicate info, and potentially harder to manage consistency.
If we don't want the countdown latch we might go back to looping with Thread.sleep in stop, or use wait/notify, and I am not sure it would be much better.

Copy link
Member

Choose a reason for hiding this comment

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

I have a suggestion. Let's do this:

    public final boolean finished() {
        // we're finished when the latch reaches 0
        return finishedLatch.getCount() == 0;
    }
    
    public final boolean isFinishing() {
        return finished();
    }

Then replace things like if (finished ... with if (finished() ...
That should minimize the changes and make them look less awkward.

* @param delay maximum delay to wait for exchanges completion, in seconds
*/
public void stop (int delay) {
if (delay < 0) {

Choose a reason for hiding this comment

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

with the use of CountDownLatch the negative check becomes superfluous i.e.

public boolean await(long timeout, TimeUnit unit) throws InterruptedException
Causes the current thread to wait until the latch has counted down to zero, unless the thread is interrupted, or the specified waiting time elapses.

. . .
If the specified waiting time elapses then the value false is returned. If the time is less than or equal to zero, the method will not wait at all.

Copy link
Member

Choose a reason for hiding this comment

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

It's not something we should change though, as it's part of the API contract in HttpServer::stop:
https://docs.oracle.com/en/java/javase/24/docs/api/jdk.httpserver/com/sun/net/httpserver/HttpServer.html#stop(int)

if (r instanceof WriteFinishedEvent) {

logger.log(Level.TRACE, "Write Finished");
int exchanges = endExchange();

Choose a reason for hiding this comment

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

startExchange is never called => endExchange can return negative ?

Copy link
Member

Choose a reason for hiding this comment

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

I hope that this is a scenario that cannot happen: to end an exchange you would have to start it first.

* complete within the imparted delay.
*
* @param delay maximum delay to wait for exchanges completion, in seconds
*/

Choose a reason for hiding this comment

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

make synchronised to allow only one thread to execute stop at any one time

Copy link
Member

Choose a reason for hiding this comment

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

Not sure that's the best solution. But if the terminating flag is already set then it means that another thread has initiated stop, and maybe in this case we should just jump to waiting on the latch - no need to post the event again or close the channel and wakeup the selector again. The part in stop that need synchronization already have it, so I don't think declaring stop as synchronized would be such a good idea. In particular it would prevent other threads to call getExchange() / endExchange() so I would be afraid it would prevent active exchanges from proceeding - unless we use wait/notify instead of a countdown latch - but that might be even more difficult to get right.

@dfuch
Copy link
Member

dfuch commented May 23, 2025

If I have read the original code correctly there is a flaw in it, that is the startExchange is never called.

@msheppar what do you mean by that? It's called in ExchangeImpl.java

@msheppar
Copy link

If I have read the original code correctly there is a flaw in it, that is the startExchange is never called.

@msheppar what do you mean by that? It's called in ExchangeImpl.java

Well then I haven't read it correctly :-( needs a deeper dive, I didn't examine the Exchange::run method in sufficient detail ... thanks for the correction .... BUT in my defence I see Dispatcher invoke handle, handle creates an Exchange, which is dispatched to an Executor and I would have expected that the run method of Exchange to call startExchange, as the log message says an exchange has started !!

Copy link
Member

@Michael-Mc-Mahon Michael-Mc-Mahon left a comment

Choose a reason for hiding this comment

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

I wonder could the two Event types be consolidated inside Event.java? It seems overkill to have them in three separate source files now.

@dfuch
Copy link
Member

dfuch commented May 26, 2025

I wonder could the two Event types be consolidated inside Event.java? It seems overkill to have them in three separate source files now.

Do you mean refactoring WriteFinishEvent and StopRequestedEvent as inner classes of Event.java?

@Michael-Mc-Mahon
Copy link
Member

I wonder could the two Event types be consolidated inside Event.java? It seems overkill to have them in three separate source files now.

Do you mean refactoring WriteFinishEvent and StopRequestedEvent as inner classes of Event.java?

Potentially, but I thought the simplest approach would be as top-level package private classes in Event.java

protected Event (ExchangeImpl t) {
this.exchange = t;
}

Choose a reason for hiding this comment

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

I don't see the purpose of this change and the value it brings other than collation and reducing explicit files for the derived Events ?
Could you elaborate the rationale please?

I do see a code smell in WriteFinished directly access the state of ExchangeImpl

@Michael-Mc-Mahon
Copy link
Member

If I have read the original code correctly there is a flaw in it, that is the startExchange is never called. Thus the code in handleEvent won’t executed as anticipated

391 int exchanges = endExchange(); 392 if (terminating && exchanges == 0) { 393 finished = true; 394 }

Exchanges will always be negative. This is inherited in the new update

WRT current PR, I don't think it is good that the state of the server is encapsulated in the state of the CountDownLatch. The use of the CountDownLatch to synchronise the stop and the Dispatcher shutdown is fine, but having the server state encapsulated in the CountDownLatch value is a bit oblique or obfuscating. I think the original volatile boolean variable is a clearer semantics

I would make stop synchronised to have only one thread executing at time. First check on entry then becomes test on the server's current state. This means that this state management needs some attention.

The addition of Stop event is a neat idea, but draws attention to the semantics of the stop. What is the desired semantics of stop, to not accept any new connections, i.e. stop the listener immediately, not accept new requests on existing connections. To allow existing Exchanges to complete and then shutdown all extant connections.

startExchange() is called in the constructor for ExchangeImpl, which is not changing in this PR.

Copy link
Member

@dfuch dfuch left a comment

Choose a reason for hiding this comment

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

LGTM

@openjdk openjdk bot added the ready Pull request is ready to be integrated label May 28, 2025
@myankelev
Copy link
Member Author

@dfuch, @msheppar, @djelinski , @Michael-Mc-Mahon, thank you for your reviews!

@myankelev
Copy link
Member Author

/integrate

@openjdk openjdk bot added the sponsor Pull request is ready to be sponsored label May 28, 2025
@openjdk
Copy link

openjdk bot commented May 28, 2025

@myankelev
Your change (at version 1ca9ecd) is now ready to be sponsored by a Committer.

Copy link
Member

@Michael-Mc-Mahon Michael-Mc-Mahon left a comment

Choose a reason for hiding this comment

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

Looks good

@Michael-Mc-Mahon
Copy link
Member

/sponsor

@openjdk
Copy link

openjdk bot commented May 28, 2025

Going to push as commit 627ef34.
Since your change was applied there have been 256 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label May 28, 2025
@openjdk openjdk bot closed this May 28, 2025
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review sponsor Pull request is ready to be sponsored labels May 28, 2025
@openjdk
Copy link

openjdk bot commented May 28, 2025

@Michael-Mc-Mahon @myankelev Pushed as commit 627ef34.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integrated Pull request has been integrated net net-dev@openjdk.org

Development

Successfully merging this pull request may close these issues.

5 participants