keycloak-aplcache

Changes

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
index 695401d..1f24f84 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
@@ -95,5 +95,10 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
         return result;
     }
 
+    @Override
+    public String toString() {
+        return "MergedUpdate" + childUpdates;
+    }
+
 
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
index 400a1cd..ca21487 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
@@ -117,6 +117,10 @@ public class SessionEntityWrapper<S extends SessionEntity> {
                 + Objects.hashCode(entity);
     }
 
+    @Override
+    public String toString() {
+        return "SessionEntityWrapper{" + "version=" + version + ", entity=" + entity + ", localMetadata=" + localMetadata + '}';
+    }
 
     public static class ExternalizerImpl implements Externalizer<SessionEntityWrapper> {
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
index feca10e..25ac2a4 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
@@ -24,7 +24,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class SessionEntity implements Serializable {
+public abstract class SessionEntity implements Serializable {
 
     private String id;
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
index d57c649..5d0edb0 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -29,6 +29,7 @@ import java.io.IOException;
 import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.util.Map;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -163,7 +164,8 @@ public class UserSessionEntity extends SessionEntity {
 
     @Override
     public String toString() {
-        return String.format("UserSessionEntity [ id=%s, realm=%s, lastSessionRefresh=%d]", getId(), getRealm(), getLastSessionRefresh());
+        return String.format("UserSessionEntity [id=%s, realm=%s, lastSessionRefresh=%d, clients=%s]", getId(), getRealm(), getLastSessionRefresh(),
+          new TreeSet(this.authenticatedClientSessions.keySet()));
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index ced77fb..8d04a50 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -292,6 +292,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         // We have userSession, which passes predicate. No need for remote lookup.
         if (predicate.test(userSession)) {
+            log.debugf("getUserSessionWithPredicate(%s): found in local cache", id);
             return userSession;
         }
 
@@ -302,6 +303,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         if (remoteCache != null) {
             UserSessionEntity remoteSessionEntity = (UserSessionEntity) remoteCache.get(id);
             if (remoteSessionEntity != null) {
+                log.debugf("getUserSessionWithPredicate(%s): remote cache contains session entity %s", id, remoteSessionEntity);
 
                 UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline);
                 if (predicate.test(remoteSessionAdapter)) {
@@ -323,6 +325,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
             }
         }
 
+        log.debugf("getUserSessionWithPredicate(%s): not found", id);
+
         return null;
     }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
index 60cda43..89fd215 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
@@ -67,7 +67,7 @@ public class RemoteCacheInvoker {
         SessionUpdateTask.CrossDCMessageStatus status = task.getCrossDCMessageStatus(sessionWrapper);
 
         if (status == SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED) {
-            logger.debugf("Skip writing to remoteCache for entity '%s' of cache '%s' and operation '%s'", key, cacheName, operation.toString());
+            logger.debugf("Skip writing to remoteCache for entity '%s' of cache '%s' and operation '%s'", key, cacheName, operation);
             return;
         }
 
@@ -127,17 +127,15 @@ public class RemoteCacheInvoker {
             // Run task on the remote session
             task.runUpdate(session);
 
-            if (logger.isDebugEnabled()) {
-                logger.debugf("Before replaceWithVersion. Written entity: %s", session.toString());
-            }
+            logger.debugf("Before replaceWithVersion. Entity to write version %d: %s", versioned.getVersion(), session);
 
             replaced = remoteCache.replaceWithVersion(key, session, versioned.getVersion(), lifespanMs, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
 
             if (!replaced) {
-                logger.debugf("Failed to replace entity '%s' . Will retry again", key);
+                logger.debugf("Failed to replace entity '%s' version %d. Will retry again", key, versioned.getVersion());
             } else {
                 if (logger.isDebugEnabled()) {
-                    logger.debugf("Replaced entity in remote cache: %s", session.toString());
+                    logger.debugf("Replaced entity version %d in remote cache: %s", versioned.getVersion(), session);
                 }
             }
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index b8df605..3f09773 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -163,6 +163,11 @@ public class UserSessionAdapter implements UserSessionModel {
                 return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
                         .getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
             }
+
+            @Override
+            public String toString() {
+                return "setLastSessionRefresh(" + lastSessionRefresh + ')';
+            }
         };
 
         update(task);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 5b70d9b..28923df 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -386,6 +386,7 @@ public class TokenEndpoint {
             }
 
         } catch (OAuthErrorException e) {
+            logger.trace(e.getMessage(), e);
             event.error(Errors.INVALID_TOKEN);
             throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
index 0f1ac27..4a3eaec 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
@@ -22,13 +22,24 @@ package org.keycloak.testsuite;
  */
 public class Retry {
 
-    public static void execute(Runnable runnable, int retryCount, long intervalMillis) {
+    /**
+     * Runs the given {@code runnable} at most {@code retryCount} times until it passes,
+     * leaving {@code intervalMillis} milliseconds between the invocations.
+     * The runnable is reexecuted if it throws a {@link RuntimeException} or {@link AssertionError}.
+     * @param runnable
+     * @param retryCount
+     * @param intervalMillis
+     * @return Index of the first successful invocation, starting from 0.
+     */
+    public static int execute(Runnable runnable, int retryCount, long intervalMillis) {
+        int executionIndex = 0;
         while (true) {
             try {
                 runnable.run();
-                return;
+                return executionIndex;
             } catch (RuntimeException | AssertionError e) {
                 retryCount--;
+                executionIndex++;
                 if (retryCount > 0) {
                     try {
                         Thread.sleep(intervalMillis);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index d42158c..4f17e9c 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -54,6 +54,7 @@ import org.keycloak.testsuite.arquillian.SuiteContext;
 import org.keycloak.util.BasicAuthHelper;
 import org.keycloak.util.JsonSerialization;
 import org.keycloak.util.TokenUtil;
+import com.google.common.base.Charsets;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 
@@ -250,8 +251,7 @@ public class OAuthClient {
     }
 
     public AccessTokenResponse doAccessTokenRequest(String code, String password) {
-        CloseableHttpClient client = newCloseableHttpClient();
-        try {
+        try (CloseableHttpClient client = newCloseableHttpClient()) {
             HttpPost post = new HttpPost(getAccessTokenUrl());
 
             List<NameValuePair> parameters = new LinkedList<NameValuePair>();
@@ -283,12 +283,7 @@ public class OAuthClient {
                 parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
             }
 
-            UrlEncodedFormEntity formEntity = null;
-            try {
-                formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
-            } catch (UnsupportedEncodingException e) {
-                throw new RuntimeException(e);
-            }
+            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charsets.UTF_8);
             post.setEntity(formEntity);
 
             try {
@@ -296,8 +291,8 @@ public class OAuthClient {
             } catch (Exception e) {
                 throw new RuntimeException("Failed to retrieve access token", e);
             }
-        } finally {
-            closeClient(client);
+        } catch (IOException ioe) {
+            throw new RuntimeException(ioe);
         }
     }
 
@@ -310,8 +305,7 @@ public class OAuthClient {
     }
 
     public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect) {
-        CloseableHttpClient client = new DefaultHttpClient();
-        try {
+        try (CloseableHttpClient client = new DefaultHttpClient()) {
             HttpPost post = new HttpPost(getTokenIntrospectionUrl());
 
             String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
@@ -332,19 +326,16 @@ public class OAuthClient {
 
             post.setEntity(formEntity);
 
-            try {
+            try (CloseableHttpResponse response = client.execute(post)) {
                 ByteArrayOutputStream out = new ByteArrayOutputStream();
 
-                CloseableHttpResponse response = client.execute(post);
                 response.getEntity().writeTo(out);
-                response.close();
-
                 return new String(out.toByteArray());
             } catch (Exception e) {
                 throw new RuntimeException("Failed to retrieve access token", e);
             }
-        } finally {
-            closeClient(client);
+        } catch (IOException ioe) {
+            throw new RuntimeException(ioe);
         }
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index 262d0b2..d2f7de6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -144,6 +144,8 @@ public abstract class AbstractKeycloakTest {
             updateMasterAdminPassword();
         }
 
+        beforeAbstractKeycloakTestRealmImport();
+
         if (testContext.getTestRealmReps() == null) {
             importTestRealms();
 
@@ -155,6 +157,9 @@ public abstract class AbstractKeycloakTest {
         oauth.init(adminClient, driver);
     }
 
+    protected void beforeAbstractKeycloakTestRealmImport() throws Exception {
+    }
+
     @After
     public void afterAbstractKeycloakTest() {
         if (resetTimeOffset) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
index 5559a5e..fe20270 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
@@ -76,7 +76,6 @@ public abstract class AbstractConcurrencyTest extends AbstractTestRealmKeycloakT
                     runnable.run(arrayIndex % numThreads, keycloaks.get(), keycloaks.get().realm(REALM_NAME));
                 } catch (Throwable ex) {
                     failures.add(ex);
-                    log.error(ex.getMessage(), ex);
                 }
                 return null;
             });
@@ -96,6 +95,7 @@ public abstract class AbstractConcurrencyTest extends AbstractTestRealmKeycloakT
         if (! failures.isEmpty()) {
             RuntimeException ex = new RuntimeException("There were failures in threads. Failures count: " + failures.size());
             failures.forEach(ex::addSuppressed);
+            failures.forEach(e -> log.error(e.getMessage(), e));
             throw ex;
         }
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
index 11e3bc0..ff6f10f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
@@ -22,7 +22,6 @@ import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,11 +45,21 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
 import org.keycloak.testsuite.util.OAuthClient;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
+import org.apache.http.client.CookieStore;
+import org.apache.http.impl.client.BasicCookieStore;
 import org.hamcrest.Matchers;
 
 
@@ -60,97 +69,95 @@ import org.hamcrest.Matchers;
  */
 public class ConcurrentLoginTest extends AbstractConcurrencyTest {
     
-    private static final int DEFAULT_THREADS = 10;
-    private static final int CLIENTS_PER_THREAD = 10;
-    private static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
-    
+    protected static final int DEFAULT_THREADS = 4;
+    protected static final int CLIENTS_PER_THREAD = 30;
+    protected static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
+
     @Before
     public void beforeTest() {
         createClients();
     }
 
     protected void createClients() {
+        final ClientsResource clients = adminClient.realm(REALM_NAME).clients();
         for (int i = 0; i < DEFAULT_CLIENTS_COUNT; i++) {
-            ClientRepresentation client = new ClientRepresentation();
-            client.setClientId("client" + i);
-            client.setDirectAccessGrantsEnabled(true);
-            client.setRedirectUris(Arrays.asList("http://localhost:8180/auth/realms/master/app/*"));
-            client.setWebOrigins(Arrays.asList("http://localhost:8180"));
-            client.setSecret("password");
-
-            log.debug("creating " + client.getClientId());
-            Response create = adminClient.realm("test").clients().create(client);
-            Assert.assertEquals(Response.Status.CREATED, create.getStatusInfo());
+            ClientRepresentation client = ClientBuilder.create()
+              .clientId("client" + i)
+              .directAccessGrants()
+              .redirectUris("http://localhost:8180/auth/realms/master/app/*")
+              .addWebOrigin("http://localhost:8180")
+              .secret("password")
+              .build();
+
+            Response create = clients.create(client);
+            String clientId = ApiUtil.getCreatedId(create);
             create.close();
+            getCleanup(REALM_NAME).addClientUuid(clientId);
+            log.debugf("created %s [uuid=%s]", client.getClientId(), clientId);
         }
         log.debug("clients created");
     }
 
     @Test
-    public void concurrentLogin() throws Throwable {
-        System.out.println("*********************************************");
+    public void concurrentLoginSingleUser() throws Throwable {
+        log.info("*********************************************");
         long start = System.currentTimeMillis();
 
         AtomicReference<String> userSessionId = new AtomicReference<>();
+        LoginTask loginTask = null;
 
         try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
+            loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList(
+              createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
+            ));
+            run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask);
+            int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
+            Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT, clientSessionsCount);
+        } finally {
+            long end = System.currentTimeMillis() - start;
+            log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
+            log.info("concurrentLoginSingleUser took " + (end/1000) + "s");
+            log.info("*********************************************");
+        }
+    }
 
-            HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, null), "test-user@localhost", "password");
-            
-            log.debug("Executing login request");
-            
-            Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("<title>AUTH_RESPONSE</title>"));
-            AtomicInteger clientIndex = new AtomicInteger();
-            ThreadLocal<OAuthClient> oauthClient = new ThreadLocal<OAuthClient>() {
-                @Override
-                protected OAuthClient initialValue() {
-                    OAuthClient oauth1 = new OAuthClient();
-                    oauth1.init(adminClient, driver);
-                    return oauth1;
-                }
-            };
-
-            run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, (threadIndex, keycloak, realm) -> {
-                int i = clientIndex.getAndIncrement();
-                OAuthClient oauth1 = oauthClient.get();
-                oauth1.clientId("client" + i);
-                log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId());
-
-                final HttpClientContext context = HttpClientContext.create();
-                String pageContent = getPageContent(oauth1.getLoginFormUrl(), httpClient, context);
-                String currentUrl = context.getRedirectLocations().get(0).toString();
-                Assert.assertThat(pageContent, Matchers.containsString("<title>AUTH_RESPONSE</title>"));
-                String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
+    protected HttpClientContext createHttpClientContextForUser(final CloseableHttpClient httpClient, String userName, String password) throws IOException {
+        final HttpClientContext context = HttpClientContext.create();
+        CookieStore cookieStore = new BasicCookieStore();
+        context.setCookieStore(cookieStore);
+        HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, context), userName, password);
+        log.debug("Executing login request");
+        Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request, context)).contains("<title>AUTH_RESPONSE</title>"));
+        return context;
+    }
 
-                OAuthClient.AccessTokenResponse accessRes = oauth1.doAccessTokenRequest(code, "password");
-                Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
-                  200, accessRes.getStatusCode());
+    @Test
+    public void concurrentLoginMultipleUsers() throws Throwable {
+        log.info("*********************************************");
+        long start = System.currentTimeMillis();
 
-                OAuthClient.AccessTokenResponse refreshRes = oauth1.doRefreshTokenRequest(accessRes.getRefreshToken(), "password");
-                Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
-                  200, refreshRes.getStatusCode());
+        AtomicReference<String> userSessionId = new AtomicReference<>();
+        LoginTask loginTask = null;
 
-                if (userSessionId.get() == null) {
-                    AccessToken token = oauth.verifyToken(accessRes.getAccessToken());
-                    userSessionId.set(token.getSessionState());
-                }
-            });
+        try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
+            loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList(
+              createHttpClientContextForUser(httpClient, "test-user@localhost", "password"),
+              createHttpClientContextForUser(httpClient, "john-doh@localhost", "password"),
+              createHttpClientContextForUser(httpClient, "roleRichUser", "password")
+            ));
 
+            run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask);
             int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
-            Assert.assertEquals(clientSessionsCount, 1 + (DEFAULT_THREADS * CLIENTS_PER_THREAD));
+            Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT / 3 + (DEFAULT_CLIENTS_COUNT % 3 <= 0 ? 0 : 1), clientSessionsCount);
         } finally {
-            logStats(start);
+            long end = System.currentTimeMillis() - start;
+            log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
+            log.info("concurrentLoginMultipleUsers took " + (end/1000) + "s");
+            log.info("*********************************************");
         }
     }
 
