Move ConcurrencyTest.java, AbstractConcurrencyTest.java to the new testsuite

Part of: #34494

Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
This commit is contained in:
Lukas Hanusovsky
2025-07-09 14:03:57 +02:00
committed by Stian Thorgersen
parent adae1bbcb1
commit cabd7cd474
6 changed files with 217 additions and 65 deletions

View File

@@ -13,7 +13,7 @@ class MariaDBTestDatabase extends AbstractContainerTestDatabase {
@Override
public JdbcDatabaseContainer<?> createContainer() {
return new MariaDBContainer<>(DockerImageName.parse(DatabaseProperties.getContainerImageName(NAME)).asCompatibleSubstituteFor(NAME));
return new MariaDBContainer<>(DockerImageName.parse(DatabaseProperties.getContainerImageName(NAME)).asCompatibleSubstituteFor(NAME)).withCommand("--character-set-server=utf8 --collation-server=utf8_unicode_ci");
}
@Override

View File

@@ -38,6 +38,11 @@ class MSSQLServerTestDatabase extends AbstractContainerTestDatabase {
return "vEry$tron9Pwd";
}
@Override
public String getJdbcUrl(boolean internal) {
return super.getJdbcUrl(internal) + ";integratedSecurity=false;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=false;";
}
@Override
public List<String> getPostStartCommand() {
return List.of("/opt/mssql-tools18/bin/sqlcmd", "-U", "sa", "-P", getPassword(), "-No", "-Q", "CREATE DATABASE " + getDatabase());

View File

@@ -21,6 +21,11 @@ class MySQLTestDatabase extends AbstractContainerTestDatabase {
return NAME;
}
@Override
public String getJdbcUrl(boolean internal) {
return super.getJdbcUrl(internal) + "?allowPublicKeyRetrieval=true";
}
@Override
public Logger getLogger() {
return LOGGER;

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.admin.concurrency;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.testframework.admin.AdminClientFactory;
import org.keycloak.testframework.annotations.InjectAdminClientFactory;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.config.Config;
import org.keycloak.testframework.realm.ManagedRealm;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class AbstractConcurrencyTest {
@InjectRealm
ManagedRealm managedRealm;
@InjectAdminClientFactory
static AdminClientFactory clientFactory;
private static final Logger LOGGER = Logger.getLogger(AbstractConcurrencyTest.class);
private static final int DEFAULT_THREADS = 4;
public static final String REALM_NAME = "default";
public static final String MASTER_REALM_NAME = "master";
// If enabled only one request is allowed at the time. Useful for checking that test is working.
private static final boolean SYNCHRONIZED = false;
protected void run(final KeycloakRunnable... runnables) {
run(DEFAULT_THREADS, runnables);
}
public static void run(final int numThreads, final KeycloakRunnable... runnables) {
final ExecutorService service = SYNCHRONIZED
? Executors.newSingleThreadExecutor()
: Executors.newFixedThreadPool(numThreads);
ThreadLocal<Keycloak> keycloaks = new ThreadLocal<Keycloak>() {
@Override
protected Keycloak initialValue() {
return clientFactory.create().realm(MASTER_REALM_NAME)
.clientId(Config.getAdminClientId())
.clientSecret(Config.getAdminClientSecret())
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.build();
}
};
AtomicInteger currentThreadIndex = new AtomicInteger();
Collection<Callable<Void>> tasks = new LinkedList<>();
Collection<Throwable> failures = new ConcurrentLinkedQueue<>();
final List<Callable<Void>> runnablesToTasks = new LinkedList<>();
// Track all used admin clients, so they can be closed after the test
Set<Keycloak> usedKeycloaks = Collections.synchronizedSet(new HashSet<>());
for (KeycloakRunnable runnable : runnables) {
runnablesToTasks.add(() -> {
int arrayIndex = currentThreadIndex.getAndIncrement() % numThreads;
try {
Keycloak keycloak = keycloaks.get();
usedKeycloaks.add(keycloak);
runnable.run(arrayIndex % numThreads, keycloak, keycloak.realm(REALM_NAME));
} catch (Throwable ex) {
failures.add(ex);
}
return null;
});
}
tasks.addAll(runnablesToTasks);
try {
service.invokeAll(tasks);
service.shutdown();
service.awaitTermination(3, TimeUnit.MINUTES);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
} finally {
for (Keycloak keycloak : usedKeycloaks) {
try {
keycloak.close();
} catch (Exception e) {
failures.add(e);
}
}
}
if (! failures.isEmpty()) {
RuntimeException ex = new RuntimeException("There were failures in threads. Failures count: " + failures.size());
failures.forEach(ex::addSuppressed);
failures.forEach(e -> LOGGER.error(e.getMessage(), e));
throw ex;
}
}
public interface KeycloakRunnable {
void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable;
}
}

View File

@@ -15,10 +15,9 @@
* limitations under the License.
*/
package org.keycloak.testsuite.admin.concurrency;
package org.keycloak.tests.admin.concurrency;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
@@ -34,9 +33,10 @@ import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.model.StoreProvider;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.tests.utils.admin.ApiUtil;
import org.keycloak.testsuite.util.userprofile.UserProfileUtil;
import java.util.List;
import java.util.Map;
@@ -44,39 +44,35 @@ import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@KeycloakIntegrationTest
public class ConcurrencyTest extends AbstractConcurrencyTest {
public void concurrentTest(KeycloakRunnable... tasks) throws Throwable {
System.out.println("***************************");
long start = System.currentTimeMillis();
run(tasks);
long end = System.currentTimeMillis() - start;
System.out.println("took " + end + " ms");
}
// KEYCLOAK-8141 Verify that no attribute values are duplicated, and there are no locking exceptions when adding attributes in parallell
// Verify that no attribute values are duplicated, and there are no locking exceptions when adding attributes in parallel
// https://github.com/keycloak/keycloak/issues/38868
@Test
@Ignore
public void createUserAttributes() throws Throwable {
AtomicInteger c = new AtomicInteger();
UsersResource users = testRealm().users();
UsersResource users = managedRealm.admin().users();
UserRepresentation u = UserBuilder.create().username("attributes").build();
Response response = users.create(u);
String userId = ApiUtil.getCreatedId(response);
response.close();
// enable unmanaged attributes
UserProfileUtil.enableUnmanagedAttributes(users.userProfile());
UserRepresentation u = UserConfigBuilder.create().username("attributes").build();
String userId;
try (Response response = users.create(u)) {
userId = ApiUtil.getCreatedId(response);
}
UserResource user = users.get(userId);
@@ -90,7 +86,6 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
// Number of attributes should be equal to created attributes, or less (concurrent requests may drop attributes added by other threads)
assertTrue(rep.getAttributes().size() <= c.get());
// All attributes should have a single value
for (Map.Entry<String, List<String>> e : rep.getAttributes().entrySet()) {
assertEquals(1, e.getValue().size());
@@ -132,9 +127,10 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
public void createClientRole() throws Throwable {
ClientRepresentation c = new ClientRepresentation();
c.setClientId("client");
Response response = adminClient.realm(REALM_NAME).clients().create(c);
final String clientId = ApiUtil.getCreatedId(response);
response.close();
final String clientId;
try (Response response = managedRealm.admin().clients().create(c)) {
clientId = ApiUtil.getCreatedId(response);
}
AtomicInteger uniqueCounter = new AtomicInteger();
concurrentTest(new CreateClientRole(uniqueCounter, clientId));
@@ -146,6 +142,14 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
run(new CreateRole(uniqueCounter));
}
public void concurrentTest(KeycloakRunnable... tasks) throws Throwable {
System.out.println("***************************");
long start = System.currentTimeMillis();
run(tasks);
long end = System.currentTimeMillis() - start;
System.out.println("took " + end + " ms");
}
private class CreateClient implements KeycloakRunnable {
private final AtomicInteger clientIndex;
@@ -156,27 +160,26 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
@Override
public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
String name = "c-" + clientIndex.getAndIncrement();
String name = "cc-" + clientIndex.getAndIncrement();
ClientRepresentation c = new ClientRepresentation();
c.setClientId(name);
Response response = realm.clients().create(c);
String id = ApiUtil.getCreatedId(response);
response.close();
String id;
try (Response response = realm.clients().create(c)) {
id = ApiUtil.getCreatedId(response);
}
c = realm.clients().get(id).toRepresentation();
assertNotNull(c);
int findAttempts = 1;
if (StoreProvider.getCurrentProvider().equals(StoreProvider.DEFAULT)) {
findAttempts = 5;
}
int findAttempts = 5;
boolean clientFound = IntStream.range(0, findAttempts)
.anyMatch(i -> realm.clients().findAll().stream()
.map(ClientRepresentation::getClientId)
.filter(Objects::nonNull)
.anyMatch(name::equals));
assertTrue("Client " + name + " not found in client list after " + findAttempts + " attempts", clientFound);
assertTrue(clientFound, "Client " + name + " not found in client list after " + findAttempts + " attempts");
}
}
@@ -190,30 +193,29 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
@Override
public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
String name = "c-" + clientIndex.getAndIncrement();
String name = "crc-" + clientIndex.getAndIncrement();
ClientRepresentation c = new ClientRepresentation();
c.setClientId(name);
final ClientsResource clients = realm.clients();
Response response = clients.create(c);
String id = ApiUtil.getCreatedId(response);
response.close();
String id;
try (Response response = clients.create(c)) {
id = ApiUtil.getCreatedId(response);
}
final ClientResource client = clients.get(id);
c = client.toRepresentation();
assertNotNull(c);
int findAttempts = 1;
if (StoreProvider.getCurrentProvider().equals(StoreProvider.DEFAULT)) {
findAttempts = 5;
}
int findAttempts = 5;
boolean clientFound = IntStream.range(0, findAttempts)
.anyMatch(i -> clients.findAll().stream()
.map(ClientRepresentation::getClientId)
.filter(Objects::nonNull)
.anyMatch(name::equals));
.map(ClientRepresentation::getClientId)
.filter(Objects::nonNull)
.anyMatch(name::equals));
assertTrue("Client " + name + " not found in client list after " + findAttempts + " attempts", clientFound);
assertTrue(clientFound, "Client " + name + " not found in client list after " + findAttempts + " attempts");
client.remove();
try {
@@ -223,11 +225,10 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
}
assertFalse("Client " + name + " should now not present in client list",
clients.findAll().stream()
assertFalse(clients.findAll().stream()
.map(ClientRepresentation::getClientId)
.filter(Objects::nonNull)
.anyMatch(name::equals));
.anyMatch(name::equals), "Client " + name + " should now not present in client list");
}
}
@@ -244,18 +245,18 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
String name = "g-" + uniqueIndex.getAndIncrement();
GroupRepresentation c = new GroupRepresentation();
c.setName(name);
Response response = realm.groups().add(c);
String id = ApiUtil.getCreatedId(response);
response.close();
String id;
try (Response response = realm.groups().add(c)) {
id = ApiUtil.getCreatedId(response);
}
c = realm.groups().group(id).toRepresentation();
assertNotNull(c);
assertTrue("Group " + name + " [" + id + "] " + " not found in group list",
realm.groups().groups().stream()
.map(GroupRepresentation::getName)
.filter(Objects::nonNull)
.anyMatch(name::equals));
assertTrue(realm.groups().groups().stream()
.map(GroupRepresentation::getName)
.filter(Objects::nonNull)
.anyMatch(name::equals), "Group " + name + " [" + id + "] " + " not found in group list");
}
}

View File

@@ -41,6 +41,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Deprecated(forRemoval = true)
public abstract class AbstractConcurrencyTest extends AbstractTestRealmKeycloakTest {
private static final int DEFAULT_THREADS = 4;