Repository: jclouds
Updated Branches:
  refs/heads/1.7.x 04a4ab002 -> 3d4d5aded


JCLOUDS-589: Reauthenticate on Keystone HTTP 401

This commit ports the Keystone 2.0 fix from JCLOUDS-178 to Keystone
1.1.


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/3d4d5ade
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/3d4d5ade
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/3d4d5ade

Branch: refs/heads/1.7.x
Commit: 3d4d5aded8bda2d4aee3d317a957529e4b74fb6d
Parents: 04a4ab0
Author: Shri Javadekar <[email protected]>
Authored: Mon Jun 9 15:10:49 2014 -0700
Committer: Andrew Gaul <[email protected]>
Committed: Sat Jun 21 19:20:47 2014 -0700

----------------------------------------------------------------------
 .../handlers/RetryOnRenewExpectTest.java        | 33 ++++++++++--
 .../openstack/handlers/RetryOnRenew.java        | 51 +++++++++++++++++--
 .../keystone/v1_1/handlers/RetryOnRenew.java    | 45 +++++++++++++++--
 .../openstack/handlers/RetryOnRenewTest.java    | 53 +++++++++++++++++++-
 .../v1_1/handlers/RetryOnRenewTest.java         | 37 ++++++++++++++
 5 files changed, 203 insertions(+), 16 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/3d4d5ade/apis/cloudservers/src/test/java/org/jclouds/cloudservers/handlers/RetryOnRenewExpectTest.java
----------------------------------------------------------------------
diff --git 
a/apis/cloudservers/src/test/java/org/jclouds/cloudservers/handlers/RetryOnRenewExpectTest.java
 
b/apis/cloudservers/src/test/java/org/jclouds/cloudservers/handlers/RetryOnRenewExpectTest.java
index a2421bc..8cc03a0 100644
--- 
a/apis/cloudservers/src/test/java/org/jclouds/cloudservers/handlers/RetryOnRenewExpectTest.java
+++ 
b/apis/cloudservers/src/test/java/org/jclouds/cloudservers/handlers/RetryOnRenewExpectTest.java
@@ -67,10 +67,10 @@ public class RetryOnRenewExpectTest extends 
BaseCloudServersRestClientExpectTest
       assert clientWhenImageExists.deleteImage(11);
    }
 
