Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3adfa98
CAMEL-24638: camel-jdbc/camel-sql implement SecretRotationAware
gnodet Sep 24, 2026
c1aa008
Address review: use getHikariPoolMXBean() to reach softEvictConnectio…
gnodet Sep 24, 2026
7be4657
Address review: extract DataSourceHelper, identity-based dedup
gnodet Sep 24, 2026
c30a53b
Address review: move onSecretRotation orchestration to DataSourceHelp…
gnodet Sep 24, 2026
2e8599a
fix: restore missing import java.util.Set in JdbcComponent
gnodet Sep 24, 2026
bc71bd1
fix: add setAccessible(true) on reflected methods to handle anonymous…
gnodet Sep 25, 2026
bd4bc27
Address review: scope eviction to component DataSource, drop setAcces…
gnodet Sep 26, 2026
35c3d96
fix: regenerate catalog docs for jdbc/sql secret rotation
gnodet Sep 26, 2026
cc0bb0f
Address review: scope eviction to component+endpoint DataSources, fix…
gnodet Sep 27, 2026
b796416
Add real HikariCP+h2 integration test, deduplicate helper tests
gnodet Sep 27, 2026
d711425
Address review: fix HikariCP integration test
gnodet Sep 27, 2026
79ad28d
Address review: clarify JMX not required, document reflection for nat…
gnodet Sep 28, 2026
ae4aa49
Add Agroal (Quarkus) pool eviction support via reflection + integrati…
gnodet Sep 28, 2026
2b58bfc
Update docs to reflect Agroal pool eviction support
gnodet Sep 28, 2026
beb6afe
Address review: make Agroal integration test meaningful
gnodet Sep 29, 2026
a54a9dc
Address review: replace Thread.sleep with Awaitility in Agroal test
gnodet Sep 29, 2026
673fa2b
Resolve Agroal flush through public AgroalDataSource interface
gnodet Sep 29, 2026
7ed1cba
Address review: resolve Agroal class via DataSource classloader, trim…
gnodet Sep 29, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,29 @@ from("timer://MoveNewCustomersEveryHour?period=3600000")
.setBody(simple("insert into processed_customer values('${body[ID]}','${body[NAME]}')"))
.to("jdbc:testdb");
----

== Secret Rotation

The JDBC component implements `SecretRotationAware`. When a secret rotation event is triggered
(e.g. by a vault provider), the component evicts stale connections from its connection pools.
This covers the component-level DataSource as well as any DataSources resolved by active endpoints
(e.g. `jdbc:myDs`), with identity-based deduplication so each pool is evicted at most once.

Currently HikariCP and Agroal (Quarkus default) pools are supported for active eviction.
HikariCP pools are evicted via `softEvictConnections()` called through `getHikariPoolMXBean()`
via reflection — JMX registration (`registerMbeans=true`) is *not* required.
Agroal pools are evicted via `flush(GRACEFUL)` called on `AgroalDataSource` via reflection.
Other pool implementations are not actively evicted — existing connections
will be replaced as they expire or are validated by the pool.

NOTE: The pool eviction uses reflection to avoid compile-time dependencies on pool libraries.
In GraalVM native mode (e.g. Quarkus native), the reflected classes and methods need to be registered
for reflection: `HikariDataSource.getHikariPoolMXBean()`, `HikariPoolMXBean.softEvictConnections()`
for HikariCP, and `AgroalDataSource.flush(FlushMode)` for Agroal.

IMPORTANT: The eviction only closes existing connections — it does *not* update the pool's credentials.
For pools configured with a static password (e.g. Spring Boot `spring.datasource.password`),
the pool will re-open connections using the _old_ credentials.
This feature works out of the box only with pools that resolve credentials dynamically,
such as `HikariCredentialsProvider`, the AWS JDBC wrapper secrets plugin,
or a custom `DataSource` that fetches credentials from a vault at connect time.
Comment thread
apupier marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1081,3 +1081,29 @@ To use this feature, add the following dependencies to your spring boot pom.xml
----

You should also include the specific database driver, if needed.

== Secret Rotation