-    protected void logStats(long start) {
-        long end = System.currentTimeMillis() - start;
-        log.info("concurrentLogin took " + (end/1000) + "s");
-        log.info("*********************************************");
-    }
-    
-    private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
-
+    protected String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
         HttpGet request = new HttpGet(url);
 
         request.setHeader("User-Agent", "Mozilla/5.0");
@@ -158,15 +165,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
                 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
         request.setHeader("Accept-Language", "en-US,en;q=0.5");
 
-        if (context != null) {
-            return parseAndCloseResponse(httpClient.execute(request, context));
-        } else {
-            return parseAndCloseResponse(httpClient.execute(request));
-        }
-
+        return parseAndCloseResponse(httpClient.execute(request, context));
     }
 
-    private String parseAndCloseResponse(CloseableHttpResponse response) {
+    protected String parseAndCloseResponse(CloseableHttpResponse response) {
         try {
             int responseCode = response.getStatusLine().getStatusCode();
             String resp = EntityUtils.toString(response.getEntity());
@@ -186,16 +188,15 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
             }
         }
     }
-    
-    private HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException {
 
-        System.out.println("Extracting form's data...");
+    protected HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException {
+        log.debug("Extracting form's data...");
 
         // Keycloak form id
         Element loginform = Jsoup.parse(html).getElementById("kc-form-login");
         String method = loginform.attr("method");
         String action = loginform.attr("action");
-        
+
         List<NameValuePair> paramList = new ArrayList<>();
 
         for (Element inputElement : loginform.getElementsByTag("input")) {
@@ -207,9 +208,9 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
                 paramList.add(new BasicNameValuePair(key, password));
             }
         }
-        
+
         boolean isPost = method != null && "post".equalsIgnoreCase(method);
-        
+
         if (isPost) {
             HttpPost req = new HttpPost(action);
 
@@ -226,8 +227,8 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
             throw new UnsupportedOperationException("not supported yet!");
         }
     }
