keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java 5(+5 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java 4(+4 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java 4(+3 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 4(+4 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java 10(+4 -6)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java 15(+13 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 27(+9 -18)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java 252(+173 -79)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java 6(+2 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java 2(+1 -1)
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>