Skip to content

Commit 06481a0

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 fe26001 commit 06481a0

10 files changed

Lines changed: 496 additions & 24 deletions

File tree

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.hc.core5.http.HttpResponse;
4444
import org.apache.hc.core5.http.HttpStatus;
4545
import org.apache.hc.core5.http.MethodNotSupportedException;
46+
import org.apache.hc.core5.http.NameValuePair;
4647
import org.apache.hc.core5.http.ProtocolException;
4748
import org.apache.hc.core5.http.message.BasicHttpResponse;
4849
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
@@ -53,8 +54,11 @@
5354
import org.apache.hc.core5.http.nio.StreamChannel;
5455
import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityProducer;
5556
import org.apache.hc.core5.http.protocol.HttpContext;
57+
import org.apache.hc.core5.net.WWWFormCodec;
5658
import org.apache.hc.core5.util.Asserts;
5759

60+
import static java.nio.charset.StandardCharsets.UTF_8;
61+
5862
/**
5963
* A handler that generates random data.
6064
*/
@@ -93,6 +97,22 @@ public void handleRequest(
9397
} catch (final URISyntaxException ex) {
9498
throw new ProtocolException(ex.getMessage(), ex);
9599
}
100+
final String query = uri.getQuery();
101+
int delayMs = 0;
102+
boolean drip = false;
103+
if (query != null) {
104+
final List<NameValuePair> params = WWWFormCodec.parse(query, UTF_8);
105+
for (final NameValuePair param : params) {
106+
final String name = param.getName();
107+
final String value = param.getValue();
108+
if ("delay".equals(name)) {
109+
delayMs = Integer.parseInt(value);
110+
} else if ("drip".equals(name)) {
111+
drip = "1".equals(value);
112+
}
113+
}
114+
}
115+
96116
final String path = uri.getPath();
97117
final int slash = path.lastIndexOf('/');
98118
if (slash != -1) {
@@ -109,7 +129,7 @@ public void handleRequest(
109129
n = 1 + (int)(Math.random() * 79.0);
110130
}
111131
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
112-
final AsyncEntityProducer entityProducer = new RandomBinAsyncEntityProducer(n);
132+
final AsyncEntityProducer entityProducer = new RandomBinAsyncEntityProducer(n, delayMs, drip);
113133
entityProducerRef.set(entityProducer);
114134
responseChannel.sendResponse(response, entityProducer, context);
115135
} else {
@@ -162,12 +182,22 @@ public static class RandomBinAsyncEntityProducer extends AbstractBinAsyncEntityP
162182
private final long length;
163183
private long remaining;
164184
private final ByteBuffer buffer;
185+
private final int delayMs;
186+
private final boolean drip;
187+
private volatile long deadline;
165188

166189
public RandomBinAsyncEntityProducer(final long len) {
190+
this(len, 0, false);
191+
}
192+
193+
public RandomBinAsyncEntityProducer(final long len, final int delayMs, final boolean drip) {
167194
super(512, ContentType.DEFAULT_TEXT);
168195
length = len;
169196
remaining = len;
170197
buffer = ByteBuffer.allocate(1024);
198+
this.delayMs = delayMs;
199+
this.deadline = System.currentTimeMillis() + (drip ? 0 : delayMs);
200+
this.drip = drip;
171201
}
172202

173203
@Override
@@ -192,6 +222,10 @@ public int availableData() {
192222

193223
@Override
194224
protected void produceData(final StreamChannel<ByteBuffer> channel) throws IOException {
225+
if (System.currentTimeMillis() < deadline) {
226+
return;
227+
}
228+
195229
final int chunk = Math.min((int) (remaining < Integer.MAX_VALUE ? remaining : Integer.MAX_VALUE), buffer.remaining());
196230
for (int i = 0; i < chunk; i++) {
197231
final byte b = RANGE[(int) (Math.random() * RANGE.length)];
@@ -205,6 +239,8 @@ protected void produceData(final StreamChannel<ByteBuffer> channel) throws IOExc
205239

206240
if (remaining <= 0 && buffer.position() == 0) {
207241
channel.endStream();
242+
} else if (drip) {
243+
deadline = System.currentTimeMillis() + delayMs;
208244
}
209245
}
210246

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

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,27 @@
2929

3030
import java.io.IOException;
3131
import java.io.InputStream;
32+
import java.io.InterruptedIOException;
3233
import java.io.OutputStream;
3334
import java.net.URI;
3435
import java.net.URISyntaxException;
3536
import java.nio.charset.StandardCharsets;
37+
import java.util.List;
3638

3739
import org.apache.hc.core5.http.ClassicHttpRequest;
3840
import org.apache.hc.core5.http.ClassicHttpResponse;
3941
import org.apache.hc.core5.http.ContentType;
4042
import org.apache.hc.core5.http.HttpException;
4143
import org.apache.hc.core5.http.HttpStatus;
4244
import org.apache.hc.core5.http.MethodNotSupportedException;
45+
import org.apache.hc.core5.http.NameValuePair;
4346
import org.apache.hc.core5.http.ProtocolException;
4447
import org.apache.hc.core5.http.io.HttpRequestHandler;
4548
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
4649
import org.apache.hc.core5.http.protocol.HttpContext;
50+
import org.apache.hc.core5.net.WWWFormCodec;
51+
52+
import static java.nio.charset.StandardCharsets.UTF_8;
4753

4854
/**
4955
* A handler that generates random data.
@@ -84,6 +90,22 @@ public void handle(final ClassicHttpRequest request,
8490
} catch (final URISyntaxException ex) {
8591
throw new ProtocolException(ex.getMessage(), ex);
8692
}
93+
final String query = uri.getQuery();
94+
int delayMs = 0;
95+
boolean drip = false;
96+
if (query != null) {
97+
final List<NameValuePair> params = WWWFormCodec.parse(query, UTF_8);
98+
for (final NameValuePair param : params) {
99+
final String name = param.getName();
100+
final String value = param.getValue();
101+
if ("delay".equals(name)) {
102+
delayMs = Integer.parseInt(value);
103+
} else if ("drip".equals(name)) {
104+
drip = "1".equals(value);
105+
}
106+
}
107+
}
108+
87109
final String path = uri.getPath();
88110
final int slash = path.lastIndexOf('/');
89111
if (slash != -1) {
@@ -100,7 +122,7 @@ public void handle(final ClassicHttpRequest request,
100122
n = 1 + (int)(Math.random() * 79.0);
101123
}
102124
response.setCode(HttpStatus.SC_OK);
103-
response.setEntity(new RandomEntity(n));
125+
response.setEntity(new RandomEntity(n, delayMs, drip));
104126
} else {
105127
throw new ProtocolException("Invalid request path: " + path);
106128
}
@@ -120,6 +142,12 @@ public static class RandomEntity extends AbstractHttpEntity {
120142
/** The length of the random data to generate. */
121143
protected final long length;
122144

145+
/** The duration of the delay before sending the response entity. */
146+
protected final int delayMs;
147+
148+
/** Whether to delay after each chunk sent. If {@code false},
149+
* will only delay at the start of the response entity. */
150+
protected final boolean drip;
123151

124152
/**
125153
* Creates a new entity generating the given amount of data.
@@ -128,8 +156,22 @@ public static class RandomEntity extends AbstractHttpEntity {
128156
* 0 to maxint
129157
*/
130158
public RandomEntity(final long len) {
159+
this(len, 0, false);
160+
}
161+
162+
/**
163+
* Creates a new entity generating the given amount of data.
164+
*
165+
* @param len the number of random bytes to generate,
166+
* 0 to maxint
167+
* @param delayMs how long to wait before sending the first byte
168+
* @param drip whether to repeat the delay after each byte
169+
*/
170+
public RandomEntity(final long len, final int delayMs, final boolean drip) {
131171
super((ContentType) null, null);
132-
length = len;
172+
this.length = len;
173+
this.delayMs = delayMs;
174+
this.drip = drip;
133175
}
134176

135177
/**
@@ -185,31 +227,48 @@ public InputStream getContent() {
185227
@Override
186228
public void writeTo(final OutputStream out) throws IOException {
187229

188-
final int blocksize = 2048;
189-
int remaining = (int) length; // range checked in constructor
190-
final byte[] data = new byte[Math.min(remaining, blocksize)];
230+
try {
231+
final int blocksize = 2048;
232+
int remaining = (int) length; // range checked in constructor
233+
final byte[] data = new byte[Math.min(remaining, blocksize)];
191234

192-
while (remaining > 0) {
193-
final int end = Math.min(remaining, data.length);
235+
out.flush();
236+
if (!drip) {
237+
delay();
238+
}
239+
while (remaining > 0) {
240+
final int end = Math.min(remaining, data.length);
194241

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();
242+
double value = 0.0;
243+
for (int i = 0; i < end; i++) {
244+
// we get 5 random characters out of one random value
245+
if (i % 5 == 0) {
246+
value = Math.random();
247+
}
248+
value = value * RANGE.length;
249+
final int d = (int) value;
250+
value = value - d;
251+
data[i] = RANGE[d];
200252
}
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();
253+
out.write(data, 0, end);
254+
out.flush();
208255

209-
remaining = remaining - end;
256+
remaining = remaining - end;
257+
if (drip) {
258+
delay();
259+
}
260+
}
261+
} finally {
262+
out.close();
210263
}
211-
out.close();
264+
}
212265

266+
private void delay() throws IOException {
267+
try {
268+
Thread.sleep(delayMs);
269+
} catch (final InterruptedException ex) {
270+
throw new InterruptedIOException();
271+
}
213272
}
214273

215274
@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)