diff --git a/examples/webserver/threads/README.md b/examples/webserver/threads/README.md new file mode 100644 index 00000000000..f3bd07ac110 --- /dev/null +++ b/examples/webserver/threads/README.md @@ -0,0 +1,23 @@ +# helidon-examples-webserver-threads + + +TODO XXXXXXXXXXXXX + +## Build and run + + +With JDK21 +```bash +mvn package +java -jar target/helidon-examples-webserver-threads.jar +``` + +## Exercise the application + +Basic: +``` +curl -X GET http://localhost:8080/thread/compute/5 +Hello World! +``` + + diff --git a/examples/webserver/threads/pom.xml b/examples/webserver/threads/pom.xml new file mode 100644 index 00000000000..1ed136989f1 --- /dev/null +++ b/examples/webserver/threads/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.0.0-SNAPSHOT + ../../../applications/se/pom.xml + + io.helidon.examples.webserver + helidon-examples-webserver-threads + 4.0.0-SNAPSHOT + + + io.helidon.examples.webserver.threads.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webclient + helidon-webclient + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/Main.java b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/Main.java new file mode 100644 index 00000000000..e31fb326c67 --- /dev/null +++ b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/Main.java @@ -0,0 +1,59 @@ + +package io.helidon.examples.webserver.threads; + + +import io.helidon.logging.common.LogConfig; +import io.helidon.config.Config; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + + + + +/** + * The application main class. + */ +public class Main { + + + /** + * Cannot be instantiated. + */ + private Main() { + } + + + /** + * Application main entry point. + * @param args command line arguments. + */ + public static void main(String[] args) { + + // load logging configuration + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + + + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(Main::routing) + .build() + .start(); + + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/thread"); + + } + + + /** + * Updates HTTP Routing. + */ + static void routing(HttpRouting.Builder routing) { + routing + .register("/thread", new ThreadService()); + } +} \ No newline at end of file diff --git a/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/ThreadService.java b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/ThreadService.java new file mode 100644 index 00000000000..7f07dffb5da --- /dev/null +++ b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/ThreadService.java @@ -0,0 +1,199 @@ +package io.helidon.examples.webserver.threads; + +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.lang.System.Logger.Level; +import java.util.concurrent.RejectedExecutionException; + +import io.helidon.common.configurable.ThreadPoolSupplier; +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class ThreadService implements HttpService { + + private static final System.Logger LOGGER = System.getLogger(ThreadService.class.getName()); + private static final Random rand = new Random(System.currentTimeMillis()); + + // ThreadPool of platform threads. + private static ExecutorService platformExecutorService; + // Executor of virtual threads. + private static ExecutorService virtualExecutorService; + + WebClient client = WebClient.builder() + .baseUri("http://localhost:8080/thread") + .build(); + + /** + * The config value for the key {@code greeting}. + */ + + ThreadService() { + this(Config.global().get("app")); + } + + ThreadService(Config appConfig) { + /* + * We create two executor services. One is a thread pool of platform threads. + * The second is a virtual thread executor service. + * See `application.yaml` for configuration of each of these. + */ + ThreadPoolSupplier platformThreadPoolSupplier = ThreadPoolSupplier.builder() + .config(appConfig.get("application-platform-executor")) + .build(); + platformExecutorService = platformThreadPoolSupplier.get(); + + ThreadPoolSupplier virtualThreadPoolSupplier = ThreadPoolSupplier.builder() + .config(appConfig.get("application-virtual-executor")) + .build(); + virtualExecutorService = virtualThreadPoolSupplier.get(); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules + .get("/compute", this::computeHandler) + .get("/compute/{iterations}", this::computeHandler) + .get("/fanout", this::fanOutHandler) + .get("/fanout/{count}", this::fanOutHandler) + .get("/sleep", this::sleepHandler) + .get("/sleep/{seconds}", this::sleepHandler); + } + + /** + * Perform a CPU intensive operation. + * The optional path parameter controls the number of iterations of the computation. The more + * iterations the longer it will take. + * + * @param request server request + * @param response server response + */ + private void computeHandler(ServerRequest request, ServerResponse response) { + String iterations = request.path().pathParameters().first("iterations").orElse("1"); + try { + // We execute the computation on a platform thread. This prevents pining of the virtual + // thread, plus provides us the ability to limit the number of concurrent computation requests + // we handle by limiting the thread pool work queue length (as defined in application.yaml) + Future future = platformExecutorService.submit(() -> compute(Integer.parseInt(iterations))); + response.send(future.get().toString()); + } catch (RejectedExecutionException e) { + // Work queue is full! We reject the request + LOGGER.log(Level.WARNING, e); + response.status(Status.SERVICE_UNAVAILABLE_503).send("Server busy"); + } catch (ExecutionException | InterruptedException e) { + LOGGER.log(Level.ERROR, e); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(); + } + } + + /** + * Sleep for a specified number of secons. + * The optional path parameter controls the number of seconds to sleep. Defaults to 1 + * + * @param request server request + * @param response server response + */ + private void sleepHandler(ServerRequest request, ServerResponse response) { + String seconds = request.path().pathParameters().first("seconds").orElse("1"); + response.send(Integer.toString(sleep(Integer.parseInt(seconds)))); + } + + /** + * Fan out a number of remote requests in parallel. + * The optional path parameter controls the number of parallel requests to make. + * + * @param request server request + * @param response server response + */ + private void fanOutHandler(ServerRequest request, ServerResponse response) { + int count = Integer.parseInt(request.path().pathParameters().first("count").orElse("1")); + LOGGER.log(Level.INFO, "Fanning out " + count + " parallel requests"); + // We simulate multiple client requests running in parallel by calling our sleep endpoint. + try { + // For this we use our virtual thread based executor. We submit the work and save the Futures + var futures = new ArrayList>(); + for (int i = 0; i < count; i++) { + futures.add(virtualExecutorService.submit(() -> callRemote(rand.nextInt(5)))); + } + + // After work has been submitted we loop through the future and block getting the results. + // We aggregate the results in a list of Strings + var responses = new ArrayList(); + for (var future : futures) { + try { + responses.add(future.get()); + } catch (InterruptedException e) { + responses.add(e.getMessage()); + } + } + + // All parallel calls are complete! + response.send(String.join(":", responses)); + } catch (ExecutionException e) { + LOGGER.log(Level.ERROR, e); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(); + } + } + + /** + * Simulate a remote client call be calling the sleep endpoint on ourself. + * + * @param seconds number of seconds the endpoint should sleep. + * @return + */ + private String callRemote(int seconds) { + LOGGER.log(Level.INFO, Thread.currentThread() + ": Calling remote sleep for " + seconds + "s"); + try (HttpClientResponse response = client.get("/sleep/" + seconds).request()) { + if (response.status().equals(Status.OK_200)) { + return response.as(String.class); + } else { + return (response.status().toString()); + } + } + } + + /** + * Sleep current thread + * @param seconds number of seconds to sleep + * @return + */ + private int sleep(int seconds) { + try { + Thread.sleep(seconds * 1_000L); + } catch (InterruptedException e) { + LOGGER.log(Level.WARNING, e); + } + return seconds; + } + + /** + * Perform a CPU intensive computation + * @param iterations: number of times to perform computation + */ + private double compute(int iterations) { + LOGGER.log(Level.INFO, Thread.currentThread() + ": Computing with " + iterations + " iterations"); + double d = 123456789.123456789 * rand.nextInt(100); + for (int i=0; i < iterations; i++) { + for (int n=0; n < 1_000_000; n++) { + for (int j = 0; j < 5; j++) { + d = Math.tan(d); + d = Math.atan(d); + } + } + } + return d; + } +} diff --git a/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/package-info.java b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/package-info.java new file mode 100644 index 00000000000..17d1693e0ef --- /dev/null +++ b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/package-info.java @@ -0,0 +1 @@ +package io.helidon.examples.webserver.threads; diff --git a/examples/webserver/threads/src/main/resources/application.yaml b/examples/webserver/threads/src/main/resources/application.yaml new file mode 100644 index 00000000000..fbda3d443a1 --- /dev/null +++ b/examples/webserver/threads/src/main/resources/application.yaml @@ -0,0 +1,14 @@ +server: + port: 8080 + host: 0.0.0.0 + +app: + greeting: "Hello" + application-platform-executor: + thread-name-prefix: "application-platform-executor-" + core-pool-size: 1 + max-pool-size: 2 + queue-capacity: 10 + application-virtual-executor: + thread-name-prefix: "application-virtual-executor-" + virtual-threads: true diff --git a/examples/webserver/threads/src/main/resources/logging.properties b/examples/webserver/threads/src/main/resources/logging.properties new file mode 100644 index 00000000000..0f674e20e0a --- /dev/null +++ b/examples/webserver/threads/src/main/resources/logging.properties @@ -0,0 +1,6 @@ +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.common.configurable.ThreadPool.level=ALL diff --git a/examples/webserver/threads/src/test/java/io/helidon/examples/webserver/threads/MainTest.java b/examples/webserver/threads/src/test/java/io/helidon/examples/webserver/threads/MainTest.java new file mode 100644 index 00000000000..274eeb2557a --- /dev/null +++ b/examples/webserver/threads/src/test/java/io/helidon/examples/webserver/threads/MainTest.java @@ -0,0 +1,9 @@ +package io.helidon.examples.webserver.threads; + +import io.helidon.webserver.testing.junit5.RoutingTest; + +@RoutingTest +class MainTest { + MainTest() { + } +} \ No newline at end of file diff --git a/examples/webserver/threads/src/test/resources/application-test.yaml b/examples/webserver/threads/src/test/resources/application-test.yaml new file mode 100644 index 00000000000..e69de29bb2d