package org.keycloak.performance;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import static org.keycloak.performance.RealmsConfigurationBuilder.EXPORT_FILENAME;
import static org.keycloak.performance.TestConfig.numOfWorkers;
/**
* # build
* mvn -f testsuite/integration-arquillian/tests/performance/gatling-perf clean install
*
* # generate benchmark-realms.json file with generated test data
* mvn -f testsuite/integration-arquillian/tests/performance/gatling-perf exec:java -Dexec.mainClass=org.keycloak.performance.RealmsConfigurationBuilder -DnumOfRealms=2 -DusersPerRealm=2 -DclientsPerRealm=2 -DrealmRoles=2 -DrealmRolesPerUser=2 -DclientRolesPerUser=2 -DclientRolesPerClient=2
*
* # use benchmark-realms.json to load the data up to Keycloak Server listening on localhost:8080
* mvn -f testsuite/integration-arquillian/tests/performance/gatling-perf exec:java -Dexec.mainClass=org.keycloak.performance.RealmsConfigurationLoader -DnumOfWorkers=5 -Dexec.args=benchmark-realms.json > perf-output.txt
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class RealmsConfigurationLoader {
static final int ERROR_CHECK_INTERVAL = 10;
// multi-thread mechanics
static final BlockingQueue<AdminJob> queue = new LinkedBlockingQueue<>(numOfWorkers);
static final ArrayList<Worker> workers = new ArrayList<>();
static final ConcurrentLinkedQueue<PendingResult> pendingResult = new ConcurrentLinkedQueue<>();
// realm caches - we completely handle one realm before starting the next
static ConcurrentHashMap<String, String> clientIdMap = new ConcurrentHashMap<>();
static ConcurrentHashMap<String, String> realmRoleIdMap = new ConcurrentHashMap<>();
static ConcurrentHashMap<String, Map<String, String>> clientRoleIdMap = new ConcurrentHashMap<>();
static boolean realmCreated;
public static void main(String [] args) throws IOException {
System.out.println("Keycloak servers: "+TestConfig.serverUrisList);
if (args.length == 0) {
args = new String[] {EXPORT_FILENAME};
}
if (args.length != 1) {
System.out.println("Usage: java " + RealmsConfigurationLoader.class.getName() + " <FILE>");
return;
}
String file = args[0];
System.out.println("Using file: " + new File(args[0]).getAbsolutePath());
System.out.println("Number of workers (numOfWorkers): " + numOfWorkers);
JsonParser p = initParser(file);
initWorkers();
try {
// read json file using JSON stream API
readRealms(p);
} finally {
completeWorkers();
}
}
private static void completeWorkers() {
try {
// wait for all jobs to finish
completePending();
} finally {
// stop workers
for (Worker w : workers) {
w.exit = true;
try {
w.join(5000);
if (w.isAlive()) {
System.out.println("Worker thread failed to stop: ");
dumpThread(w);
}
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted");
}
}
}
}
private static void readRealms(JsonParser p) throws IOException {
JsonToken t = p.nextToken();
while (t != JsonToken.END_OBJECT && t != JsonToken.END_ARRAY) {
if (t != JsonToken.START_ARRAY) {
readRealm(p);
}
t = p.nextToken();
}
}
private static void initWorkers() {
// configure job queue and worker threads
for (int i = 0; i < numOfWorkers; i++) {
workers.add(new Worker());
}
}
private static JsonParser initParser(String file) {
JsonParser p;
try {
JsonFactory f = new JsonFactory();
p = f.createParser(new File(file));
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
p.setCodec(mapper);
} catch (Exception e) {
throw new RuntimeException("Failed to parse file " + new File(file).getAbsolutePath(), e);
}
return p;
}
private static void dumpThread(Worker w) {
StringBuilder b = new StringBuilder();
for (StackTraceElement e: w.getStackTrace()) {
b.append(e.toString()).append("\n");
}
System.out.print(b);
}
private static void readRealm(JsonParser p) throws IOException {
// as soon as we encounter users, roles, clients we create a CreateRealmJob
// TODO: if after that point in a realm we encounter realm attribute, we report a warning but continue
RealmRepresentation r = new RealmRepresentation();
JsonToken t = p.nextToken();
while (t != JsonToken.END_OBJECT) {
//System.out.println(t + ", name: " + p.getCurrentName() + ", text: '" + p.getText() + "', value: " + p.getValueAsString());
switch (p.getCurrentName()) {
case "realm":
r.setRealm(getStringValue(p));
break;
case "enabled":
r.setEnabled(getBooleanValue(p));
break;
case "accessTokenLifespan":
r.setAccessCodeLifespan(getIntegerValue(p));
break;
case "registrationAllowed":
r.setRegistrationAllowed(getBooleanValue(p));
break;
case "passwordPolicy":
r.setPasswordPolicy(getStringValue(p));
break;
case "users":
ensureRealm(r);
readUsers(r, p);
break;
case "roles":
ensureRealm(r);
readRoles(r, p);
break;
case "clients":
ensureRealm(r);
readClients(r, p);
break;
default: {
// if we don't understand the field we ignore it - but report that
System.out.println("Realm attribute ignored: " + p.getCurrentName());
consumeAttribute(p);
}
}
t = p.nextToken();
}
// we wait for realm to complete
completePending();
// reset realm specific cache
realmCreated = false;
clientIdMap.clear();
realmRoleIdMap.clear();
clientRoleIdMap.clear();
}
private static void ensureRealm(RealmRepresentation r) {
if (!realmCreated) {
createRealm(r);
realmCreated = true;
}
}
private static void createRealm(RealmRepresentation r) {
try {
queue.put(new CreateRealmJob(r));
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
}
// now wait for job to appear
PendingResult next = pendingResult.poll();
while (next == null) {
waitForAwhile();
next = pendingResult.poll();
}
// then wait for the job to complete
while (!next.isDone()) {
waitForAwhile();
}
try {
next.get();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
} catch (ExecutionException e) {
throw new RuntimeException("Execution failed", e.getCause());
}
}
private static void enqueueCreateUser(RealmRepresentation r, UserRepresentation u) {
try {
queue.put(new CreateUserJob(r, u));
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
}
}
private static void enqueueCreateRealmRole(RealmRepresentation r, RoleRepresentation role) {
try {
queue.put(new CreateRealmRoleJob(r, role));
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
}
}
private static void enqueueCreateClientRole(RealmRepresentation r, RoleRepresentation role, String client) {
try {
queue.put(new CreateClientRoleJob(r, role, client));
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
}
}
private static void enqueueCreateClient(RealmRepresentation r, ClientRepresentation client) {
try {
queue.put(new CreateClientJob(r, client));
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
}
}
private static void waitForAwhile() {
waitForAwhile(100, "Interrupted");
}
private static void waitForAwhile(int millis) {
waitForAwhile(millis, "Interrupted");
}
private static void waitForAwhile(int millis, String interruptMessage) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(interruptMessage);
}
}
private static void readUsers(RealmRepresentation r, JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.START_ARRAY) {
throw new RuntimeException("Error reading field 'users'. Expected array of users [" + t + "]");
}
int count = 0;
t = p.nextToken();
while (t == JsonToken.START_OBJECT) {
UserRepresentation u = p.readValueAs(UserRepresentation.class);
enqueueCreateUser(r, u);
t = p.nextToken();
count += 1;
// every some users check to see pending errors
// in order to short-circuit if any errors have occurred
if (count % ERROR_CHECK_INTERVAL == 0) {
checkPendingErrors();
}
}
}
private static void readRoles(RealmRepresentation r, JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.START_OBJECT) {
throw new RuntimeException("Error reading field 'roles'. Expected start of object [" + t + "]");
}
t = p.nextToken();
if (t != JsonToken.FIELD_NAME) {
throw new RuntimeException("Error reading field 'roles'. Expected field 'realm' or 'client' [" + t + "]");
}
while (t != JsonToken.END_OBJECT) {
switch (p.getCurrentName()) {
case "realm":
readRealmRoles(r, p);
break;
case "client":
waitForClientsCompleted();
readClientRoles(r, p);
break;
default:
throw new RuntimeException("Unexpected field in roles: " + p.getCurrentName());
}
t = p.nextToken();
}
}
private static void waitForClientsCompleted() {
completePending();
}
private static void readClientRoles(RealmRepresentation r, JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.START_OBJECT) {
throw new RuntimeException("Expected start_of_object on 'roles/client' [" + t + "]");
}
t = p.nextToken();
int count = 0;
while (t == JsonToken.FIELD_NAME) {
String client = p.getCurrentName();
t = p.nextToken();
if (t != JsonToken.START_ARRAY) {
throw new RuntimeException("Expected start_of_array on 'roles/client/" + client + " [" + t + "]");
}
t = p.nextToken();
while (t != JsonToken.END_ARRAY) {
RoleRepresentation u = p.readValueAs(RoleRepresentation.class);
enqueueCreateClientRole(r, u, client);
t = p.nextToken();
count += 1;
// every some roles check to see pending errors
// in order to short-circuit if any errors have occurred
if (count % ERROR_CHECK_INTERVAL == 0) {
checkPendingErrors();
}
}
t = p.nextToken();
}
}
private static void readRealmRoles(RealmRepresentation r, JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.START_ARRAY) {
throw new RuntimeException("Expected start_of_array on 'roles/realm' [" + t + "]");
}
t = p.nextToken();
int count = 0;
while (t == JsonToken.START_OBJECT) {
RoleRepresentation u = p.readValueAs(RoleRepresentation.class);
enqueueCreateRealmRole(r, u);
t = p.nextToken();
count += 1;
// every some roles check to see pending errors
// in order to short-circuit if any errors have occurred
if (count % ERROR_CHECK_INTERVAL == 0) {
checkPendingErrors();
}
}
}
private static void readClients(RealmRepresentation r, JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.START_ARRAY) {
throw new RuntimeException("Error reading field 'clients'. Expected array of clients [" + t + "]");
}
int count = 0;
t = p.nextToken();
while (t == JsonToken.START_OBJECT) {
ClientRepresentation u = p.readValueAs(ClientRepresentation.class);
enqueueCreateClient(r, u);
t = p.nextToken();
count += 1;
// every some users check to see pending errors
if (count % ERROR_CHECK_INTERVAL == 0) {
checkPendingErrors();
}
}
}
private static void checkPendingErrors() {
// now wait for job to appear
PendingResult next = pendingResult.peek();
while (next == null) {
waitForAwhile();
next = pendingResult.peek();
}
// now process then
Iterator<PendingResult> it = pendingResult.iterator();
while (it.hasNext()) {
next = it.next();
if (next.isDone() && !next.isCompletedExceptionally()) {
it.remove();
} else if (next.isCompletedExceptionally()) {
try {
next.get();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted");
} catch (ExecutionException e) {
throw new RuntimeException("Execution failed", e.getCause());
}
}
}
}
private static void completePending() {
// wait for queue to empty up
while (queue.size() > 0) {
waitForAwhile();
}
PendingResult next;
while ((next = pendingResult.poll()) != null) {
try {
next.get();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted");
} catch (ExecutionException e) {
throw new RuntimeException("Execution failed", e.getCause());
}
}
}
private static Integer getIntegerValue(JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.VALUE_NUMBER_INT) {
throw new RuntimeException("Error while reading field '" + p.getCurrentName() + "'. Expected integer value [" + t + "]");
}
return p.getValueAsInt();
}
private static void consumeAttribute(JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t == JsonToken.START_OBJECT || t == JsonToken.START_ARRAY) {
p.skipChildren();
}
}
private static Boolean getBooleanValue(JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.VALUE_TRUE && t != JsonToken.VALUE_FALSE) {
throw new RuntimeException("Error while reading field '" + p.getCurrentName() + "'. Expected boolean value [" + t + "]");
}
return p.getValueAsBoolean();
}
private static String getStringValue(JsonParser p) throws IOException {
JsonToken t = p.nextToken();
if (t != JsonToken.VALUE_STRING) {
throw new RuntimeException("Error while reading field '" + p.getCurrentName() + "'. Expected string value [" + t + "]");
}
return p.getText();
}
static class Worker extends Thread {
volatile boolean exit = false;
Worker() {
start();
}
public void run() {
while (!exit) {
Job r = queue.poll();
if (r == null) {
waitForAwhile(50, "Worker thread " + this.getName() + " interrupted");
continue;
}
PendingResult pending = new PendingResult(r);
pendingResult.add(pending);
try {
r.run();
pending.complete(true);
} catch (Throwable t) {
pending.completeExceptionally(t);
}
}
}
}
static class CreateRealmJob extends AdminJob {
private RealmRepresentation realm;
CreateRealmJob(RealmRepresentation r) {
this.realm = r;
}
@Override
public void run() {
admin().realms().create(realm);
}
}
static class CreateUserJob extends AdminJob {
private RealmRepresentation realm;
private UserRepresentation user;
CreateUserJob(RealmRepresentation r, UserRepresentation u) {
this.realm = r;
this.user = u;
}
@Override
public void run() {
Response response = admin().realms().realm(realm.getRealm()).users().create(user);
response.close();
if (response.getStatus() != 201) {
throw new RuntimeException("Failed to create user with status: " + response.getStatusInfo().getReasonPhrase());
}
String userId = extractIdFromResponse(response);
List<CredentialRepresentation> creds = user.getCredentials();
for (CredentialRepresentation cred: creds) {
admin().realms().realm(realm.getRealm()).users().get(userId).resetPassword(cred);
}
List<String> realmRoles = user.getRealmRoles();
if (realmRoles != null && !realmRoles.isEmpty()) {
List<RoleRepresentation> roles = convertRealmRoleNamesToRepresentation(user.getRealmRoles());
if (!roles.isEmpty()) {
admin().realms().realm(realm.getRealm()).users().get(userId).roles().realmLevel().add(roles);
}
}
Map<String, List<String>> clientRoles = user.getClientRoles();
if (clientRoles != null && !clientRoles.isEmpty()) {
for (String clientId: clientRoles.keySet()) {
List<String> roleNames = clientRoles.get(clientId);
if (roleNames != null && !roleNames.isEmpty()) {
List<RoleRepresentation> reps = convertClientRoleNamesToRepresentation(clientId, roleNames);
if (!reps.isEmpty()) {
String idOfClient = clientIdMap.get(clientId);
if (idOfClient == null) {
throw new RuntimeException("No client created for clientId: " + clientId);
}
admin().realms().realm(realm.getRealm()).users().get(userId).roles().clientLevel(idOfClient).add(reps);
}
}
}
}
}
private List<RoleRepresentation> convertClientRoleNamesToRepresentation(String clientId, List<String> roles) {
LinkedList<RoleRepresentation> result = new LinkedList<>();
Map<String, String> roleIdMap = clientRoleIdMap.get(clientId);
if (roleIdMap == null || roleIdMap.isEmpty()) {
throw new RuntimeException("No client roles created for clientId: " + clientId);
}
for (String role: roles) {
RoleRepresentation r = new RoleRepresentation();
String id = roleIdMap.get(role);
if (id == null) {
throw new RuntimeException("No client role created on client '" + clientId + "' for name: " + role);
}
r.setId(id);
r.setName(role);
result.add(r);
}
return result;
}
private List<RoleRepresentation> convertRealmRoleNamesToRepresentation(List<String> roles) {
LinkedList<RoleRepresentation> result = new LinkedList<>();
for (String role: roles) {
RoleRepresentation r = new RoleRepresentation();
String id = realmRoleIdMap.get(role);
if (id == null) {
throw new RuntimeException("No realm role created for name: " + role);
}
r.setId(id);
r.setName(role);
result.add(r);
}
return result;
}
}
static class CreateRealmRoleJob extends AdminJob {
private RealmRepresentation realm;
private RoleRepresentation role;
CreateRealmRoleJob(RealmRepresentation r, RoleRepresentation role) {
this.realm = r;
this.role = role;
}
@Override
public void run() {
admin().realms().realm(realm.getRealm()).roles().create(role);
// we need the id but it's not returned by REST API - we have to perform a get on the created role and save the returned id
RoleRepresentation rr = admin().realms().realm(realm.getRealm()).roles().get(role.getName()).toRepresentation();
realmRoleIdMap.put(rr.getName(), rr.getId());
}
}
static class CreateClientRoleJob extends AdminJob {
private RealmRepresentation realm;
private RoleRepresentation role;
private String clientId;
CreateClientRoleJob(RealmRepresentation r, RoleRepresentation role, String clientId) {
this.realm = r;
this.role = role;
this.clientId = clientId;
}
@Override
public void run() {
String id = clientIdMap.get(clientId);
if (id == null) {
throw new RuntimeException("No client created for clientId: " + clientId);
}
admin().realms().realm(realm.getRealm()).clients().get(id).roles().create(role);
// we need the id but it's not returned by REST API - we have to perform a get on the created role and save the returned id
RoleRepresentation rr = admin().realms().realm(realm.getRealm()).clients().get(id).roles().get(role.getName()).toRepresentation();
Map<String, String> roleIdMap = clientRoleIdMap.get(clientId);
if (roleIdMap == null) {
roleIdMap = clientRoleIdMap.computeIfAbsent(clientId, (k) -> new ConcurrentHashMap<>());
}
roleIdMap.put(rr.getName(), rr.getId());
}
}
static class CreateClientJob extends AdminJob {
private ClientRepresentation client;
private RealmRepresentation realm;
public CreateClientJob(RealmRepresentation r, ClientRepresentation client) {
this.realm = r;
this.client = client;
}
@Override
public void run() {
Response response = admin().realms().realm(realm.getRealm()).clients().create(client);
response.close();
if (response.getStatus() != 201) {
throw new RuntimeException("Failed to create client with status: " + response.getStatusInfo().getReasonPhrase());
}
String id = extractIdFromResponse(response);
clientIdMap.put(client.getClientId(), id);
}
}
static String extractIdFromResponse(Response response) {
String location = response.getHeaderString("Location");
if (location == null)
return null;
int last = location.lastIndexOf("/");
if (last == -1) {
return null;
}
String id = location.substring(last + 1);
if (id == null || "".equals(id)) {
throw new RuntimeException("Failed to extract 'id' of created resource");
}
return id;
}
static abstract class AdminJob extends Job {
static Keycloak admin = Keycloak.getInstance(TestConfig.serverUrisList.get(0), TestConfig.authRealm, TestConfig.authUser, TestConfig.authPassword, TestConfig.authClient);
static Keycloak admin() {
return admin;
}
}
static abstract class Job implements Runnable {
}
static class PendingResult extends CompletableFuture<Boolean> {
Job job;
PendingResult(Job job) {
this.job = job;
}
}
}