This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 3e0b456f3561 CAMEL-20199: Replace synchronized with ReentrantLock in
OAuth and security components (#25246)
3e0b456f3561 is described below
commit 3e0b456f35617c0b5ce0e153ee4b1e1a26f4d39f
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Jul 31 07:35:08 2026 +0200
CAMEL-20199: Replace synchronized with ReentrantLock in OAuth and security
components (#25246)
CAMEL-20199: Replace synchronized with ReentrantLock in OAuth and security
components
Convert synchronized blocks to ReentrantLock in camel-oauth
(DefaultOAuthTokenValidationFactory, JwksCache), camel-keycloak
(KeycloakPublicKeyResolver), and camel-pqc (KeyRotationScheduler)
to eliminate virtual thread pinning.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../security/KeycloakPublicKeyResolver.java | 41 ++++++++------
.../oauth/DefaultOAuthTokenValidationFactory.java | 10 +++-
.../java/org/apache/camel/oauth/JwksCache.java | 17 ++++--
.../pqc/lifecycle/KeyRotationScheduler.java | 66 ++++++++++++----------
4 files changed, 80 insertions(+), 54 deletions(-)
diff --git
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakPublicKeyResolver.java
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakPublicKeyResolver.java
index 8381685047b9..745a92b0c69b 100644
---
a/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakPublicKeyResolver.java
+++
b/components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakPublicKeyResolver.java
@@ -27,6 +27,7 @@ import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpGet;
@@ -46,6 +47,7 @@ public class KeycloakPublicKeyResolver {
private final String serverUrl;
private final String realm;
private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
+ private final ReentrantLock lock = new ReentrantLock();
private volatile long lastRefreshTime = 0;
private static final long CACHE_REFRESH_INTERVAL_MS = 300_000; // 5 minutes
@@ -106,24 +108,29 @@ public class KeycloakPublicKeyResolver {
/**
* Refreshes the public keys from the JWKS endpoint.
*/
- public synchronized void refreshKeys() throws IOException {
- String jwksUrl =
String.format("%s/realms/%s/protocol/openid-connect/certs", serverUrl, realm);
- LOG.debug("Fetching public keys from: {}", jwksUrl);
-
- try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
- HttpGet request = new HttpGet(jwksUrl);
-
- String responseBody = httpClient.execute(request, response -> {
- int statusCode = response.getCode();
- if (statusCode != 200) {
- throw new IOException("Failed to fetch JWKS: HTTP " +
statusCode);
- }
- return EntityUtils.toString(response.getEntity());
- });
+ public void refreshKeys() throws IOException {
+ lock.lock();
+ try {
+ String jwksUrl =
String.format("%s/realms/%s/protocol/openid-connect/certs", serverUrl, realm);
+ LOG.debug("Fetching public keys from: {}", jwksUrl);
+
+ try (CloseableHttpClient httpClient = HttpClients.createDefault())
{
+ HttpGet request = new HttpGet(jwksUrl);
+
+ String responseBody = httpClient.execute(request, response -> {
+ int statusCode = response.getCode();
+ if (statusCode != 200) {
+ throw new IOException("Failed to fetch JWKS: HTTP " +
statusCode);
+ }
+ return EntityUtils.toString(response.getEntity());
+ });
- parseJwks(responseBody);
- lastRefreshTime = System.currentTimeMillis();
- LOG.debug("Successfully loaded {} public keys from JWKS endpoint",
keyCache.size());
+ parseJwks(responseBody);
+ lastRefreshTime = System.currentTimeMillis();
+ LOG.debug("Successfully loaded {} public keys from JWKS
endpoint", keyCache.size());
+ }
+ } finally {
+ lock.unlock();
}
}
diff --git
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactory.java
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactory.java
index 07eaecc907ea..318e87c96ace 100644
---
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactory.java
+++
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/DefaultOAuthTokenValidationFactory.java
@@ -27,6 +27,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.LongSupplier;
import com.google.gson.JsonArray;
@@ -58,7 +59,7 @@ public class DefaultOAuthTokenValidationFactory implements
OAuthTokenValidationF
private static final long MIN_DISCOVERY_RETRY_INTERVAL_MILLIS = 30_000L;
private static final ConcurrentMap<String, DiscoveryCacheEntry>
DISCOVERY_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, FailureRecord>
DISCOVERY_FAILURES = new ConcurrentHashMap<>();
- private static final ConcurrentMap<String, Object> DISCOVERY_LOCKS = new
ConcurrentHashMap<>();
+ private static final ConcurrentMap<String, ReentrantLock> DISCOVERY_LOCKS
= new ConcurrentHashMap<>();
private static volatile LongSupplier currentTimeMillis =
System::currentTimeMillis;
@Override
@@ -279,8 +280,9 @@ public class DefaultOAuthTokenValidationFactory implements
OAuthTokenValidationF
return cached.json;
}
- Object lock = DISCOVERY_LOCKS.computeIfAbsent(discoveryUrl, key -> new
Object());
- synchronized (lock) {
+ ReentrantLock lock = DISCOVERY_LOCKS.computeIfAbsent(discoveryUrl, key
-> new ReentrantLock());
+ lock.lock();
+ try {
now = now();
cached = DISCOVERY_CACHE.get(discoveryUrl);
if (cached != null &&
!cached.isExpired(config.getOidcDiscoveryCacheTtlSeconds(), now)) {
@@ -308,6 +310,8 @@ public class DefaultOAuthTokenValidationFactory implements
OAuthTokenValidationF
DISCOVERY_FAILURES.put(discoveryUrl, new FailureRecord(now,
e));
throw new OAuthException("Failed to discover OIDC endpoints
from " + discoveryUrl, e);
}
+ } finally {
+ lock.unlock();
}
}
diff --git
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java
index fef379ce1e74..f781b17f7cc9 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/JwksCache.java
@@ -20,6 +20,7 @@ import java.net.URI;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.LongSupplier;
import com.nimbusds.jose.jwk.JWKSet;
@@ -45,7 +46,7 @@ final class JwksCache {
private final ConcurrentMap<String, CacheEntry> cache = new
ConcurrentHashMap<>();
private final ConcurrentMap<String, Long> forcedRefreshAttempts = new
ConcurrentHashMap<>();
private final ConcurrentMap<String, FailureRecord> normalRefreshFailures =
new ConcurrentHashMap<>();
- private final ConcurrentMap<String, Object> refreshLocks = new
ConcurrentHashMap<>();
+ private final ConcurrentMap<String, ReentrantLock> refreshLocks = new
ConcurrentHashMap<>();
private volatile LongSupplier currentTimeMillis =
System::currentTimeMillis;
private JwksCache() {
@@ -64,8 +65,9 @@ final class JwksCache {
}
JWKSet refreshJwkSet(String jwksEndpoint, int connectTimeoutMs, int
readTimeoutMs) {
- Object lock = refreshLocks.computeIfAbsent(jwksEndpoint, key -> new
Object());
- synchronized (lock) {
+ ReentrantLock lock = refreshLocks.computeIfAbsent(jwksEndpoint, key ->
new ReentrantLock());
+ lock.lock();
+ try {
CacheEntry existing = cache.get(jwksEndpoint);
long now = now();
if (existing != null && !canAttemptRefresh(jwksEndpoint, now)) {
@@ -78,12 +80,15 @@ final class JwksCache {
normalRefreshFailures.remove(jwksEndpoint);
cache.put(jwksEndpoint, refreshed);
return refreshed.jwkSet;
+ } finally {
+ lock.unlock();
}
}
private JWKSet fetchAndCache(String jwksEndpoint, long ttlSeconds, int
connectTimeoutMs, int readTimeoutMs) {
- Object lock = refreshLocks.computeIfAbsent(jwksEndpoint, key -> new
Object());
- synchronized (lock) {
+ ReentrantLock lock = refreshLocks.computeIfAbsent(jwksEndpoint, key ->
new ReentrantLock());
+ lock.lock();
+ try {
CacheEntry existing = cache.get(jwksEndpoint);
long now = now();
if (existing != null && !existing.isExpired(ttlSeconds, now)) {
@@ -113,6 +118,8 @@ final class JwksCache {
}
throw e;
}
+ } finally {
+ lock.unlock();
}
}
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyRotationScheduler.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyRotationScheduler.java
index d936576a43b3..93a319f86347 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyRotationScheduler.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyRotationScheduler.java
@@ -25,6 +25,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -79,6 +80,7 @@ public class KeyRotationScheduler extends ServiceSupport
implements CamelContext
private Function<KeyMetadata, String> keyIdStrategy =
KeyRotationScheduler::defaultRotatedKeyId;
private KeyRotationListener listener;
+ private final ReentrantLock lock = new ReentrantLock();
private final AtomicLong checksPerformed = new AtomicLong();
private final AtomicLong rotationsPerformed = new AtomicLong();
private final AtomicLong rotationFailures = new AtomicLong();
@@ -110,41 +112,47 @@ public class KeyRotationScheduler extends ServiceSupport
implements CamelContext
*
* @return the number of keys rotated during this pass
*/
- public synchronized int checkAndRotate() {
- checksPerformed.incrementAndGet();
- lastCheckAt = Instant.now();
-
- List<KeyMetadata> keys;
+ public int checkAndRotate() {
+ lock.lock();
try {
- keys = keyManager.listKeys();
- } catch (Exception e) {
- LOG.warn("Failed to list keys during rotation check", e);
- notifyError(null, e);
- return 0;
- }
+ checksPerformed.incrementAndGet();
+ lastCheckAt = Instant.now();
- int rotated = 0;
- for (KeyMetadata metadata : keys) {
- if (metadata == null || !keyFilter.test(metadata)) {
- continue;
- }
- String keyId = metadata.getKeyId();
+ List<KeyMetadata> keys;
try {
- if (keyManager.needsRotation(keyId, maxKeyAge, maxKeyUsage)) {
- String newKeyId = keyIdStrategy.apply(metadata);
- LOG.info("Rotating PQC key '{}' -> '{}' (algorithm={})",
keyId, newKeyId, metadata.getAlgorithm());
- KeyPair newKeyPair = keyManager.rotateKey(keyId, newKeyId,
metadata.getAlgorithm());
- rotationsPerformed.incrementAndGet();
- rotated++;
- notifyRotated(keyId, newKeyId, metadata, newKeyPair);
- }
+ keys = keyManager.listKeys();
} catch (Exception e) {
- rotationFailures.incrementAndGet();
- LOG.warn("Failed to rotate PQC key '{}'", keyId, e);
- notifyError(keyId, e);
+ LOG.warn("Failed to list keys during rotation check", e);
+ notifyError(null, e);
+ return 0;
+ }
+
+ int rotated = 0;
+ for (KeyMetadata metadata : keys) {
+ if (metadata == null || !keyFilter.test(metadata)) {
+ continue;
+ }
+ String keyId = metadata.getKeyId();
+ try {
+ if (keyManager.needsRotation(keyId, maxKeyAge,
maxKeyUsage)) {
+ String newKeyId = keyIdStrategy.apply(metadata);
+ LOG.info("Rotating PQC key '{}' -> '{}'
(algorithm={})", keyId, newKeyId,
+ metadata.getAlgorithm());
+ KeyPair newKeyPair = keyManager.rotateKey(keyId,
newKeyId, metadata.getAlgorithm());
+ rotationsPerformed.incrementAndGet();
+ rotated++;
+ notifyRotated(keyId, newKeyId, metadata, newKeyPair);
+ }
+ } catch (Exception e) {
+ rotationFailures.incrementAndGet();
+ LOG.warn("Failed to rotate PQC key '{}'", keyId, e);
+ notifyError(keyId, e);
+ }
}
+ return rotated;
+ } finally {
+ lock.unlock();
}
- return rotated;
}
@Override