diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc index a27a009804d..b5a9c44fc6b 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc @@ -171,7 +171,8 @@ This behavior is enabled by default and can be controlled with the server option * `--truststore-kubernetes-enabled=true|false` (default: `true`) -No changes are required for most deployments. If you previously relied on the Operator to manage these truststore entries, the server now performs the same function directly. +No changes are required for most deployments. +If you previously relied on the Operator to manage these truststore entries, the server now performs the same function directly. WARNING: when `https-client-auth` is set to `required` or `request` without an explicit `https-trust-store-file`, mTLS client certificate validation falls back to the system truststore. With `truststore-kubernetes-enabled=true`, this means certificates signed by the Kubernetes cluster CA will be accepted as valid client certificates. If this is not desired, either set `https-trust-store-file` explicitly or disable `truststore-kubernetes-enabled`. @@ -259,6 +260,11 @@ As part of this change, the database configuration documentation has been update The previously documented `utf8mb3` (or `utf8`) character set has been removed from the documentation due to its limitations in storing certain Unicode characters. * Previously recommended JDBC driver settings for Oracle, MySQL, and MariaDB have been removed from the documentation, as current versions of these databases use appropriate default values. +=== Automatic database connection timeout defaults + +To improve failover behavior and startup resilience during network issues, {project_name} now sets a default database connection timeout of 10 seconds. +See https://www.keycloak.org/server/db[Configuring the database] for the list of databases, and on how to change this default. + // ------------------------ Deprecated features ------------------------ // == Deprecated features diff --git a/docs/guides/server/db.adoc b/docs/guides/server/db.adoc index 58341ef53ac..38e0e609622 100644 --- a/docs/guides/server/db.adoc +++ b/docs/guides/server/db.adoc @@ -281,6 +281,78 @@ For example: <@kc.start parameters="--db mssql --db-url-properties=';sendStringParametersAsUnicode=true'"/> +== Automatic database connection timeout + +When {project_name} connects to the database, network problems can occur, especially during failovers or switchovers. +To improve resilience and ensure faster recovery, {project_name} automatically sets a default connection timeout of 10 seconds for selected database vendors when using the standard JDBC driver. + +The following table lists the affected vendors, the JDBC driver property used, and the default value applied by {project_name}: + +[%autowidth] +|=== +|Database |JDBC driver property |Default value |Unit + +|MySQL +|`connectTimeout` +|`10000` +|milliseconds + +|MariaDB +|`connectTimeout` +|`10000` +|milliseconds + +|PostgreSQL +|`connectTimeout` +|`10` +|seconds + +|Oracle Database +|`oracle.net.CONNECT_TIMEOUT` +|`10000` +|milliseconds + +|Microsoft SQL Server +|`loginTimeout` +|`10` +|seconds + +|=== + +{project_name} applies these defaults automatically, but only when all of the following conditions are met: + +* The database vendor is configured via `--db`. +* {project_name} is using the standard JDBC driver for that vendor. +* The timeout property has not already been set explicitly by the user in `db-url` or `db-url-properties`. + +=== Overriding the default connection timeout + +To use a different connection timeout, set the relevant JDBC driver property explicitly via `db-url` or `db-url-properties`. + +For MySQL: + +<@kc.start parameters="--db mysql --db-url-properties='?connectTimeout=30000'"/> + +For MariaDB: + +<@kc.start parameters="--db mariadb --db-url-properties='?connectTimeout=30000'"/> + +For Microsoft SQL Server: + +<@kc.start parameters="--db mssql --db-url-properties=';loginTimeout=20'"/> + +For PostgreSQL: + +<@kc.start parameters="--db postgres --db-url-properties='?connectTimeout=30'"/> + +[NOTE] +==== +When using `db-url-properties`, prepend the correct delimiter for your vendor's JDBC URL format: + +* PostgreSQL, MySQL, and MariaDB: use `?` as the first property delimiter, or `&` for subsequent properties. +* Microsoft SQL Server: use `;` as the property delimiter. +==== + == Preparing for PostgreSQL === Writer and reader instances diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java index 010864815f9..55b9d962894 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java @@ -126,7 +126,36 @@ public class DatabaseOptions { .defaultValue("false") .hidden() .build(); - + public static final Option DB_MYSQL_CONNECT_TIMEOUT = new OptionBuilder<>("db-mysql-connect-timeout", String.class) + .category(OptionCategory.DATABASE) + .defaultValue("10000") // 10 seconds in milliseconds + .hidden() + .build(); + public static final Option DB_MARIADB_CONNECT_TIMEOUT = new OptionBuilder<>("db-mariadb-connect-timeout", String.class) + .category(OptionCategory.DATABASE) + .defaultValue("10000") // 10 seconds in milliseconds + .hidden() + .build(); + public static final Option DB_ORACLE_CONNECT_TIMEOUT = new OptionBuilder<>("db-oracle-connect-timeout", String.class) + .category(OptionCategory.DATABASE) + .defaultValue("10000") // 10 seconds in milliseconds + .hidden() + .build(); + public static final Option DB_MSSQL_CONNECT_TIMEOUT = new OptionBuilder<>("db-mssql-login-timeout", String.class) + .category(OptionCategory.DATABASE) + .defaultValue("10") // 10 seconds, unit is SECONDS + .hidden() + .build(); + public static final Option DB_POSTGRES_CONNECT_TIMEOUT = new OptionBuilder<>("db-postgres-connect-timeout", String.class) + .category(OptionCategory.DATABASE) + .defaultValue("10") // 10 seconds, unit is SECONDS + .hidden() + .build(); + public static final Option DB_TIDB_CONNECT_TIMEOUT = new OptionBuilder<>("db-tidb-connect-timeout", String.class) + .category(OptionCategory.DATABASE) + .defaultValue("10000") // 10 seconds in milliseconds + .hidden() + .build(); public static final class Datasources { /** * Options that have their sibling for a named datasource diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index bc0decd77b2..a1aa56693af 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -38,6 +38,13 @@ import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper. public final class DatabasePropertyMappers implements PropertyMapperGrouping { public static final String PG_TARGET_SERVER_TYPE = "quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType"; public static final String MSSQL_SEND_STRING_PARAMETER_AS_UNICODE = "quarkus.datasource.jdbc.additional-jdbc-properties.sendStringParametersAsUnicode"; + public static final String MYSQL_CONNECT_TIMEOUT = "quarkus.datasource.jdbc.additional-jdbc-properties.connectTimeout"; + public static final String MARIADB_CONNECT_TIMEOUT = "quarkus.datasource.jdbc.additional-jdbc-properties.connectTimeout"; + public static final String ORACLEDB_CONNECT_TIMEOUT = "quarkus.datasource.jdbc.additional-jdbc-properties.oracle.net.CONNECT_TIMEOUT"; + public static final String MSSQL_CONNECT_TIMEOUT = "quarkus.datasource.jdbc.additional-jdbc-properties.loginTimeout"; + private static final String POSTGRES_CONNECT_TIMEOUT = "quarkus.datasource.jdbc.additional-jdbc-properties.connectTimeout"; + private static final String TIDB_CONNECT_TIMEOUT = "quarkus.datasource.jdbc.additional-jdbc-properties.connectTimeout"; + private static final Logger log = Logger.getLogger(DatabasePropertyMappers.class); /** @@ -76,6 +83,30 @@ public final class DatabasePropertyMappers implements PropertyMapperGrouping { .to(MSSQL_SEND_STRING_PARAMETER_AS_UNICODE) .isEnabled(() -> isMssqlSendStringParametersAsUnicode()) .build(), + fromOption(DatabaseOptions.DB_MYSQL_CONNECT_TIMEOUT) + .to(MYSQL_CONNECT_TIMEOUT) + .isEnabled(() -> isMysqlConnectTimeoutEnabled()) + .build(), + fromOption(DatabaseOptions.DB_MARIADB_CONNECT_TIMEOUT) + .to(MARIADB_CONNECT_TIMEOUT) + .isEnabled(() -> isMariadbConnectTimeoutEnabled()) + .build(), + fromOption(DatabaseOptions.DB_ORACLE_CONNECT_TIMEOUT) + .to(ORACLEDB_CONNECT_TIMEOUT) + .isEnabled(() -> isOracleConnectTimeoutEnabled()) + .build(), + fromOption(DatabaseOptions.DB_MSSQL_CONNECT_TIMEOUT) + .to(MSSQL_CONNECT_TIMEOUT) + .isEnabled(() -> isMssqlLoginTimeoutEnabled()) + .build(), + fromOption(DatabaseOptions.DB_POSTGRES_CONNECT_TIMEOUT) + .to(POSTGRES_CONNECT_TIMEOUT) + .isEnabled(() -> isPostgresConnectTimeoutEnabled()) + .build(), + fromOption(DatabaseOptions.DB_TIDB_CONNECT_TIMEOUT) + .to(TIDB_CONNECT_TIMEOUT) + .isEnabled(() -> isTidbConnectTimeoutEnabled()) + .build(), fromOption(DatabaseOptions.DB_URL_HOST) .paramLabel("hostname") .build(), @@ -196,14 +227,58 @@ public final class DatabasePropertyMappers implements PropertyMapperGrouping { if (!Objects.equals(Database.getDriver(db, true).orElse(null), dbDriver) && !Objects.equals(Database.getDriver(db, false).orElse(null), dbDriver)) { - // Custom JDBC-Driver, for example, AWS JDBC Wrapper. return false; } - // sendStringParametersAsUnicode already set by user in db-url or db-url-properties, ignore return !dbUrl.contains("sendStringParametersAsUnicode") && !dbUrlProperties.contains("sendStringParametersAsUnicode"); } + + public static boolean isMysqlConnectTimeoutEnabled() { + return isConnectTimeoutEnabled(Database.Vendor.MYSQL, "connectTimeout"); + } + + public static boolean isMariadbConnectTimeoutEnabled() { + return isConnectTimeoutEnabled(Database.Vendor.MARIADB, "connectTimeout"); + } + + public static boolean isOracleConnectTimeoutEnabled() { + return isConnectTimeoutEnabled(Database.Vendor.ORACLE, "oracle.net.CONNECT_TIMEOUT"); + } + + public static boolean isMssqlLoginTimeoutEnabled() { + return isConnectTimeoutEnabled(Database.Vendor.MSSQL, "loginTimeout"); + } + + public static boolean isPostgresConnectTimeoutEnabled() { + return isConnectTimeoutEnabled(Database.Vendor.POSTGRES, "connectTimeout"); + } + + public static boolean isTidbConnectTimeoutEnabled() { + return isConnectTimeoutEnabled(Database.Vendor.TIDB, "connectTimeout"); + } + + private static boolean isConnectTimeoutEnabled(Database.Vendor expectedVendor, String timeoutProperty) { + String db = Configuration.getConfigValue(DB).getValue(); + Database.Vendor vendor = Database.getVendor(db).orElse(null); + if (vendor != expectedVendor) { + return false; + } + + String dbDriver = Configuration.getConfigValue(DatabaseOptions.DB_DRIVER).getValue(); + if (!Objects.equals(Database.getDriver(db, true).orElse(null), dbDriver) && + !Objects.equals(Database.getDriver(db, false).orElse(null), dbDriver)) { + // Custom JDBC driver (e.g. AWS JDBC Wrapper) — do not inject defaults + return false; + } + + String dbUrl = Configuration.getConfigValue(DatabaseOptions.DB_URL).getValueOrDefault(""); + String dbUrlProperties = Configuration.getKcConfigValue(DatabaseOptions.DB_URL_PROPERTIES.getKey()).getValueOrDefault(""); + + // Property already set explicitly by the user — do not override + return !dbUrl.contains(timeoutProperty) && !dbUrlProperties.contains(timeoutProperty); + } + /** * Starting with H2 version 2.x, marking "VALUE" as a non-keyword is necessary as some columns are named "VALUE" in the Keycloak schema. *

diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java index fcf0a90dca2..b18ba2f560a 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java @@ -712,4 +712,61 @@ public class ConfigurationTest extends AbstractConfigurationTest { private static Config.Scope cacheEmbeddedConfiguration() { return initConfig(CacheEmbeddedConfigProviderSpi.SPI_NAME, DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID); } + + @Test + public void testDefaultDatabaseConnectTimeouts() { + + ConfigArgsConfigSource.setCliArgs("--db=mysql"); + SmallRyeConfig config = createConfig(); + assertTrue(DatabasePropertyMappers.isMysqlConnectTimeoutEnabled()); + assertEquals("10000", config.getConfigValue(DatabasePropertyMappers.MYSQL_CONNECT_TIMEOUT).getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=mysql", "--db-url-properties=?connectTimeout=5000"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isMysqlConnectTimeoutEnabled()); + + ConfigArgsConfigSource.setCliArgs("--db=mysql", "--db-url=jdbc:mysql://localhost:3306/keycloak?connectTimeout=5000"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isMysqlConnectTimeoutEnabled()); + + ConfigArgsConfigSource.setCliArgs("--db=mariadb"); + config = createConfig(); + assertTrue(DatabasePropertyMappers.isMariadbConnectTimeoutEnabled()); + assertEquals("10000", config.getConfigValue(DatabasePropertyMappers.MARIADB_CONNECT_TIMEOUT).getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=mariadb", "--db-url=jdbc:mariadb://localhost:3306/keycloak?connectTimeout=5000"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isMariadbConnectTimeoutEnabled()); + + // MariaDB: connectTimeout + ConfigArgsConfigSource.setCliArgs("--db=mariadb", "--db-url-properties=?connectTimeout=5000"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isMariadbConnectTimeoutEnabled()); + + ConfigArgsConfigSource.setCliArgs("--db=oracle"); + config = createConfig(); + assertTrue(DatabasePropertyMappers.isOracleConnectTimeoutEnabled()); + assertEquals("10000", config.getConfigValue(DatabasePropertyMappers.ORACLEDB_CONNECT_TIMEOUT).getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=oracle", "--db-url-properties=?oracle.net.CONNECT_TIMEOUT=5000"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isOracleConnectTimeoutEnabled()); + + ConfigArgsConfigSource.setCliArgs("--db=oracle", "--db-url=jdbc:oracle:thin:@//localhost:1521/keycloak?oracle.net.CONNECT_TIMEOUT=5000"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isOracleConnectTimeoutEnabled()); + + ConfigArgsConfigSource.setCliArgs("--db=mssql"); + config = createConfig(); + assertTrue(DatabasePropertyMappers.isMssqlLoginTimeoutEnabled()); + assertEquals("10", config.getConfigValue(DatabasePropertyMappers.MSSQL_CONNECT_TIMEOUT).getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=mssql", "--db-url-properties=;loginTimeout=20"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isMssqlLoginTimeoutEnabled()); + + ConfigArgsConfigSource.setCliArgs("--db=mssql", "--db-url=jdbc:sqlserver://localhost:1433;databaseName=keycloak;loginTimeout=20"); + config = createConfig(); + assertFalse(DatabasePropertyMappers.isMssqlLoginTimeoutEnabled()); + } }