KEYCLOAK-9536 DB Allocator Plugin

This commit is contained in:
Sebastian Laskawiec 2019-03-19 11:41:42 +01:00 committed by Hynek Mlnařík
parent ccc8e06f9a
commit 2e7f717e50
22 changed files with 1132 additions and 31 deletions

View file

@ -56,13 +56,10 @@ Stop MySQl:
Using built-in profiles to run database tests using docker containers Using built-in profiles to run database tests using docker containers
------- -------
The project provides specific profiles to run database tests using containers. The supported databases and their respective profiles are: The project provides specific profiles to run database tests using containers. Below is a just a sample of implemented profiles. In order to get a full list, please invoke (`mvn help:all-profiles -pl testsuite/integration-arquillian | grep -- db- | grep -v allocator`):
* `db-mysql` * `db-mysql`
* `db-postgres` * `db-postgres`
* `db-mariadb`
* `db-mssql2017`
* `db-oracle11g`
As an example, to run tests using a MySQL docker container on Undertow auth-server: As an example, to run tests using a MySQL docker container on Undertow auth-server:
@ -94,3 +91,31 @@ name or tag for the image.
Note that Docker containers may occupy some space even after termination, and Note that Docker containers may occupy some space even after termination, and
especially with databases that might be easily a gigabyte. It is thus especially with databases that might be easily a gigabyte. It is thus
advisable to run `docker system prune` occasionally to reclaim that space. advisable to run `docker system prune` occasionally to reclaim that space.
Using DB Allocator Service
-------
The testsuite can use the DB Allocator Service to allocate and release desired database automatically.
Since some of the database properties (such as JDBC URL, Username or Password) need to be used when building the Auth Server,
the allocation and deallocation need to happen when building the `integration-arquillian` project (instead of `tests/base` as
it happens in other cases).
In order to use the DB Allocator Service, you must use the `jpa` profile with one of the `db-allocator-*`. Here's a full example to
run JPA with Auth Server Wildfly and MSSQL 2016:
```
mvn -f testsuite/integration-arquillian/pom.xml \
-Pjpa,auth-server-wildfly,db-allocator-db-mssql2016 \
-Ddballocator.uri=<<db-allocator-servlet-url>> \
-Ddballocator.user=<<db-allocator-user>> \
-Dmaven.test.failure.ignore=true
```
Using `-Dmaven.test.failure.ignore=true` is not strictly required but highly recommended. After running the tests,
the DB Allocator Plugin should release the allocated database.
Below is a just a sample of implemented profiles. In order to get a full list, please invoke (`mvn help:all-profiles -pl testsuite/integration-arquillian | grep -- db-allocator-db-`):
* `db-allocator-db-postgres` - dor testing with Postgres 9.6.x
* `db-allocator-db-mysql` - dor testing with MySQL 5.7

View file

