8000 Evaluate YugabyteDB as a PostgreSQL database in tests by ahus1 · Pull Request #54731 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/actions/run-store-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ inputs:
db:
description: The database to use for tests
required: true
server-start-timeout:
description: Seconds to wait for the Keycloak server to start, which includes creating the schema
default: "360"

runs:
using: composite
Expand All @@ -16,12 +19,12 @@ runs:
- name: Run new base tests
shell: bash
run: |
KC_TEST_DATABASE=${{ inputs.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=DatabaseTestSuite -Dkc.test.server.start.timeout=360
KC_TEST_DATABASE=${{ inputs.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=DatabaseTestSuite -Dkc.test.server.start.timeout=${{ inputs.server-start-timeout }}

- name: Run stateless tests
shell: bash
run: |
KC_TEST_DATABASE=${{ inputs.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=StatelessTestSuite -Dkc.test.server.start.timeout=360
KC_TEST_DATABASE=${{ inputs.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=StatelessTestSuite -Dkc.test.server.start.timeout=${{ inputs.server-start-timeout }}

- uses: ./.github/actions/upload-flaky-tests
name: Upload flaky tests
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
ci-aurora: ${{ steps.auroradb-tests.outputs.run-aurora-tests }}
ci-azure: ${{ steps.azure-tests.outputs.run-azure-tests }}
ci-additional-dbs: ${{ steps.additional-dbs-tests.outputs.run-additional-dbs-tests }}
ci-yugabyte: ${{ steps.yugabyte-tests.outputs.run-yugabyte-tests }}
ci-admin-v2: ${{ steps.conditional.outputs.admin-v2 }}
ci-jackson3: ${{ steps.conditional.outputs.ci-jackson3 }}
permissions:
Expand Down Expand Up @@ -98,6 +99,24 @@ jobs:
env:
PRIVATE_DBS_QUAY_USERNAME: ${{ secrets.PRIVATE_DBS_QUAY_USERNAME }}
PRIVATE_DBS_QUAY_TOKEN: ${{ secrets.PRIVATE_DBS_QUAY_TOKEN }}
- name: YugabyteDB conditional check
id: yugabyte-tests
# Runs for manually or nightly triggered runs, and for pushes/PRs when any commit message in the PR/push contains "[yugabyte ci]"
run: |
RUN_YUGABYTE_TESTS=false
if [[ $GITHUB_EVENT_NAME == "pull_request" ]]; then
COMMIT_MESSAGE="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/commits" --jq '.[].commit.message')"
else
COMMIT_MESSAGE="${PUSH_COMMIT_MESSAGES}"
fi
if [[ $GITHUB_EVENT_NAME == "workflow_dispatch" || $GITHUB_EVENT_NAME == "schedule" || "${COMMIT_MESSAGE}" == *"[yugabyte ci]"* ]]; then
RUN_YUGABYTE_TESTS=true
fi
echo "run-yugabyte-tests=$RUN_YUGABYTE_TESTS" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PUSH_COMMIT_MESSAGES: ${{ join(github.event.commits.*.message, '\n') }}
testsuite-deprecation-check:
name: Testsuite Deprecation Check
runs-on: ubuntu-latest
Expand Down Expand Up @@ -877,6 +896,23 @@ jobs:
with:
db: ${{ matrix.db }}

store-integration-tests-yugabyte:
name: Store IT (YugabyteDB)
needs: [build, conditional]
runs-on: ubuntu-latest
timeout-minutes: 85
if: needs.conditional.outputs.ci-yugabyte == 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- id: run-store-tests
name: Run Store Integration Tests - yugabyte
uses: ./.github/actions/run-store-tests
with:
db: yugabyte
# creating the schema takes several minutes
server-start-timeout: 900

store-model-tests:
name: Store Model Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1362,6 +1398,7 @@ jobs:
- jdk-integration-tests
- store-integration-tests
- store-integration-tests-additional
- store-integration-tests-yugabyte
- volatile-sessions-tests
- http-client-v2-tests
- store-model-tests
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@
<!-- Databases - also published to db.adoc as "Tested Version" -->
<tidb.version>v8.5.2</tidb.version>
<tidb.container>mirror.gcr.io/pingcap/tidb:${tidb.version}</tidb.container>
<yugabyte.version>2025.2.8.1-b8</yugabyte.version>
<yugabyte.container>mirror.gcr.io/yugabytedb/yugabyte:${yugabyte.version}</yugabyte.container>
<mysql.version>8.4</mysql.version>
<mysql.container>mirror.gcr.io/mysql:${mysql.version}</mysql.container>
<mysql-jdbc.version>9.7.0</mysql-jdbc.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ private void createIndexes(Connection connection, List<IndexInfo> indexes) throw
logger.infov("Dropping invalid index {0} before recreating", qualifiedIndex);
try (var stmt = connection.createStatement()) {
stmt.execute("DROP INDEX CONCURRENTLY IF EXISTS " + qualifiedIndex);
} catch (SQLException e) {
// PostgreSQL-compatible databases like YugabyteDB don't support DROP INDEX CONCURRENTLY.
logger.debugv(e, "DROP INDEX CONCURRENTLY failed for {0}, retrying without CONCURRENTLY", qualifiedIndex);
try (var stmt = connection.createStatement()) {
stmt.execute("DROP INDEX IF EXISTS " + qualifiedIndex);
}
}
}
String sql = addOnlineSyntax(info.sql, isPostgres, isOracle, isMssql);
Expand Down
6 changes: 6 additions & 0 deletions test-framework/bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId& 8000 gt;
<artifactId>keycloak-test-framework-db-yugabyte</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-oracle</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mariadb.container=${mariadb.container}
mssql.container=${mssql.container}
oracle.container=${oracledb.container}
tidb.container=${tidb.container}
yugabyte.container=${yugabyte.container}

## Infinispan container ##
infinispan.container=${infinispan.container}
Expand Down
45 changes: 45 additions & 0 deletions test-framework/db-yugabyte/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0"?>
<!--
~ 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.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-test-framework-parent</artifactId>
<groupId>org.keycloak.testframework</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>keycloak-test-framework-db-yugabyte</artifactId>
<name>Keycloak Test Framework - YugabyteDB support</name>
<packaging>jar</packaging>
<description>YugabyteDB support for Keycloak Test Framework</description>

<dependencies>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-test-containers</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-yugabytedb</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.keycloak.testframework.database;

public class YugabyteDatabaseSupplier extends AbstractContainerDatabaseSupplier {

@Override
public String getAlias() {
return YugabyteTestDatabase.NAME;
}

@Override
TestDatabase getTestDatabase() {
return new YugabyteTestDatabase();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.keycloak.testframework.database;

import java.util.List;

import org.keycloak.testframework.util.ContainerImages;

import org.jboss.logging.Logger;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.YugabyteDBYSQLContainer;
import org.testcontainers.utility.DockerImageName;

class YugabyteTestDatabase extends AbstractContainerTestDatabase {

private static final Logger LOGGER = Logger.getLogger(YugabyteTestDatabase.class);

public static final String NAME = "yugabyte";

// Database created by the image, used to connect before the colocated database exists
private static final String DEFAULT_DATABASE = "yugabyte";

@Override
public JdbcDatabaseContainer<?> createContainer() {
return new YugabyteDBYSQLContainer(DockerImageName.parse(ContainerImages.getContainerImageName(NAME)).asCompatibleSubstituteFor("yugabytedb/yugabyte")) {
// Keycloak and the test framework connect with the PostgreSQL driver, as YugabyteDB is tested as a plain PostgreSQL database
@Override
public String getDriverClassName() {
return "org.postgresql.Driver";
}

@Override
public String getJdbcUrl() {
return "jdbc:postgresql://" + getHost() + ":" + getMappedPort(5433) + "/" + getDatabaseName() + constructUrlParameters("?", "&");
}
};
}

@Override
public void withDatabaseAndUser(String database, String username, String password) {
// The requested database is created colocated in the post start command, as the image would create it non-colocated
container.withDatabaseName(DEFAULT_DATABASE);
container.withUsername(username);
container.withPassword(password);
}

@Override
public List<String> getPostStartCommand() {
// Colocation stores all tables in a single tablet, which is faster and lighter for the many small tables of Keycloak.
// The command runs on every start, including re-used containers, so it only creates the database if it doesn't exist yet.
String ysqlsh = "PGPASSWORD='" + getPassword() + "' ysqlsh -h \"$(hostname)\" -U " + getUsername() + " -d " + DEFAULT_DATABASE;
return List.of("bash", "-c",
ysqlsh + " -tAc \"SELECT 1 FROM pg_database WHERE datname = '" + getDatabase() + "'\" | grep -q 1"
+ " || " + ysqlsh + " -c 'CREATE DATABASE \"" + getDatabase() + "\" WITH COLOCATION = true'");
}

@Override
public String getJdbcUrl(boolean internal) {
return super.getJdbcUrl(internal).replaceFirst("/" + DEFAULT_DATABASE + "(\\?|$)", "/" + getDatabase() + "$1");
}

@Override
public String getDatabaseVendor() {
// YugabyteDB is tested as a plain PostgreSQL database
return "postgres";
}

@Override
public Logger getLogger() {
return LOGGER;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.keycloak.testframework.database;

import java.util.List;

import org.keycloak.testframework.TestFrameworkExtension;
import org.keycloak.testframework.injection.Supplier;

public class YugabyteTestFrameworkExtension implements TestFrameworkExtension {

@Override
public List<Supplier<?, ?>> suppliers() {
return List.of(new YugabyteDatabaseSupplier());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.keycloak.testframework.database.YugabyteTestFrameworkExtension
1 change: 1 addition & 0 deletions test-framework/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
<module>db-oracle</module>
<module>db-postgres</module>
<module>db-tidb</module>
<module>db-yugabyte</module>
<module>email-server</module>
<module>test-providers</module>
<module>tests</module>
Expand Down
4 changes: 4 additions & 0 deletions test-framework/tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-tidb</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-yugabyte</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-oracle</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions tests/base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-tidb</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-yugabyte</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.jdbc</groupId>
<artifactId>aws-advanced-jdbc-wrapper</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,18 +227,19 @@ private static void createInvalidIndex(JpaConnectionProviderFactory factory, Str
try (Connection connection = factory.getConnection()) {
connection.setAutoCommit(true);
try (Statement stmt = connection.createStatement()) {
stmt.executeUpdate(String.format("INSERT INTO %s (%s) VALUES (%s)", qualifiedTable, columns, String.format(values, "__test_invalid_idx_1")));
stmt.executeUpdate(String.format("INSERT INTO %s (%s) VALUES (%s)", qualifiedTable, columns, String.format(values, "__test_invalid_idx_2")));
executeUpdateWithRetry(stmt, String.format("INSERT INTO %s (%s) VALUES (%s)", qualifiedTable, columns, String.format(values, "__test_invalid_idx_1")));
executeUpdateWithRetry(stmt, String.format("INSERT INTO %s (%s) VALUES (%s)", qualifiedTable, columns, String.format(values, "__test_invalid_idx_2")));

try {
stmt.executeUpdate(String.format("CREATE UNIQUE INDEX CONCURRENTLY %s ON %s (realm_id)", qualifiedIndex, qualifiedTable));
executeUpdateWithRetry(stmt, String.format("CREATE UNIQUE INDEX CONCURRENTLY %s ON %s (realm_id)", qualifiedIndex, qualifiedTable));
} catch (SQLException e) {
if (!"23505".equals(e.getSQLState())) {
// YugabyteDB wraps the error and doesn't report the SQL state 23505
if (!"23505".equals(e.getSQLState()) && !e.getMessage().contains("duplicate key value violates unique constraint")) {
throw e;
}
}

stmt.executeUpdate(String.format("DELETE FROM %s WHERE user_session_id IN ('__test_invalid_idx_1', '__test_invalid_idx_2')", qualifiedTable));
executeUpdateWithRetry(stmt, String.format("DELETE FROM %s WHERE user_session_id IN ('__test_invalid_idx_1', '__test_invalid_idx_2')", qualifiedTable));
}
}
} catch (SQLException e) {
Expand All @@ -259,6 +260,26 @@ private static void createInvalidIndex(JpaConnectionProviderFactory factory, Str
}
}

// YugabyteDB fails the first statements after a DDL change with a stale catalog version, a retry succeeds
private static void executeUpdateWithRetry(Statement stmt, String sql) throws SQLException {
for (int attempt = 1; ; attempt++) {
try {
stmt.executeUpdate(sql);
return;
} catch (SQLException e) {
if (attempt >= 10 || e.getMessage() == null || !e.getMessage().contains("schema version mismatch")) {
throw e;
}
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw e;
}
}
}
}

private static void renameIndex(JpaConnectionProviderFactory factory, String fromName, String toName) {
try (Connection connection = factory.getConnection();
Statement stmt = connection.createStatement()) {
Expand Down
Loading
0