Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x mp threading example #72

Merged
merged 7 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/microprofile/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
<module>http-status-count-mp</module>
<module>lra</module>
<module>telemetry</module>
<module>threads</module>
</modules>
</project>
94 changes: 94 additions & 0 deletions examples/microprofile/threads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Helidon MP Threading Example

Helidon's adoption of virtual threads has eliminated a lot of the headaches
of thread pools and thread pool tuning. But there are still cases where using
application specific executors might be desirable. This example illustrates two
such cases:

1. Using a virtual thread executor to execute multiple tasks in parallel.
2. Using a platform thread executor to execute long-running CPU intensive operations.

To accomplish this, the example uses two techniques:

1. Using Helidon's `ThreadPoolSupplier` to manually create a virtual thread executor service.
2. Using Helidon's `@ExecuteOn` annotation to run a handler on a platform thread.

## Build and run

```bash
mvn package
java -jar target/helidon-examples-microprofile-threads.jar
```

## Exercise the application

__Compute:__
```
curl -X GET http://localhost:8080/thread/compute/5
```
The `compute` endpoint runs a costly floating point computation using a platform thread.
Increase the number to make the computation more costly (and take longer).

The request returns the results of the computation (not important!).

__Fanout:__
```
curl -X GET http://localhost:8080/thread/fanout/5
```
The `fanout` endpoint simulates a fanout of remote calls that are run in parallel using
virtual threads. Each remote call invokes the server's `sleep` endpoint sleeping anywhere from
0 to 4 seconds. Since the remote requests are executed in parallel the curl request should not
take longer than 4 seconds to return. Increase the number to have more remote calls made
in parallel.

The request returns a list of numbers showing the sleep value of each remote client call.

__Sleep:__
```
curl -X GET http://localhost:8080/thread/sleep/4
```
This is a simple endpoint that just sleeps for the specified number of seconds. It is
used by the `fanout` endpoint.

The request returns the number of seconds requested to sleep.

## Further Discussion

### Use Case 1: Virtual Threads: Executing Tasks in Parallel

Sometimes an endpoint needs to perform multiple blocking operations in parallel:
querying a database, calling another service, etc. Virtual threads are a
good fit for this because they are lightweight and do not consume platform
threads when performing blocking operations (like network I/O).

The `fanout` endpoint in this example demonstrates this use case. You pass the endpoint
the number of parallel tasks to execute and it simulates remote client calls by using
the Helidon WebClient to call the `sleep` endpoint on the server.

### Use Case 2: Platform Threads: Executing a CPU Intensive Task

If you have an endpoint that performs an in-memory, CPU intensive task, then
platform threads might be a better match. This is because a virtual thread would be pinned to
a platform thread throughout the computation -- potentially causing unbounded consumption
of platform threads. Instead, the example uses a small, bounded pool of platform
threads to perform computations. Bounded meaning that the number of threads and the
size of the work queue are both limited and will reject work when they fill up.
This gives the application tight control over the resources allocated to these CPU intensive tasks.

The `compute` endpoint in this example demonstrates this use case. You pass the endpoint
the number of times you want to make the computation, and it uses a small bounded pool
of platform threads to execute the task.

### Logging

In `logging.properties` the log level for `io.helidon.common.configurable.ThreadPool`
is increased so that you can see the values used to configure the platform thread pool.
When you exercise the application you will see a line like
```
ThreadPool 'application-platform-executor-thread-pool-2' {corePoolSize=1, maxPoolSize=2,
queueCapacity=10, growthThreshold=1000, growthRate=0%, averageQueueSize=0.00, peakQueueSize=0, averageActiveThreads=0.00, peakPoolSize=0, currentPoolSize=0, completedTasks=0, failedTasks=0, rejectedTasks=0}
```
This reflects the configuration of the platform thread pool created by the application
and used by the `compute` endpoint. At most the thread pool will consume two platform
threads for computations. The work queue is limited to 10 entries to allow for small
bursts of requests.
87 changes: 87 additions & 0 deletions examples/microprofile/threads/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright (c) 2024 Oracle and/or its affiliates.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.applications</groupId>
<artifactId>helidon-mp</artifactId>
<version>4.1.0-SNAPSHOT</version>
<relativePath/>
</parent>
<groupId>io.helidon.examples.microprofile</groupId>
<artifactId>helidon-examples-microprofile-threads</artifactId>
<version>1.0.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>io.helidon.microprofile.bundles</groupId>
<artifactId>helidon-microprofile-core</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.logging</groupId>
<artifactId>helidon-logging-jul</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>jandex</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.testing</groupId>
<artifactId>helidon-microprofile-testing-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.smallrye</groupId>
<artifactId>jandex-maven-plugin</artifactId>
<executions>
<execution>
<id>make-index</id>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.examples.microprofile.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 io.helidon.common.configurable.ThreadPoolSupplier;
import io.helidon.microprofile.cdi.ExecuteOn;
import io.helidon.microprofile.server.ServerCdiExtension;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.Response;

