You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bind the connection permit to the channel on connect
NettyConnectListener.onSuccess takes the per-host permit off the future
with takePartitionKeyLock(), a getAndSet(null), after which the future
can no longer release it: a later abort() finds the field null and
returns nothing. Ownership only passed to the channel when
attachSemaphoreToChannelClose ran, which on the TLS paths happened after
the handshake completed. Every exit taken before that point dropped the
token in a dead local and leaked the permit permanently.
That window is not just a race. A TLS handshake that fails - bad
certificate, peer reset, or Netty's own handshake timeout - always
leaves through it, as does an AsyncHandler callback throwing in the
same window. Since onFailure may then retry, and the retry finds a null
partitionKeyLock and acquires a fresh permit, a run of failures against
one origin drains maxConnectionsPerHost and pins it: every later
request to that host fails with TooManyConnectionsPerHostException with
no live connection to account for it. The combined limiter releases the
global slot through the same call, so one bad origin also starves
unrelated hosts. Only recreating the client recovers.
Bind the permit to channel.closeFuture() the moment it leaves the
future, and route every release through one getAndSet so it happens
exactly once - a double release would push the semaphore above the cap
and can prematurely prune a live per-host entry. Close-release is now
the default rather than a per-branch opt-in, so the failure paths are
covered by the close they already perform, and a new branch cannot
reintroduce the leak by forgetting to opt in.
Also publish the channel on the future before the handshake starts.
Until then future.channel() is null and NettyRequestSender.abort only
closes a non-null channel, so a request timeout firing mid-handshake
could not close the socket and left it, and its permit, alive until
handshakeTimeout. The field is now volatile: it is written on the event
loop but read by the timer thread in TimeoutTimerTask.expire and by
cancel() on the caller thread.
Fixes#2189
0 commit comments