@ -0,0 +1,76 @@
<?xml version="1.0"?>
<!--
~ Copyright 2019 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.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<parent>
<artifactId>keycloak-testsuite-pom</artifactId>
<groupId>org.keycloak</groupId>
<version>6.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>db-allocator-plugin</artifactId>
<packaging>maven-plugin</packaging>
<name>DB Allocator Plugin</name>
<properties>
<maven.version>3.6.0</maven.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>${maven.version}</version>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,176 @@
package org.keycloak.testsuite.dballocator;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.keycloak.testsuite.dballocator.client.data.AllocationResult;
import org.keycloak.testsuite.dballocator.client.DBAllocatorServiceClient;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException;
import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Allocated a DB from DB Allocator Service.
*/
@Mojo(name = "allocate", defaultPhase = LifecyclePhase.PROCESS_TEST_RESOURCES)
public class AllocateDBMojo extends AbstractMojo {
private final Log logger = getLog();
@Parameter(defaultValue = "${project}", required = true, readonly = true)
protected MavenProject project;
@Parameter(defaultValue = "${reactorProjects}", readonly = true)
List<MavenProject> reactorProjects;
/**
* Enables printing out a summary after execution.
*/
@Parameter(property = Constants.PROPERTY_PRINT_SUMMARY, defaultValue = "true")
private boolean printSummary;
/**
* Skips the execution of this Mojo.
*/
@Parameter(property = Constants.PROPERTY_SKIP, defaultValue = "false")
private boolean skip;
/**
* The number of retries for reaching the DB Allocator Service
*/
@Parameter(property = Constants.PROPERTY_RETRY_TOTAL_RETRIES, defaultValue = "3")
private int totalRetries;
/**
* Backoff time for reaching out the DB Allocator Service.
*/
@Parameter(property = Constants.PROPERTY_RETRY_BACKOFF_SECONDS, defaultValue = "10")
private int backoffTimeSeconds;
/**
* URI to the DB Allocator Service.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_URI)
private String dbAllocatorURI;
/**
* Username used for allocating DBs.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_USER)
private String user;
/**
* Fallback username used for allocating DBs.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_USER_FALLBACK)
private String fallbackUser;
/**
* Type of the database to be used.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_DATABASE_TYPE)
private String type;
/**
* Expiration in minutes.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_EXPIRATION_MIN, defaultValue = "1440")
private int expirationInMinutes;
/**
* Preferred DB location.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_LOCATION, defaultValue = "geo_RDU")
private String location;
/**
* A property set as an output of this Mojo for JDBC Driver.
*/
@Parameter(property = Constants.PROPERTY_TO_BE_SET_DRIVER, defaultValue = "keycloak.connectionsJpa.driver")
private String propertyDriver;
/**
* A property set as an output of this Mojo for Database Schema.
*/
@Parameter(property = Constants.PROPERTY_TO_BE_SET_DATABASE, defaultValue = "keycloak.connectionsJpa.database")
private String propertyDatabase;
/**
* A property set as an output of this Mojo for DB Username.
*/
@Parameter(property = Constants.PROPERTY_TO_BE_SET_USER, defaultValue = "keycloak.connectionsJpa.user")
private String propertyUser;
/**
* A property set as an output of this Mojo for DB Password.
*/
@Parameter(property = Constants.PROPERTY_TO_BE_SET_PASSWORD, defaultValue = "keycloak.connectionsJpa.password")
private String propertyPassword;
/**
* A property set as an output of this Mojo for JDBC Connection URI.
*/
@Parameter(property = Constants.PROPERTY_TO_BE_SET_JDBC_URL, defaultValue = "keycloak.connectionsJpa.url")
private String propertyURL;
@Override
public void execute() throws MojoFailureException {
if (skip) {
logger.info("Skipping");
return;
}
try {
IncrementalBackoffRetryPolicy retryPolicy = new IncrementalBackoffRetryPolicy(totalRetries, backoffTimeSeconds, TimeUnit.SECONDS);
DBAllocatorServiceClient client = new DBAllocatorServiceClient(dbAllocatorURI, retryPolicy);
setFallbackUserIfNecessary();
AllocationResult allocate = client.allocate(user, type, expirationInMinutes, TimeUnit.MINUTES, location);
reactorProjects.forEach((project) -> setPropertiesToProject(project, allocate));
if (printSummary) {
logger.info("Allocated database:");
logger.info("-- UUID: " + allocate.getUUID());
logger.info("-- Driver: " + allocate.getDriver());
logger.info("-- Database: " + allocate.getDatabase());
logger.info("-- User: " + allocate.getUser());
logger.info("-- Password: " + allocate.getPassword());
logger.info("-- URL: " + allocate.getURL());
}
} catch (DBAllocatorException e) {
String error = e.getMessage();
if (e.getErrorResponse() != null) {
error = String.format("[%s](%s)", e.getErrorResponse().getStatus(), e.getErrorResponse().readEntity(String.class));
}
throw new MojoFailureException("An error occurred while communicating with DBAllocator (" + error + ")", e);
}
}
private void setFallbackUserIfNecessary() {
if (StringUtils.isBlank(user)) {
if (StringUtils.isBlank(fallbackUser)) {
throw new IllegalArgumentException("Both " + Constants.PROPERTY_DB_ALLOCATOR_USER + " and " + Constants.PROPERTY_DB_ALLOCATOR_USER_FALLBACK + " are empty");
}
user = fallbackUser;
}
}
private void setPropertiesToProject(MavenProject project, AllocationResult allocate) {
project.getProperties().setProperty(propertyDriver, allocate.getDriver());
project.getProperties().setProperty(propertyDatabase, allocate.getDatabase());
project.getProperties().setProperty(propertyUser, allocate.getUser());
project.getProperties().setProperty(propertyPassword, allocate.getPassword());
project.getProperties().setProperty(propertyURL, allocate.getURL());
project.getProperties().setProperty(Constants.PROPERTY_ALLOCATED_DB, allocate.getUUID());
}
}

View file

@ -0,0 +1,20 @@
package org.keycloak.testsuite.dballocator;
public interface Constants {
String PROPERTY_ALLOCATED_DB = "dballocator.allocated.uuid";
String PROPERTY_DB_ALLOCATOR_URI = "dballocator.uri";
String PROPERTY_DB_ALLOCATOR_USER = "dballocator.user";
String PROPERTY_DB_ALLOCATOR_USER_FALLBACK = "user.name";
String PROPERTY_DB_ALLOCATOR_DATABASE_TYPE = "dballocator.type";
String PROPERTY_DB_ALLOCATOR_EXPIRATION_MIN = "dballocator.expirationMin";
String PROPERTY_DB_ALLOCATOR_LOCATION = "dballocator.location";
String PROPERTY_TO_BE_SET_DRIVER = "dballocator.properties.driver";
String PROPERTY_TO_BE_SET_DATABASE = "dballocator.properties.database";
String PROPERTY_TO_BE_SET_USER = "dballocator.properties.user";
String PROPERTY_TO_BE_SET_PASSWORD = "dballocator.properties.password";
String PROPERTY_TO_BE_SET_JDBC_URL = "dballocator.properties.url";
String PROPERTY_PRINT_SUMMARY = "dballocator.summary";
String PROPERTY_SKIP = "dballocator.skip";
String PROPERTY_RETRY_TOTAL_RETRIES = "dballocator.retry.totalRetries";
String PROPERTY_RETRY_BACKOFF_SECONDS = "dballocator.retry.backoffSeconds";
}

View file

@ -0,0 +1,92 @@
package org.keycloak.testsuite.dballocator;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.keycloak.testsuite.dballocator.client.data.AllocationResult;
import org.keycloak.testsuite.dballocator.client.DBAllocatorServiceClient;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException;
import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy;
import org.keycloak.testsuite.dballocator.client.data.ReleaseResult;
import java.net.URI;
import java.util.concurrent.TimeUnit;
/**
* Releases a DB from DB Allocator Service.
*/
@Mojo(name = "release", defaultPhase = LifecyclePhase.TEST)
public class ReleaseDBMojo extends AbstractMojo {
private final Log logger = getLog();
@Parameter(defaultValue = "${project}", required = true)
protected MavenProject project;
/**
* Enables printing out a summary after execution.
*/
@Parameter(property = Constants.PROPERTY_PRINT_SUMMARY, defaultValue = "true")
private boolean printSummary;
/**
* Skips the execution of this Mojo.
*/
@Parameter(property = Constants.PROPERTY_SKIP, defaultValue = "false")
private boolean skip;
/**
* The number of retries for reaching the DB Allocator Service
*/
@Parameter(property = Constants.PROPERTY_RETRY_TOTAL_RETRIES, defaultValue = "3")
private int totalRetries;
/**
* Backoff time for reaching out the DB Allocator Service.
*/
@Parameter(property = Constants.PROPERTY_RETRY_BACKOFF_SECONDS, defaultValue = "10")
private int backoffTimeSeconds;
/**
* URI to the DB Allocator Service.
*/
@Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_URI)
private String dbAllocatorURI;
/**
* UUID for releasing the allocated DB.
*/
@Parameter(property = Constants.PROPERTY_ALLOCATED_DB)
private String allocatedUUID;
@Override
public void execute() throws MojoFailureException {
if (skip) {
logger.info("Skipping");
return;
}
try {
IncrementalBackoffRetryPolicy retryPolicy = new IncrementalBackoffRetryPolicy(totalRetries, backoffTimeSeconds, TimeUnit.SECONDS);
DBAllocatorServiceClient client = new DBAllocatorServiceClient(dbAllocatorURI, retryPolicy);
ReleaseResult release = client.release(AllocationResult.forRelease(allocatedUUID));
if (printSummary) {
logger.info("Released database:");
logger.info("-- UUID: " + release.getUUID());
}
} catch (DBAllocatorException e) {
String error = e.getMessage();
if (e.getErrorResponse() != null) {
error = String.format("[%s](%s)", e.getErrorResponse().getStatus(), e.getErrorResponse().readEntity(String.class));
}
throw new MojoFailureException("An error occurred while communicating with DBAllocator (" + error + ")", e);
}
}
}

View file

@ -0,0 +1,11 @@
package org.keycloak.testsuite.dballocator.client;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException;
import javax.ws.rs.core.Response;
import java.util.concurrent.Callable;
@FunctionalInterface
public interface BackoffRetryPolicy {
Response retryTillHttpOk(Callable<Response> callableSupplier) throws DBAllocatorUnavailableException;
}

View file

@ -0,0 +1,100 @@
package org.keycloak.testsuite.dballocator.client;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine;
import org.keycloak.testsuite.dballocator.client.data.AllocationResult;
import org.keycloak.testsuite.dballocator.client.data.ReleaseResult;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException;
import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class DBAllocatorServiceClient {
private static final int TIMEOUT = 10_000;
private final Client restClient;
private final URI allocatorServletURI;
private final BackoffRetryPolicy retryPolicy;
public DBAllocatorServiceClient(String allocatorServletURI, BackoffRetryPolicy retryPolicy) {
Objects.requireNonNull(allocatorServletURI, "DB Allocator URI must not be null");
this.allocatorServletURI = URI.create(allocatorServletURI);
this.retryPolicy = retryPolicy != null ? retryPolicy : new IncrementalBackoffRetryPolicy();
this.restClient = new ResteasyClientBuilder().httpEngine(createEngine()).build();
}
private final ApacheHttpClient43Engine createEngine() {
RequestConfig reqConfig = RequestConfig.custom()
.setConnectTimeout(TIMEOUT)
.setSocketTimeout(TIMEOUT)
.setConnectionRequestTimeout(TIMEOUT)
.build();
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(reqConfig)
.setMaxConnTotal(1)
.build();
ApacheHttpClient43Engine engine = new ApacheHttpClient43Engine(httpClient);
engine.setFollowRedirects(true);
return engine;
}
public AllocationResult allocate(String user, String type, int expiration, TimeUnit expirationTimeUnit, String location) throws DBAllocatorException {
Objects.requireNonNull(user, "User can not be null");
Objects.requireNonNull(type, "DB Type must not be null");
try {
String typeWithLocation = location != null ? type + "&&" + location : type;
Invocation.Builder target = restClient
.target(allocatorServletURI)
.queryParam("operation", "allocate")
.queryParam("requestee", user)
.queryParam("expression", typeWithLocation)
.queryParam("expiry", expirationTimeUnit.toMinutes(expiration))
.request();
Response response = retryPolicy.retryTillHttpOk(() -> target.get());
Properties properties = new Properties();
String content = response.readEntity(String.class);
if (content != null) {
try(InputStream is = new ByteArrayInputStream(content.getBytes())) {
properties.load(is);
}
}
return AllocationResult.successful(properties);
} catch (IOException e) {
throw new DBAllocatorException(e);
}
}
public ReleaseResult release(AllocationResult allocationResult) throws DBAllocatorException {
Objects.requireNonNull(allocationResult, "Previous allocation result must not be null");
Objects.requireNonNull(allocationResult.getUUID(), "UUID must not be null");
Invocation.Builder target = restClient
.target(allocatorServletURI)
.queryParam("operation", "dealloc")
.queryParam("uuid", allocationResult.getUUID())
.request();
retryPolicy.retryTillHttpOk(() -> target.get());
return ReleaseResult.successful(allocationResult.getUUID());
}
}