-    
-    private Map<String, String> getQueryFromUrl(String url) throws URISyntaxException {
+
+    private static Map<String, String> getQueryFromUrl(String url) throws URISyntaxException {
         Map<String, String> m = new HashMap<>();
         List<NameValuePair> pairs = URLEncodedUtils.parse(new URI(url), "UTF-8");
         for (NameValuePair p : pairs) {
@@ -236,5 +237,98 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
         return m;
     }
 
+    public class LoginTask implements KeycloakRunnable {
+
+        private final AtomicInteger clientIndex = new AtomicInteger();
+        private final ThreadLocal<OAuthClient> oauthClient = new ThreadLocal<OAuthClient>() {
+                @Override
+                protected OAuthClient initialValue() {
+                    OAuthClient oauth1 = new OAuthClient();
+                    oauth1.init(adminClient, driver);
+                    return oauth1;
+                }
+            };
+
+        private final CloseableHttpClient httpClient;
+        private final AtomicReference<String> userSessionId;
+
+        private final int retryDelayMs;
+        private final int retryCount;
+        private final AtomicInteger[] retryHistogram;
+        private final AtomicInteger totalInvocations = new AtomicInteger();
+        private final List<HttpClientContext> clientContexts;
+
+        public LoginTask(CloseableHttpClient httpClient, AtomicReference<String> userSessionId, int retryDelayMs, int retryCount, List<HttpClientContext> clientContexts) {
+            this.httpClient = httpClient;
+            this.userSessionId = userSessionId;
+            this.retryDelayMs = retryDelayMs;
+            this.retryCount = retryCount;
+            this.retryHistogram = new AtomicInteger[retryCount];
+            for (int i = 0; i < retryHistogram.length; i ++) {
+                retryHistogram[i] = new AtomicInteger();
+            }
+            this.clientContexts = clientContexts;
+        }
+
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            int i = clientIndex.getAndIncrement();
+            OAuthClient oauth1 = oauthClient.get();
+            oauth1.clientId("client" + i);
+            log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId());
+
+            final HttpClientContext templateContext = clientContexts.get(i % clientContexts.size());
+            final HttpClientContext context = HttpClientContext.create();
+            context.setCookieStore(templateContext.getCookieStore());
+            String pageContent = getPageContent(oauth1.getLoginFormUrl(), httpClient, context);
+            Assert.assertThat(pageContent, Matchers.containsString("<title>AUTH_RESPONSE</title>"));
+            Assert.assertThat(context.getRedirectLocations(), Matchers.notNullValue());
+            Assert.assertThat(context.getRedirectLocations(), Matchers.not(Matchers.empty()));
+            String currentUrl = context.getRedirectLocations().get(0).toString();
+            String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
+
+            AtomicReference<OAuthClient.AccessTokenResponse> accessResRef = new AtomicReference<>();
+            totalInvocations.incrementAndGet();
+
+            // obtain access + refresh token via code-to-token flow
+            OAuthClient.AccessTokenResponse accessRes = oauth1.doAccessTokenRequest(code, "password");
+            Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
+              200, accessRes.getStatusCode());
+            accessResRef.set(accessRes);
+
+            // Refresh access + refresh token using refresh token
+            int invocationIndex = Retry.execute(() -> {
+                OAuthClient.AccessTokenResponse refreshRes = oauth1.doRefreshTokenRequest(accessResRef.get().getRefreshToken(), "password");
+                Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
+                  200, refreshRes.getStatusCode());
+            }, retryCount, retryDelayMs);
+
+            retryHistogram[invocationIndex].incrementAndGet();
+
+            if (userSessionId.get() == null) {
+                AccessToken token = oauth1.verifyToken(accessResRef.get().getAccessToken());
+                userSessionId.set(token.getSessionState());
+            }
+        }
+
+        public int getRetryDelayMs() {
+            return retryDelayMs;
+        }
+
+        public int getRetryCount() {
+            return retryCount;
+        }
+
+        public Map<Integer, Integer> getHistogram() {
+            Map<Integer, Integer> res = new LinkedHashMap<>(retryCount);
+            for (int i = 0; i < retryHistogram.length; i ++) {
+                AtomicInteger item = retryHistogram[i];
+
+                res.put(i * retryDelayMs, item.get());
+            }
+            return res;
+        }
+    }
+
     
 }
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
index 3c755f4..0986c17 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
@@ -22,7 +22,6 @@ import java.util.List;
 
 import org.jboss.arquillian.container.test.api.ContainerController;
 import org.jboss.arquillian.test.api.ArquillianResource;
