|
| 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