From 8656c53dafdc9e54cf069e276839a500e1e165e9 Mon Sep 17 00:00:00 2001 From: jarebudev <23311805+jarebudev@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:30:17 +0000 Subject: [PATCH 1/2] added a configurable thread factory to allow naming of threads Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com> --- .../dev/openfeature/sdk/EventProvider.java | 4 +- .../dev/openfeature/sdk/EventSupport.java | 4 +- .../openfeature/sdk/ProviderRepository.java | 8 +- .../internal/ConfigurableThreadFactory.java | 58 ++++++++++++++ .../ConfigurableThreadFactoryTest.java | 79 +++++++++++++++++++ 5 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java create mode 100644 src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java diff --git a/src/main/java/dev/openfeature/sdk/EventProvider.java b/src/main/java/dev/openfeature/sdk/EventProvider.java index 0d7e897c2..4ccac184e 100644 --- a/src/main/java/dev/openfeature/sdk/EventProvider.java +++ b/src/main/java/dev/openfeature/sdk/EventProvider.java @@ -1,5 +1,6 @@ package dev.openfeature.sdk; +import dev.openfeature.sdk.internal.ConfigurableThreadFactory; import dev.openfeature.sdk.internal.TriConsumer; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -21,7 +22,8 @@ @Slf4j public abstract class EventProvider implements FeatureProvider { private EventProviderListener eventProviderListener; - private final ExecutorService emitterExecutor = Executors.newCachedThreadPool(); + private final ExecutorService emitterExecutor = + Executors.newCachedThreadPool(new ConfigurableThreadFactory("openfeature-event-emitter-thread")); void setEventProviderListener(EventProviderListener eventProviderListener) { this.eventProviderListener = eventProviderListener; diff --git a/src/main/java/dev/openfeature/sdk/EventSupport.java b/src/main/java/dev/openfeature/sdk/EventSupport.java index 8396795bd..0b446c6b2 100644 --- a/src/main/java/dev/openfeature/sdk/EventSupport.java +++ b/src/main/java/dev/openfeature/sdk/EventSupport.java @@ -1,5 +1,6 @@ package dev.openfeature.sdk; +import dev.openfeature.sdk.internal.ConfigurableThreadFactory; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -26,7 +27,8 @@ class EventSupport { private static final String DEFAULT_CLIENT_UUID = UUID.randomUUID().toString(); private final Map handlerStores = new ConcurrentHashMap<>(); private final HandlerStore globalHandlerStore = new HandlerStore(); - private final ExecutorService taskExecutor = Executors.newCachedThreadPool(); + private final ExecutorService taskExecutor = + Executors.newCachedThreadPool(new ConfigurableThreadFactory("openfeature-event-handler-thread")); /** * Run all the event handlers associated with this domain. diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index ab024a750..147074a58 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -2,6 +2,7 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; +import dev.openfeature.sdk.internal.ConfigurableThreadFactory; import java.util.List; import java.util.Map; import java.util.Optional; @@ -22,11 +23,8 @@ class ProviderRepository { private final Map stateManagers = new ConcurrentHashMap<>(); private final AtomicReference defaultStateManger = new AtomicReference<>(new FeatureProviderStateManager(new NoOpProvider())); - private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> { - final Thread thread = new Thread(runnable); - thread.setDaemon(true); - return thread; - }); + private final ExecutorService taskExecutor = + Executors.newCachedThreadPool(new ConfigurableThreadFactory("openfeature-provider-thread", true)); private final Object registerStateManagerLock = new Object(); private final OpenFeatureAPI openFeatureAPI; diff --git a/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java b/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java new file mode 100644 index 000000000..f26bc8813 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java @@ -0,0 +1,58 @@ +package dev.openfeature.sdk.internal; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A configurable thread factory for internal use in the SDK. + * Allows daemon or non-daemon threads to be created with a custom name prefix, + * and a delegate ThreadFactory if required. + */ +public final class ConfigurableThreadFactory implements ThreadFactory { + + private final AtomicInteger counter = new AtomicInteger(); + private final String namePrefix; + private final ThreadFactory delegate; + private final boolean daemon; + + /** + * {@link ConfigurableThreadFactory}'s constructor. + * + * @param namePrefix Prefix used for setting the new thread's name. + */ + public ConfigurableThreadFactory(String namePrefix) { + this(namePrefix, Executors.defaultThreadFactory(), false); + } + + /** + * {@link ConfigurableThreadFactory}'s constructor. + * + * @param namePrefix Prefix used for setting the new thread's name. + * @param daemon Whether daemon or non-daemon threads will be created. + */ + public ConfigurableThreadFactory(String namePrefix, boolean daemon) { + this(namePrefix, Executors.defaultThreadFactory(), daemon); + } + + /** + * {@link ConfigurableThreadFactory}'s constructor. + * + * @param namePrefix Prefix used for setting the new thread's name. + * @param delegate Delegate ThreadFactory for creating threads. + * @param daemon Whether daemon or non-daemon threads will be created. + */ + public ConfigurableThreadFactory(String namePrefix, ThreadFactory delegate, boolean daemon) { + this.namePrefix = namePrefix; + this.delegate = delegate; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable runnable) { + final Thread thread = delegate.newThread(runnable); + thread.setDaemon(daemon); + thread.setName(namePrefix + "-" + counter.incrementAndGet()); + return thread; + } +} diff --git a/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java b/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java new file mode 100644 index 000000000..10ce00610 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java @@ -0,0 +1,79 @@ +package dev.openfeature.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ThreadFactory; +import org.junit.jupiter.api.Test; + +class ConfigurableThreadFactoryTest { + + private static final String THREAD_NAME = "testthread"; + private final Runnable runnable = new Runnable() { + @Override + public void run() {} + }; + + @Test + void verifyNewThreadHasNamePrefix() { + + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME); + var thread = configurableThreadFactory.newThread(runnable); + + assertThat(thread.getName()).isEqualTo(THREAD_NAME + "-1"); + assertThat(thread.isDaemon()).isFalse(); + } + + @Test + void verifyNewThreadHasNamePrefixWithIncrement() { + + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME); + var threadOne = configurableThreadFactory.newThread(runnable); + var threadTwo = configurableThreadFactory.newThread(runnable); + + assertThat(threadOne.getName()).isEqualTo(THREAD_NAME + "-1"); + assertThat(threadTwo.getName()).isEqualTo(THREAD_NAME + "-2"); + } + + @Test + void verifyNewThreadWithDelegateHasNamePrefix() { + + var threadFactoryMock = mock(ThreadFactory.class); + var threadMock = mock(Thread.class); + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME, threadFactoryMock, false); + when(threadFactoryMock.newThread(eq(runnable))).thenReturn(threadMock); + + var thread = configurableThreadFactory.newThread(runnable); + + verify(threadMock, times(1)).setName(THREAD_NAME + "-1"); + verify(threadMock, times(1)).setDaemon(false); + } + + @Test + void verifyNewDaemonThreadWithDelegateHasNamePrefix() { + + var threadFactoryMock = mock(ThreadFactory.class); + var threadMock = mock(Thread.class); + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME, threadFactoryMock, true); + when(threadFactoryMock.newThread(eq(runnable))).thenReturn(threadMock); + + var thread = configurableThreadFactory.newThread(runnable); + + verify(threadMock, times(1)).setName(THREAD_NAME + "-1"); + verify(threadMock, times(1)).setDaemon(true); + } + + @Test + void verifyNewDaemonThreadHasNamePrefix() { + + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME, true); + var thread = configurableThreadFactory.newThread(runnable); + + assertThat(thread.getName()).isEqualTo(THREAD_NAME + "-1"); + assertThat(thread.isDaemon()).isTrue(); + } +} From f07ae2bf709d43e539dccfb4b6bc9f1e70eb1e7d Mon Sep 17 00:00:00 2001 From: jarebudev <23311805+jarebudev@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:06:27 +0000 Subject: [PATCH 2/2] tidy up Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com> --- .../internal/ConfigurableThreadFactory.java | 21 ++-------- .../ConfigurableThreadFactoryTest.java | 39 +------------------ 2 files changed, 4 insertions(+), 56 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java b/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java index f26bc8813..8d5e77db8 100644 --- a/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java +++ b/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java @@ -1,19 +1,16 @@ package dev.openfeature.sdk.internal; -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** * A configurable thread factory for internal use in the SDK. - * Allows daemon or non-daemon threads to be created with a custom name prefix, - * and a delegate ThreadFactory if required. + * Allows daemon or non-daemon threads to be created with a custom name prefix. */ public final class ConfigurableThreadFactory implements ThreadFactory { private final AtomicInteger counter = new AtomicInteger(); private final String namePrefix; - private final ThreadFactory delegate; private final boolean daemon; /** @@ -22,7 +19,7 @@ public final class ConfigurableThreadFactory implements ThreadFactory { * @param namePrefix Prefix used for setting the new thread's name. */ public ConfigurableThreadFactory(String namePrefix) { - this(namePrefix, Executors.defaultThreadFactory(), false); + this(namePrefix, false); } /** @@ -32,25 +29,13 @@ public ConfigurableThreadFactory(String namePrefix) { * @param daemon Whether daemon or non-daemon threads will be created. */ public ConfigurableThreadFactory(String namePrefix, boolean daemon) { - this(namePrefix, Executors.defaultThreadFactory(), daemon); - } - - /** - * {@link ConfigurableThreadFactory}'s constructor. - * - * @param namePrefix Prefix used for setting the new thread's name. - * @param delegate Delegate ThreadFactory for creating threads. - * @param daemon Whether daemon or non-daemon threads will be created. - */ - public ConfigurableThreadFactory(String namePrefix, ThreadFactory delegate, boolean daemon) { this.namePrefix = namePrefix; - this.delegate = delegate; this.daemon = daemon; } @Override public Thread newThread(Runnable runnable) { - final Thread thread = delegate.newThread(runnable); + final Thread thread = new Thread(runnable); thread.setDaemon(daemon); thread.setName(namePrefix + "-" + counter.incrementAndGet()); return thread; diff --git a/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java b/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java index 10ce00610..0de360ae6 100644 --- a/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java +++ b/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java @@ -1,22 +1,13 @@ package dev.openfeature.sdk.internal; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import java.util.concurrent.ThreadFactory; import org.junit.jupiter.api.Test; class ConfigurableThreadFactoryTest { private static final String THREAD_NAME = "testthread"; - private final Runnable runnable = new Runnable() { - @Override - public void run() {} - }; + private final Runnable runnable = () -> {}; @Test void verifyNewThreadHasNamePrefix() { @@ -39,34 +30,6 @@ void verifyNewThreadHasNamePrefixWithIncrement() { assertThat(threadTwo.getName()).isEqualTo(THREAD_NAME + "-2"); } - @Test - void verifyNewThreadWithDelegateHasNamePrefix() { - - var threadFactoryMock = mock(ThreadFactory.class); - var threadMock = mock(Thread.class); - var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME, threadFactoryMock, false); - when(threadFactoryMock.newThread(eq(runnable))).thenReturn(threadMock); - - var thread = configurableThreadFactory.newThread(runnable); - - verify(threadMock, times(1)).setName(THREAD_NAME + "-1"); - verify(threadMock, times(1)).setDaemon(false); - } - - @Test - void verifyNewDaemonThreadWithDelegateHasNamePrefix() { - - var threadFactoryMock = mock(ThreadFactory.class); - var threadMock = mock(Thread.class); - var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME, threadFactoryMock, true); - when(threadFactoryMock.newThread(eq(runnable))).thenReturn(threadMock); - - var thread = configurableThreadFactory.newThread(runnable); - - verify(threadMock, times(1)).setName(THREAD_NAME + "-1"); - verify(threadMock, times(1)).setDaemon(true); - } - @Test void verifyNewDaemonThreadHasNamePrefix() {