-import org.junit.After;
 import org.junit.Before;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.representations.idm.RealmRepresentation;
@@ -75,9 +74,8 @@ public class ConcurrentLoginClusterTest extends ConcurrentLoginTest {
 
 
     @Override
-    protected void logStats(long start) {
-        super.logStats(start);
-
+    public void concurrentLoginSingleUser() throws Throwable {
+        super.concurrentLoginSingleUser();
         JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getJgroupsStats();
         log.info("JGroups statistics: " + stats.statsAsString());
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
index cb21255..91e968b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
@@ -84,7 +84,7 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
     }
 
     @Before
-    public void InitRESTClientsForStartedNodes() {
+    public void initRESTClientsForStartedNodes() {
         log.debug("Init REST clients for automatically started nodes");
         this.suiteContext.getDcAuthServerBackendsInfo().stream()
                 .flatMap(List::stream)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java
index fde1285..b710943 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java
@@ -17,18 +17,26 @@
 
 package org.keycloak.testsuite.crossdc;
 
-import java.util.LinkedList;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
 import java.util.List;
 
 import org.jboss.arquillian.container.test.api.ContainerController;
 import org.jboss.arquillian.test.api.ArquillianResource;
-import org.junit.Before;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.testsuite.Assert;
 import org.keycloak.testsuite.admin.concurrency.ConcurrentLoginTest;
 import org.keycloak.testsuite.arquillian.ContainerInfo;
 import org.keycloak.testsuite.arquillian.LoadBalancerController;
 import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.junit.Ignore;
+import org.junit.Test;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -42,42 +50,64 @@ public class ConcurrentLoginCrossDCTest extends ConcurrentLoginTest {
     @ArquillianResource
     protected ContainerController containerController;
 
+    private static final int INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE = 10;
+    private static final int LOGIN_TASK_DELAY_MS = 100;
+    private static final int LOGIN_TASK_RETRIES = 15;
 
-    // Need to postpone that
     @Override
-    public void addTestRealms(List<RealmRepresentation> testRealms) {
-    }
-
-
-    @Before
-    @Override
-    public void beforeTest() {
-        log.debug("Initializing load balancer - only enabling started nodes in the first DC");
+    public void beforeAbstractKeycloakTestRealmImport() {
+        log.debug("Initializing load balancer - enabling all started nodes across DCs");
         this.loadBalancerCtrl.disableAllBackendNodes();
 
-        // This should enable only the started nodes in first datacenter
-        this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream()
+        this.suiteContext.getDcAuthServerBackendsInfo().stream()
+                .flatMap(List::stream)
                 .filter(ContainerInfo::isStarted)
                 .map(ContainerInfo::getQualifier)
                 .forEach(loadBalancerCtrl::enableBackendNodeByName);
+    }
 
-        this.suiteContext.getDcAuthServerBackendsInfo().get(1).stream()
-                .filter(ContainerInfo::isStarted)
-                .map(ContainerInfo::getQualifier)
-                .forEach(loadBalancerCtrl::enableBackendNodeByName);
+    @Test
+    public void concurrentLoginWithRandomDcFailures() throws Throwable {
+        log.info("*********************************************");
+        long start = System.currentTimeMillis();
+
+        AtomicReference<String> userSessionId = new AtomicReference<>();
+        LoginTask loginTask = null;
+
+        try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
+            loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, Arrays.asList(
+              createHttpClientContextForUser(httpClient, "test-user@localhost", "password")
+            ));
+            HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, HttpClientContext.create()), "test-user@localhost", "password");
+            log.debug("Executing login request");
+            org.junit.Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("<title>AUTH_RESPONSE</title>"));
+
+            run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask, new SwapDcAvailability());
+            int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get());
+            org.junit.Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT, clientSessionsCount);
+        } finally {
+            long end = System.currentTimeMillis() - start;
+            log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram());
+            log.info("concurrentLoginWithRandomDcFailures took " + (end/1000) + "s");
+            log.info("*********************************************");
+        }
+    }
 
