From cebd1fde67d67cb82fefb8a541e83a980938b062 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 8 Jun 2021 14:07:32 +0200 Subject: [PATCH] GH-707 Fix Concurrent Modification exception on lookup Rare condition but it appears that in some runtimes there could be multiple threads invoking lookup operation Added test to validate Resolves #707 --- .../catalog/SimpleFunctionRegistry.java | 4 ++-- ...BeanFactoryAwareFunctionRegistryTests.java | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java index eae913ab4..88fcd34ea 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java @@ -25,12 +25,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; @@ -90,7 +90,7 @@ public class SimpleFunctionRegistry implements FunctionRegistry, FunctionInspect private final Field headersField; - private final Set> functionRegistrations = new HashSet<>(); + private final Set> functionRegistrations = new CopyOnWriteArraySet<>(); private final Map wrappedFunctionDefinitions = new HashMap<>(); diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java index 0b725f06d..ac222d1fd 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java @@ -26,6 +26,10 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; @@ -626,7 +630,26 @@ public class BeanFactoryAwareFunctionRegistryTests { assertThat(result.size()).isEqualTo(3); } + @Test + // see GH-707 + public void testConcurrencyOnLookup() throws Exception { + AtomicInteger counter = new AtomicInteger(); + ExecutorService executor = Executors.newFixedThreadPool(10); + for (int i = 0; i < 10; i++) { + FunctionCatalog catalog = this.configureCatalog(SampleFunctionConfiguration.class); + for (int y = 0; y < 10; y++) { + executor.execute(() -> { + assertThat((FunctionInvocationWrapper) catalog.lookup("uppercase|reverse", "application/json")).isNotNull(); + counter.incrementAndGet(); + }); + } + } + + executor.shutdown(); + executor.awaitTermination(10000, TimeUnit.MILLISECONDS); + assertThat(counter.get()).isEqualTo(100); + } @EnableAutoConfiguration public static class PojoToMessageFunctionCompositionConfiguration {