Skip to content

Commit 5901f71

Browse files
authored
Add H2 stress tests (#12610)
Add H2 stress test that uses client aborts and asserts that threads are not left stuck on locks Signed-off-by: Ludovic Orban <[email protected]>
1 parent e1e35f5 commit 5901f71

File tree

2 files changed

+278
-1
lines changed

2 files changed

+278
-1
lines changed

jetty-ee10/jetty-ee10-tests/jetty-ee10-test-http2-webapp/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@
2222
</dependency>
2323
<dependency>
2424
<groupId>org.eclipse.jetty.http2</groupId>
25-
<artifactId>jetty-http2-client</artifactId>
25+
<artifactId>jetty-http2-client-transport</artifactId>
2626
</dependency>
2727
<dependency>
2828
<groupId>jakarta.servlet</groupId>
2929
<artifactId>jakarta.servlet-api</artifactId>
3030
<scope>provided</scope>
3131
</dependency>
3232

33+
<dependency>
34+
<groupId>org.awaitility</groupId>
35+
<artifactId>awaitility</artifactId>
36+
<scope>test</scope>
37+
</dependency>
3338
<dependency>
3439
<groupId>org.eclipse.jetty</groupId>
3540
<artifactId>jetty-alpn-java-server</artifactId>
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
//
2+
// ========================================================================
3+
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
4+
//
5+
// This program and the accompanying materials are made available under the
6+
// terms of the Eclipse Public License v. 2.0 which is available at
7+
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
8+
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
9+
//
10+
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
11+
// ========================================================================
12+
//
13+
14+
package org.eclipse.jetty.test.webapp;
15+
16+
import java.io.ByteArrayOutputStream;
17+
import java.io.IOException;
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.concurrent.CompletableFuture;
22+
import java.util.concurrent.CopyOnWriteArrayList;
23+
import java.util.concurrent.ExecutorService;
24+
import java.util.concurrent.Executors;
25+
import java.util.concurrent.Future;
26+
import java.util.concurrent.ThreadFactory;
27+
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicInteger;
29+
30+
import jakarta.servlet.http.HttpServlet;
31+
import jakarta.servlet.http.HttpServletRequest;
32+
import jakarta.servlet.http.HttpServletResponse;
33+
import org.eclipse.jetty.client.BytesRequestContent;
34+
import org.eclipse.jetty.client.HttpClient;
35+
import org.eclipse.jetty.client.Request;
36+
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
37+
import org.eclipse.jetty.http.HttpMethod;
38+
import org.eclipse.jetty.http2.client.HTTP2Client;
39+
import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2;
40+
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
41+
import org.eclipse.jetty.server.ConnectionFactory;
42+
import org.eclipse.jetty.server.HttpConfiguration;
43+
import org.eclipse.jetty.server.Server;
44+
import org.eclipse.jetty.server.ServerConnector;
45+
import org.eclipse.jetty.util.IO;
46+
import org.eclipse.jetty.util.component.LifeCycle;
47+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
48+
import org.junit.jupiter.api.AfterEach;
49+
import org.junit.jupiter.api.Tag;
50+
import org.junit.jupiter.api.Test;
51+
import org.junit.jupiter.api.Timeout;
52+
53+
import static org.awaitility.Awaitility.await;
54+
55+
@Tag("stress")
56+
@Timeout(value = 5, unit = TimeUnit.MINUTES)
57+
public class StressHTTP2Test
58+
{
59+
private static final int N_THREADS = Runtime.getRuntime().availableProcessors() * 2;
60+
private static final byte[] DATA = """
61+
[start]
62+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
63+
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
64+
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
65+
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
66+
[end]
67+
""".repeat(50).getBytes(StandardCharsets.UTF_8);
68+
69+
private ExecutorService executorService;
70+
private Server server;
71+
72+
@Test
73+
public void testOutputWithAborts() throws Exception
74+
{
75+
int iterations = 100;
76+
start(N_THREADS, new HttpServlet()
77+
{
78+
@Override
79+
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
80+
{
81+
response.setStatus(200);
82+
response.getOutputStream().write(DATA);
83+
}
84+
});
85+
86+
stress(N_THREADS, (httpClient) ->
87+
{
88+
for (int i = 0; i < iterations; i++)
89+
{
90+
CompletableFuture<Object> cf = new CompletableFuture<>();
91+
Request request = httpClient.newRequest(server.getURI());
92+
request.path("/")
93+
.method(HttpMethod.GET)
94+
.send(result ->
95+
{
96+
if (result.isSucceeded())
97+
cf.complete(null);
98+
else
99+
cf.completeExceptionally(result.getFailure());
100+
});
101+
102+
if (i % (iterations / 10) == 0)
103+
request.abort(new Exception("client abort"));
104+
105+
try
106+
{
107+
cf.get();
108+
}
109+
catch (Exception e)
110+
{
111+
// ignore
112+
}
113+
114+
// if ((j + 1) % 100 == 0)
115+
// System.err.println(Thread.currentThread().getName() + " processed " + (j + 1));
116+
}
117+
});
118+
}
119+
120+
@Test
121+
public void testInputWithAborts() throws Exception
122+
{
123+
int iterations = 100_000;
124+
start(N_THREADS, new HttpServlet()
125+
{
126+
@Override
127+
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
128+
{
129+
response.setStatus(200);
130+
IO.copy(request.getInputStream(), new ByteArrayOutputStream());
131+
}
132+
});
133+
134+
stress(N_THREADS, (httpClient) ->
135+
{
136+
for (int i = 0; i < iterations; i++)
137+
{
138+
CompletableFuture<Object> cf = new CompletableFuture<>();
139+
Request request = httpClient.newRequest(server.getURI());
140+
request.path("/")
141+
.method(HttpMethod.POST)
142+
.body(new BytesRequestContent(DATA))
143+
.send(result ->
144+
{
145+
if (result.isSucceeded())
146+
cf.complete(null);
147+
else
148+
cf.completeExceptionally(result.getFailure());
149+
});
150+
151+
if (i % (iterations / 10) == 0)
152+
{
153+
request.abort(new Exception("client abort"));
154+
}
155+
156+
try
157+
{
158+
cf.get();
159+
}
160+
catch (Exception e)
161+
{
162+
// ignore
163+
}
164+
165+
// if ((j + 1) % 100 == 0)
166+
// System.err.println(Thread.currentThread().getName() + " processed " + (j + 1));
167+
}
168+
});
169+
}
170+
171+
@AfterEach
172+
public void tearDown()
173+
{
174+
try
175+
{
176+
// Assert that no thread is stuck in WAITING state, i.e.: blocked on some lock.
177+
await().atMost(30, TimeUnit.SECONDS).until(() ->
178+
{
179+
TestQueuedThreadPool queuedThreadPool = server.getBean(TestQueuedThreadPool.class);
180+
for (Thread thread : queuedThreadPool.getCreatedThreads())
181+
{
182+
Thread.State state = thread.getState();
183+
if (state == Thread.State.WAITING)
184+
return false;
185+
}
186+
return true;
187+
});
188+
}
189+
finally
190+
{
191+
if (executorService != null)
192+
executorService.shutdownNow();
193+
LifeCycle.stop(server);
194+
}
195+
}
196+
197+
private void stress(int threadCount, Job job) throws Exception
198+
{
199+
List<Future<?>> futures = new ArrayList<>(threadCount);
200+
for (int i = 0; i < threadCount; i++)
201+
{
202+
Future<Object> future = executorService.submit(() ->
203+
{
204+
try (HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client())))
205+
{
206+
httpClient.start();
207+
208+
job.run(httpClient);
209+
}
210+
return null;
211+
});
212+
futures.add(future);
213+
}
214+
215+
for (Future<?> future : futures)
216+
{
217+
future.get();
218+
}
219+
}
220+
221+
private void start(int threadCount, HttpServlet httpServlet) throws Exception
222+
{
223+
executorService = Executors.newFixedThreadPool(threadCount);
224+
225+
QueuedThreadPool qtp = new TestQueuedThreadPool(threadCount);
226+
server = new Server(qtp);
227+
228+
HttpConfiguration httpConfiguration = new HttpConfiguration();
229+
httpConfiguration.setOutputBufferSize(1);
230+
ConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(httpConfiguration);
231+
ServerConnector serverConnector = new ServerConnector(server, connectionFactory);
232+
serverConnector.setPort(0);
233+
server.addConnector(serverConnector);
234+
235+
ServletContextHandler targetContextHandler = new ServletContextHandler();
236+
targetContextHandler.setContextPath("/");
237+
targetContextHandler.addServlet(httpServlet, "/*");
238+
239+
server.setHandler(targetContextHandler);
240+
241+
server.start();
242+
}
243+
244+
@FunctionalInterface
245+
private interface Job
246+
{
247+
void run(HttpClient httpClient) throws Exception;
248+
}
249+
250+
private static class TestQueuedThreadPool extends QueuedThreadPool
251+
{
252+
private static final List<Thread> CREATED_THREADS = new CopyOnWriteArrayList<>();
253+
private static final AtomicInteger COUNTER = new AtomicInteger();
254+
private static final ThreadFactory THREAD_FACTORY = r ->
255+
{
256+
Thread thread = new Thread(r);
257+
thread.setName("Server-" + COUNTER.incrementAndGet());
258+
CREATED_THREADS.add(thread);
259+
return thread;
260+
};
261+
262+
public TestQueuedThreadPool(int threadCount)
263+
{
264+
super(threadCount * 2, 8, 60000, -1, null, null, THREAD_FACTORY);
265+
}
266+
267+
public List<Thread> getCreatedThreads()
268+
{
269+
return CREATED_THREADS;
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)