The SQL component implements `SecretRotationAware`. When a secret rotation event is triggered
(e.g. by a vault provider), the component evicts stale connections from its connection pools.
This covers the component-level DataSource as well as any DataSources resolved by active endpoints
(e.g. `sql:...?dataSource=#myDs`), with identity-based deduplication so each pool is evicted at most once.

Currently HikariCP and Agroal (Quarkus default) pools are supported for active eviction.
HikariCP pools are evicted via `softEvictConnections()` called through `getHikariPoolMXBean()`
via reflection — JMX registration (`registerMbeans=true`) is *not* required.
Agroal pools are evicted via `flush(GRACEFUL)` called on `AgroalDataSource` via reflection.
Other pool implementations are not actively evicted — existing connections
will be replaced as they expire or are validated by the pool.

NOTE: The pool eviction uses reflection to avoid compile-time dependencies on pool libraries.
In GraalVM native mode (e.g. Quarkus native), the reflected classes and methods need to be registered
for reflection: `HikariDataSource.getHikariPoolMXBean()`, `HikariPoolMXBean.softEvictConnections()`
for HikariCP, and `AgroalDataSource.flush(FlushMode)` for Agroal.

IMPORTANT: The eviction only closes existing connections — it does *not* update the pool's credentials.
For pools configured with a static password (e.g. Spring Boot `spring.datasource.password`),
the pool will re-open connections using the _old_ credentials.
This feature works out of the box only with pools that resolve credentials dynamically,
such as `HikariCredentialsProvider`, the AWS JDBC wrapper secrets plugin,
or a custom `DataSource` that fetches credentials from a vault at connect time.
26 changes: 26 additions & 0 deletions components/camel-jdbc/src/main/docs/jdbc-component.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,29 @@ from("timer://MoveNewCustomersEveryHour?period=3600000")
.setBody(simple("insert into processed_customer values('${body[ID]}','${body[NAME]}')"))
.to("jdbc:testdb");
----

== Secret Rotation

The JDBC component implements `SecretRotationAware`. When a secret rotation event is triggered
(e.g. by a vault provider), the component evicts stale connections from its connection pools.
This covers the component-level DataSource as well as any DataSources resolved by active endpoints
(e.g. `jdbc:myDs`), with identity-based deduplication so each pool is evicted at most once.

Currently HikariCP and Agroal (Quarkus default) pools are supported for active eviction.
HikariCP pools are evicted via `softEvictConnections()` called through `getHikariPoolMXBean()`
via reflection — JMX registration (`registerMbeans=true`) is *not* required.
Agroal pools are evicted via `flush(GRACEFUL)` called on `AgroalDataSource` via reflection.
Other pool implementations are not actively evicted — existing connections
will be replaced as they expire or are validated by the pool.

NOTE: The pool eviction uses reflection to avoid compile-time dependencies on pool libraries.
In GraalVM native mode (e.g. Quarkus native), the reflected classes and methods need to be registered
for reflection: `HikariDataSource.getHikariPoolMXBean()`, `HikariPoolMXBean.softEvictConnections()`
for HikariCP, and `AgroalDataSource.flush(FlushMode)` for Agroal.

