From af5b6bdf79b99891547e6db2bc7b7a86d52cf7f9 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 24 Jul 2023 22:15:48 -0700 Subject: [PATCH 1/2] Add `maxLifetime` to reactive datasource configurations This enables configuration of the new`maxLifetime` value provided by Vert.x 4.3 that ensures connections are recycled after a maximum duration. Because `maxLifetime` could conceivable be set beyond the maximum value that milliseconds can store in an `int` (24.8 days) I created a `UnitisedTime` class. The `UnitisedTime` class has a `unitised` method that converts a `Duration` into the smallest possible `TimeUnit` that can be stored in an int, starting with milliseconds. For consistency, I updated the `idleTimeout` to use the same `UnitisedTime.unitised` utitlity for conversion. --- .../main/asciidoc/reactive-sql-clients.adoc | 18 ++++++- .../DataSourceReactiveRuntimeConfig.java | 7 +++ .../datasource/runtime/UnitisedTime.java | 53 +++++++++++++++++++ .../db2/client/runtime/DB2PoolRecorder.java | 11 ++-- ...pplication-changing-credentials.properties | 1 + .../client/runtime/MSSQLPoolRecorder.java | 11 ++-- ...pplication-changing-credentials.properties | 3 +- .../client/runtime/MySQLPoolRecorder.java | 10 +++- ...pplication-changing-credentials.properties | 1 + .../client/runtime/OraclePoolRecorder.java | 11 ++-- ...pplication-changing-credentials.properties | 1 + .../pg/client/runtime/PgPoolRecorder.java | 11 ++-- 12 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/UnitisedTime.java diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 256cac41461f8..7041c828fbb7c 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -710,7 +710,7 @@ quarkus.datasource.reactive.url[2]=postgresql://host3:5432/default == Pooled Connection `idle-timeout` -Reactive datasources can be configured with an `idle-timeout` (in milliseconds). +Reactive datasources can be configured with an `idle-timeout`. It is the maximum time a connection remains unused in the pool before it is closed. NOTE: The `idle-timeout` is disabled by default. @@ -722,6 +722,22 @@ For example, you could expire idle connections after 60 minutes: quarkus.datasource.reactive.idle-timeout=PT60M ---- +== Pooled Connection `max-lifetime` + +In addition to `idle-timeout`, reactive datasources can also be configured with a `max-lifetime`. +It is the maximum time a connection remains in the pool before it is closed and replaced as needed. +The `max-lifetime` allows ensuring the pool has fresh connections with up-to-date configuration. + +NOTE: The `max-lifetime` is disabled by default but is an important configuration when using a credentials +provider that provides time limited credentials, like the link:credentials-provider.adoc[Vault credentials provider]. + +For example, you could ensure connections are recycled after 60 minutes: + +[source,properties] +---- +quarkus.datasource.reactive.max-lifetime=PT60M +---- + == Customizing pool creation Sometimes, the database connection pool cannot be configured only by declaration. diff --git a/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java b/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java index bb7684f069189..3099923f7efde 100644 --- a/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java +++ b/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java @@ -121,6 +121,13 @@ public interface DataSourceReactiveRuntimeConfig { @ConfigDocDefault("no timeout") Optional idleTimeout(); + /** + * The maximum time a connection remains in the pool, after which it will be closed + * upon return and replaced as necessary. + */ + @ConfigDocDefault("no timeout") + Optional maxLifetime(); + /** * Set to true to share the pool among datasources. * There can be multiple shared pools distinguished by name, when no specific name is set, diff --git a/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/UnitisedTime.java b/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/UnitisedTime.java new file mode 100644 index 0000000000000..5a04ce44e7255 --- /dev/null +++ b/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/UnitisedTime.java @@ -0,0 +1,53 @@ +package io.quarkus.reactive.datasource.runtime; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public class UnitisedTime { + + public final int value; + public final TimeUnit unit; + + public UnitisedTime(int value, TimeUnit unit) { + this.value = value; + this.unit = unit; + } + + /** + * Convert a {@link Duration} to a {@link UnitisedTime} with the smallest possible + * {@link TimeUnit} starting from {@link TimeUnit#MILLISECONDS}. + * + * @param duration Duration to convert + * + * @return UnitisedTime + */ + public static UnitisedTime unitised(Duration duration) { + if (duration.isNegative()) { + throw new IllegalArgumentException("Duration cannot be negative."); + } + + long millis = duration.toMillis(); + if (millis < Integer.MAX_VALUE) { + return new UnitisedTime((int) millis, TimeUnit.MILLISECONDS); + } + + long seconds = duration.getSeconds(); + if (seconds < Integer.MAX_VALUE) { + return new UnitisedTime((int) seconds, TimeUnit.SECONDS); + } + + long minutes = duration.toMinutes(); + if (minutes < Integer.MAX_VALUE) { + return new UnitisedTime((int) minutes, TimeUnit.MINUTES); + } + + long hours = duration.toHours(); + if (hours < Integer.MAX_VALUE) { + return new UnitisedTime((int) hours, TimeUnit.HOURS); + } + + long days = duration.toDays(); + return new UnitisedTime((int) days, TimeUnit.DAYS); + + } +} diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java index 55e13995babb1..90681e5e63bde 100644 --- a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java @@ -2,6 +2,7 @@ import static io.quarkus.credentials.CredentialsProvider.PASSWORD_PROPERTY_NAME; import static io.quarkus.credentials.CredentialsProvider.USER_PROPERTY_NAME; +import static io.quarkus.reactive.datasource.runtime.UnitisedTime.unitised; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksKeyCertOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksTrustOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configurePemKeyCertOptions; @@ -11,7 +12,6 @@ import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; @@ -108,8 +108,13 @@ private PoolOptions toPoolOptions(Integer eventLoopCount, poolOptions.setMaxSize(dataSourceReactiveRuntimeConfig.maxSize()); if (dataSourceReactiveRuntimeConfig.idleTimeout().isPresent()) { - int idleTimeout = Math.toIntExact(dataSourceReactiveRuntimeConfig.idleTimeout().get().toMillis()); - poolOptions.setIdleTimeout(idleTimeout).setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + var idleTimeout = unitised(dataSourceReactiveRuntimeConfig.idleTimeout().get()); + poolOptions.setIdleTimeout(idleTimeout.value).setIdleTimeoutUnit(idleTimeout.unit); + } + + if (dataSourceReactiveRuntimeConfig.maxLifetime().isPresent()) { + var maxLifetime = unitised(dataSourceReactiveRuntimeConfig.maxLifetime().get()); + poolOptions.setMaxLifetime(maxLifetime.value).setMaxLifetimeUnit(maxLifetime.unit); } if (dataSourceReactiveRuntimeConfig.shared()) { diff --git a/extensions/reactive-mssql-client/deployment/src/test/resources/application-changing-credentials.properties b/extensions/reactive-mssql-client/deployment/src/test/resources/application-changing-credentials.properties index 5ecf52b7f0e90..69027df795250 100644 --- a/extensions/reactive-mssql-client/deployment/src/test/resources/application-changing-credentials.properties +++ b/extensions/reactive-mssql-client/deployment/src/test/resources/application-changing-credentials.properties @@ -3,3 +3,4 @@ quarkus.datasource.credentials-provider=changing quarkus.datasource.reactive.url=${reactive-mssql.url} quarkus.datasource.reactive.max-size=1 quarkus.datasource.reactive.idle-timeout=PT1S +quarkus.datasource.reactive.max-lifetime=PT2s diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java index 8f8832035617a..3c9dc91c63deb 100644 --- a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java @@ -2,6 +2,7 @@ import static io.quarkus.credentials.CredentialsProvider.PASSWORD_PROPERTY_NAME; import static io.quarkus.credentials.CredentialsProvider.USER_PROPERTY_NAME; +import static io.quarkus.reactive.datasource.runtime.UnitisedTime.unitised; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksKeyCertOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksTrustOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configurePemKeyCertOptions; @@ -11,7 +12,6 @@ import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; @@ -108,8 +108,13 @@ private PoolOptions toPoolOptions(Integer eventLoopCount, poolOptions.setMaxSize(dataSourceReactiveRuntimeConfig.maxSize()); if (dataSourceReactiveRuntimeConfig.idleTimeout().isPresent()) { - int idleTimeout = Math.toIntExact(dataSourceReactiveRuntimeConfig.idleTimeout().get().toMillis()); - poolOptions.setIdleTimeout(idleTimeout).setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + var idleTimeout = unitised(dataSourceReactiveRuntimeConfig.idleTimeout().get()); + poolOptions.setIdleTimeout(idleTimeout.value).setIdleTimeoutUnit(idleTimeout.unit); + } + + if (dataSourceReactiveRuntimeConfig.maxLifetime().isPresent()) { + var maxLifetime = unitised(dataSourceReactiveRuntimeConfig.maxLifetime().get()); + poolOptions.setMaxLifetime(maxLifetime.value).setMaxLifetimeUnit(maxLifetime.unit); } if (dataSourceReactiveRuntimeConfig.shared()) { diff --git a/extensions/reactive-mysql-client/deployment/src/test/resources/application-changing-credentials.properties b/extensions/reactive-mysql-client/deployment/src/test/resources/application-changing-credentials.properties index 173159df97db6..e52558d57c982 100644 --- a/extensions/reactive-mysql-client/deployment/src/test/resources/application-changing-credentials.properties +++ b/extensions/reactive-mysql-client/deployment/src/test/resources/application-changing-credentials.properties @@ -2,4 +2,5 @@ quarkus.datasource.db-kind=mysql quarkus.datasource.credentials-provider=changing quarkus.datasource.reactive.url=${reactive-mysql.url} quarkus.datasource.reactive.max-size=1 -quarkus.datasource.reactive.idle-timeout=PT1S \ No newline at end of file +quarkus.datasource.reactive.idle-timeout=PT1S +quarkus.datasource.reactive.max-lifetime=PT2s diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java index fa692890e8a11..b3db354d08583 100644 --- a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java @@ -2,6 +2,7 @@ import static io.quarkus.credentials.CredentialsProvider.PASSWORD_PROPERTY_NAME; import static io.quarkus.credentials.CredentialsProvider.USER_PROPERTY_NAME; +import static io.quarkus.reactive.datasource.runtime.UnitisedTime.unitised; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksKeyCertOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksTrustOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configurePemKeyCertOptions; @@ -107,8 +108,13 @@ private PoolOptions toPoolOptions(Integer eventLoopCount, poolOptions.setMaxSize(dataSourceReactiveRuntimeConfig.maxSize()); if (dataSourceReactiveRuntimeConfig.idleTimeout().isPresent()) { - int idleTimeout = Math.toIntExact(dataSourceReactiveRuntimeConfig.idleTimeout().get().toMillis()); - poolOptions.setIdleTimeout(idleTimeout).setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + var idleTimeout = unitised(dataSourceReactiveRuntimeConfig.idleTimeout().get()); + poolOptions.setIdleTimeout(idleTimeout.value).setIdleTimeoutUnit(idleTimeout.unit); + } + + if (dataSourceReactiveRuntimeConfig.maxLifetime().isPresent()) { + var maxLifetime = unitised(dataSourceReactiveRuntimeConfig.maxLifetime().get()); + poolOptions.setMaxLifetime(maxLifetime.value).setMaxLifetimeUnit(maxLifetime.unit); } if (dataSourceReactiveRuntimeConfig.shared()) { diff --git a/extensions/reactive-oracle-client/deployment/src/test/resources/application-changing-credentials.properties b/extensions/reactive-oracle-client/deployment/src/test/resources/application-changing-credentials.properties index 43e17fcf6bce7..3d6c5522ec16b 100644 --- a/extensions/reactive-oracle-client/deployment/src/test/resources/application-changing-credentials.properties +++ b/extensions/reactive-oracle-client/deployment/src/test/resources/application-changing-credentials.properties @@ -3,3 +3,4 @@ quarkus.datasource.credentials-provider=changing quarkus.datasource.reactive.url=${reactive-oracledb.url} quarkus.datasource.reactive.max-size=1 quarkus.datasource.reactive.idle-timeout=PT1S +quarkus.datasource.reactive.max-lifetime=PT2s diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java index e2e9a2d101d3d..e217cc1a818af 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java @@ -2,10 +2,10 @@ import static io.quarkus.credentials.CredentialsProvider.PASSWORD_PROPERTY_NAME; import static io.quarkus.credentials.CredentialsProvider.USER_PROPERTY_NAME; +import static io.quarkus.reactive.datasource.runtime.UnitisedTime.unitised; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; @@ -104,8 +104,13 @@ private PoolOptions toPoolOptions(Integer eventLoopCount, poolOptions.setMaxSize(dataSourceReactiveRuntimeConfig.maxSize()); if (dataSourceReactiveRuntimeConfig.idleTimeout().isPresent()) { - int idleTimeout = Math.toIntExact(dataSourceReactiveRuntimeConfig.idleTimeout().get().toMillis()); - poolOptions.setIdleTimeout(idleTimeout).setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + var idleTimeout = unitised(dataSourceReactiveRuntimeConfig.idleTimeout().get()); + poolOptions.setIdleTimeout(idleTimeout.value).setIdleTimeoutUnit(idleTimeout.unit); + } + + if (dataSourceReactiveRuntimeConfig.maxLifetime().isPresent()) { + var maxLifetime = unitised(dataSourceReactiveRuntimeConfig.maxLifetime().get()); + poolOptions.setMaxLifetime(maxLifetime.value).setMaxLifetimeUnit(maxLifetime.unit); } if (dataSourceReactiveRuntimeConfig.shared()) { diff --git a/extensions/reactive-pg-client/deployment/src/test/resources/application-changing-credentials.properties b/extensions/reactive-pg-client/deployment/src/test/resources/application-changing-credentials.properties index d76f5d77b20c4..89a9160b25143 100644 --- a/extensions/reactive-pg-client/deployment/src/test/resources/application-changing-credentials.properties +++ b/extensions/reactive-pg-client/deployment/src/test/resources/application-changing-credentials.properties @@ -3,3 +3,4 @@ quarkus.datasource.credentials-provider=changing quarkus.datasource.reactive.url=${reactive-postgres.url} quarkus.datasource.reactive.max-size=1 quarkus.datasource.reactive.idle-timeout=PT1S +quarkus.datasource.reactive.max-lifetime=PT2s diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java index 27836f376dad1..7988a6f86afa7 100644 --- a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java @@ -2,6 +2,7 @@ import static io.quarkus.credentials.CredentialsProvider.PASSWORD_PROPERTY_NAME; import static io.quarkus.credentials.CredentialsProvider.USER_PROPERTY_NAME; +import static io.quarkus.reactive.datasource.runtime.UnitisedTime.unitised; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksKeyCertOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configureJksTrustOptions; import static io.quarkus.vertx.core.runtime.SSLConfigHelper.configurePemKeyCertOptions; @@ -12,7 +13,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; @@ -106,8 +106,13 @@ private PoolOptions toPoolOptions(Integer eventLoopCount, poolOptions.setMaxSize(dataSourceReactiveRuntimeConfig.maxSize()); if (dataSourceReactiveRuntimeConfig.idleTimeout().isPresent()) { - int idleTimeout = Math.toIntExact(dataSourceReactiveRuntimeConfig.idleTimeout().get().toMillis()); - poolOptions.setIdleTimeout(idleTimeout).setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + var idleTimeout = unitised(dataSourceReactiveRuntimeConfig.idleTimeout().get()); + poolOptions.setIdleTimeout(idleTimeout.value).setIdleTimeoutUnit(idleTimeout.unit); + } + + if (dataSourceReactiveRuntimeConfig.maxLifetime().isPresent()) { + var maxLifetime = unitised(dataSourceReactiveRuntimeConfig.maxLifetime().get()); + poolOptions.setMaxLifetime(maxLifetime.value).setMaxLifetimeUnit(maxLifetime.unit); } if (dataSourceReactiveRuntimeConfig.shared()) { From 7503e3601192496719172cfd757416c751be6817 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 24 Jul 2023 22:44:38 -0700 Subject: [PATCH 2/2] Add requirements for time-limited credentials to credentials provider docs --- .../main/asciidoc/credentials-provider.adoc | 34 +++++++++++++++++++ .../main/asciidoc/reactive-sql-clients.adoc | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/credentials-provider.adoc b/docs/src/main/asciidoc/credentials-provider.adoc index e37d833d8f304..7a5c745b6007a 100644 --- a/docs/src/main/asciidoc/credentials-provider.adoc +++ b/docs/src/main/asciidoc/credentials-provider.adoc @@ -83,6 +83,40 @@ An alternative is to define both username and password in Vault and drop the `qu property from configuration. All consuming extensions do support the ability to fetch both the username and password from the provider, or just the password. +== Time Limited Credentials + +A Credentials Provider may provide time limited credentials. For instance, the `vault` extension. When using +time limited credentials, it is important to understand that consuming extensions will not have their +credentials refreshed automatically by the Credentials Provider. Each extension must be configured to recycle its +connections before the credentials expire. + +=== Datasources + +Datastore connections are typically pooled. When using a time limited credentials provider, the pool must be +configured to recycle connections before each connection's credentials expire. Both JDBC and Reactive datasources +have a `max-lifetime` configuration property that can be used to achieve this. + +.JDBC Datasource +[source, properties] +---- +quarkus.datasource.jdbc.max-lifetime=60m +---- + +.Reactive Datasource +[source, properties] +---- +quarkus.datasource.reactive.max-lifetime=60m +---- + +NOTE: It is the developer's responsibility to ensure that the configuration of the datasource's `max-lifetime` +property is less than the credentials expiration time. + +=== RabbitMQ + +When using the `smallrye-reactive-messaging-rabbitmq` extension there is no configuration needed. The +extension will automatically recycle connections before their credentials expire based on the expiration +timestamp provided by the Credentials Provider. + == Custom Credentials Provider Implementing a custom credentials provider is the only option when a vault product is not yet supported in Quarkus, or if credentials need to be retrieved from a custom store. diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 7041c828fbb7c..5ca5ef3922837 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -708,7 +708,7 @@ quarkus.datasource.reactive.url[1]=postgresql://host2:5432/default quarkus.datasource.reactive.url[2]=postgresql://host3:5432/default ---- -== Pooled Connection `idle-timeout` +== Pooled connection `idle-timeout` Reactive datasources can be configured with an `idle-timeout`. It is the maximum time a connection remains unused in the pool before it is closed.