diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CacheKeyGenerator.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CacheKeyGenerator.java index addb1d20..ccbf7b54 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CacheKeyGenerator.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CacheKeyGenerator.java @@ -31,6 +31,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; /** * @author Marta Medio * @author Ignacio Lozano + * @author Simone Gerevini */ public class CacheKeyGenerator { @@ -38,19 +39,21 @@ public class CacheKeyGenerator { private static final byte[] KEY_SEPARATOR_BYTES = KEY_SEPARATOR.getBytes(); - private final MessageDigest messageDigest; + private final ThreadLocal messageDigest; /* for testing */ static final List DEFAULT_KEY_VALUE_GENERATORS = List.of( new UriKeyValueGenerator(), new HeaderKeyValueGenerator(HttpHeaders.AUTHORIZATION, KEY_SEPARATOR), new CookiesKeyValueGenerator(KEY_SEPARATOR)); public CacheKeyGenerator() { - try { - messageDigest = MessageDigest.getInstance("MD5"); - } - catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Error creating CacheKeyGenerator", e); - } + messageDigest = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Error creating CacheKeyGenerator", e); + } + }); } public String generateMetadataKey(ServerHttpRequest request, String... varyHeaders) { @@ -63,7 +66,7 @@ public class CacheKeyGenerator { public String generateKey(ServerHttpRequest request, List varyHeaders) { byte[] rawKey = generateRawKey(request, varyHeaders); - byte[] digest = messageDigest.digest(rawKey); + byte[] digest = messageDigest.get().digest(rawKey); return Base64.getEncoder().encodeToString(digest); } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java index b22e9e2b..260b93a0 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java @@ -16,7 +16,11 @@ package org.springframework.cloud.gateway.filter.factory.cache; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import org.junit.jupiter.api.Test; @@ -30,6 +34,7 @@ import static org.springframework.http.HttpHeaders.AUTHORIZATION; /** * @author Ignacio Lozano + * @author Simone Gerevini */ class CacheKeyGeneratorTest { @@ -201,4 +206,49 @@ class CacheKeyGeneratorTest { assertThat(keyWithoutVary).isEqualTo(keyWithFirstVary); } + @Test + public void shouldNotFailWhenRunningInParallel() throws InterruptedException { + MockServerHttpRequest request = MockServerHttpRequest.get("http://this").build(); + int numberOfThreads = 100; + + List exceptions = executeInParallel(Executors.newFixedThreadPool(numberOfThreads), numberOfThreads, + () -> cacheKeyGenerator.generateKey(request)); + + assertThat(exceptions.size()).isEqualTo(0); + } + + private List executeInParallel(Executor executor, int nThreads, Runnable action) + throws InterruptedException { + CountDownLatch ready = new CountDownLatch(nThreads); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(nThreads); + List exceptions = new ArrayList<>(nThreads); + + for (int i = 0; i < nThreads; i++) { + executor.execute(() -> { + ready.countDown(); + try { + start.await(); + action.run(); + + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + catch (RuntimeException e) { + exceptions.add(e); + } + finally { + done.countDown(); + } + }); + } + + ready.await(); + start.countDown(); + done.await(); + + return exceptions; + } + }