Skip to content

Commit cfcd63e

Browse files
fix(http): time out idle connections at the connection limit (#7020)
1 parent 2bd5891 commit cfcd63e

2 files changed

Lines changed: 177 additions & 1 deletion

File tree

‎framework/src/main/java/org/tron/common/application/HttpService.java‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ public abstract class HttpService extends AbstractService {
4343

4444
protected long maxRequestSize = 4 * 1024 * 1024; // 4MB
4545

46+
// Once maxHttpConnectNumber is reached, Jetty applies this idle timeout to every open
47+
// connection, so connections that send nothing cannot hold every slot. While the limit is
48+
// reached, a keep-alive connection that stays idle this long between two requests is closed
49+
// as well; time spent processing a request does not count as idle.
50+
private long connectionLimitIdleTimeoutMs = 10_000;
51+
52+
@VisibleForTesting
53+
protected void setConnectionLimitIdleTimeoutMs(long connectionLimitIdleTimeoutMs) {
54+
this.connectionLimitIdleTimeoutMs = connectionLimitIdleTimeoutMs;
55+
}
56+
4657
@VisibleForTesting
4758
public long getMaxRequestSize() {
4859
return this.maxRequestSize;
@@ -80,7 +91,9 @@ protected void initServer() {
8091
this.apiServer = new Server(this.port);
8192
int maxHttpConnectNumber = Args.getInstance().getMaxHttpConnectNumber();
8293
if (maxHttpConnectNumber > 0) {
83-
this.apiServer.addBean(new ConnectionLimit(maxHttpConnectNumber, this.apiServer));
94+
ConnectionLimit connectionLimit = new ConnectionLimit(maxHttpConnectNumber, this.apiServer);
95+
connectionLimit.setIdleTimeout(connectionLimitIdleTimeoutMs);
96+
this.apiServer.addBean(connectionLimit);
8497
}
8598
this.apiServer.setErrorHandler(new OversizedRequestErrorHandler());
8699
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package org.tron.common.jetty;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.io.OutputStream;
7+
import java.net.InetSocketAddress;
8+
import java.net.Socket;
9+
import java.net.SocketTimeoutException;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.concurrent.TimeUnit;
14+
import javax.servlet.http.HttpServlet;
15+
import javax.servlet.http.HttpServletRequest;
16+
import javax.servlet.http.HttpServletResponse;
17+
import org.eclipse.jetty.server.AbstractConnector;
18+
import org.eclipse.jetty.servlet.ServletContextHandler;
19+
import org.eclipse.jetty.servlet.ServletHolder;
20+
import org.junit.AfterClass;
21+
import org.junit.Assert;
22+
import org.junit.BeforeClass;
23+
import org.junit.ClassRule;
24+
import org.junit.Test;
25+
import org.junit.rules.TemporaryFolder;
26+
import org.tron.common.TestConstants;
27+
import org.tron.common.application.HttpService;
28+
import org.tron.common.utils.PublicMethod;
29+
import org.tron.core.config.args.Args;
30+
31+
/**
32+
* Tests the connection limit configured in {@link HttpService}: once connections that send
33+
* nothing hold every slot, the limit's idle timeout closes idle connections and frees a slot for
34+
* a new client, instead of waiting for the connector's 30-second idle timeout. As soon as a slot
35+
* is free, Jetty restores the connector's timeout on the remaining connections, so only one idle
36+
* connection is guaranteed to be closed. The test shortens the limit's idle timeout to one
37+
* second (production uses ten) so it finishes in a few seconds.
38+
*/
39+
public class ConnectionLimitTest {
40+
41+
private static final int MAX_CONNECTIONS = 2;
42+
43+
private static final long LIMIT_IDLE_TIMEOUT_MS = 1_000;
44+
45+
// Well above the limit's idle timeout applied twice (Jetty half-closes an idle connection
46+
// before closing it), and below the connector's default 30-second idle timeout, so the test
47+
// fails if only the connector's timeout frees a slot.
48+
private static final int TIMEOUT_MS = 10_000;
49+
50+
// How long a connection that is still open is read before it counts as not closed.
51+
private static final int CLOSE_CHECK_TIMEOUT_MS = 1_000;
52+
53+
@ClassRule
54+
public static final TemporaryFolder temporaryFolder = new TemporaryFolder();
55+
56+
private static TestHttpService httpService;
57+
private static int port;
58+
59+
public static class OkServlet extends HttpServlet {
60+
@Override
61+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
62+
resp.setStatus(HttpServletResponse.SC_OK);
63+
resp.getWriter().print("ok");
64+
}
65+
}
66+
67+
static class TestHttpService extends HttpService {
68+
TestHttpService(int port) {
69+
this.port = port;
70+
this.contextPath = "/";
71+
setConnectionLimitIdleTimeoutMs(LIMIT_IDLE_TIMEOUT_MS);
72+
}
73+
74+
@Override
75+
protected void addServlet(ServletContextHandler context) {
76+
context.addServlet(new ServletHolder(new OkServlet()), "/*");
77+
}
78+
79+
int connectedEndPoints() {
80+
return ((AbstractConnector) apiServer.getConnectors()[0]).getConnectedEndPoints().size();
81+
}
82+
}
83+
84+
@BeforeClass
85+
public static void setup() throws Exception {
86+
Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()},
87+
TestConstants.TEST_CONF);
88+
Args.getInstance().setMaxHttpConnectNumber(MAX_CONNECTIONS);
89+
port = PublicMethod.chooseRandomPort();
90+
httpService = new TestHttpService(port);
91+
httpService.start().get(10, TimeUnit.SECONDS);
92+
}
93+
94+
@AfterClass
95+
public static void teardown() throws Exception {
96+
try {
97+
if (httpService != null) {
98+
httpService.stop();
99+
}
100+
} finally {
101+
Args.clearParam();
102+
}
103+
}
104+
105+
@Test(timeout = 30_000)
106+
public void testIdleConnectionsDoNotLockOutClients() throws Exception {
107+
List<Socket> idleSockets = new ArrayList<>();
108+
try {
109+
for (int i = 0; i < MAX_CONNECTIONS; i++) {
110+
Socket socket = new Socket();
111+
socket.connect(new InetSocketAddress("localhost", port), TIMEOUT_MS);
112+
idleSockets.add(socket);
113+
awaitConnectedEndPoints(i + 1);
114+
}
115+
116+
// The new request is only served once the limit's idle timeout frees a slot.
117+
Assert.assertEquals("HTTP/1.1 200 OK", get());
118+
Assert.assertTrue("no idle connection was closed by the server",
119+
idleSockets.stream().anyMatch(ConnectionLimitTest::isClosedByServer));
120+
} finally {
121+
for (Socket socket : idleSockets) {
122+
socket.close();
123+
}
124+
}
125+
}
126+
127+
private static void awaitConnectedEndPoints(int expected) throws InterruptedException {
128+
long deadline = System.currentTimeMillis() + TIMEOUT_MS;
129+
while (httpService.connectedEndPoints() < expected) {
130+
Assert.assertTrue("server did not accept connection " + expected,
131+
System.currentTimeMillis() < deadline);
132+
Thread.sleep(10);
133+
}
134+
}
135+
136+
private static String get() throws IOException {
137+
try (Socket socket = new Socket()) {
138+
socket.connect(new InetSocketAddress("localhost", port), TIMEOUT_MS);
139+
socket.setSoTimeout(TIMEOUT_MS);
140+
OutputStream out = socket.getOutputStream();
141+
out.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
142+
.getBytes(StandardCharsets.US_ASCII));
143+
out.flush();
144+
BufferedReader in = new BufferedReader(
145+
new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
146+
return in.readLine();
147+
}
148+
}
149+
150+
// A connection the server closed reads end-of-stream, or fails with a reset, right away; one
151+
// that is still open times out.
152+
private static boolean isClosedByServer(Socket socket) {
153+
try {
154+
socket.setSoTimeout(CLOSE_CHECK_TIMEOUT_MS);
155+
return socket.getInputStream().read() == -1;
156+
} catch (SocketTimeoutException e) {
157+
return false;
158+
} catch (IOException e) {
159+
// a reset also means the server dropped the connection
160+
return true;
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)