From 9d7c006dcbfb4400e5cf8b52068222b415540fa6 Mon Sep 17 00:00:00 2001 From: "Breno A." Date: Tue, 30 Apr 2024 11:33:44 -0300 Subject: [PATCH] feat: optional thread executor --- docs/.vitepress/config.mts | 34 +++--- docs/src/examples/index.md | 1 + docs/src/guide/threading.md | 31 +++++ .../brenoepics/at4j/AzureApiBuilder.java | 23 +++- .../brenoepics/at4j/core/AzureApiImpl.java | 6 +- .../at4j/core/thread/ThreadPool.java | 57 +-------- .../at4j/core/thread/ThreadPoolImpl.java | 114 ++++-------------- .../at4j/core/thread/ThreadPoolImplTest.java | 67 +++------- 8 files changed, 123 insertions(+), 210 deletions(-) create mode 100644 docs/src/guide/threading.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 887e8822..f78875df 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,5 +1,5 @@ -import { defineConfigWithTheme } from 'vitepress' -import type { ThemeConfig } from 'vitepress-carbon' +import {defineConfigWithTheme} from 'vitepress' +import type {ThemeConfig} from 'vitepress-carbon' import baseConfig from 'vitepress-carbon/config' const nav = [ @@ -7,19 +7,19 @@ const nav = [ text: 'Docs', activeMatch: `^/(guide|examples)/`, items: [ - { text: 'Guide', link: '/guide/introduction' }, - { text: 'Examples', link: '/examples/' }, - { text: 'Error Reference', link: '/error-reference/' }, - { text: 'JavaDoc', link: 'https://brenoepics.github.io/at4j/javadoc/' } + {text: 'Guide', link: '/guide/introduction'}, + {text: 'Examples', link: '/examples/'}, + {text: 'Error Reference', link: '/error-reference/'}, + {text: 'JavaDoc', link: 'https://brenoepics.github.io/at4j/javadoc/'} ] }, { text: 'About', activeMatch: `^/about/`, items: [ - { text: 'FAQ', link: '/about/faq' }, - { text: 'Releases', link: '/about/releases' }, - { text: 'Code of Conduct', link: '/about/coc' } + {text: 'FAQ', link: '/about/faq'}, + {text: 'Releases', link: '/about/releases'}, + {text: 'Code of Conduct', link: '/about/coc'} ] }, ] @@ -28,7 +28,7 @@ const sidebar = { { text: 'Getting Started', items: [ - { text: 'Introduction', link: '/guide/introduction' }, + {text: 'Introduction', link: '/guide/introduction'}, { text: 'Download/Installation', link: '/guide/installation' @@ -41,6 +41,10 @@ const sidebar = { text: 'Basic Usage', link: '/guide/basic-usage' }, + { + text: 'Threading', + link: '/guide/threading' + }, { text: 'Examples', link: '/examples/' @@ -79,10 +83,10 @@ export default defineConfigWithTheme({ base: '/at4j/', head: [ - ['meta', { name: 'theme-color', content: '#3c8772' }], - ['meta', { property: 'og:url', content: 'https://github.com/brenoepics/at4j' }], - ['meta', { property: 'og:type', content: 'Repository' }], - ['meta', { property: 'og:title', content: 'AT4J' }], + ['meta', {name: 'theme-color', content: '#3c8772'}], + ['meta', {property: 'og:url', content: 'https://github.com/brenoepics/at4j'}], + ['meta', {property: 'og:type', content: 'Repository'}], + ['meta', {property: 'og:title', content: 'AT4J'}], [ 'meta', { @@ -109,7 +113,7 @@ export default defineConfigWithTheme({ }, link: 'https://www.postman.com/maintenance-astronaut-2993290/workspace/brenoepics/collection/18589822-dfe7a640-9b94-47a8-b19f-46cb9cc8843e?action=share&creator=18589822' }, - { icon: 'github', link: 'https://github.com/brenoepics/at4j' } + {icon: 'github', link: 'https://github.com/brenoepics/at4j'} ], diff --git a/docs/src/examples/index.md b/docs/src/examples/index.md index 2c6c53bb..74253039 100644 --- a/docs/src/examples/index.md +++ b/docs/src/examples/index.md @@ -66,3 +66,4 @@ public class ExampleDetector { } } ``` + diff --git a/docs/src/guide/threading.md b/docs/src/guide/threading.md new file mode 100644 index 00000000..de137047 --- /dev/null +++ b/docs/src/guide/threading.md @@ -0,0 +1,31 @@ +# Creating an AzureApi with a Custom ExecutorService + +This guide will walk you through the process of creating an `AzureApiBuilder` with a custom `ExecutorService`. + +## Steps + +1. **Create a custom ExecutorService** + + For this example, we'll create a custom `ExecutorService` using the Virtual Threads API introduced in Java 21: + + ```java + ExecutorService customExecutorService = Executors.newVirtualThreadPerTaskExecutor(); + ``` + +2. **Create an AzureApiBuilder** + + You can create an `AzureApi` by using the `AzureApiBuilder` class. Here's an example: + + ```java + String azureKey = ""; + String azureRegion = ""; + + AzureApi azureApi = new AzureApiBuilder() + .setKey(azureKey) + .region(azureRegion) + .executorService(customExecutorService).build(); + ``` + This will create an `AzureApi` with the settings you specified in the `AzureApiBuilder`. + +That's it! You've successfully created an `AzureApi` with a custom `ExecutorService`. You can now use this `AzureApi` to +make requests to the Azure API. diff --git a/src/main/java/io/github/brenoepics/at4j/AzureApiBuilder.java b/src/main/java/io/github/brenoepics/at4j/AzureApiBuilder.java index f2f83a22..0f886ebf 100644 --- a/src/main/java/io/github/brenoepics/at4j/AzureApiBuilder.java +++ b/src/main/java/io/github/brenoepics/at4j/AzureApiBuilder.java @@ -9,6 +9,7 @@ import java.net.ProxySelector; import java.net.http.HttpClient; import java.time.Duration; +import java.util.concurrent.ExecutorService; /** * Builder class for constructing instances of AzureApi. @@ -23,6 +24,7 @@ public class AzureApiBuilder { private SSLContext sslContext; private SSLParameters sslParameters; private Duration connectTimeout; + private ExecutorService executorService; /** Default constructor initializes the base URL to the global endpoint. */ public AzureApiBuilder() { @@ -125,6 +127,21 @@ public AzureApiBuilder sslParameters(SSLParameters sslParameters) { return this; } + + /** + * Sets the executor service for the Azure API. + * + * @param executorService The executor service for the Azure API. + * @return The current instance of AzureApiBuilder for method chaining. + * @see ExecutorService + */ + public AzureApiBuilder executorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + /** * Builds and returns an instance of AzureApi with the configured parameters. * @@ -156,6 +173,10 @@ public AzureApi build() { httpClient.connectTimeout(connectTimeout); } - return new AzureApiImpl<>(httpClient.build(), baseURL, subscriptionKey, subscriptionRegion); + if (executorService != null) { + httpClient.executor(executorService); + } + + return new AzureApiImpl<>(httpClient.build(), baseURL, subscriptionKey, subscriptionRegion, executorService); } } diff --git a/src/main/java/io/github/brenoepics/at4j/core/AzureApiImpl.java b/src/main/java/io/github/brenoepics/at4j/core/AzureApiImpl.java index 839ca257..26f34e77 100644 --- a/src/main/java/io/github/brenoepics/at4j/core/AzureApiImpl.java +++ b/src/main/java/io/github/brenoepics/at4j/core/AzureApiImpl.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; /** * This class is an implementation of the AzureApi interface. It provides methods to interact with @@ -45,7 +46,7 @@ public class AzureApiImpl implements AzureApi { private final RateLimitManager ratelimitManager = new RateLimitManager<>(this); /** The thread pool which is used internally. */ - private final ThreadPoolImpl threadPool = new ThreadPoolImpl(); + private final ThreadPool threadPool; /** * Constructor for AzureApiImpl. @@ -56,11 +57,12 @@ public class AzureApiImpl implements AzureApi { * @param subscriptionRegion The subscription region for this instance. */ public AzureApiImpl( - HttpClient httpClient, BaseURL baseURL, String subscriptionKey, String subscriptionRegion) { + HttpClient httpClient, BaseURL baseURL, String subscriptionKey, String subscriptionRegion, ExecutorService executor) { this.httpClient = httpClient; this.baseURL = baseURL; this.subscriptionKey = subscriptionKey; this.subscriptionRegion = subscriptionRegion; + this.threadPool = new ThreadPoolImpl(executor); } @Override diff --git a/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPool.java b/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPool.java index 96f1abf2..613ac2f8 100644 --- a/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPool.java +++ b/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPool.java @@ -18,61 +18,8 @@ public interface ThreadPool { ExecutorService getExecutorService(); /** - * Gets the used scheduler. + * Shutdowns the thread pool. * - * @return The used scheduler. */ - ScheduledExecutorService getScheduler(); - - /** - * Gets the used daemon scheduler. - * - * @return The used daemon scheduler. - */ - ScheduledExecutorService getDaemonScheduler(); - - /** - * Gets an executor service which only uses a single thread. - * - * @param threadName The thread name of the executor service. Will create a new one if the thread - * name is used the first time. - * @return The executor service with the given thread name. Never {@code null}! - */ - ExecutorService getSingleThreadExecutorService(String threadName); - - /** - * Gets an executor service which only uses a single daemon thread. - * - * @param threadName The thread name of the executor service. Will create a new one if the thread - * name is used the first time. - * @return The executor service with the given thread name. Never {@code null}! - */ - ExecutorService getSingleDaemonThreadExecutorService(String threadName); - - /** - * Removes an existing executor service. - * - *

This allows you to get a fresh executor service when calling {@link - * #getSingleThreadExecutorService(String)} again. - * - * @param threadName The thread name of the executor service. - * @return The removed and shutdown executor service with the given thread name. - */ - Optional removeAndShutdownSingleThreadExecutorService(String threadName); - - /** - * Executes code after a given duration. - * - *

Tasks will be scheduled on the daemon executor, allowing the bot to shut down without all - * tasks being executed. This method is not meant to persist a scheduled task over multiple bot - * life cycles. - * - * @param task The code to run. - * @param duration The duration to run the code after. - * @param unit The unit of the duration given. - * @return A future that completes when the scheduled task is finished. - * @param The return type of the future. - */ - CompletableFuture runAfter( - Supplier> task, long duration, TimeUnit unit); + void shutdown(); } diff --git a/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImpl.java b/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImpl.java index 9d75e9c7..df08f54b 100644 --- a/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImpl.java +++ b/src/main/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImpl.java @@ -1,8 +1,9 @@ package io.github.brenoepics.at4j.core.thread; -import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.concurrent.*; -import java.util.function.Supplier; /** The implementation of {@link ThreadPool}. */ public class ThreadPoolImpl implements ThreadPool { @@ -11,102 +12,39 @@ public class ThreadPoolImpl implements ThreadPool { private static final int MAXIMUM_POOL_SIZE = Integer.MAX_VALUE; private static final int KEEP_ALIVE_TIME = 60; private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + private final Logger logger = LoggerFactory.getLogger(ThreadPoolImpl.class); - private final ExecutorService executorService = - new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAXIMUM_POOL_SIZE, - KEEP_ALIVE_TIME, - TIME_UNIT, - new SynchronousQueue<>(), - new AT4JThreadFactory("AT4J - Central ExecutorService - %d", false)); - private final ScheduledExecutorService scheduler = - Executors.newScheduledThreadPool( - CORE_POOL_SIZE, new AT4JThreadFactory("AT4J - Central Scheduler - %d", false)); - private final ScheduledExecutorService daemonScheduler = - Executors.newScheduledThreadPool( - CORE_POOL_SIZE, new AT4JThreadFactory("AT4J - Central Daemon Scheduler - %d", true)); - private final ConcurrentHashMap executorServiceSingleThreads = - new ConcurrentHashMap<>(); - - /** Shutdowns the thread pool. */ - public void shutdown() { - executorService.shutdown(); - scheduler.shutdown(); - daemonScheduler.shutdown(); - executorServiceSingleThreads.values().forEach(ExecutorService::shutdown); - } - - @Override - public ExecutorService getExecutorService() { - return executorService; - } + private final ExecutorService executorService; - @Override - public ScheduledExecutorService getScheduler() { - return scheduler; - } - - @Override - public ScheduledExecutorService getDaemonScheduler() { - return daemonScheduler; - } + public ThreadPoolImpl(ExecutorService executorService) { + if (executorService == null) { + logger.debug("Starting with default AT4J executor service."); + this.executorService = newAt4jDefault(); + return; + } - @Override - public ExecutorService getSingleThreadExecutorService(String threadName) { - return executorServiceSingleThreads.computeIfAbsent( - threadName, - key -> - new ThreadPoolExecutor( - 0, - 1, - KEEP_ALIVE_TIME, - TIME_UNIT, - new LinkedBlockingQueue<>(), - new AT4JThreadFactory("AT4J - " + threadName, false))); + logger.debug("Starting with custom AT4J executor service."); + this.executorService = executorService; } @Override - public ExecutorService getSingleDaemonThreadExecutorService(String threadName) { - return executorServiceSingleThreads.computeIfAbsent( - threadName, - key -> - new ThreadPoolExecutor( - 0, - 1, - KEEP_ALIVE_TIME, - TIME_UNIT, - new LinkedBlockingQueue<>(), - new AT4JThreadFactory("AT4J - " + threadName, true))); + public void shutdown() { + logger.debug("Shutting down AT4J executor service."); + executorService.shutdown(); } @Override - public Optional removeAndShutdownSingleThreadExecutorService(String threadName) { - ExecutorService takenExecutorService = executorServiceSingleThreads.remove(threadName); - if (takenExecutorService != null) { - takenExecutorService.shutdown(); - } - return Optional.ofNullable(takenExecutorService); + public ExecutorService getExecutorService() { + return executorService; } - @Override - public CompletableFuture runAfter( - Supplier> task, long duration, TimeUnit unit) { - CompletableFuture future = new CompletableFuture<>(); - getDaemonScheduler() - .schedule( - () -> - task.get() - .whenComplete( - (result, ex) -> { - if (ex != null) { - future.completeExceptionally(ex); - } else { - future.complete(result); - } - }), - duration, - unit); - return future; + public static ExecutorService newAt4jDefault() { + return new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAXIMUM_POOL_SIZE, + KEEP_ALIVE_TIME, + TIME_UNIT, + new SynchronousQueue<>(), + new AT4JThreadFactory("AT4J - Central ExecutorService - %d", false)); } } diff --git a/src/test/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java b/src/test/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java index 876fa3f7..e125155a 100644 --- a/src/test/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java +++ b/src/test/java/io/github/brenoepics/at4j/core/thread/ThreadPoolImplTest.java @@ -1,12 +1,11 @@ package io.github.brenoepics.at4j.core.thread; -import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadPoolExecutor; import static org.junit.jupiter.api.Assertions.*; @@ -16,7 +15,12 @@ class ThreadPoolImplTest { @BeforeEach void setUp() { - threadPool = new ThreadPoolImpl(); + threadPool = new ThreadPoolImpl(null); + } + + @AfterEach + void tearDown() { + threadPool.shutdown(); } @Test @@ -26,52 +30,17 @@ void shouldReturnExecutorService() { } @Test - void shouldReturnScheduler() { - ScheduledExecutorService scheduler = threadPool.getScheduler(); - assertNotNull(scheduler); - } - - @Test - void shouldReturnDaemonScheduler() { - ScheduledExecutorService daemonScheduler = threadPool.getDaemonScheduler(); - assertNotNull(daemonScheduler); - } - - @Test - void shouldReturnSingleThreadExecutorService() { - ExecutorService singleThreadExecutorService = - threadPool.getSingleThreadExecutorService("TestThread"); - assertNotNull(singleThreadExecutorService); - } - - @Test - void shouldReturnSingleDaemonThreadExecutorService() { - ExecutorService singleDaemonThreadExecutorService = - threadPool.getSingleDaemonThreadExecutorService("TestDaemonThread"); - assertNotNull(singleDaemonThreadExecutorService); - } - - @Test - void shouldRemoveAndShutdownSingleThreadExecutorService() { - threadPool.getSingleThreadExecutorService("TestThread"); - assertTrue(threadPool.removeAndShutdownSingleThreadExecutorService("TestThread").isPresent()); - } - - @Test - void shouldNotRemoveNonExistentSingleThreadExecutorService() { - assertFalse( - threadPool.removeAndShutdownSingleThreadExecutorService("NonExistentThread").isPresent()); - } - - @Test - void shouldRunAfterGivenDuration() { - Assertions.assertDoesNotThrow(() -> threadPool.runAfter(() -> null, 1, TimeUnit.SECONDS)); + void shouldShutdownExecutorService() { + ExecutorService executorService = threadPool.getExecutorService(); + assertFalse(executorService.isShutdown()); + threadPool.shutdown(); + assertTrue(executorService.isShutdown()); } @Test - void shouldShutdownAllServices() { - threadPool.getSingleThreadExecutorService("TestThread"); - threadPool.getSingleDaemonThreadExecutorService("TestDaemonThread"); - assertDoesNotThrow(() -> threadPool.shutdown()); + void shouldCreateNewAt4jDefault() { + ExecutorService executorService = ThreadPoolImpl.newAt4jDefault(); + assertNotNull(executorService); + assertInstanceOf(ThreadPoolExecutor.class, executorService); } -} +} \ No newline at end of file