Skip to content

Complete I/O reactor termination when the selector has been closed by another thread - #718

Merged
ok2c merged 1 commit into
apache:masterfrom
AdzerKI:closed-selector-on-terminate
Oct 2, 2026
Merged

ok2c merged 1 commit into
apache:masterfrom
AdzerKI:closed-selector-on-terminate

Conversation

@AdzerKI

@AdzerKI AdzerKI commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

We hit this with HttpClient 5 talking to a TLS 1.3 server that does not answer close_notify (JDK default, jdk.tls.acknowledgeCloseNotify=false). CloseableHttpAsyncClient#close() waits for the graceful shutdown, gives up after 5 seconds, and MultiCoreIOReactor then closes each SingleCoreIOReactor with CloseMode.IMMEDIATE from the calling thread. That closes the selector while the I/O dispatch thread is still running. Its doTerminate calls closeOpenChannels, selector.keys() throws, and the default LoggingExceptionCallback logs an ERROR on every client shutdown:

java.nio.channels.ClosedSelectorException
	at java.base/sun.nio.ch.SelectorImpl.ensureOpen(Unknown Source)
	at java.base/sun.nio.ch.SelectorImpl.keys(Unknown Source)
	at org.apache.hc.core5.reactor.SingleCoreIOReactor.closeOpenChannels(SingleCoreIOReactor.java:418)
	at org.apache.hc.core5.reactor.SingleCoreIOReactor.doTerminate(SingleCoreIOReactor.java:117)
	at org.apache.hc.core5.reactor.AbstractSingleCoreIOReactor.execute(AbstractSingleCoreIOReactor.java:99)
	at org.apache.hc.core5.reactor.IOReactorWorker.run(IOReactorWorker.java:44)

The exception also skips processClosedSessions, so the handlers of sessions closed by close(CloseMode) never get disconnected.

By then close(CloseMode) has already closed the registered channels, so closeOpenChannels now just returns when the selector is closed and termination goes on. doExecute already ignores ClosedSelectorException for the same reason.

Test: TestSingleCoreIOReactor#terminatesWhenSelectorClosedByAnotherThread holds the dispatch thread in connected, closes the reactor from the test thread, then lets the dispatch thread go. It expects no reported exception and a disconnected call. Fails on master with ClosedSelectorException, passes with the fix; httpcore5 builds with all tests.

The same code is on 5.4.x (the stack trace above is from 5.4.3).

… another thread

closeOpenChannels threw ClosedSelectorException once close(CloseMode) from
another thread had closed the selector, so doTerminate reported it through
the exception callback and skipped processClosedSessions: event handlers of
the closed sessions were never notified of the disconnect.
@ok2c
ok2c merged commit 4c24974 into apache:master Oct 2, 2026
12 checks passed
@ok2c

ok2c commented Oct 2, 2026

Copy link
Copy Markdown
Member

@AdzerKI Cherry-picked to 5.4.x

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.

2 participants