View file

@ -0,0 +1,81 @@
package org.keycloak.testsuite.dballocator.client.data;
import java.util.Properties;
public class AllocationResult {
private final String uuid;
private final String driver;
private final String database;
private final String user;
private final String password;
private final String url;
private AllocationResult(String uuid) {
this.uuid = uuid;
this.driver = null;
this.database = null;
this.user = null;
this.password = null;
this.url = null;
}
private AllocationResult(String uuid, String driver, String database, String user, String password, String url) {
this.uuid = uuid;
this.driver = driver;
this.database = database;
this.user = user;
this.password = password;
this.url = url;
}
public static AllocationResult forRelease(String uuid) {
return new AllocationResult(uuid);
}
public static AllocationResult successful(Properties properties) {
return new AllocationResult(
properties.getProperty("uuid"),
properties.getProperty("db.jdbc_class"),
properties.getProperty("db.name"),
properties.getProperty("db.username"),
properties.getProperty("db.password"),
properties.getProperty("db.jdbc_url"));
}
public String getDriver() {
return driver;
}
public String getDatabase() {
return database;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
public String getURL() {
return url;
}
public String getUUID() {
return uuid;
}
@Override
public String toString() {
return "AllocationResult{" +
"uuid='" + uuid + '\'' +
", driver='" + driver + '\'' +
", database='" + database + '\'' +
", user='" + user + '\'' +
", password='" + password + '\'' +
", url='" + url + '\'' +
'}';
}
}

View file

@ -0,0 +1,25 @@
package org.keycloak.testsuite.dballocator.client.data;
public class ReleaseResult {
private final String uuid;
private ReleaseResult(String uuid) {
this.uuid = uuid;
}
public static ReleaseResult successful(String uuid) {
return new ReleaseResult(uuid);
}
public String getUUID() {
return uuid;
}
@Override
public String toString() {
return "ReleaseResult{" +
"uuid='" + uuid + '\'' +
'}';
}
}

View file

@ -0,0 +1,25 @@
package org.keycloak.testsuite.dballocator.client.exceptions;
import javax.ws.rs.core.Response;
public class DBAllocatorException extends Exception {
private Response errorResponse;
public DBAllocatorException(Response errorResponse) {
this.errorResponse = errorResponse;
}
public DBAllocatorException(Response errorResponse, Throwable throwable) {
super(throwable);
this.errorResponse = errorResponse;
}
public DBAllocatorException(Throwable throwable) {
super(throwable);
}
public Response getErrorResponse() {
return errorResponse;
}
}

View file

@ -0,0 +1,11 @@
package org.keycloak.testsuite.dballocator.client.exceptions;
import javax.ws.rs.core.Response;
public class DBAllocatorUnavailableException extends DBAllocatorException {
public DBAllocatorUnavailableException(Response errorResponse) {
super(errorResponse);
}
}

View file

@ -0,0 +1,57 @@
package org.keycloak.testsuite.dballocator.client.retry;
import org.keycloak.testsuite.dballocator.client.BackoffRetryPolicy;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException;
import javax.ws.rs.core.Response;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class IncrementalBackoffRetryPolicy implements BackoffRetryPolicy {
public static final int DEFAULT_TOTAL_RETRIES = 3;
public static final int DEFAULT_BACKOFF_TIME = 10;
public static final TimeUnit DEFAULT_BACKOFF_TIME_UNIT = TimeUnit.SECONDS;
private final int totalRetries;
private final int backoffTime;
private final TimeUnit backoffTimeUnit;
public IncrementalBackoffRetryPolicy() {
this(DEFAULT_TOTAL_RETRIES, DEFAULT_BACKOFF_TIME, DEFAULT_BACKOFF_TIME_UNIT);
}
public IncrementalBackoffRetryPolicy(int totalRetries, int backoffTime, TimeUnit backoffTimeUnit) {
this.backoffTime = backoffTime;
this.backoffTimeUnit = backoffTimeUnit;
this.totalRetries = totalRetries;
}
@Override
public Response retryTillHttpOk(Callable<Response> callableSupplier) throws DBAllocatorUnavailableException {
return retryTillHttpOk(callableSupplier, totalRetries, backoffTime, backoffTimeUnit);
}
public Response retryTillHttpOk(Callable<Response> callableSupplier, int totalRetries, int backoffTime, TimeUnit backoffTimeUnit) throws DBAllocatorUnavailableException {
int retryCount = 0;
Response response = null;
while(true) {
try {
response = callableSupplier.call();
} catch (Exception e) {
response = null;
}
if (response != null && response.getStatus() == 200) {
return response;
}
if (++retryCount > totalRetries) {
throw new DBAllocatorUnavailableException(response);
}
LockSupport.parkNanos(backoffTimeUnit.toNanos(backoffTime * retryCount));
}
}
}

View file

@ -0,0 +1,67 @@
package org.keycloak.testsuite.dballocator.client;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.testsuite.dballocator.client.data.AllocationResult;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException;
import org.keycloak.testsuite.dballocator.client.mock.MockResponse;
import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
public class DBAllocatorServiceClientTest {
@Test
public void testSuccessfulAllocation() throws Exception {
//given
String mockURI = "http://localhost:8080/test";
String testProperties = null;
try(InputStream is = DBAllocatorServiceClientTest.class.getResourceAsStream("/db-allocator-response.properties")) {
testProperties = IOUtils.toString(is, Charset.defaultCharset());
}
Response successfulResponse = new MockResponse(200, testProperties);
BackoffRetryPolicy retryPolicyMock = callableSupplier -> successfulResponse;
DBAllocatorServiceClient client = new DBAllocatorServiceClient(mockURI, retryPolicyMock);
//when
AllocationResult allocationResult = client.allocate("user", "mariadb_galera_101", 1440, TimeUnit.SECONDS, "geo_RDU");
//then
Assert.assertEquals("d328bb0e-3dcc-42da-8ce1-83738a8dfede", allocationResult.getUUID());
Assert.assertEquals("org.mariadb.jdbc.Driver", allocationResult.getDriver());
Assert.assertEquals("dbname", allocationResult.getDatabase());
Assert.assertEquals("username", allocationResult.getUser());
Assert.assertEquals("password", allocationResult.getPassword());
Assert.assertEquals("jdbc:mariadb://mariadb-101-galera.keycloak.org:3306", allocationResult.getURL());
}
@Test
public void testFailureAllocation() throws Exception {
//given
String mockURI = "http://localhost:8080/test";
Response serverErrorResponse = new MockResponse(500, null);
BackoffRetryPolicy retryPolicyMock = callableSupplier -> {
throw new DBAllocatorUnavailableException(serverErrorResponse);
};
DBAllocatorServiceClient client = new DBAllocatorServiceClient(mockURI, retryPolicyMock);
//when
try {
client.allocate("user", "mariadb_galera_101", 1440, TimeUnit.SECONDS, "geo_RDU");
Assert.fail();
} catch (DBAllocatorException e) {
Assert.assertEquals(500, e.getErrorResponse().getStatus());
}
}
}

View file

@ -0,0 +1,159 @@
package org.keycloak.testsuite.dballocator.client.mock;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class MockResponse extends Response {
private final int status;
private final String entity;
public MockResponse(int status, String entity) {
this.status = status;
this.entity = entity;
}
@Override
public int getStatus() {
return status;
}
@Override
public StatusType getStatusInfo() {
throw new UnsupportedOperationException();
}
@Override
public Object getEntity() {
return entity;
}
@Override
public <T> T readEntity(Class<T> aClass) {
if (aClass.isAssignableFrom(String.class)) {
return (T) entity;
}
throw new UnsupportedOperationException();
}
@Override
public <T> T readEntity(GenericType<T> genericType) {
throw new UnsupportedOperationException();
}
@Override
public <T> T readEntity(Class<T> aClass, Annotation[] annotations) {
throw new UnsupportedOperationException();
}
@Override
public <T> T readEntity(GenericType<T> genericType, Annotation[] annotations) {
throw new UnsupportedOperationException();
}
@Override
public boolean hasEntity() {
throw new UnsupportedOperationException();
}
@Override
public boolean bufferEntity() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
throw new UnsupportedOperationException();
}
@Override
public MediaType getMediaType() {
throw new UnsupportedOperationException();
}
@Override
public Locale getLanguage() {
throw new UnsupportedOperationException();
}
@Override
public int getLength() {
throw new UnsupportedOperationException();
}
@Override
public Set<String> getAllowedMethods() {
throw new UnsupportedOperationException();
}
@Override
public Map<String, NewCookie> getCookies() {
throw new UnsupportedOperationException();
}
@Override
public EntityTag getEntityTag() {
throw new UnsupportedOperationException();
}
@Override
public Date getDate() {
throw new UnsupportedOperationException();
}
@Override
public Date getLastModified() {
throw new UnsupportedOperationException();
}
@Override
public URI getLocation() {
throw new UnsupportedOperationException();
}
@Override
public Set<Link> getLinks() {
throw new UnsupportedOperationException();
}
@Override
public boolean hasLink(String s) {
throw new UnsupportedOperationException();
}
@Override
public Link getLink(String s) {
throw new UnsupportedOperationException();
}
@Override
public Link.Builder getLinkBuilder(String s) {
throw new UnsupportedOperationException();
}
@Override
public MultivaluedMap<String, Object> getMetadata() {
throw new UnsupportedOperationException();
}
@Override
public MultivaluedMap<String, String> getStringHeaders() {
throw new UnsupportedOperationException();
}
@Override
public String getHeaderString(String s) {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,68 @@
package org.keycloak.testsuite.dballocator.client.retry;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException;
import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy;
import javax.ws.rs.core.Response;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
public class IncrementalBackoffRetryPolicyTest {
static class BackoffCounter implements Callable<Response> {
LongAdder adder = new LongAdder();
Response responseToReport;
public BackoffCounter(Response responseToReport) {
this.responseToReport = responseToReport;
}
@Override
public Response call() throws Exception {
adder.add(1);
return responseToReport;
}
public Long getCounter() {
return adder.longValue();
}
}
@Test
public void testBackoffLoop() {
//given
long expectedNumberOfRetries = 2;
long expectedNumberOfInvocations = expectedNumberOfRetries + 1;
BackoffCounter counter = new BackoffCounter(Response.serverError().build());
IncrementalBackoffRetryPolicy backoffRetryPolicy = new IncrementalBackoffRetryPolicy((int) expectedNumberOfRetries, 0, TimeUnit.NANOSECONDS);
//when
try {
backoffRetryPolicy.retryTillHttpOk(counter);
Assert.fail();
} catch (DBAllocatorUnavailableException e) {
//then
Assert.assertEquals(expectedNumberOfInvocations, counter.getCounter().longValue());
}
}
@Test
public void testIgnoringBackoffWhenGettingSuccessfulResponse() throws Exception {
//given
BackoffCounter counter = new BackoffCounter(Response.ok().build());
IncrementalBackoffRetryPolicy backoffRetryPolicy = new IncrementalBackoffRetryPolicy(3, 0, TimeUnit.NANOSECONDS);
//when
Response response = backoffRetryPolicy.retryTillHttpOk(counter);
//then
Assert.assertEquals(1, counter.getCounter().longValue());
Assert.assertEquals(200, response.getStatus());
}
}

View file

@ -0,0 +1,25 @@
#Generated by DBAllocator
#Mon Mar 18 12:49:24 UTC 2019
db.password=password
hibernate.connection.password=password
hibernate41.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
broken=false
db.username=username
server_geo=RDU
dballoc.db_type=clustered
db.name=dbname
db.jdbc_url=jdbc\:mariadb\://mariadb-101-galera.keycloak.org\:3306
datasource.class.xa=org.mariadb.jdbc.MySQLDataSource
server_uid=RDU_mariadb_galera_101
hibernate33.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
hibernate.connection.username=username
server_labels=mariadb_galera_101
db.jdbc_class=org.mariadb.jdbc.Driver
db.schema=dballo01
hibernate.connection.driver_class=org.mariadb.jdbc.Driver
uuid=d328bb0e-3dcc-42da-8ce1-83738a8dfede
db.primary_label=mariadb_galera_101
server_label_primary=mariadb_galera_101
hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
hibernate.connection.url=jdbc\:mariadb\://mariadb-101-galera-01.keycloak.org\:3306
hibernate.connection.schema=dballo01

View file

@ -0,0 +1,5 @@
log4j.rootLogger=info, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n

View file

@ -717,6 +717,7 @@ The exact steps to configure Docker depend on the operating system.
By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand. By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand.
The exact command line arguments depend on the operating system. The exact command line arguments depend on the operating system.
### General guidelines ### General guidelines
If docker daemon doesn't run locally, or if you're not running on Linux, you may need If docker daemon doesn't run locally, or if you're not running on Linux, you may need

View file

@ -94,6 +94,8 @@
<keycloak.connectionsJpa.password></keycloak.connectionsJpa.password> <keycloak.connectionsJpa.password></keycloak.connectionsJpa.password>
<keycloak.connectionsJpa.url>jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url> <keycloak.connectionsJpa.url>jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url>
<keycloak.connectionsJpa.schema>DEFAULT</keycloak.connectionsJpa.schema> <keycloak.connectionsJpa.schema>DEFAULT</keycloak.connectionsJpa.schema>
<dballocator.skip>true</dballocator.skip>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@ -244,6 +246,18 @@
<build> <build>
<pluginManagement> <pluginManagement>
<plugins> <plugins>
<plugin>
<groupId>org.keycloak</groupId>
<artifactId>db-allocator-plugin</artifactId>
<version>${project.version}</version>
<configuration>
<propertyDatabase>keycloak.connectionsJpa.database</propertyDatabase>
<propertyDriver>keycloak.connectionsJpa.driver</propertyDriver>
<propertyURL>keycloak.connectionsJpa.url</propertyURL>
<propertyUser>keycloak.connectionsJpa.user</propertyUser>
<propertyPassword>keycloak.connectionsJpa.password</propertyPassword>
</configuration>
</plugin>
<plugin> <plugin>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version> <version>2.19.1</version>
@ -272,6 +286,27 @@
</plugin> </plugin>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
<plugins>
<plugin>
<groupId>org.keycloak</groupId>
<artifactId>db-allocator-plugin</artifactId>
<inherited>false</inherited>
<executions>
<execution>
<id>allocate-db</id>
<goals>
<goal>allocate</goal>
</goals>
</execution>
<execution>
<id>release-db</id>
<goals>
<goal>release</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build> </build>
<modules> <modules>
@ -379,6 +414,16 @@
<docker.database.wait-for-log-regex>(?si)Ready for start up.*ready [^\n]{0,30}connections</docker.database.wait-for-log-regex> <docker.database.wait-for-log-regex>(?si)Ready for start up.*ready [^\n]{0,30}connections</docker.database.wait-for-log-regex>
</properties> </properties>
</profile> </profile>
<profile>
<id>db-allocator-db-mysql</id>
<properties>
<jdbc.mvn.groupId>mysql</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>mysql-connector-java</jdbc.mvn.artifactId>
<jdbc.mvn.version>${mysql.version}</jdbc.mvn.version>
<dballocator.type>mysql57</dballocator.type>
<dballocator.skip>false</dballocator.skip>
</properties>
</profile>
<profile> <profile>
<id>db-postgres</id> <id>db-postgres</id>
<properties> <properties>
@ -396,6 +441,16 @@
<docker.database.wait-for-log-regex>(?si)Ready for start up.*ready [^\n]{0,30}connections</docker.database.wait-for-log-regex> <docker.database.wait-for-log-regex>(?si)Ready for start up.*ready [^\n]{0,30}connections</docker.database.wait-for-log-regex>
</properties> </properties>
</profile> </profile>
<profile>
<id>db-allocator-db-postgres</id>
<properties>
<jdbc.mvn.groupId>org.postgresql</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>postgresql</jdbc.mvn.artifactId>
<jdbc.mvn.version>${postgresql.version}</jdbc.mvn.version>
<dballocator.type>postgresql96</dballocator.type>
<dballocator.skip>false</dballocator.skip>
</properties>
</profile>
<profile> <profile>
<id>db-mariadb</id> <id>db-mariadb</id>
<properties> <properties>
@ -413,6 +468,16 @@
<docker.database.wait-for-log-regex>(?si)Ready for start up.*ready [^\n]{0,30}connections</docker.database.wait-for-log-regex> <docker.database.wait-for-log-regex>(?si)Ready for start up.*ready [^\n]{0,30}connections</docker.database.wait-for-log-regex>
</properties> </properties>
</profile> </profile>
<profile>
<id>db-allocator-db-mariadb</id>
<properties>
<jdbc.mvn.groupId>org.mariadb.jdbc</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>mariadb-java-client</jdbc.mvn.artifactId>
<jdbc.mvn.version>${mariadb.version}</jdbc.mvn.version>
<dballocator.type>mariadb_galera_101</dballocator.type>
<dballocator.skip>false</dballocator.skip>
</properties>
</profile>
<profile> <profile>
<id>db-mssql2017</id> <id>db-mssql2017</id>
<properties> <properties>
@ -431,6 +496,16 @@
<jdbc.mvn.version>${mssql.version}</jdbc.mvn.version> <jdbc.mvn.version>${mssql.version}</jdbc.mvn.version>
</properties> </properties>
</profile> </profile>
<profile>
<id>db-allocator-db-mssql2016</id>
<properties>
<jdbc.mvn.groupId>com.microsoft.sqlserver</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>mssql-jdbc</jdbc.mvn.artifactId>
<jdbc.mvn.version>${mssql.version}</jdbc.mvn.version>
<dballocator.type>mssql2016</dballocator.type>
<dballocator.skip>false</dballocator.skip>
</properties>
</profile>
<profile> <profile>
<id>db-oracle11g</id> <id>db-oracle11g</id>
<properties> <properties>
@ -450,6 +525,16 @@
<jdbc.mvn.version>12.1.0</jdbc.mvn.version> <jdbc.mvn.version>12.1.0</jdbc.mvn.version>
</properties> </properties>
</profile> </profile>
<profile>
<id>db-allocator-db-oracle11g</id>
<properties>
<jdbc.mvn.groupId>com.oracle</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>ojdbc7</jdbc.mvn.artifactId>
<jdbc.mvn.version>12.1.0</jdbc.mvn.version>
<dballocator.type>oracle11gR1</dballocator.type>
<dballocator.skip>false</dballocator.skip>
</properties>
</profile>
</profiles> </profiles>
</project> </project>

View file

@ -103,6 +103,9 @@
</target> </target>
<target name="configure-server-jpa" depends="update-jpa-schema"> <target name="configure-server-jpa" depends="update-jpa-schema">
<!-- I'm intentionally leaving this here. This shows up environment variables that are used during the build.
If anything goes wrong, this is the first place to look at -->
<echoproperties/>
<copy todir="${cli.tmp.dir}"> <copy todir="${cli.tmp.dir}">
<resources> <resources>
<file file="${common.resources}/jboss-cli/configure-server-jpa.cli"/> <file file="${common.resources}/jboss-cli/configure-server-jpa.cli"/>

View file

@ -63,6 +63,10 @@
<!-- default ant scenario --> <!-- default ant scenario -->
<ant.scenario>scenario-standalone</ant.scenario> <ant.scenario>scenario-standalone</ant.scenario>
<session.cache.owners>1</session.cache.owners>
<offline.session.cache.owners>1</offline.session.cache.owners>
<login.failure.cache.owners>1</login.failure.cache.owners>
</properties> </properties>
<profiles> <profiles>
@ -216,16 +220,16 @@
</goals> </goals>
<configuration> <configuration>
<target> <target>
<ant antfile="${common.resources}/ant/configure.xml" target="${ant.scenario}-generate"> <ant antfile="${common.resources}/ant/configure.xml" target="${ant.scenario}-generate" >
<!-- These properties become equivalent to properties defined on the command line. --> <!-- In most of the cases, Ant Plugin picks up properties automatically.
<!-- Without specifying those the default values would be used regardless what is --> However, in some rare cases, it will not detect if a property has been overriden
<!-- defined via -Dproperty=value when executing maven command --> in the command line using "-D" switch (see why here: https://technotes.khitrenovich.com/properties-resolution-maven-implications-antrun-plugin/
<property name="auth.server.worker.io-threads">${auth.server.worker.io-threads}</property> There's also another case, when we have a dynamic property (like "keycloak.connectionsJpa.url")
<property name="auth.server.worker.task-max-threads">${auth.server.worker.task-max-threads}</property> that can change in the runtime. In such cases, we CAN NOT put is as a property (or
<!-- Following properties are cluster specific --> Ant will see outdated values, not the dynamic ones). -->
<property name="session.cache.owners">${session.cache.owners}</property> <property name="session.cache.owners" value="${session.cache.owners}" />
<property name="offline.session.cache.owners">${offline.session.cache.owners}</property> <property name="offline.session.cache.owners" value="${offline.session.cache.owners}" />
<property name="login.failure.cache.owners">${login.failure.cache.owners}</property> <property name="login.failure.cache.owners" value="${login.failure.cache.owners}" />
</ant> </ant>
</target> </target>
</configuration> </configuration>
@ -415,18 +419,7 @@
</goals> </goals>
<configuration> <configuration>
<target> <target>
<ant antfile="${common.resources}/ant/configure.xml" target="configure-server-jpa"> <ant antfile="${common.resources}/ant/configure.xml" target="configure-server-jpa" />
<!-- These properties become equivalent to properties defined on the command line. -->
<!-- Without specifying those the default values would be used regardless what is -->
<!-- defined via -Dproperty=value when executing maven command -->
<property name="jdbc.driver.tmp.dir">${jdbc.driver.tmp.dir}</property>
<property name="jdbc.mvn.artifactId">${jdbc.mvn.artifactId}</property>
<property name="jdbc.mvn.version">${jdbc.mvn.version}</property>
<property name="keycloak.connectionsJpa.url">${keycloak.connectionsJpa.url}</property>
<property name="keycloak.connectionsJpa.user">${keycloak.connectionsJpa.user}</property>
<property name="keycloak.connectionsJpa.password">${keycloak.connectionsJpa.password}</property>
<property name="keycloak.connectionsJpa.schema">${keycloak.connectionsJpa.schema}</property>
</ant>
</target> </target>
</configuration> </configuration>
</execution> </execution>
@ -574,11 +567,6 @@
<id>auth-server-cluster</id> <id>auth-server-cluster</id>
<properties> <properties>
<ant.scenario>scenario-cluster</ant.scenario> <ant.scenario>scenario-cluster</ant.scenario>
<session.cache.owners>1</session.cache.owners>
<offline.session.cache.owners>1</offline.session.cache.owners>
<login.failure.cache.owners>1</login.failure.cache.owners>
<load.metric>simple</load.metric> <load.metric>simple</load.metric>
<!-- The default value 'simple' configures mod-cluster with simple-load-provider. <!-- The default value 'simple' configures mod-cluster with simple-load-provider.
Any other value configures it with dynamic-load-provider using the particular `load.metric`. Any other value configures it with dynamic-load-provider using the particular `load.metric`.

View file

@ -43,6 +43,7 @@
</plugins> </plugins>
</build> </build>
<modules> <modules>
<module>db-allocator-plugin</module>
<module>integration-arquillian</module> <module>integration-arquillian</module>
<module>utils</module> <module>utils</module>
</modules> </modules>