+    private class SwapDcAvailability implements KeycloakRunnable {
 
+        private final AtomicInteger invocationCounter = new AtomicInteger();
 
-        // Import realms
-        log.info("Importing realms");
-        List<RealmRepresentation> testRealms = new LinkedList<>();
-        super.addTestRealms(testRealms);
-        for (RealmRepresentation testRealm : testRealms) {
-            importRealm(testRealm);
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            final int currentInvocarion = invocationCounter.getAndIncrement();
+            if (currentInvocarion % INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE == 0) {
+                int failureIndex = currentInvocarion / INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE;
+                int dcToEnable = failureIndex % 2;
+                int dcToDisable = (failureIndex + 1) % 2;
+                suiteContext.getDcAuthServerBackendsInfo().get(dcToDisable).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier()));
+                suiteContext.getDcAuthServerBackendsInfo().get(dcToEnable).forEach(c -> loadBalancerCtrl.enableBackendNodeByName(c.getQualifier()));
+            }
         }
-        log.info("Realms imported");
-
-        // Finally create clients
-        createClients();
     }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
index 8533f9c..8f74373 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
@@ -18,8 +18,9 @@
 log4j.rootLogger=info, keycloak
 
 log4j.appender.keycloak=org.apache.log4j.ConsoleAppender