-   @Test(expectedExceptions = AuthorizationException.class)
-   public void testDoesNotReauthenticateOnFatal401() {
+   public void testReauthenticateOn401ForFailedCommand() {
+      String requestUrl = 
"https://lon.servers.api.rackspacecloud.com/v1.0/10001786/images/11?now=1257695648897";;
       HttpRequest deleteImage = HttpRequest.builder().method("DELETE")
-            
.endpoint("https://lon.servers.api.rackspacecloud.com/v1.0/10001786/images/11?now=1257695648897";)
+            .endpoint(requestUrl)
             .addHeader("X-Auth-Token", authToken).build();
 
       HttpResponse unauthResponse = HttpResponse
@@ -80,8 +80,31 @@ public class RetryOnRenewExpectTest extends 
BaseCloudServersRestClientExpectTest
             .payload("[{\"unauthorized\":{\"message\":\"Fatal 
unauthorized.\",\"code\":401}}]")
             .build();
 
-      CloudServersClient client = orderedRequestsSendResponses(initialAuth, 
responseWithAuth, deleteImage,
-            unauthResponse);
+      // second auth uses same creds as initial one
+      HttpRequest redoAuth = initialAuth;
+
+      String authToken2 = "12345678-9012-47c0-9770-2c5097da25fc";
+      HttpResponse responseWithUrls2 = responseWithAuth.toBuilder()
+            .payload(responseWithAuth.getPayload().getRawContent().toString()
+                     .replace(authToken, authToken2)).build();
+
+      HttpRequest deleteImage2 = HttpRequest
+            .builder().method("DELETE")
+            .endpoint(requestUrl).addHeader("X-Auth-Token", 
authToken2).build();
+
+      HttpResponse imageDeleted = HttpResponse.builder().statusCode(204)
+            .message("HTTP/1.1 204 No Content").build();
+
+      // The sequence of events simulated here is as follows:
+      // 1. First auth succeeds.
+      // 2. The token returned in #1 is used in the deleteImage command.
+      // 3. The deleteImage command fails with a 401 error.
+      // 4. This should result in a new auth request which succeeds.
+      // 5. The new token is used in the next deleteImage command.
+      // 6. Succeed that command.
+      CloudServersClient client = orderedRequestsSendResponses(initialAuth,
+            responseWithAuth, deleteImage, unauthResponse, redoAuth,
+            responseWithUrls2, deleteImage2, imageDeleted);
 
       client.deleteImage(11);
    }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3d4d5ade/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java
----------------------------------------------------------------------
diff --git 
a/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java
 
b/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java
index 22b3fa9..705e04d 100644
--- 
a/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java
+++ 
b/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java
@@ -19,8 +19,12 @@ package org.jclouds.openstack.handlers;
 import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
 import static org.jclouds.http.HttpUtils.releasePayload;
 
+import java.util.concurrent.TimeUnit;
+
 import javax.annotation.Resource;
+import javax.inject.Named;
 
+import org.jclouds.Constants;
 import org.jclouds.domain.Credentials;
 import org.jclouds.http.HttpCommand;
 import org.jclouds.http.HttpResponse;
@@ -29,8 +33,12 @@ import org.jclouds.logging.Logger;
 import org.jclouds.openstack.domain.AuthenticationResponse;
 import org.jclouds.openstack.reference.AuthHeaders;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -45,8 +53,23 @@ public class RetryOnRenew implements HttpRetryHandler {
    @Resource
    protected Logger logger = Logger.NULL;
 
+   @VisibleForTesting
+   @Inject(optional = true)
+   @Named(Constants.PROPERTY_MAX_RETRIES)
+   static int NUM_RETRIES = 5;
+
    private final LoadingCache<Credentials, AuthenticationResponse> 
authenticationResponseCache;
 
+   /*
+    * The reason retries need to be tracked is that it is possible that a token
+    * can be expired at any time. The reason we track by request is that only
+    * some requests might return a 401 (such as temporary URLs). However
+    * consistent failures of the magnitude this code tracks should indicate a
+    * problem.
+    */
+   private static final Cache<HttpCommand, Integer> retryCountMap = 
CacheBuilder
+         .newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
+
    @Inject
    protected RetryOnRenew(LoadingCache<Credentials, AuthenticationResponse> 
authenticationResponseCache) {
       this.authenticationResponseCache = authenticationResponseCache;
@@ -64,16 +87,34 @@ public class RetryOnRenew implements HttpRetryHandler {
                         && headers.containsKey(AuthHeaders.AUTH_KEY) && 
!headers.containsKey(AuthHeaders.AUTH_TOKEN)) {
                   retry = false;
                } else {
-                  byte[] content = closeClientButKeepContentStream(response);
-                  if (content != null && new String(content).contains("lease 
renew")) {
-                     logger.debug("invalidating authentication token");
+                  closeClientButKeepContentStream(response);
+                  // This is not an authentication request returning 401
+                  // Check if we already had seen this request
+                  Integer count = retryCountMap.getIfPresent(command);
+
+                  if (count == null) {
+                     // First time this non-authentication request failed
+                     logger.debug("invalidating authentication token - first 
time for %s", command);
+                     retryCountMap.put(command, 1);
                      authenticationResponseCache.invalidateAll();
                      retry = true;
                   } else {
-                     retry = false;
+                     // This request has failed before
+                     if (count + 1 >= NUM_RETRIES) {
+                        logger.debug("too many 401s - giving up after: %s for 
%s", count, command);
+                        retry = false;
+                     } else {
+                        // Retry just in case
+                        logger.debug("invalidating authentication token - 
retry %s for %s", count, command);
+                        retryCountMap.put(command, count + 1);
+                        // Wait between retries
+                        authenticationResponseCache.invalidateAll();
+                        Uninterruptibles.sleepUninterruptibly(5, 
TimeUnit.SECONDS);
+                        retry = true;
+                     }
                   }
                }
-               break;
+            break;
          }
          return retry;
 

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3d4d5ade/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenew.java
----------------------------------------------------------------------
diff --git 
a/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenew.java
 
b/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenew.java
index a9446fe..3689a0f 100644
--- 
a/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenew.java
+++ 
b/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenew.java
@@ -19,6 +19,8 @@ package org.jclouds.openstack.keystone.v1_1.handlers;
 import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
 import static org.jclouds.http.HttpUtils.releasePayload;
 
+import java.util.concurrent.TimeUnit;
+
 import javax.annotation.Resource;
 import javax.inject.Named;
 
@@ -32,8 +34,12 @@ import org.jclouds.logging.Logger;
 import org.jclouds.openstack.keystone.v1_1.domain.Auth;
 import org.jclouds.openstack.reference.AuthHeaders;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -45,9 +51,10 @@ import com.google.inject.Singleton;
  */
 @Singleton
 public class RetryOnRenew implements HttpRetryHandler {
+   @VisibleForTesting
    @Inject(optional = true)
    @Named(Constants.PROPERTY_MAX_RETRIES)
-   private int retryCountLimit = 5;
+   static int NUM_RETRIES = 5;
 
    @Resource
    protected Logger logger = Logger.NULL;
@@ -63,6 +70,16 @@ public class RetryOnRenew implements HttpRetryHandler {
       this.backoffHandler = backoffHandler;
    }
 
+   /*
+    * The reason retries need to be tracked is that it is possible that a token
+    * can be expired at any time. The reason we track by request is that only
+    * some requests might return a 401 (such as temporary URLs). However
+    * consistent failures of the magnitude this code tracks should indicate a
+    * problem.
+    */
+   private static final Cache<HttpCommand, Integer> retryCountMap = 
CacheBuilder
+         .newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
+
    @Override
    public boolean shouldRetryRequest(HttpCommand command, HttpResponse 
response) {
       boolean retry = false; // default
@@ -75,13 +92,31 @@ public class RetryOnRenew implements HttpRetryHandler {
                         && headers.containsKey(AuthHeaders.AUTH_KEY) && 
!headers.containsKey(AuthHeaders.AUTH_TOKEN)) {
                   retry = false;
                } else {
-                  byte[] content = closeClientButKeepContentStream(response);
-                  if (content != null && new String(content).contains("lease 
renew")) {
-                     logger.debug("invalidating authentication token");
+                  closeClientButKeepContentStream(response);
+                  // This is not an authentication request returning 401
+                  // Check if we already had seen this request
+                  Integer count = retryCountMap.getIfPresent(command);
+
+                  if (count == null) {
+                     // First time this non-authentication request failed
+                     logger.debug("invalidating authentication token - first 
time for %s", command);
+                     retryCountMap.put(command, 1);
                      authenticationResponseCache.invalidateAll();
                      retry = true;
                   } else {
-                     retry = false;
+                     // This request has failed before
+                     if (count + 1 >= NUM_RETRIES) {
+                        logger.debug("too many 401s - giving up after: %s for 
%s", count, command);
+                        retry = false;
+                     } else {
+                        // Retry just in case
+                        logger.debug("invalidating authentication token - 
retry %s for %s", count, command);
+                        retryCountMap.put(command, count + 1);
+                        // Wait between retries
+                        authenticationResponseCache.invalidateAll();
+                        Uninterruptibles.sleepUninterruptibly(5, 
TimeUnit.SECONDS);
+                        retry = true;
+                     }
                   }
                }
                break;

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3d4d5ade/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java
----------------------------------------------------------------------
diff --git 
a/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java
 
b/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java
index 4a6dba9..d81d598 100644
--- 
a/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java
+++ 
b/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java
@@ -21,6 +21,7 @@ import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
 import org.jclouds.domain.Credentials;
@@ -53,7 +54,8 @@ public class RetryOnRenewTest {
       cache.invalidateAll();
       expectLastCall();
 
-      expect(response.getPayload()).andReturn(Payloads.newStringPayload("token 
expired, please renew")).anyTimes();
+      expect(response.getPayload()).andReturn(Payloads.newStringPayload(""))
+            .anyTimes();
       expect(response.getStatusCode()).andReturn(401).atLeastOnce();
 
       replay(command);
@@ -68,4 +70,53 @@ public class RetryOnRenewTest {
       verify(response);
       verify(cache);
    }
+
+   /**
+    * We have three types of authentication failures: a) When the session
+    * (token) expires b) When you hit a URL you don't have access to (because 
of
+    * permissions) c) When you attempt to authenticate to the service (with bad
+    * credentials)
+    *
+    * In case c), which is detectable, we do not retry, as usually this means
+    * your credentials are broken. Case a) and b) cannot be distinguished 
easily
+    * at this point. Different providers will request token re-authentication 
in
+    * different ways (but usually preceded or by an authentication failure). To
+    * attempt to distinguish between case a) and b) this code tracks failures
+    * for specific calls. Multiple failures for the same call almost certainly
+    * indicates a permissions issue. A success results in a successful
+    * re-authentication.
+    */
+   @Test
+   public void test401ShouldRetry4Times() {
+      HttpCommand command = createMock(HttpCommand.class);
+      HttpRequest request = createMock(HttpRequest.class);
+      HttpResponse response = createMock(HttpResponse.class);
+
+      @SuppressWarnings("unchecked")
+      LoadingCache<Credentials, AuthenticationResponse> cache = 
createMock(LoadingCache.class);
+
+      expect(command.getCurrentRequest()).andReturn(request).anyTimes();
+      expect(request.getHeaders()).andStubReturn(null);
+
+      cache.invalidateAll();
+      expectLastCall().anyTimes();
+
+      expect(response.getPayload()).andReturn(Payloads.newStringPayload(""))
+            .anyTimes();
+      expect(response.getStatusCode()).andReturn(401).anyTimes();
+
+      replay(command, request, response, cache);
+
+      RetryOnRenew retry = new RetryOnRenew(cache);
+
+      for (int n = 0; n < RetryOnRenew.NUM_RETRIES - 1; n++) {
+         assertTrue(retry.shouldRetryRequest(command, response),
+               "Expected retry to succeed");
+      }
+
+      assertFalse(retry.shouldRetryRequest(command, response),
+            "Expected retry to fail on attempt 5");
+
+      verify(command, response, cache);
+   }
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3d4d5ade/common/openstack/src/test/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenewTest.java
----------------------------------------------------------------------
diff --git 
a/common/openstack/src/test/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenewTest.java
 
b/common/openstack/src/test/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenewTest.java
index b68c8f1..a04bce3 100644
--- 
a/common/openstack/src/test/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenewTest.java
+++ 
b/common/openstack/src/test/java/org/jclouds/openstack/keystone/v1_1/handlers/RetryOnRenewTest.java
@@ -21,6 +21,7 @@ import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
 import org.jclouds.domain.Credentials;
@@ -30,6 +31,7 @@ import org.jclouds.http.HttpResponse;
 import org.jclouds.http.handlers.BackoffLimitedRetryHandler;
 import org.jclouds.io.Payloads;
 import org.jclouds.openstack.keystone.v1_1.domain.Auth;
+import org.jclouds.openstack.keystone.v1_1.handlers.RetryOnRenew;
 import org.testng.annotations.Test;
 
 import com.google.common.cache.LoadingCache;
@@ -74,6 +76,41 @@ public class RetryOnRenewTest {
    }
 
    @Test
+   public void test401ShouldRetry4Times() {
+      HttpCommand command = createMock(HttpCommand.class);
+      HttpRequest request = createMock(HttpRequest.class);
+      HttpResponse response = createMock(HttpResponse.class);
+
+      @SuppressWarnings("unchecked")
+      LoadingCache<Credentials, Auth> cache = createMock(LoadingCache.class);
+      BackoffLimitedRetryHandler backoffHandler = 
createMock(BackoffLimitedRetryHandler.class);
+
+      expect(command.getCurrentRequest()).andReturn(request).anyTimes();
+      expect(request.getHeaders()).andStubReturn(null);
+
+      cache.invalidateAll();
+      expectLastCall().anyTimes();
+
+      expect(response.getPayload()).andReturn(Payloads.newStringPayload(""))
+            .anyTimes();
+      expect(response.getStatusCode()).andReturn(401).anyTimes();
+
+      replay(command, request, response, cache);
+
+      RetryOnRenew retry = new RetryOnRenew(cache, backoffHandler);
+
+      for (int i = 0; i < RetryOnRenew.NUM_RETRIES - 1; ++i) {
+         assertTrue(retry.shouldRetryRequest(command, response),
+               "Expected retry to succeed");
+      }
+
+      assertFalse(retry.shouldRetryRequest(command, response),
+            "Expected retry to fail on attempt " + RetryOnRenew.NUM_RETRIES);
+
+      verify(command, response, cache);
+   }
+
+   @Test
    public void test408ShouldRetry() {
       HttpCommand command = createMock(HttpCommand.class);
       HttpRequest request = createMock(HttpRequest.class);

Reply via email to