Skip to content

Commit 92b148e

Browse files
authored
Merge pull request #1797 from cescoffier/virtual-threads-blog-2
Virtual Thread - Blog post 2 (CRUD)
2 parents 34f5284 + 13f975e commit 92b148e

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
layout: post
3+
title: 'Writing CRUD applications using virtual threads'
4+
date: 2023-09-25
5+
tags: virtual-threads, reactive, crud, database
6+
synopsis: 'Describe how you can implement a CRUD / RESTFul application using virtual threads and Quarkus.'
7+
author: cescoffier
8+
---
9+
:imagesdir: /assets/images/posts/virtual-threads
10+
11+
Last week, we published a video demonstrating the creation of a CRUD application using virtual threads in Quarkus. It's as simple as adding the `@RunOnVirtualThread` annotation on your HTTP resource (or your controller class if you use the Spring compatibility layer).
12+
13+
+++
14+
<iframe style="margin-left: auto; margin-right: auto; display: block;" width="560" height="315" src="https://www.youtube.com/embed/sJ49s7ctpf8?si=XfBB10eabMzGQCKz" title="Writing CRUD applications using virtual threads" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
15+
+++
16+
17+
This companion post explains how it works behind the scenes.
18+
19+
== The code
20+
The application is a simple implementation of the https://todobackend.com/[Todo Backend].
21+
The complete code of this post is available https://github.com/quarkusio/virtual-threads-demos/tree/main/crud-example[here].
22+
23+
The important part is the https://github.com/quarkusio/virtual-threads-demos/blob/main/crud-example/src/main/java/org/acme/crud/TodoResource.java[TodoResource.java]:
24+
25+
[source, java]
26+
----
27+
package org.acme.crud;
28+
29+
import io.quarkus.logging.Log;
30+
import io.quarkus.panache.common.Sort;
31+
32+
import io.smallrye.common.annotation.NonBlocking;
33+
import io.smallrye.common.annotation.RunOnVirtualThread;
34+
import jakarta.transaction.Transactional;
35+
import jakarta.validation.Valid;
36+
import jakarta.ws.rs.*;
37+
import jakarta.ws.rs.core.Response;
38+
import jakarta.ws.rs.core.Response.Status;
39+
import java.util.List;
40+
41+
42+
@Path("/api")
43+
@RunOnVirtualThread
44+
public class TodoResource {
45+
46+
/**
47+
* Just print on which thread the method is invoked.
48+
*/
49+
private void log() {
50+
Log.infof("Called on %s", Thread.currentThread());
51+
}
52+
53+
@GET
54+
public List<Todo> getAll() {
55+
log();
56+
return Todo.listAll(Sort.by("order"));
57+
}
58+
59+
@GET
60+
@Path("/{id}")
61+
public Todo getOne(@PathParam("id") Long id) {
62+
log();
63+
Todo entity = Todo.findById(id);
64+
if (entity == null) {
65+
throw new WebApplicationException("Todo with id of " + id + " does not exist.",
66+
Status.NOT_FOUND);
67+
}
68+
return entity;
69+
}
70+
71+
@POST
72+
@Transactional
73+
public Response create(@Valid Todo item) {
74+
log();
75+
item.persist();
76+
return Response.status(Status.CREATED).entity(item).build();
77+
}
78+
79+
@PATCH
80+
@Path("/{id}")
81+
@Transactional
82+
public Response update(@Valid Todo todo, @PathParam("id") Long id) {
83+
log();
84+
Todo entity = Todo.findById(id);
85+
entity.id = id;
86+
entity.completed = todo.completed;
87+
entity.order = todo.order;
88+
entity.title = todo.title;
89+
entity.url = todo.url;
90+
return Response.ok(entity).build();
91+
}
92+
93+
@DELETE
94+
@Transactional
95+
public Response deleteCompleted() {
96+
log();
97+
Todo.deleteCompleted();
98+
return Response.noContent().build();
99+
}
100+
101+
@DELETE
102+
@Transactional
103+
@Path("/{id}")
104+
public Response deleteOne(@PathParam("id") Long id) {
105+
log();
106+
Todo entity = Todo.findById(id);
107+
if (entity == null) {
108+
throw new WebApplicationException("Todo with id of " + id + " does not exist.",
109+
Status.NOT_FOUND);
110+
}
111+
entity.delete();
112+
return Response.noContent().build();
113+
}
114+
115+
}
116+
----
117+
118+
The application uses:
119+
120+
- RESTEasy Reactive - the recommended REST stack for Quarkus. It supports virtual threads.
121+
- Hibernate Validation - to validate the Todos created by the user.
122+
- Hibernate ORM with Panache - to interact with the database.
123+
- The Argroal connection pool - to manage and recycle database connections.
124+
- The Narayana transaction manager - to run our code inside transactions.
125+
- The PostgreSQL driver - as we use a PostgreSQL database
126+
127+
The code is similar to a regular implementation of a CRUD service with Quarkus, except for https://github.com/quarkusio/virtual-threads-demos/blob/main/crud-example/src/main/java/org/acme/crud/TodoResource.java#L17[one line].
128+
We added the `@RunOnVirtualThread` annotation on the resource class (line 17).
129+
It instructs Quarkus to invoke these methods on virtual threads instead of regular platform threads (learn more about the difference in the https://quarkus.io/blog/virtual-thread-1/[previous blog post]), including `@Transactional` methods.
130+
131+
### The threading model
132+
133+
As we have seen in the code, the development model is synchronous.
134+
The interactions with the database uses blocking APIs: you wait for the replies.
135+
That's where virtual thread introduces their magic.
136+
Instead of blocking a platform thread, it only blocks the virtual threads:
137+
138+
image::crud-database.png[Threading model of the application,400,float="right",align="center"]
139+
140+
Thus, when another request comes, the carrier thread can handle it.
141+
It radically reduces the number of platform threads required when there are many concurrent requests.
142+
As a result, the number of worker threads, generally used when using a synchronous and blocking development model, is not the bottleneck anymore.
143+
144+
However, that's not because you use virtual threads that your application has no more concurrency limit.
145+
There is a new bottleneck: the **database connection pool**.
146+
When you interact with the database, you ask for a connection to the connection pool (Agroal in our case).
147+
The number of connections is not infinite (20 by default).
148+
Once all the connections are used, you must wait until another processing completes and releases its connection.
149+
You can still handle many requests concurrently, but they will wait for database connections to be available, reducing the response time.
150+
151+
### A note about pinning
152+
153+
As the https://quarkus.io/blog/virtual-thread-1/[previous blog post] described, pinning happens when the virtual thread cannot be unmounted from the carrier thread.
154+
In this case, blocking the virtual thread also blocks the carrier thread:
155+
156+
image::pinning.png[Pinning of the carrier thread,400,float="right",align="center"]
157+
158+
Fortunately, in this application, there is no pinning.
159+
The PostgreSQL driver is one of the only JDBC drivers that does not pin.
160+
If you plan to use another database, check first.
161+
We will be discussing how to detect pinning in the next post.
162+
Quarkus, Narayana and Hibernate have been patched to avoid the pinning.
163+
164+
Pinning is one of many problems that can arise.
165+
The application will suffer from the default object pooling mechanism used by Jackson.
166+
Fortunately, we contributed an SPI to https://github.com/FasterXML/jackson-core/pull/1064[Jackson] that will allow us to remove this allocation hog.
167+
168+
## Conclusion
169+
170+
This post explains implementing a CRUD application using virtual threads in Quarkus.
171+
You can now use an imperative development model without compromising the application's concurrency.
172+
It's as simple as using RESTEasy Reactive and adding one annotation: `@RunOnVirtualThread` on your resource.
173+
174+
We tailored Quarkus and upstream projects (such as Hibernate, Narayana, SmallRye Mutiny, etc.) to become virtual-thread-friendly.
175+
As we will see in other posts, most Quarkus extensions are ready to be used with virtual threads.
176+
177+
That said, while virtual threads increase the concurrency, you will likely hit other bottlenecks, such as the number of database connections managed in the pool.
178+
179+
In the next post and video, we will see how to test our application and detect pinning.
180+

0 commit comments

Comments
 (0)