Skip to content

Auto-detect native transport by default when its library is available - #2216

Merged
hyperxpro merged 5 commits into
mainfrom
native-transport-auto-detect
Aug 3, 2026
Merged

hyperxpro merged 5 commits into
mainfrom
native-transport-auto-detect

Conversation

@hyperxpro

Copy link
Copy Markdown
Member

Motivation:

AHC defaulted to NIO and only used epoll/io_uring when an app explicitly called setUseNativeTransport(true), so Linux apps that had the native library on the classpath but didn't set the flag silently ran on the slower NIO transport.

Modification:

When neither useNativeTransport nor a custom EventLoopGroup is set, auto-select a native transport (io_uring/epoll on Linux, kqueue on macOS) if its library is present, otherwise NIO, with no warning. The explicit setUseNativeTransport(true) path keeps A4's warn-and-fallback. Force NIO with -Dio.netty.transport.noNative=true.

Result:

Native transport is used out of the box wherever its library is available.

@hyperxpro
hyperxpro force-pushed the native-transport-auto-detect branch 5 times, most recently from 665b215 to 4a85394 Compare August 2, 2026 13:57
hyperxpro added a commit that referenced this pull request Aug 2, 2026
… io_uring fallback

Fixes for PR #2216 CI failures on io_uring:
- InputStreamMultipartPart: prevent infinite loop with slowTarget backpressure
- StackTraceInspector: detect native transport connection resets via errno matching
- NettyChannelConnector: annotate connect failures with host:port for io_uring
- ChannelManager: fallback from io_uring to epoll on ring allocation failure
  (io_uring rings charge against RLIMIT_MEMLOCK, not always available)

Rebased on #2288 (Fix per-host connection permit leak) for proper semaphore handling.
hyperxpro added a commit that referenced this pull request Aug 2, 2026
… io_uring fallback

Fixes for PR #2216 CI failures on io_uring:
- InputStreamMultipartPart: prevent infinite loop with slowTarget backpressure
- StackTraceInspector: detect native transport connection resets via errno matching
- NettyChannelConnector: annotate connect failures with host:port for io_uring
- ChannelManager: fallback from io_uring to epoll on ring allocation failure
  (io_uring rings charge against RLIMIT_MEMLOCK, not always available)

Rebased on #2288 (Fix per-host connection permit leak) for proper semaphore handling.
@hyperxpro
hyperxpro force-pushed the native-transport-auto-detect branch from 4a85394 to a6b7357 Compare August 2, 2026 14:00
hyperxpro added a commit that referenced this pull request Aug 2, 2026
… io_uring fallback

Fixes for PR #2216 CI failures on io_uring:
- InputStreamMultipartPart: prevent infinite loop with slowTarget backpressure
- StackTraceInspector: detect native transport connection resets via errno matching
- NettyChannelConnector: annotate connect failures with host:port for io_uring
- ChannelManager: fallback from io_uring to epoll on ring allocation failure
  (io_uring rings charge against RLIMIT_MEMLOCK, not always available)

Rebased on #2288 (Fix per-host connection permit leak) for proper semaphore handling.
@hyperxpro
hyperxpro force-pushed the native-transport-auto-detect branch from a6b7357 to d223fcb Compare August 2, 2026 14:04
… io_uring fallback

Fixes for PR #2216 CI failures on io_uring:
- InputStreamMultipartPart: prevent infinite loop with slowTarget backpressure
- StackTraceInspector: detect native transport connection resets via errno matching
- NettyChannelConnector: annotate connect failures with host:port for io_uring
- ChannelManager: fallback from io_uring to epoll on ring allocation failure
  (io_uring rings charge against RLIMIT_MEMLOCK, not always available)

Rebased on #2288 (Fix per-host connection permit leak) for proper semaphore handling.
@hyperxpro
hyperxpro force-pushed the native-transport-auto-detect branch from d223fcb to 0e31b49 Compare August 2, 2026 15:21
C1: InputStreamMultipartPart - restore position check to prevent hang on
    streams that don't EOF after declared length (socket-backed, etc)

C2: annotateConnectException - preserve ConnectException type to avoid
    breaking retry predicates that key on ClosedChannelException or
    SslHandler.disconnect stack frames
@hyperxpro
hyperxpro force-pushed the native-transport-auto-detect branch from 1181c12 to c9326b5 Compare August 2, 2026 18:17
The existing zero-copy test drains every write, so nothing exercised the
path io_uring takes when its staging buffer fills. Adds a bounded channel
that returns 0 until the caller yields, which is what #2216 spun on.

Catches a second bug it turned up: the part still read once past the
declared length, so a socket-backed stream would block there.
@hyperxpro
hyperxpro merged commit 791df2d into main Aug 3, 2026
17 checks passed
@hyperxpro
hyperxpro deleted the native-transport-auto-detect branch August 3, 2026 17:07
hyperxpro added a commit that referenced this pull request Sep 21, 2026
NettyConnectListener applies maxRequestRetry to a failure on a NEW
channel only when StackTraceInspector deems it recoverable, and that
check searched the cause chain for the frame
sun.nio.ch.SocketChannelImpl.checkConnect. That frame exists up to
JDK 12 only: JDK 13 moved the completion of a non-blocking connect to
sun.nio.ch.Net.pollConnect, and the native transports report the
failure as a ConnectException from io.netty.channel.unix.Errors with
no sun.nio.ch frame at all. A refused TCP connect was therefore
retried on JDK 11 with NIO and silently not retried on JDK 13, 17, 21
and 25, nor on epoll/kqueue/io_uring, which became the default where
the library is present in #2216.

Match the refusal by type as well, and keep scanning from the cause
rather than from the throwable itself: what the listener receives is
an annotating wrapper, and NettyChannelConnector wraps anything that
is not already a ConnectException in one, so the wrapper's own type
carries no information. Netty's connect timeout is a ConnectException
too and is excluded explicitly, so a blackholed host still fails after
one connectTimeout rather than maxRequestRetry of them. That
exclusion, like the whole predicate, only governs a request's first
attempt: retry() sets ChannelState.RECONNECTED, and from then on the
gate in NettyConnectListener.onFailure short-circuits on the state and
never consults the predicate. That is long-standing behaviour and is
left alone here.

Behaviour change: on JDK 13+ and on the native transports a refused
connect is retried up to maxRequestRetry again, immediately and with
DNS re-resolved per attempt, so onTcpConnectAttempt and
onTcpConnectFailure fire up to (1 + maxRequestRetry) times the number
of resolved addresses, and the time to fail against a dead port rises
accordingly. Known residual, pre-existing and not introduced here: the
native transports map ENETUNREACH and EHOSTUNREACH to a bare
NoRouteToHostException, a sibling of ConnectException with no
sun.nio.ch frame, so an unreachable peer is retried on NIO and not on
a native transport. No connect backoff is added.

Claude Code on behalf of Aayush Atharva

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant