Skip to content

Commit 42e24ea

Browse files
committed
Add socket timeout integration tests
This change adds integration test coverage for various types of read timeouts. The test coverage includes HTTP, HTTPS, and HTTP over UDS if available. The `/random` request handlers have been augmented with delay support, so that the clients can read the response headers and then time out while reading the entity. The tests make use of `ConnectionConfig`, `ResponseTimeout`, and (on the sync client only) `SocketConfig`, and demonstrate the precedence among these config options when more than one is supplied. One issue uncovered by these tests is that graceful closure of the async client does not work when UDS socket timeouts have fired. To work around this until the issue can be root caused, the async UDS tests use `CloseMode.IMMEDIATE`.
1 parent c5bd9af commit 42e24ea

10 files changed

Lines changed: 482 additions & 24 deletions

File tree

httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AsyncRandomHandler.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ public void handleRequest(
9393
} catch (final URISyntaxException ex) {
9494
throw new ProtocolException(ex.getMessage(), ex);
9595
}
96+
final String query = uri.getQuery();
97+
int delayMs = 0;
98+
boolean drip = false;
99+
if (query != null) {
100+
final String[] params = query.split("&");
101+
for (final String param : params) {
102+
if (param.startsWith("delay=")) {
103+
delayMs = Integer.parseInt(param.substring(1 + param.indexOf('=')));
104+
} else if (param.equals("drip=1")) {
105+
drip = true;
106+
}
107+
}
108+
}
109+
96110
final String path = uri.getPath();
97111
final int slash = path.lastIndexOf('/');
98112
if (slash != -1) {
@@ -109,7 +123,7 @@ public void handleRequest(
109123
n = 1 + (int)(Math.random() * 79.0);
110124
}
111125
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
112-
final AsyncEntityProducer entityProducer = new RandomBinAsyncEntityProducer(n);
126+
final AsyncEntityProducer entityProducer = new RandomBinAsyncEntityProducer(n, delayMs, drip);
113127
entityProducerRef.set(entityProducer);
114128
responseChannel.sendResponse(response, entityProducer, context);
115129
} else {
@@ -162,12 +176,22 @@ public static class RandomBinAsyncEntityProducer extends AbstractBinAsyncEntityP
162176
private final long length;
163177
private long remaining;
164178
private final ByteBuffer buffer;
179+
private final int delayMs;
180+
private final boolean drip;
181+
private volatile long deadline;
165182

166183
public RandomBinAsyncEntityProducer(final long len) {
184+
this(len, 0, false);
185+
}
186+
187+
public RandomBinAsyncEntityProducer(final long len, final int delayMs, final boolean drip) {
167188
super(512, ContentType.DEFAULT_TEXT);
168189
length = len;
169190
remaining = len;
170191
buffer = ByteBuffer.allocate(1024);
192+
this.delayMs = delayMs;
193+
this.deadline = System.currentTimeMillis() + (drip ? 0 : delayMs);
194+
this.drip = drip;
171195
}
172196

173197
@Override
@@ -192,6 +216,10 @@ public int availableData() {
192216

193217
@Override
194218
protected void produceData(final StreamChannel<ByteBuffer> channel) throws IOException {
219+
if (System.currentTimeMillis() < deadline) {
220+
return;
221+
}
222+
195223
final int chunk = Math.min((int) (remaining < Integer.MAX_VALUE ? remaining : Integer.MAX_VALUE), buffer.remaining());
196224
for (int i = 0; i < chunk; i++) {
197225
final byte b = RANGE[(int) (Math.random() * RANGE.length)];
@@ -205,6 +233,8 @@ protected void produceData(final StreamChannel<ByteBuffer> channel) throws IOExc
205233

206234
if (remaining <= 0 && buffer.position() == 0) {
207235
channel.endStream();
236+
} else if (drip) {
237+
deadline = System.currentTimeMillis() + delayMs;
208238
}
209239
}
210240

httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/RandomHandler.java

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ public void handle(final ClassicHttpRequest request,
8484
} catch (final URISyntaxException ex) {
8585
throw new ProtocolException(ex.getMessage(), ex);
8686
}
87+
final String query = uri.getQuery();
88+
int delayMs = 0;
89+
boolean drip = false;
90+
if (query != null) {
91+
final String[] params = query.split("&");
92+
for (final String param : params) {
93+
if (param.startsWith("delay=")) {
94+
delayMs = Integer.parseInt(param.substring(1 + param.indexOf('=')));
95+
} else if (param.equals("drip=1")) {
96+
drip = true;
97+
}
98+
}
99+
}
100+
87101
final String path = uri.getPath();
88102
final int slash = path.lastIndexOf('/');
89103
if (slash != -1) {
@@ -100,7 +114,7 @@ public void handle(final ClassicHttpRequest request,
100114
n = 1 + (int)(Math.random() * 79.0);
101115
}
102116
response.setCode(HttpStatus.SC_OK);
103-
response.setEntity(new RandomEntity(n));
117+
response.setEntity(new RandomEntity(n, delayMs, drip));
104118
} else {
105119
throw new ProtocolException("Invalid request path: " + path);
106120
}
@@ -120,6 +134,12 @@ public static class RandomEntity extends AbstractHttpEntity {
120134
/** The length of the random data to generate. */
121135
protected final long length;
122136

137+
/** The duration of the delay before sending the response entity. */
138+
protected final int delayMs;
139+
140+
/** Whether to delay after each chunk sent. If {@code false},
141+
* will only delay at the start of the response entity. */
142+
protected final boolean drip;
123143

124144
/**
125145
* Creates a new entity generating the given amount of data.
@@ -128,8 +148,22 @@ public static class RandomEntity extends AbstractHttpEntity {
128148
* 0 to maxint
129149
*/
130150
public RandomEntity(final long len) {
151+
this(len, 0, false);
152+
}
153+
154+
/**
155+
* Creates a new entity generating the given amount of data.
156+
*
157+
* @param len the number of random bytes to generate,
158+
* 0 to maxint
159+
* @param delayMs how long to wait before sending the first byte
160+
* @param drip whether to repeat the delay after each byte
161+
*/
162+
public RandomEntity(final long len, final int delayMs, final boolean drip) {
131163
super((ContentType) null, null);
132-
length = len;
164+
this.length = len;
165+
this.delayMs = delayMs;
166+
this.drip = drip;
133167
}
134168

135169
/**
@@ -185,31 +219,48 @@ public InputStream getContent() {
185219
@Override
186220
public void writeTo(final OutputStream out) throws IOException {
187221

188-
final int blocksize = 2048;
189-
int remaining = (int) length; // range checked in constructor
190-
final byte[] data = new byte[Math.min(remaining, blocksize)];
222+
try {
223+
final int blocksize = 2048;
224+
int remaining = (int) length; // range checked in constructor
225+
final byte[] data = new byte[Math.min(remaining, blocksize)];
191226

192-
while (remaining > 0) {
193-
final int end = Math.min(remaining, data.length);
227+
out.flush();
228+
if (!drip) {
229+
delay();
230+
}
231+
while (remaining > 0) {
232+
final int end = Math.min(remaining, data.length);
194233

195-
double value = 0.0;
196-
for (int i = 0; i < end; i++) {
197-
// we get 5 random characters out of one random value
198-
if (i % 5 == 0) {
199-
value = Math.random();
234+
double value = 0.0;
235+
for (int i = 0; i < end; i++) {
236+
// we get 5 random characters out of one random value
237+
if (i % 5 == 0) {
238+
value = Math.random();
239+
}
240+
value = value * RANGE.length;
241+
final int d = (int) value;
242+
value = value - d;
243+
data[i] = RANGE[d];
200244
}
201-
value = value * RANGE.length;
202-
final int d = (int) value;
203-
value = value - d;
204-
data[i] = RANGE[d];
205-
}
206-
out.write(data, 0, end);
207-
out.flush();
245+
out.write(data, 0, end);
246+
out.flush();
208247

209-
remaining = remaining - end;
248+
remaining = remaining - end;
249+
if (drip) {
250+
delay();
251+
}
252+
}
253+
} finally {
254+
out.close();
210255
}
211-
out.close();
256+
}
212257

258+
private void delay() throws IOException {
259+
try {
260+
Thread.sleep(delayMs);
261+
} catch (final InterruptedException ex) {
262+
throw new IOException(ex);
263+
}
213264
}
214265

215266
@Override

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
package org.apache.hc.client5.testing.async;
2929

3030
import java.net.InetSocketAddress;
31+
import java.nio.file.Path;
3132
import java.util.function.Consumer;
3233

3334
import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel;
@@ -48,9 +49,17 @@ abstract class AbstractIntegrationTestBase {
4849

4950
@RegisterExtension
5051
private final TestAsyncResources testResources;
52+
private final boolean useUnixDomainSocket;
5153

5254
protected AbstractIntegrationTestBase(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
55+
this(scheme, clientProtocolLevel, serverProtocolLevel, false);
56+
}
57+
58+
protected AbstractIntegrationTestBase(
59+
final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel,
60+
final ServerProtocolLevel serverProtocolLevel, final boolean useUnixDomainSocket) {
5361
this.testResources = new TestAsyncResources(scheme, clientProtocolLevel, serverProtocolLevel, TIMEOUT);
62+
this.useUnixDomainSocket = useUnixDomainSocket;
5463
}
5564

5665
public URIScheme scheme() {
@@ -72,6 +81,9 @@ public void configureServer(final Consumer<TestAsyncServerBootstrap> serverCusto
7281
public HttpHost startServer() throws Exception {
7382
final TestAsyncServer server = testResources.server();
7483
final InetSocketAddress inetSocketAddress = server.start();
84+
if (useUnixDomainSocket) {
85+
testResources.udsProxy().start();
86+
}
7587
return new HttpHost(testResources.scheme().id, "localhost", inetSocketAddress.getPort());
7688
}
7789

@@ -80,9 +92,21 @@ public void configureClient(final Consumer<TestAsyncClientBuilder> clientCustomi
8092
}
8193

8294
public TestAsyncClient startClient() throws Exception {
95+
if (useUnixDomainSocket) {
96+
final Path socketPath = getUnixDomainSocket();
97+
testResources.configureClient(builder -> {
98+
builder.setUnixDomainSocket(socketPath);
99+
});
100+
}
83101
final TestAsyncClient client = testResources.client();
84102
client.start();
85103
return client;
86104
}
87105

106+
public Path getUnixDomainSocket() throws Exception {
107+
if (useUnixDomainSocket) {
108+
return testResources.udsProxy().getSocketPath();
109+
}
110+
return null;
111+
}
88112
}

0 commit comments

Comments
 (0)