8216561: HttpClient: The logic of retry on connect exception is inverted
authordfuchs
Fri, 18 Jan 2019 17:06:29 +0000
changeset 53387 c9622e15ba29
parent 53386 d5f6540c6bb1
child 53388 8c08552a1fbd
8216561: HttpClient: The logic of retry on connect exception is inverted Summary: Allows retry on connect exception by default, ensuring that the second attempt takes into account the time spent in the first attempt in order to honor the connect timeout value (if present). Reviewed-by: chegar
src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java	Fri Jan 18 17:05:41 2019 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java	Fri Jan 18 17:06:29 2019 +0000
@@ -33,8 +33,10 @@
 import java.net.URISyntaxException;
 import java.net.URLPermission;
 import java.security.AccessControlContext;
+import java.time.Duration;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.function.Function;
@@ -125,6 +127,10 @@
         return request;
     }
 
+    public Optional<Duration> remainingConnectTimeout() {
+        return multi.remainingConnectTimeout();
+    }
+
     HttpClientImpl client() {
         return client;
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java	Fri Jan 18 17:05:41 2019 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java	Fri Jan 18 17:06:29 2019 +0000
@@ -160,7 +160,7 @@
     private final CookieHandler cookieHandler;
     private final Duration connectTimeout;
     private final Redirect followRedirects;
-    private final Optional<ProxySelector> userProxySelector;
+    private final ProxySelector userProxySelector;
     private final ProxySelector proxySelector;
     private final Authenticator authenticator;
     private final Version version;
@@ -286,12 +286,12 @@
         connectTimeout = builder.connectTimeout;
         followRedirects = builder.followRedirects == null ?
                 Redirect.NEVER : builder.followRedirects;
-        this.userProxySelector = Optional.ofNullable(builder.proxy);
-        this.proxySelector = userProxySelector
+        this.userProxySelector = builder.proxy;
+        this.proxySelector = Optional.ofNullable(userProxySelector)
                 .orElseGet(HttpClientImpl::getDefaultProxySelector);
         if (debug.on())
             debug.log("proxySelector is %s (user-supplied=%s)",
-                      this.proxySelector, userProxySelector.isPresent());
+                      this.proxySelector, userProxySelector != null);
         authenticator = builder.authenticator;
         if (builder.version == null) {
             version = HttpClient.Version.HTTP_2;
@@ -1149,7 +1149,7 @@
 
     @Override
     public Optional<ProxySelector> proxy() {
-        return this.userProxySelector;
+        return Optional.ofNullable(userProxySelector);
     }
 
     // Return the effective proxy that this client uses.
--- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java	Fri Jan 18 17:05:41 2019 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java	Fri Jan 18 17:06:29 2019 +0000
@@ -26,12 +26,14 @@
 package jdk.internal.net.http;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.net.ConnectException;
 import java.net.http.HttpConnectTimeoutException;
+import java.time.Duration;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.security.AccessControlContext;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.CompletionException;
@@ -39,6 +41,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Flow;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Function;
 
 import java.net.http.HttpClient;
@@ -71,6 +74,7 @@
 
     private final HttpRequest userRequest; // the user request
     private final HttpRequestImpl request; // a copy of the user request
+    private final ConnectTimeoutTracker connectTimeout; // null if no timeout
     final AccessControlContext acc;
     final HttpClientImpl client;
     final HttpResponse.BodyHandler<T> responseHandler;
@@ -107,6 +111,38 @@
     // RedirectHandler
     volatile int numberOfRedirects = 0;
 
+    // This class is used to keep track of the connection timeout
+    // across retries, when a ConnectException causes a retry.
+    // In that case - we will retry the connect, but we don't
+    // want to double the timeout by starting a new timer with
+    // the full connectTimeout again.
+    // Instead we use the ConnectTimeoutTracker to return a new
+    // duration that takes into account the time spent in the
+    // first connect attempt.
+    // If however, the connection gets connected, but we later
+    // retry the whole operation, then we reset the timer before
+    // retrying (since the connection used for the second request
+    // will not necessarily be the same: it could be a new
+    // unconnected connection) - see getExceptionalCF().
+    private static final class ConnectTimeoutTracker {
+        final Duration max;
+        final AtomicLong startTime = new AtomicLong();
+        ConnectTimeoutTracker(Duration connectTimeout) {
+            this.max = Objects.requireNonNull(connectTimeout);
+        }
+
+        Duration getRemaining() {
+            long now = System.nanoTime();
+            long previous = startTime.compareAndExchange(0, now);
+            if (previous == 0 || max.isZero()) return max;
+            Duration remaining = max.minus(Duration.ofNanos(now - previous));
+            assert remaining.compareTo(max) <= 0;
+            return remaining.isNegative() ? Duration.ZERO : remaining;
+        }
+
+        void reset() { startTime.set(0); }
+    }
+
     /**
      * MultiExchange with one final response.
      */
@@ -135,7 +171,8 @@
         } else {
             pushGroup = null;
         }
-
+        this.connectTimeout = client.connectTimeout()
+                .map(ConnectTimeoutTracker::new).orElse(null);
         this.exchange = new Exchange<>(request, this);
     }
 
@@ -161,6 +198,11 @@
         this.exchange = exchange;
     }
 
+    public Optional<Duration> remainingConnectTimeout() {
+        return Optional.ofNullable(connectTimeout)
+                .map(ConnectTimeoutTracker::getRemaining);
+    }
+
     private void cancelTimer() {
         if (responseTimerEvent != null) {
             client.cancelTimer(responseTimerEvent);
@@ -355,7 +397,7 @@
         return s.isEmpty() ? true : Boolean.parseBoolean(s);
     }
 
-    private static boolean retryConnect() {
+    private static boolean disableRetryConnect() {
         String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect");
         if (s == null)
             return false;
@@ -365,7 +407,7 @@
     /** True if ALL ( even non-idempotent ) requests can be automatic retried. */
     private static final boolean RETRY_ALWAYS = retryPostValue();
     /** True if ConnectException should cause a retry. Enabled by default */
-    private static final boolean RETRY_CONNECT = retryConnect();
+    private static final boolean RETRY_CONNECT = !disableRetryConnect();
 
     /** Returns true is given request has an idempotent method. */
     private static boolean isIdempotentRequest(HttpRequest request) {
@@ -416,10 +458,13 @@
             Throwable cause = retryCause(t);
 
             if (!(t instanceof ConnectException)) {
+                // we may need to start a new connection, and if so
+                // we want to start with a fresh connect timeout again.
+                if (connectTimeout != null) connectTimeout.reset();
                 if (!canRetryRequest(currentreq)) {
                     return failedFuture(cause); // fails with original cause
                 }
-            }
+            } // ConnectException: retry, but don't reset the connectTimeout.
 
             // allow the retry mechanism to do its work
             retryCause = cause;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java	Fri Jan 18 17:05:41 2019 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java	Fri Jan 18 17:06:29 2019 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -65,7 +65,7 @@
      */
     private ConnectTimerEvent newConnectTimer(Exchange<?> exchange,
                                               CompletableFuture<Void> cf) {
-        Duration duration = client().connectTimeout().orElse(null);
+        Duration duration = exchange.remainingConnectTimeout().orElse(null);
         if (duration != null) {
             ConnectTimerEvent cte = new ConnectTimerEvent(duration, exchange, cf);
             return cte;