IMPORTANT: The eviction only closes existing connections — it does *not* update the pool's credentials.
For pools configured with a static password (e.g. Spring Boot `spring.datasource.password`),
the pool will re-open connections using the _old_ credentials.
This feature works out of the box only with pools that resolve credentials dynamically,
such as `HikariCredentialsProvider`, the AWS JDBC wrapper secrets plugin,
or a custom `DataSource` that fetches credentials from a vault at connect time.
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@
import org.apache.camel.Endpoint;
import org.apache.camel.NoSuchBeanException;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.SecretRotationAware;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.DataSourceHelper;
import org.apache.camel.support.DefaultComponent;
import org.apache.camel.util.PropertiesHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component("jdbc")
public class JdbcComponent extends DefaultComponent {
public class JdbcComponent extends DefaultComponent implements SecretRotationAware {

private static final Logger LOG = LoggerFactory.getLogger(JdbcComponent.class);

Expand Down Expand Up @@ -114,6 +116,16 @@ public void setConnectionStrategy(ConnectionStrategy connectionStrategy) {
this.connectionStrategy = connectionStrategy;
}

@Override
public void onSecretRotation(Object source) throws Exception {
DataSourceHelper.evictComponentDataSources(
this.dataSource,
getCamelContext().getEndpoints(),
this,
ep -> ep instanceof JdbcEndpoint ? ((JdbcEndpoint) ep).getDataSource() : null,
source);
}
Comment thread
gnodet marked this conversation as resolved.

private static boolean isDefaultDataSourceName(String remaining) {
return "dataSource".equalsIgnoreCase(remaining) || "default".equalsIgnoreCase(remaining);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.camel.component.jdbc;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spi.SecretRotationAware;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Verifies that {@link JdbcComponent} implements {@link SecretRotationAware} and correctly evicts stale connections on
* rotation.
*/
class JdbcComponentSecretRotationAwareTest {

@Test
void implementsSecretRotationAware() {
assertInstanceOf(SecretRotationAware.class, new JdbcComponent());
}

@Test
void onSecretRotation_withComponentOwnedDataSource_evictsConnections() throws Exception {
// Arrange: DataSource injected directly on the component
HikariLikeDataSource hikariLike = new HikariLikeDataSource();

JdbcComponent component = new JdbcComponent();
component.setDataSource(hikariLike);
DefaultCamelContext ctx = new DefaultCamelContext();
component.setCamelContext(ctx);

// Act
component.onSecretRotation("vault-rotation");

// Assert
assertTrue(hikariLike.mxBean.softEvictCalled.get(),
"Component-owned DataSource should have been soft-evicted");
}

@Test
void onSecretRotation_withEndpointDataSource_evictsEndpointConnections() throws Exception {
// Arrange: DataSource only on the endpoint (not on the component — the typical jdbc:myDs case)
HikariLikeDataSource endpointDs = new HikariLikeDataSource();

DefaultCamelContext ctx = new DefaultCamelContext();
JdbcComponent component = new JdbcComponent();
component.setCamelContext(ctx);
// component.dataSource stays null — matches the default jdbc:myDs usage

JdbcEndpoint endpoint = new JdbcEndpoint("jdbc:myDs", component, endpointDs);
ctx.addEndpoint("jdbc:myDs", endpoint);

// Act
component.onSecretRotation("vault-rotation");

// Assert
assertTrue(endpointDs.mxBean.softEvictCalled.get(),
"Endpoint-owned DataSource should have been soft-evicted");
}

@Test
void onSecretRotation_withoutDataSource_doesNotThrow() throws Exception {
// Arrange: no DataSource on component
JdbcComponent component = new JdbcComponent();
DefaultCamelContext ctx = new DefaultCamelContext();
component.setCamelContext(ctx);

// Act — must not throw even with no DataSource configured
component.onSecretRotation("vault-rotation");
}

// ---------------------------------------------------------------------------
// DataSource stubs — public so that reflection in DataSourceHelper
// can invoke methods without setAccessible(true)
// ---------------------------------------------------------------------------

/** Simulates a HikariPoolMXBean with a trackable {@code softEvictConnections()} call. */
public static class MockPoolMXBean {
public final AtomicBoolean softEvictCalled = new AtomicBoolean(false);

public void softEvictConnections() {
softEvictCalled.set(true);
}
}

/**
* Simulates a {@code HikariDataSource} by exposing {@code getHikariPoolMXBean()}, which returns a
* {@link MockPoolMXBean}. This matches the real HikariCP API where {@code softEvictConnections()} lives on
* {@code HikariPoolMXBean}, not on {@code HikariDataSource} itself.
*/
public static class HikariLikeDataSource implements DataSource {
public final MockPoolMXBean mxBean = new MockPoolMXBean();

public Object getHikariPoolMXBean() {
return mxBean;
}

@Override
public Connection getConnection() throws SQLException {
throw new UnsupportedOperationException();
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
throw new UnsupportedOperationException();
}

@Override
public PrintWriter getLogWriter() {
return null;
}

@Override
public void setLogWriter(PrintWriter out) {
}

@Override
public void setLoginTimeout(int seconds) {
}

@Override
public int getLoginTimeout() {
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
throw new SQLException("Not a wrapper for " + iface);
}

@Override
public boolean isWrapperFor(Class<?> iface) {
return false;
}
}

}
18 changes: 18 additions & 0 deletions components/camel-sql/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@
<version>${h2-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.agroal</groupId>
<artifactId>agroal-api</artifactId>
<version>${agroal-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.agroal</groupId>
<artifactId>agroal-pool</artifactId>
<version>${agroal-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
Expand Down
26 changes: 26 additions & 0 deletions components/camel-sql/src/main/docs/sql-component.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1081,3 +1081,29 @@ To use this feature, add the following dependencies to your spring boot pom.xml
----

You should also include the specific database driver, if needed.

== Secret Rotation

The SQL component implements `SecretRotationAware`. When a secret rotation event is triggered
(e.g. by a vault provider), the component evicts stale connections from its connection pools.
This covers the component-level DataSource as well as any DataSources resolved by active endpoints
(e.g. `sql:...?dataSource=#myDs`), with identity-based deduplication so each pool is evicted at most once.

Currently HikariCP and Agroal (Quarkus default) pools are supported for active eviction.
HikariCP pools are evicted via `softEvictConnections()` called through `getHikariPoolMXBean()`
via reflection — JMX registration (`registerMbeans=true`) is *not* required.
Agroal pools are evicted via `flush(GRACEFUL)` called on `AgroalDataSource` via reflection.
Other pool implementations are not actively evicted — existing connections
will be replaced as they expire or are validated by the pool.

NOTE: The pool eviction uses reflection to avoid compile-time dependencies on pool libraries.
In GraalVM native mode (e.g. Quarkus native), the reflected classes and methods need to be registered
for reflection: `HikariDataSource.getHikariPoolMXBean()`, `HikariPoolMXBean.softEvictConnections()`
for HikariCP, and `AgroalDataSource.flush(FlushMode)` for Agroal.

IMPORTANT: The eviction only closes existing connections — it does *not* update the pool's credentials.
For pools configured with a static password (e.g. Spring Boot `spring.datasource.password`),
the pool will re-open connections using the _old_ credentials.
This feature works out of the box only with pools that resolve credentials dynamically,
such as `HikariCredentialsProvider`, the AWS JDBC wrapper secrets plugin,
or a custom `DataSource` that fetches credentials from a vault at connect time.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import org.apache.camel.CamelContext;
import org.apache.camel.Endpoint;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.SecretRotationAware;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.DataSourceHelper;
import org.apache.camel.support.HealthCheckComponent;
import org.apache.camel.support.PropertyBindingSupport;
import org.apache.camel.util.ObjectHelper;
Expand All @@ -35,7 +37,7 @@
* queries.
*/
@Component("sql")
public class SqlComponent extends HealthCheckComponent {
public class SqlComponent extends HealthCheckComponent implements SecretRotationAware {

@Metadata(autowired = true)
private DataSource dataSource;
Expand Down Expand Up @@ -151,6 +153,16 @@ protected Endpoint createEndpoint(String uri, String remaining, Map<String, Obje
return endpoint;
}

@Override
public void onSecretRotation(Object source) throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, nothing updates the pool's credentials before this eviction. Hikari re-reads HikariConfig.getCredentials() for each new connection, so with a statically configured password the evicted connections are reopened with the old secret. CAMEL-24638 asks to refresh the credentials the pool uses as well. If that isn't feasible generically, please document the limitation in the sql/jdbc docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd4bc27. Documented the limitation in the Javadoc of evictDataSourceConnections() and in both component docs (jdbc-component.adoc, sql-component.adoc). The docs now clearly state that eviction only helps pools that resolve credentials dynamically (HikariCredentialsProvider, AWS JDBC wrapper, custom vault-aware DataSource). Pools with static passwords will re-open connections with the old credentials.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs updated in cc0bb0f — the credential refresh limitation wording now aligns with the IMPORTANT note across all four doc copies (jdbc/sql source + catalog).

DataSourceHelper.evictComponentDataSources(
this.dataSource,
getCamelContext().getEndpoints(),
this,
ep -> ep instanceof DefaultSqlEndpoint ? ((DefaultSqlEndpoint) ep).getDataSource() : null,
source);
}
Comment thread
gnodet marked this conversation as resolved.

/**
* Sets the DataSource to use to communicate with the database.
*/
Expand Down
Loading
Loading