Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
- "sample-operators/leader-election"
- "sample-operators/operations"
- "sample-operators/kotlin-operator"
# requires Java 21+, only built with such a JDK, see sample-operators/pom.xml
- "sample-operators/virtual-threads"
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
41 changes: 41 additions & 0 deletions docs/content/en/docs/documentation/operations/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,47 @@ Operator operator = new Operator( override -> override
.withLeaderElectionConfiguration(new LeaderElectionConfiguration("bar", "barNS")));
```

## Running on Virtual Threads

Reconciliations and workflow steps are executed on the executors provided by
`ConfigurationService`, which are fixed size thread pools by default. On Java 21 and newer, these
can be replaced with virtual threads, so that a reconciliation waiting on a remote call does not
occupy a platform thread:
Comment on lines +30 to +31

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
can be replaced with virtual threads, so that a reconciliation waiting on a remote call does not
occupy a platform thread:
can be replaced with virtual threads.

Each virtual thread always runs on platform thread in the background.


```java
Operator operator = new Operator(override -> override
.withExecutorService(Executors.newVirtualThreadPerTaskExecutor())
.withWorkflowExecutorService(Executors.newVirtualThreadPerTaskExecutor()));
```

Since a virtual thread per task executor is not a pool, `concurrentReconciliationThreads` and
`concurrentWorkflowExecutorThreads` have no effect in this case: the number of parallel
reconciliations is not limited anymore. Reconciliations of the same resource are still serialized
by the framework.

The Kubernetes client runs its asynchronous tasks - most notably the informer event handlers the
framework registers - on its own task executor, which can be configured similarly:

```java
KubernetesClient client = new KubernetesClientBuilder()
.withTaskExecutorSupplier(new KubernetesClientBuilder.ExecutorSupplier() {
@Override
public Executor get() {
return Executors.newVirtualThreadPerTaskExecutor();
}

@Override
public void onClose(Executor executor) {
((ExecutorService) executor).shutdownNow();
}
})
.build();
```

See the
[virtual threads sample](https://github.com/operator-framework/java-operator-sdk/tree/main/sample-operators/virtual-threads)
for a complete operator using both.

## Reconciler-Level Configuration

While reconcilers are typically configured using the `@ControllerConfiguration` annotation, you can also override configuration at runtime when registering the reconciler with the operator. You can either:
Expand Down
5 changes: 5 additions & 0 deletions docs/content/en/docs/getting-started/bootstrap-and-samples.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ The [sample-operators](https://github.com/java-operator-sdk/java-operator-sdk/tr
- **Key Features**: Multiple controllers managing related custom resources
- **Good for**: Understanding complex operators with multiple resource types and relationships

**[virtual-threads](https://github.com/operator-framework/java-operator-sdk/tree/main/sample-operators/virtual-threads)**
- **Purpose**: Runs reconciliations and Kubernetes client callbacks on virtual threads
- **Key Features**: Requires Java 21 or newer, configures both the framework and the client executors
- **Good for**: Operators with blocking reconciliations, see also [configuration](../../documentation/operations/configuration#running-on-virtual-threads)

## Running the Samples

### Prerequisites
Expand Down
13 changes: 13 additions & 0 deletions sample-operators/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,17 @@
<module>operations</module>
<module>kotlin-operator</module>
</modules>

<profiles>
<profile>
<!-- Virtual threads require Java 21, while the project itself targets Java 17. -->
<id>virtual-threads-sample</id>
<activation>
<jdk>[21,)</jdk>
</activation>
<modules>
<module>virtual-threads</module>
</modules>
</profile>
</profiles>
</project>
66 changes: 66 additions & 0 deletions sample-operators/virtual-threads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Virtual Threads Sample

This sample runs an operator on [Java virtual threads](https://openjdk.org/jeps/444).
**It requires Java 21 or newer**, therefore the module is only part of the build when the JDK
used is at least version 21 (see the profile in the parent `sample-operators/pom.xml`).

Two independent places execute the code of an operator, both are configured in
[`VirtualThreads`](src/main/java/io/javaoperatorsdk/operator/sample/VirtualThreads.java):

1. The framework's executors, used to run reconciliations and workflow steps:

```java
new Operator(overrider -> overrider
.withExecutorService(Executors.newVirtualThreadPerTaskExecutor())
.withWorkflowExecutorService(Executors.newVirtualThreadPerTaskExecutor()));
```

2. The Kubernetes client's task executor, used for the asynchronous tasks of the client, most
notably the informer event handlers registered by the framework:

```java
new KubernetesClientBuilder()
.withTaskExecutorSupplier(new KubernetesClientBuilder.ExecutorSupplier() {
@Override
public Executor get() {
return Executors.newVirtualThreadPerTaskExecutor();
}

@Override
public void onClose(Executor executor) {
((ExecutorService) executor).shutdownNow();
}
})
.build();
```

Note that a virtual thread per task executor is not a pool, therefore
`concurrentReconciliationThreads` and `concurrentWorkflowExecutorThreads` have no effect: the
number of parallel reconciliations is not limited anymore. Reconciliations of the same resource
are still serialized by the framework, as with the default executors.

The [reconciler](src/main/java/io/javaoperatorsdk/operator/sample/VirtualThreadsReconciler.java)
of this sample simulates a slow remote call by sleeping for a second, and records in the status
whether it was reconciled on a virtual thread. Since a virtual thread does not occupy a platform
thread while it waits, an arbitrary number of such reconciliations can run in parallel.

## Running the Sample

```shell
kubectl apply -f target/classes/META-INF/fabric8/virtualthreadscustomresources.sample.javaoperatorsdk-v1.yml
mvn exec:java -Dexec.mainClass="io.javaoperatorsdk.operator.sample.VirtualThreadsOperator"
```

Then create a custom resource:

```shell
kubectl apply -f k8s/virtual-threads-custom-resource.yaml
```

The status of the resource shows that the reconciliation happened on a virtual thread:

```yaml
status:
observedValue: "initial value"
reconciledOnVirtualThread: true
```
75 changes: 75 additions & 0 deletions sample-operators/virtual-threads/k8s/operator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#
# Copyright Java Operator SDK Authors
#
# 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.
#

apiVersion: v1
kind: ServiceAccount
metadata:
name: virtual-threads-operator

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: virtual-threads-operator
spec:
selector:
matchLabels:
app: virtual-threads-operator
replicas: 1
template:
metadata:
labels:
app: virtual-threads-operator
spec:
serviceAccountName: virtual-threads-operator
containers:
- name: operator
image: virtual-threads-operator
imagePullPolicy: Never

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: virtual-threads-operator-admin
subjects:
- kind: ServiceAccount
name: virtual-threads-operator
namespace: default
roleRef:
kind: ClusterRole
name: virtual-threads-operator
apiGroup: ""

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: virtual-threads-operator
rules:
- apiGroups:
- "apiextensions.k8s.io"
resources:
- customresourcedefinitions
verbs:
- '*'
- apiGroups:
- "sample.javaoperatorsdk"
resources:
- virtualthreadscustomresources
- virtualthreadscustomresources/status
verbs:
- '*'
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# Copyright Java Operator SDK Authors
#
# 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.
#

apiVersion: "sample.javaoperatorsdk/v1"
kind: VirtualThreadsCustomResource
metadata:
name: test1
spec:
value: "initial value"
111 changes: 111 additions & 0 deletions sample-operators/virtual-threads/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright Java Operator SDK Authors

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>sample-operators</artifactId>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>sample-virtual-threads-operator</artifactId>
<packaging>jar</packaging>
<name>Operator SDK - Samples - Virtual Threads</name>
<description>Runs reconciliations and Kubernetes client callbacks on virtual threads, requires Java 21+</description>

<properties>
<!-- Virtual threads are a Java 21 feature, this sample is only built with JDK 21 or newer.
See the profile activating this module in the parent pom. -->
<java.version>21</java.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-bom</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib-maven-plugin.version}</version>
<configuration>
<from>
<image>gcr.io/distroless/java21-debian12</image>
</from>
<to>
<image>virtual-threads-operator</image>
</to>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>crd-generator-maven-plugin</artifactId>
<version>${fabric8-client.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Loading
Loading