-log4j.appender.keycloak.layout=org.apache.log4j.PatternLayout
-log4j.appender.keycloak.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
+log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout
+keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
+log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern}
 
 # Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug )
 log4j.logger.org.keycloak=${keycloak.logging.level:info}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 3906c87..55292cb 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -85,6 +85,7 @@
         <keycloak.connectionsInfinispan.remoteStorePort>12232</keycloak.connectionsInfinispan.remoteStorePort>
         <keycloak.connectionsInfinispan.remoteStorePort.2>13232</keycloak.connectionsInfinispan.remoteStorePort.2>
         <keycloak.connectionsJpa.url.crossdc>jdbc:h2:mem:test-dc-shared</keycloak.connectionsJpa.url.crossdc>
+        <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} %-5p [%c] %m%n</keycloak.testsuite.logging.pattern>
 
         <adapter.test.props/>
         <migration.import.properties/>
@@ -284,6 +285,7 @@
                             <keycloak.connectionsInfinispan.remoteStorePort>${keycloak.connectionsInfinispan.remoteStorePort}</keycloak.connectionsInfinispan.remoteStorePort>
                             <keycloak.connectionsInfinispan.remoteStorePort.2>${keycloak.connectionsInfinispan.remoteStorePort.2}</keycloak.connectionsInfinispan.remoteStorePort.2>
                             <keycloak.connectionsInfinispan.remoteStoreServer>${keycloak.connectionsInfinispan.remoteStoreServer}</keycloak.connectionsInfinispan.remoteStoreServer>
