mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-06 06:49:53 -06:00
Added section on recommended isolation level to db guides
Closes #44611 Signed-off-by: Sebastian Schuster <sebastian.schuster@bosch.com> Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
committed by
GitHub
parent
52bf0face3
commit
b5178a2bec
@@ -54,6 +54,18 @@ The SPIFFE Identity Provider preview feature now uses the `trustDomain` configur
|
||||
was done to make it more explicit that a `trustDomain` is configured and not the `iss` of the token, which is
|
||||
usually not set in SPIFFE JWT SVIDs.
|
||||
|
||||
=== Running on Microsoft SQL Server now recommends READ_COMMITTED_SNAPSHOT
|
||||
|
||||
On MS SQL Server, the default transaction isolation level is `READ_COMMITTED`, which can lead to deadlocks during high load. Therefore, the recommended isolation level for {project_name} is `READ_COMMITTED_SNAPSHOT`.
|
||||
|
||||
Use the following statement to changes this for your database:
|
||||
|
||||
[source,sql]
|
||||
----
|
||||
ALTER DATABASE [your-database-name] SET READ_COMMITTED_SNAPSHOT ON;
|
||||
----
|
||||
|
||||
|
||||
=== `session_state` and `sid` are no longer UUIDs
|
||||
|
||||
In OpenID connect, there are several places where the protocol shares a `session_state` and a `sid`.
|
||||
|
||||
@@ -361,6 +361,17 @@ Beginning with MySQL 8.0.30, MySQL supports generated invisible primary keys for
|
||||
If this feature is enabled, the database schema initialization and also migrations will fail with the error message `Multiple primary key defined (1068)`.
|
||||
You then need to disable it by setting the parameter `sql_generate_invisible_primary_key` to `OFF` in your MySQL server configuration before installing or upgrading {project_name}.
|
||||
|
||||
== Preparing for MS SQL server
|
||||
|
||||
On MS SQL Server, the default transaction isolation level is `READ_COMMITTED`, which can lead to deadlocks during high load. Therefore, the recommended isolation level for {project_name} is `READ_COMMITTED_SNAPSHOT`. This isolation level is used by default on Azure SQL.
|
||||
|
||||
However, on MS SQL Server, the database isolation level needs to be modified by executing the following command on your database:
|
||||
|
||||
[source,sql]
|
||||
----
|
||||
ALTER DATABASE <your-database-name> SET READ_COMMITTED_SNAPSHOT ON;
|
||||
----
|
||||
|
||||
== Changing database locking timeout in a cluster configuration
|
||||
|
||||
Because cluster nodes can boot concurrently, they take extra time for database actions. For example, a booting server instance may perform some database migration, importing, or first time initializations. A database lock prevents start actions from conflicting with each other when cluster nodes boot up concurrently.
|
||||
|
||||
@@ -99,6 +99,7 @@ public class QuarkusJpaConnectionProviderFactory extends AbstractJpaConnectionPr
|
||||
super.postInit(factory);
|
||||
|
||||
checkMySQLWaitTimeout();
|
||||
checkMSSQLIsolationLevel();
|
||||
|
||||
String id = null;
|
||||
String version = null;
|
||||
@@ -322,4 +323,30 @@ public class QuarkusJpaConnectionProviderFactory extends AbstractJpaConnectionPr
|
||||
logger.warnf(e, "Unable to validate %s 'wait_timeout' due to database exception", vendor);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkMSSQLIsolationLevel() {
|
||||
String db = Configuration.getConfigValue(DatabaseOptions.DB).getValue();
|
||||
Database.Vendor vendor = Database.getVendor(db).orElseThrow();
|
||||
if (Database.Vendor.MSSQL != vendor) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
Statement statement2 = connection.createStatement();
|
||||
ResultSet rs = statement.executeQuery("DBCC USEROPTIONS");
|
||||
ResultSet dbnameRs = statement2.executeQuery("SELECT DB_NAME() as db")) {
|
||||
dbnameRs.next();
|
||||
String dbName = dbnameRs.getString(1);
|
||||
while (rs.next()) {
|
||||
String option = rs.getString(1);
|
||||
String value = rs.getString(2);
|
||||
if ("isolation level".equalsIgnoreCase(option) && (!"read committed snapshot".equalsIgnoreCase(value))) {
|
||||
logger.warnf("%s 'isolation level' for database '%s' is set to '%s'. Keycloak recommends 'read committed snapshot' isolation level to avoid deadlocks under high load. Please adjust the isolation level by executing 'ALTER DATABASE %s SET READ_COMMITTED_SNAPSHOT ON'.", vendor, dbName, rs.getString(2), dbName);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.warnf(e, "Unable to validate %s 'isolation level' due to database exception", vendor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2021 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.it.storage.database;
|
||||
|
||||
import org.keycloak.it.junit5.extension.CLIResult;
|
||||
import org.keycloak.it.junit5.extension.CLITest;
|
||||
import org.keycloak.it.junit5.extension.WithDatabase;
|
||||
import org.keycloak.quarkus.runtime.cli.command.AbstractAutoBuildCommand;
|
||||
|
||||
import io.quarkus.test.junit.main.Launch;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@CLITest
|
||||
@WithDatabase(alias = "mssql")
|
||||
public class MssqlSQLTest extends BasicDatabaseTest {
|
||||
|
||||
@Override
|
||||
protected void assertWrongUsername(CLIResult cliResult) {
|
||||
cliResult.assertMessage("ErrorCode: 18456");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertWrongPassword(CLIResult cliResult) {
|
||||
cliResult.assertMessage("ErrorCode: 18456");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start", AbstractAutoBuildCommand.OPTIMIZED_BUILD_OPTION_LONG, "--http-enabled=true", "--hostname-strict=false" })
|
||||
protected void testWarningIsolationLevel(CLIResult cliResult) {
|
||||
cliResult.assertMessage("mssql 'isolation level' for database 'master' is set to 'read committed'.");
|
||||
cliResult.assertStarted();
|
||||
}
|
||||
}
|
||||
31
quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/MssqlDistTest.java
vendored
Normal file
31
quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/MssqlDistTest.java
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 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.it.storage.database.dist;
|
||||
|
||||
import org.keycloak.it.junit5.extension.DistributionTest;
|
||||
import org.keycloak.it.junit5.extension.WithDatabase;
|
||||
import org.keycloak.it.storage.database.MssqlSQLTest;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
|
||||
@DistributionTest(removeBuildOptionsAfterBuild = true)
|
||||
@WithDatabase(alias = "mssql")
|
||||
@Tag(DistributionTest.STORAGE)
|
||||
public class MssqlDistTest extends MssqlSQLTest {
|
||||
|
||||
}
|
||||
@@ -18,9 +18,12 @@
|
||||
package org.keycloak.it.junit5.extension;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.keycloak.it.utils.KeycloakDistribution;
|
||||
|
||||
import org.jboss.logmanager.Level;
|
||||
import org.jboss.logmanager.LogManager;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.JdbcDatabaseContainer;
|
||||
import org.testcontainers.containers.MSSQLServerContainer;
|
||||
@@ -110,7 +113,20 @@ public class DatabaseContainer {
|
||||
return configureJdbcContainer(new MySQLContainer<>(MYSQL));
|
||||
case "mssql":
|
||||
DockerImageName MSSQL = DockerImageName.parse(MSSQL_IMAGE).asCompatibleSubstituteFor("sqlserver");
|
||||
return configureJdbcContainer(new MSSQLServerContainer<>(MSSQL));
|
||||
return configureJdbcContainer(new MSSQLServerContainer(MSSQL) {
|
||||
@Override
|
||||
public void start() {
|
||||
// avoid WARNING [com.microsoft.sqlserver.jdbc.internals.SQLServerConnection] (main) ConnectionID:32 ClientConnectionId: Prelogin error ...
|
||||
Logger mssqlLogger = LogManager.getLogManager().getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerConnection");
|
||||
java.util.logging.Level level = mssqlLogger.getLevel();
|
||||
try {
|
||||
mssqlLogger.setLevel(Level.ERROR);
|
||||
super.start();
|
||||
} finally {
|
||||
mssqlLogger.setLevel(level);
|
||||
}
|
||||
}
|
||||
});
|
||||
case "tidb":
|
||||
DockerImageName TIDB = DockerImageName.parse(TIDB_IMAGE).asCompatibleSubstituteFor("pingcap/tidb");
|
||||
return configureJdbcContainer(new TiDBContainer(TIDB));
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.keycloak.testframework.config.Config;
|
||||
import org.keycloak.testframework.logging.JBossLogConsumer;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.testcontainers.containers.Container;
|
||||
import org.testcontainers.containers.JdbcDatabaseContainer;
|
||||
|
||||
public abstract class AbstractContainerTestDatabase implements TestDatabase {
|
||||
@@ -42,8 +43,14 @@ public abstract class AbstractContainerTestDatabase implements TestDatabase {
|
||||
List<String> postStartCommand = getPostStartCommand();
|
||||
if (postStartCommand != null) {
|
||||
getLogger().tracev("Running post start command: {0}", String.join(" ", postStartCommand));
|
||||
String result = container.execInContainer(postStartCommand.toArray(new String[0])).getStdout();
|
||||
getLogger().tracev(result);
|
||||
Container.ExecResult execResult = container.execInContainer(postStartCommand.toArray(new String[0]));
|
||||
String stdout = execResult.getStdout();
|
||||
String stderr = execResult.getStderr();
|
||||
getLogger().tracev(stdout);
|
||||
getLogger().tracev(stderr);
|
||||
if (execResult.getExitCode() != 0) {
|
||||
throw new RuntimeException("Post start command failed with exit code: " + execResult.getExitCode() + ". stdout: " + stdout + ". stderr: " + stderr);
|
||||
}
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.util.List;
|
||||
import org.keycloak.testframework.util.ContainerImages;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.logmanager.Level;
|
||||
import org.jboss.logmanager.LogManager;
|
||||
import org.testcontainers.containers.JdbcDatabaseContainer;
|
||||
import org.testcontainers.containers.MSSQLServerContainer;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
@@ -43,12 +45,27 @@ class MSSQLServerTestDatabase extends AbstractContainerTestDatabase {
|
||||
|
||||
@Override
|
||||
public String getJdbcUrl(boolean internal) {
|
||||
return super.getJdbcUrl(internal) + ";integratedSecurity=false;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=false;";
|
||||
return super.getJdbcUrl(internal) + ";integratedSecurity=false;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=false;databaseName=" + getDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(DatabaseConfiguration config) {
|
||||
// avoid WARNING [com.microsoft.sqlserver.jdbc.internals.SQLServerConnection] (main) ConnectionID:32 ClientConnectionId: Prelogin error ...
|
||||
java.util.logging.Logger mssqlLogger = LogManager.getLogManager().getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerConnection");
|
||||
java.util.logging.Level level = mssqlLogger.getLevel();
|
||||
try {
|
||||
mssqlLogger.setLevel(Level.ERROR);
|
||||
super.start(config);
|
||||
} finally {
|
||||
mssqlLogger.setLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPostStartCommand() {
|
||||
return List.of("/opt/mssql-tools18/bin/sqlcmd", "-U", "sa", "-P", getPassword(), "-No", "-Q", "CREATE DATABASE " + getDatabase());
|
||||
return List.of("/opt/mssql-tools18/bin/sqlcmd", "-U", "sa", "-P", getPassword(), "-No", "-Q", "CREATE DATABASE " + getDatabase() + "; " +
|
||||
// READ_COMMITTED_SNAPSHOT is recommended for MSSQL to avoid deadlocks
|
||||
"ALTER DATABASE " + getDatabase() + " SET READ_COMMITTED_SNAPSHOT ON;");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -531,7 +531,7 @@
|
||||
<docker.database.image>${mssql.container}</docker.database.image>
|
||||
<docker.database.port>1433</docker.database.port>
|
||||
<docker.database.skip>false</docker.database.skip>
|
||||
<docker.database.postStart>/opt/mssql-tools18/bin/sqlcmd -e -U sa -P ${keycloak.connectionsJpa.password} -No -d master -Q CREATE\ DATABASE\ ${keycloak.connectionsJpa.database}</docker.database.postStart>
|
||||
<docker.database.postStart>/opt/mssql-tools18/bin/sqlcmd -e -U sa -P ${keycloak.connectionsJpa.password} -No -d master -Q CREATE\ DATABASE\ ${keycloak.connectionsJpa.database};\ ALTER\ DATABASE\ ${keycloak.connectionsJpa.database}\ SET\ READ_COMMITTED_SNAPSHOT\ ON</docker.database.postStart>
|
||||
<docker.database.cmd>/bin/sh -c /opt/mssql/bin/sqlservr</docker.database.cmd>
|
||||
<docker.database.wait-for-log-regex>Recovery is complete. This is an informational message only. No user action is required.</docker.database.wait-for-log-regex>
|
||||
<keycloak.storage.connections.vendor>mssql</keycloak.storage.connections.vendor>
|
||||
|
||||
Reference in New Issue
Block a user