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: threading example #8576

Merged
merged 9 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
98 changes: 98 additions & 0 deletions examples/webserver/threads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Helidon SE 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 is 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.

## Build and run

```bash
mvn package
java -jar target/helidon-examples-webserver-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.

### Use of Helidon's ThreadPoolSupplier and Configuration

This example uses `io.helidon.common.configurable.ThreadPoolSupplier` to create the
two executors used in the example. This provides a few benefits:

1. ThreadPoolSupplier supports a number of tuning parameters that enable us to configure a small, bounded threadpool.
2. You can drive the thread pool configuration via Helidon config -- see this example's `application.yaml`
3. You get propagation of Helidon's Context which supports Helidon's features as well as direct use by the application.

### 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 start the application you will see a line like
```
ThreadPool 'application-platform-executor-thread-pool-1' {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.
85 changes: 85 additions & 0 deletions examples/webserver/threads/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?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-se</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../../../applications/se/pom.xml</relativePath>
</parent>
<groupId>io.helidon.examples.webserver</groupId>
<artifactId>helidon-examples-webserver-threads</artifactId>
<version>4.0.0-SNAPSHOT</version>

<properties>
<mainClass>io.helidon.examples.webserver.threads.Main</mainClass>
</properties>

<dependencies>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.logging</groupId>
<artifactId>helidon-logging-jul</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webserver.testing.junit5</groupId>
<artifactId>helidon-webserver-testing-junit5</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>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.webserver.threads;

import io.helidon.logging.common.LogConfig;
import io.helidon.config.Config;
import io.helidon.webclient.api.WebClient;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.http.HttpRouting;

/**
* The application main class.
*/
public class Main {

static WebClient webclient;

/**
* 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 webserver = WebServer.builder()
.config(config.get("server"))
.routing(Main::routing)
.build()
.start();

// Construct webclient here using port of running server
webclient = WebClient.builder()
.baseUri("http://localhost:" + webserver.port() + "/thread")
.build();

System.out.println("WEB server is up! http://localhost:" + webserver.port() + "/thread");
}


/**
* Updates HTTP Routing.
*/
static void routing(HttpRouting.Builder routing) {
routing
.register("/thread", new ThreadService());
}
}
Loading