/**
* Resource class for demonstrating threading.
*/
@Path("/thread")
@ApplicationScoped
public class ThreadResource {

private static final System.Logger LOGGER = System.getLogger(ThreadResource.class.getName());
private static final Random RAND = new Random(System.currentTimeMillis());
private static final Client CLIENT = ClientBuilder.newClient();

@Inject
private ServerCdiExtension serverExtension;

// Executor of virtual threads.
private final ExecutorService virtualExecutorService = ThreadPoolSupplier.builder()
.threadNamePrefix("application-virtual-executor-")
.virtualThreads(true)
.build()
.get();
/**
* Performs a CPU intensive operation. Uses the @ExecuteOn annotation
* to have this handler executed on a platform thread (instead of a virtual
* thread which is the default for Helidon 4).
* @param iterations number of compute iterations to perform
* @return Result of computation
*/
@Path("/compute/{iterations}")
@GET
@ExecuteOn(ExecuteOn.ThreadType.PLATFORM)
public String computeHandler(@PathParam("iterations") int iterations) {
if (iterations < 1) {
iterations = 1;
}
return Double.toString(compute(iterations));
}

/**
* Perform a fanout operation to simulate concurrent calls to remove services.
*
* @param count number of remote calls to make
* @return aggregated values returned by remote call
*/
@Path("/fanout/{count}")
@GET
public Response fanoutHandler(@PathParam("count") int count) {
if (count < 1) {
count = 1;
}

// 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<Future<String>>();
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<String>();
for (var future : futures) {
try {
responses.add(future.get());
} catch (InterruptedException e) {
responses.add(e.getMessage());
}
}

// All parallel calls are complete!
return Response.ok().entity(String.join(":", responses)).build();
} catch (ExecutionException e) {
LOGGER.log(System.Logger.Level.ERROR, e);
return Response.status(500).build();
}
}

/**
* Sleep for a specified number of seconds.
* The optional path parameter controls the number of seconds to sleep. Defaults to 1
*
* @param seconds number of seconds to sleep
* @return number of seconds requested to sleep
*/
@Path("/sleep/{seconds}")
@GET
public String sleepHandler(@PathParam("seconds") int seconds) {
if (seconds < 1) {
seconds = 1;
}
sleep(seconds);
return String.valueOf(seconds);
}

/**
* Perform a CPU intensive computation.
*
* @param iterations: number of times to perform computation
* @return result of computation
*/
private double compute(int iterations) {
LOGGER.log(System.Logger.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;
}

/**
* Sleep current thread.
*
* @param seconds number of seconds to sleep
* @return number of seconds requested to sleep
*/
private void sleep(int seconds) {
try {
Thread.sleep(seconds * 1_000L);
} catch (InterruptedException e) {
LOGGER.log(System.Logger.Level.WARNING, e);
}
}

/**
* Simulate a remote client call by calling this server's sleep endpoint.
*
* @param seconds number of seconds the endpoint should sleep.
* @return string response from client
*/
private String callRemote(int seconds) {
LOGGER.log(System.Logger.Level.INFO, Thread.currentThread() + ": Calling remote sleep for " + seconds + "s");
Response response = CLIENT.target("http://localhost:" + serverExtension.port() + "/thread/sleep/" + seconds)
.request()
.get();

String msg;
if (response.getStatus() == 200) {
msg = response.readEntity(String.class);
} else {
msg = Integer.toString(response.getStatus());
}
response.close();
return msg;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.examples.microprofile.threads;
Loading