Skip to content

Commit e7c09a2

Browse files
joerg1985VietND96
andauthored
[grid] enable the httpclient to perform async requests #14403 (#14409)
Co-authored-by: Viet Nguyen Duc <[email protected]>
1 parent f391cd0 commit e7c09a2

File tree

3 files changed

+99
-27
lines changed

3 files changed

+99
-27
lines changed

java/src/org/openqa/selenium/remote/http/HttpClient.java

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.net.URL;
2424
import java.util.ServiceLoader;
2525
import java.util.Set;
26+
import java.util.concurrent.CompletableFuture;
2627
import java.util.stream.Collectors;
2728
import java.util.stream.StreamSupport;
2829
import org.openqa.selenium.internal.Require;
@@ -32,6 +33,10 @@ public interface HttpClient extends Closeable, HttpHandler {
3233

3334
WebSocket openSocket(HttpRequest request, WebSocket.Listener listener);
3435

36+
default CompletableFuture<HttpResponse> executeAsync(HttpRequest req) {
37+
return CompletableFuture.supplyAsync(() -> execute(req));
38+
}
39+
3540
default void close() {}
3641

3742
interface Factory {

java/src/org/openqa/selenium/remote/http/jdk/JdkHttpClient.java

+64-27
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.util.concurrent.ExecutionException;
4545
import java.util.concurrent.ExecutorService;
4646
import java.util.concurrent.Executors;
47+
import java.util.concurrent.Future;
4748
import java.util.concurrent.TimeUnit;
4849
import java.util.concurrent.atomic.AtomicInteger;
4950
import java.util.function.Supplier;
@@ -369,9 +370,64 @@ private URI getWebSocketUri(HttpRequest request) throws URISyntaxException {
369370
return uri;
370371
}
371372

373+
@Override
374+
public CompletableFuture<HttpResponse> executeAsync(HttpRequest request) {
375+
// the facade for this http request
376+
CompletableFuture<HttpResponse> cf = new CompletableFuture<>();
377+
378+
// the actual http request
379+
Future<?> future =
380+
executorService.submit(
381+
() -> {
382+
try {
383+
HttpResponse response = handler.execute(request);
384+
385+
cf.complete(response);
386+
} catch (Throwable t) {
387+
cf.completeExceptionally(t);
388+
}
389+
});
390+
391+
// try to interrupt the http request in case of a timeout, to avoid
392+
// https://bugs.openjdk.org/browse/JDK-8258397
393+
cf.exceptionally(
394+
(throwable) -> {
395+
if (throwable instanceof java.util.concurrent.TimeoutException) {
396+
// interrupts the thread
397+
future.cancel(true);
398+
}
399+
400+
// nobody will read this result
401+
return null;
402+
});
403+
404+
// will complete exceptionally with a java.util.concurrent.TimeoutException
405+
return cf.orTimeout(readTimeout.toMillis(), TimeUnit.MILLISECONDS);
406+
}
407+
372408
@Override
373409
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
374-
return handler.execute(req);
410+
try {
411+
// executeAsync does define a timeout, no need to use a timeout here
412+
return executeAsync(req).get();
413+
} catch (CancellationException e) {
414+
throw new WebDriverException(e.getMessage(), e);
415+
} catch (InterruptedException e) {
416+
Thread.currentThread().interrupt();
417+
throw new WebDriverException(e.getMessage(), e);
418+
} catch (ExecutionException e) {
419+
Throwable cause = e.getCause();
420+
421+
if (cause instanceof java.util.concurrent.TimeoutException) {
422+
throw new TimeoutException(cause);
423+
} else if (cause instanceof RuntimeException) {
424+
throw (RuntimeException) cause;
425+
} else if (cause instanceof Error) {
426+
throw (Error) cause;
427+
}
428+
429+
throw new WebDriverException((cause != null) ? cause : e);
430+
}
375431
}
376432

377433
private HttpResponse execute0(HttpRequest req) throws UncheckedIOException {
@@ -390,34 +446,13 @@ private HttpResponse execute0(HttpRequest req) throws UncheckedIOException {
390446
// - avoid a downgrade of POST requests, see the javadoc of j.n.h.HttpClient.Redirect
391447
// - not run into https://bugs.openjdk.org/browse/JDK-8304701
392448
for (int i = 0; i < 100; i++) {
393-
java.net.http.HttpRequest request = messages.createRequest(req, method, rawUri);
394-
java.net.http.HttpResponse<byte[]> response;
395-
396-
// use sendAsync to not run into https://bugs.openjdk.org/browse/JDK-8258397
397-
CompletableFuture<java.net.http.HttpResponse<byte[]>> future =
398-
client.sendAsync(request, byteHandler);
399-
400-
try {
401-
response = future.get(readTimeout.toMillis(), TimeUnit.MILLISECONDS);
402-
} catch (CancellationException e) {
403-
throw new WebDriverException(e.getMessage(), e);
404-
} catch (ExecutionException e) {
405-
Throwable cause = e.getCause();
406-
407-
if (cause instanceof HttpTimeoutException) {
408-
throw new TimeoutException(cause);
409-
} else if (cause instanceof IOException) {
410-
throw (IOException) cause;
411-
} else if (cause instanceof RuntimeException) {
412-
throw (RuntimeException) cause;
413-
}
414-
415-
throw new WebDriverException((cause != null) ? cause : e);
416-
} catch (java.util.concurrent.TimeoutException e) {
417-
future.cancel(true);
418-
throw new TimeoutException(e);
449+
if (Thread.interrupted()) {
450+
throw new InterruptedException("http request has been interrupted");
419451
}
420452

453+
java.net.http.HttpRequest request = messages.createRequest(req, method, rawUri);
454+
java.net.http.HttpResponse<byte[]> response = client.send(request, byteHandler);
455+
421456
switch (response.statusCode()) {
422457
case 303:
423458
method = HttpMethod.GET;
@@ -454,6 +489,8 @@ private HttpResponse execute0(HttpRequest req) throws UncheckedIOException {
454489
}
455490

456491
throw new ProtocolException("Too many redirects: 101");
492+
} catch (HttpTimeoutException e) {
493+
throw new TimeoutException(e);
457494
} catch (IOException e) {
458495
throw new UncheckedIOException(e);
459496
} catch (InterruptedException e) {

java/test/org/openqa/selenium/remote/internal/HttpClientTestBase.java

+30
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.List;
3636
import java.util.Map;
3737
import java.util.TreeMap;
38+
import java.util.concurrent.atomic.AtomicInteger;
3839
import java.util.logging.Logger;
3940
import java.util.stream.StreamSupport;
4041
import org.junit.jupiter.api.AfterAll;
@@ -233,6 +234,35 @@ public void shouldAllowConfigurationFromSystemProperties() {
233234
}
234235
}
235236

237+
@Test
238+
public void shouldStopRequestAfterTimeout() throws InterruptedException {
239+
AtomicInteger counter = new AtomicInteger();
240+
241+
delegate =
242+
req -> {
243+
counter.incrementAndGet();
244+
try {
245+
Thread.sleep(1600);
246+
} catch (InterruptedException ex) {
247+
Thread.currentThread().interrupt();
248+
}
249+
HttpResponse response = new HttpResponse();
250+
response.setStatus(302);
251+
response.addHeader("Location", "/");
252+
return response;
253+
};
254+
ClientConfig clientConfig = ClientConfig.defaultConfig().readTimeout(Duration.ofMillis(800));
255+
256+
try (HttpClient client =
257+
createFactory().createClient(clientConfig.baseUri(URI.create(server.whereIs("/"))))) {
258+
HttpRequest request = new HttpRequest(GET, "/delayed");
259+
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> client.execute(request));
260+
Thread.sleep(4200);
261+
262+
assertThat(counter.get()).isEqualTo(1);
263+
}
264+
}
265+
236266
private HttpResponse getResponseWithHeaders(final Multimap<String, String> headers) {
237267
return executeWithinServer(
238268
new HttpRequest(GET, "/foo"),

0 commit comments

Comments
 (0)