Skip to content

Commit 9b6b591

Browse files
authored
Don't start the timeout if the connection was already closed (#2011)
There are some race situations where `startReadTimeoutHandler` is called after the channel has been closed/deregistered. This results in the addBefore call failing because DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER is not in the pipeline anymore. I added a simple guard to short circuit when the channel is already closed
1 parent 3d447db commit 9b6b591

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

zuul-core/src/main/java/com/netflix/zuul/netty/connectionpool/PooledConnection.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,7 @@ public boolean release() {
186186

187187
if (!isShouldClose() && connectionState != ConnectionState.WRITE_READY) {
188188
CurrentPassport passport = CurrentPassport.fromChannel(channel);
189-
LOG.info(
190-
"Error - Attempt to put busy connection into the pool = {}, {}",
191-
this.toString(),
192-
String.valueOf(passport));
189+
LOG.info("Error - Attempt to put busy connection into the pool = {}, {}", this, passport);
193190
this.shouldClose = true;
194191
}
195192

@@ -213,8 +210,12 @@ private void removeHandlerFromPipeline(String handlerName, ChannelPipeline pipel
213210
}
214211

215212
public void startReadTimeoutHandler(Duration readTimeout) {
216-
getChannel()
217-
.pipeline()
213+
Channel channel = getChannel();
214+
if (!channel.isActive()) {
215+
LOG.debug("Tried to start read timeout handler, but channel is not active");
216+
return;
217+
}
218+
channel.pipeline()
218219
.addBefore(
219220
DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER,
220221
READ_TIMEOUT_HANDLER_NAME,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.zuul.netty.connectionpool;
18+
19+
import static com.netflix.zuul.netty.connectionpool.PooledConnection.READ_TIMEOUT_HANDLER_NAME;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import com.netflix.spectator.api.NoopRegistry;
23+
import com.netflix.zuul.discovery.DiscoveryResult;
24+
import io.netty.channel.ChannelInboundHandlerAdapter;
25+
import io.netty.channel.embedded.EmbeddedChannel;
26+
import java.time.Duration;
27+
import java.util.List;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.mockito.Mock;
32+
import org.mockito.junit.jupiter.MockitoExtension;
33+
34+
/**
35+
* @author Justin Guerra
36+
* @since 10/3/25
37+
*/
38+
@ExtendWith(MockitoExtension.class)
39+
class PooledConnectionTest {
40+
41+
@Mock
42+
private ClientChannelManager manager;
43+
44+
private EmbeddedChannel channel;
45+
private PooledConnection connection;
46+
47+
@BeforeEach
48+
public void setup() {
49+
channel = new EmbeddedChannel();
50+
NoopRegistry noopRegistry = new NoopRegistry();
51+
connection = new PooledConnection(
52+
channel,
53+
DiscoveryResult.EMPTY,
54+
manager,
55+
noopRegistry.counter("close"),
56+
noopRegistry.counter("closeBusy"));
57+
}
58+
59+
@Test
60+
void startReadTimeoutHandler() {
61+
channel.pipeline()
62+
.addLast(DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER, new ChannelInboundHandlerAdapter());
63+
connection.startReadTimeoutHandler(Duration.ofSeconds(1));
64+
List<String> names = channel.pipeline().names();
65+
assertThat(names.get(0)).isEqualTo(READ_TIMEOUT_HANDLER_NAME);
66+
assertThat(names.get(1)).isEqualTo(DefaultOriginChannelInitializer.ORIGIN_NETTY_LOGGER);
67+
}
68+
69+
@Test
70+
void startReadTimeoutHandlerInactive() {
71+
channel.close();
72+
connection.startReadTimeoutHandler(Duration.ofSeconds(1));
73+
assertThat(channel.pipeline().get(READ_TIMEOUT_HANDLER_NAME)).isNull();
74+
}
75+
}

0 commit comments

Comments
 (0)