+                            <keycloak.testsuite.logging.pattern>${keycloak.testsuite.logging.pattern}</keycloak.testsuite.logging.pattern>
 
                             <keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
                         </systemPropertyVariables>
@@ -386,6 +388,7 @@
                 <auth.server.crossdc>true</auth.server.crossdc>
                 <cache.server.jboss>true</cache.server.jboss>
                 <cache.server.config.dir>${cache.server.home}/standalone/configuration</cache.server.config.dir>                
+                <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n</keycloak.testsuite.logging.pattern>
             </properties>
             <dependencies>
                 <dependency>
@@ -460,6 +463,7 @@
                 <auth.server.crossdc>true</auth.server.crossdc>
                 <cache.server.jboss>true</cache.server.jboss>
                 <cache.server.config.dir>${cache.server.home}/standalone/configuration</cache.server.config.dir>
+                <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n</keycloak.testsuite.logging.pattern>
             </properties>
             <dependencies>
                 <dependency>
@@ -584,6 +588,8 @@
                 <auth.server.backend2.home>${containers.home}/auth-server-${auth.server}-backend2</auth.server.backend2.home>
 
                 <auth.server.config.dir>${auth.server.backend1.home}/standalone/configuration</auth.server.config.dir>
+
+                <keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n</keycloak.testsuite.logging.pattern>
             </properties>
             <build>
                 <plugins>