Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
11 changes: 11 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ Starting with this release, and when using the original PostgreSQL driver, {proj

You can override this behavior by setting your own value for `targetServerType` in the DB URL or additional properties.

=== MySQL and MariaDB wait_timeout validation

In order to prevent connections being closed unexpectedly by the database server, it is necessary to ensure that the {project_name}
connection pool is correctly configured with respect to the server's `wait_timeout` setting.

Starting with this release, {project_name} defines a default `db-pool-max-lifetime` of 7 hours and 50 minutes for MySQL
and MariaDB databases as the default `wait_timeout` is 8 hours.

If the server defines a `wait_timeout` which is greater than the default, or provided, `db-pool-max-lifetime` value, then
a warning will be logged on {project_name} startup.

=== Cache configuration removed from cache-ispn.xml

The `conf/cache-ispn.xml` file no longer contains the default cache configurations.
Expand Down
14 changes: 14 additions & 0 deletions docs/guides/server/db.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,17 @@ In the same way the migrationExport to point to a specific file and location:
For more information, check the link:{upgrading_guide_link}#_migrate_db[Migrating the database] documentation.

</@tmpl.guide>

== Configuring the connection pool

=== MySQL and MariaDB

In order to prevent 'No operations allowed after connection closed' exceptions from being thrown, it is necessary to ensure
that {project_name}'s connection pool has a connection maximum lifetime that is less than the server's configured `wait_timeout`.
When using the MySQL and MariaDB database, {project_name} configures a default max lifetime of 7 hours and 50 minutes, as
this is less than the default server value of 8 hours.

If you are explicitly configuring the `wait_timeout` in your database, it is necessary to ensure that you configure a
`db-pool-max-lifetime` value that is less than the `wait_timeout`. The recommended best practice, is to define this value
to be your `wait_timeout` minus a few minutes. Failure to correctly configure the `db-pool-max-lifetime` will result in
{project_name} logging a warning on startup.
4 changes: 2 additions & 2 deletions quarkus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,15 @@ There are also some container based tests to check if Keycloak starts using one

These tests are disabled by default. They using Quarkus development mode predefined database containers by default and can be run in the `tests` subdirectory by using e.g.

../mvnw clean install -Dkc.test.storage.database=true -Dtest=MariaDBStartDatabaseTest
../mvnw clean install -Ptest-database -Dtest=MariaDBDistTest

to spin up a MariaDB container and start Keycloak with it.

To use a specific database container image, use the option `-Dkc.db.postgresql.container.image` to specify the image tag of the postgres image to use or `-Dkc.db.mariadb.container.image=<name:tag>` for mariadb.

Example:

../mvnw clean install -Dkc.test.storage.database=true -Dtest=PostgreSQLDistTest -Dkc.db.postgresql.container.image=postgres:alpine
../mvnw clean install -Ptest-database -Dtest=PostgreSQLDistTest -Dkc.db.postgresql.container.image=postgres:alpine

### Updating Expectations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.util.Optional;
import java.util.function.Consumer;

import static org.keycloak.config.OptionsUtil.DURATION_DESCRIPTION;

public class DatabaseOptions {

public static final Option<String> DB_DIALECT = new OptionBuilder<>("db-dialect", String.class)
Expand Down Expand Up @@ -89,6 +91,11 @@ public class DatabaseOptions {
.description("The maximum size of the connection pool.")
.build();

public static final Option<String> DB_POOL_MAX_LIFETIME = new OptionBuilder<>("db-pool-max-lifetime", String.class)
.category(OptionCategory.DATABASE)
.description("The maximum time a connection remains in the pool, after which it will be closed upon return and replaced as necessary. " + DURATION_DESCRIPTION)
.build();

public static final Option<Boolean> DB_SQL_JPA_DEBUG = new OptionBuilder<>("db-debug-jpql", Boolean.class)
.category(OptionCategory.DATABASE)
.defaultValue(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import org.keycloak.common.crypto.FipsMode;

import static org.keycloak.config.OptionsUtil.DURATION_DESCRIPTION;

public class HttpOptions {

public static final Option<Boolean> HTTP_ENABLED = new OptionBuilder<>("http-enabled", Boolean.class)
Expand Down Expand Up @@ -65,7 +67,7 @@ public enum ClientAuth {

public static final Option<String> HTTPS_CERTIFICATES_RELOAD_PERIOD = new OptionBuilder<>("https-certificates-reload-period", String.class)
.category(OptionCategory.HTTP)
.description("Interval on which to reload key store, trust store, and certificate files referenced by https-* options. May be a java.time.Duration value, an integer number of seconds, or an integer followed by one of [ms, h, m, s, d]. Must be greater than 30 seconds. Use -1 to disable.")
.description("Interval on which to reload key store, trust store, and certificate files referenced by https-* options. " + DURATION_DESCRIPTION + " Must be greater than 30 seconds. Use -1 to disable.")
.defaultValue("1h")
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.io.File;
import java.util.List;

import static org.keycloak.config.OptionsUtil.DURATION_DESCRIPTION;

/**
* Options for the management interface that handles management endpoints (f.e. health and metrics endpoints)
*/
Expand Down Expand Up @@ -105,8 +107,8 @@ public enum Scheme {
public static final Option<String> HTTPS_MANAGEMENT_CERTIFICATES_RELOAD_PERIOD = new OptionBuilder<>("https-management-certificates-reload-period", String.class)
.category(OptionCategory.MANAGEMENT)
.description("Interval on which to reload key store, trust store, and certificate files referenced by https-management-* options for the management server. " +
"May be a java.time.Duration value, an integer number of seconds, or an integer followed by one of [ms, h, m, s, d]. " +
"Must be greater than 30 seconds. Use -1 to disable. " +
DURATION_DESCRIPTION +
" Must be greater than 30 seconds. Use -1 to disable. " +
"If not given, the value is inherited from HTTP options. " + RELEVANT_MSG)
.defaultValue("1h")
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2025 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.config;

public interface OptionsUtil {
String DURATION_DESCRIPTION = "May be an ISO 8601 duration value, an integer number of seconds, or an integer followed by one of [ms, h, m, s, d].";
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public List<PropertyMapper<?>> getPropertyMappers() {
.to("quarkus.datasource.jdbc.max-size")
.paramLabel("size")
.build(),
fromOption(DatabaseOptions.DB_POOL_MAX_LIFETIME)
.to("quarkus.datasource.jdbc.max-lifetime")
.mapFrom(DB, DatabasePropertyMappers::transformPoolMaxLifetime)
.paramLabel("duration")
.build(),
fromOption(DatabaseOptions.DB_SQL_JPA_DEBUG)
.build(),
fromOption(DatabaseOptions.DB_SQL_LOG_SLOW_QUERIES)
Expand Down Expand Up @@ -179,6 +184,15 @@ private static String transformMinPoolSize(String database, ConfigSourceIntercep
return isDevModeDatabase(database) ? "1" : getParentPoolMinSize.get();
}

private static String transformPoolMaxLifetime(String db, ConfigSourceInterceptorContext context) {
Database.Vendor vendor = Database.getVendor(db).orElseThrow();
return switch (vendor) {
// Default to max lifetime slightly less than the default `wait_timeout` of 8 hours
case MYSQL, MARIADB -> "PT7H50M";
default -> "";
};
}

public static final class Datasources {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.List;
import java.util.Map;

import io.quarkus.runtime.configuration.DurationConverter;
import jakarta.enterprise.inject.Instance;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
Expand All @@ -40,6 +41,8 @@
import org.jboss.logging.Logger;
import org.keycloak.ServerStartupError;
import org.keycloak.common.Version;
import org.keycloak.config.DatabaseOptions;
import org.keycloak.config.database.Database;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.migration.MigrationModelManager;
Expand All @@ -52,6 +55,7 @@
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.Configuration;

/**
* @author <a href="mailto:[email protected]">Stian Thorgersen</a>
Expand Down Expand Up @@ -94,6 +98,8 @@ private void addSpecificNamedQueries(KeycloakSession session) {
public void postInit(KeycloakSessionFactory factory) {
super.postInit(factory);

checkMySQLWaitTimeout();

String id = null;
String version = null;
String schema = getSchema();
Expand Down Expand Up @@ -292,4 +298,28 @@ private void export(Connection connection, String schema, File databaseUpdateFil
dbLock2.releaseLock();
}
}

private void checkMySQLWaitTimeout() {
String db = Configuration.getConfigValue(DatabaseOptions.DB).getValue();
Database.Vendor vendor = Database.getVendor(db).orElseThrow();
if (!(Database.Vendor.MYSQL == vendor || Database.Vendor.MARIADB == vendor))
return;

try (Connection connection = getConnection();
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery("SHOW VARIABLES LIKE 'wait_timeout'")) {
if (rs.next()) {
var waitTimeout = rs.getInt(2);
var poolMaxLifetime = DurationConverter.parseDuration(Configuration.getConfigValue(DatabaseOptions.DB_POOL_MAX_LIFETIME).getValue());
if (poolMaxLifetime.getSeconds() >= waitTimeout) {
logger.warnf("%1$s 'wait_timeout=%2$d' is less than or equal to the configured '%3$s' duration. " +
"This can cause 'No operations allowed after connection closed' exceptions, which can impact Keycloak operations. " +
"To avoid such issues, set '%3$s' to a duration smaller than '%2$d' seconds.",
vendor, waitTimeout, DatabaseOptions.DB_POOL_MAX_LIFETIME.getKey(), poolMaxLifetime);
}
}
} catch (SQLException e) {
logger.warnf(e, "Unable to validate %s 'wait_timeout' due to database exception", vendor);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public void failUnknownOptionWhitespaceSeparatorNotShowingValue() {
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString(Help.defaultColorScheme(Help.Ansi.AUTO)
.errorText("Unknown option: '--db-pasword'")
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-debug-jpql, --db-log-slow-queries-threshold, --db-driver, --db"));
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-pool-max-lifetime, --db-debug-jpql, --db-log-slow-queries-threshold, --db-driver, --db"));
}

@Test
Expand All @@ -271,7 +271,7 @@ public void failUnknownOptionEqualsSeparatorNotShowingValue() {
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString(Help.defaultColorScheme(Help.Ansi.AUTO)
.errorText("Unknown option: '--db-pasword'")
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-debug-jpql, --db-log-slow-queries-threshold, --db-driver, --db"));
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-pool-max-lifetime, --db-debug-jpql, --db-log-slow-queries-threshold, --db-driver, --db"));
}

@Test
Expand All @@ -281,7 +281,7 @@ public void failWithFirstOptionOnMultipleUnknownOptions() {
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString(Help.defaultColorScheme(Help.Ansi.AUTO)
.errorText("Unknown option: '--db-pasword'")
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-debug-jpql, --db-log-slow-queries-threshold, --db-driver, --db"));
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-pool-max-lifetime, --db-debug-jpql, --db-log-slow-queries-threshold, --db-driver, --db"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,11 @@ public void testKeycloakDbUpdateScript(CLIResult cliResult, RawDistRootPath rawD
assertManualDbInitialization(cliResult, rawDistRootPath);
}

@Tag(DistributionTest.STORAGE)
@Test
@Launch({"start", AbstractAutoBuildCommand.OPTIMIZED_BUILD_OPTION_LONG, "--http-enabled=true", "--hostname-strict=false", "--db-pool-max-lifetime=28800"})
public void testWarningForTooShortLifetime(CLIResult cliResult) {
cliResult.assertMessage("set 'db-pool-max-lifetime' to a duration smaller than '28800' seconds.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,11 @@ protected void testSuccessful(CLIResult result) {
public void testKeycloakDbUpdateScript(CLIResult cliResult, RawDistRootPath rawDistRootPath) {
assertManualDbInitialization(cliResult, rawDistRootPath);
}

@Tag(DistributionTest.STORAGE)
@Test
@Launch({"start", AbstractAutoBuildCommand.OPTIMIZED_BUILD_OPTION_LONG, "--http-enabled=true", "--hostname-strict=false", "--db-pool-max-lifetime=28800"})
public void testWarningForTooShortLifetime(CLIResult cliResult) {
cliResult.assertMessage("set 'db-pool-max-lifetime' to a duration smaller than '28800' seconds.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ Database:
The password of the database user.
--db-pool-initial-size <size>
The initial size of the connection pool.
--db-pool-max-lifetime <duration>
The maximum time a connection remains in the pool, after which it will be
closed upon return and replaced as necessary. May be an ISO 8601 duration
value, an integer number of seconds, or an integer followed by one of [ms,
h, m, s, d].
--db-pool-max-size <size>
The maximum size of the connection pool. Default: 100.
--db-pool-min-size <size>
Expand Down Expand Up @@ -175,8 +180,8 @@ Management:
details. Available only when http-management-scheme is inherited.
--https-management-certificates-reload-period <reload period>
Interval on which to reload key store, trust store, and certificate files
referenced by https-management-* options for the management server. May be a
java.time.Duration value, an integer number of seconds, or an integer
referenced by https-management-* options for the management server. May be
an ISO 8601 duration value, an integer number of seconds, or an integer
followed by one of [ms, h, m, s, d]. Must be greater than 30 seconds. Use -1
to disable. If not given, the value is inherited from HTTP options. Relevant
only when something is exposed on the management interface - see the guide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ Database:
The password of the database user.
--db-pool-initial-size <size>
The initial size of the connection pool.
--db-pool-max-lifetime <duration>
The maximum time a connection remains in the pool, after which it will be
closed upon return and replaced as necessary. May be an ISO 8601 duration
value, an integer number of seconds, or an integer followed by one of [ms,
h, m, s, d].
--db-pool-max-size <size>
The maximum size of the connection pool. Default: 100.
--db-pool-min-size <size>
Expand Down Expand Up @@ -177,8 +182,8 @@ Management:
details. Available only when http-management-scheme is inherited.
--https-management-certificates-reload-period <reload period>
Interval on which to reload key store, trust store, and certificate files
referenced by https-management-* options for the management server. May be a
java.time.Duration value, an integer number of seconds, or an integer
referenced by https-management-* options for the management server. May be
an ISO 8601 duration value, an integer number of seconds, or an integer
followed by one of [ms, h, m, s, d]. Must be greater than 30 seconds. Use -1
to disable. If not given, the value is inherited from HTTP options. Relevant
only when something is exposed on the management interface - see the guide
Expand Down
Loading
Loading