Jooby is a flexible performant microframework providing both blocking and non-blocking APIs for building web applications in Java and Kotlin.
In this chapter we are going to learn about Jooby execution model, more specifically:
-
Execute code on the event loop
-
Safely execution of blocking code
-
Working with non-blocking types, like:
CompletableFuture, Reactive Streams, Kotlin Coroutines, etc.
The javadoc:ExecutionMode[EVENT_LOOP] mode allows us to run a route handler from the event loop (a.k.a as non-blocking mode).
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/", ctx -> "I'm non-blocking!" );
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App::new);
}
}import io.jooby.ExecutionMode.EVENT_LOOP
import io.jooby.Jooby.runApp
fun main(args: Array<String>) {
runApp(args, EVENT_LOOP) {
get("/") { "I'm non-blocking!" }
}
}The javadoc:ExecutionMode[EVENT_LOOP] mode is the more advanced execution mode and requires you carefully design and implement your application due to that BLOCKING IS NOT ALLOWED
What if you need to block?
The javadoc:Router[dispatch, java.lang.Runnable] operator moves execution to a worker executor which allows to do blocking calls:
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/", ctx -> {
return "I'm non-blocking!";
});
dispatch(() -> {
// All the routes defined here are allowed to block:
get("/db-list", ctx -> {
/** Safe to block! */
Object result = ...; // Remote service, db call, etc..
return result;
});
});
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App::new);
}
}import io.jooby.ExecutionMode.EVENT_LOOP
import io.jooby.Jooby.runApp
fun main(args: Array<String>) {
runApp(args, EVENT_LOOP) {
get("/") {
"I'm non-blocking!"
}
dispatch {
// All the routes defined here are allowed to block:
get("/db-list") {
/** Safe to block! */
val result = ...; // Remote service, db call, etc..
result
}
}
}
}By default, the javadoc:Router[dispatch, java.lang.Runnable] operator moves execution to the server worker executor (executor provided by web server).
You can provide your own worker executor at application level or at dispatch level:
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
// Application level executor
worker(Executors.newCachedThreadPool());
// Dispatch to application level executor which is cached thread pool
dispatch(() -> {
...
});
// Dispatch to a explicit executor
Executor cpuIntensive = Executors.newSingleThreadExecutor();
dispatch(cpuIntesive, () -> {
...
});
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App:new);
}
}import io.jooby.ExecutionMode.EVENT_LOOP
import io.jooby.Jooby.runApp
fun main(args: Array<String>) {
runApp(args, EVENT_LOOP) {
// Application level executor
worker(Executors.newCachedThreadPool())
// Dispatch to application level executor which is cached thread pool
dispatch {
...
}
// Dispatch to a explicit executor
Executor cpuIntensive = Executors.newSingleThreadExecutor()
dispatch(cpuIntesive) {
...
}
}
}The javadoc:ExecutionMode[WORKER] mode allows us to do blocking calls from a route handler (a.k.a blocking mode). You just write code without worrying about blocking calls.
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/", ctx -> {
/** Safe to block! */
Object result = // Remote service, db call, etc..
return result;
});
}
public static void main(String[] args) {
runApp(args, WORKER, App::new);
}
}import io.jooby.ExecutionMode.WORKER
import io.jooby.Jooby.runApp
fun main(args: Array<String>) {
runApp(args, WORKER) {
get("/") {
/** Safe to block! */
val result = ...;// Remote service, db call, etc..
result
}
}
}Like with javadoc:ExecutionMode[EVENT_LOOP] mode, you can provide your own worker executor:
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
worker(Executors.newCachedThreadPool());
get("/", ctx -> {
/** Safe to block from cached thread pool! */
Object result = // Remote service, db call, etc..
return result;
});
}
public static void main(String[] args) {
runApp(args, WORKER, App::new);
}
}import io.jooby.ExecutionMode.WORKER
import io.jooby.Jooby.runApp
fun main(args: Array<String>) {
runApp(args, WORKER) {
worker(Executors.newCachedThreadPool())
get("/") {
/** Safe to block from cached thread pool! */
val result = ...;// Remote service, db call, etc..
result
}
}
}|
Note
|
While running in javadoc:ExecutionMode[WORKER] mode, Jooby internally does the dispatch call to the worker executor. This is done per route, not globally. |
The javadoc:ExecutionMode[DEFAULT] execution mode is a mix between javadoc:ExecutionMode[WORKER] and javadoc:ExecutionMode[EVENT_LOOP] modes. This (as name implies) is the default execution mode in Jooby.
Jooby detects the route response type and determines which execution mode fits better.
If the response type is considered non-blocking, then it uses the event loop. Otherwise, it uses the worker executor.
A response type is considered non-blocking when route handler produces:
-
A
CompletableFuturetype
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/non-blocking", ctx -> {
return CompletableFuture
.supplyAsync(() -> "I'm non-blocking!") // (1)
});
get("/blocking", ctx -> {
return "I'm blocking"; // (2)
});
}
public static void main(String[] args) {
runApp(args, App::new);
}
}import io.jooby.Jooby.runApp
fun main(args: Array<String>) {
runApp(args) {
get("/non-blocking") {
CompletableFuture
.supplyAsync { "I'm non-blocking!" } // (1)
}
get("/blocking") {
"I'm blocking" // (2)
}
}
}-
CompletableFutureis a non-blocking type, run in event loop -
Stringis a blocking type, run in worker executor
|
Tip
|
You are free to use non-blocking types in all the other execution mode too. Non-blocking response types are not specific to the default mode execution. All the default mode does with them is to dispatch or not to a worker executor. |
This section described some details about the default worker executor provided by web server. The worker executor is used when:
-
Application mode was set to javadoc:ExecutionMode[WORKER]
-
Application mode was set to javadoc:ExecutionMode[EVENT_LOOP] and there is a javadoc:Router[dispatch, java.lang.Runnable] call
Each web server provides a default worker executor:
-
Netty: The javadoc:netty.Netty[text=Netty server] implementation multiply the number of available processors (with a minimum of 2) by 8.
workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8
For example 8 cores gives us 64 worker threads.
-
Undertow: The javadoc:utow.Utow[text=Undertow server] implementation multiply the number of available processors (with a minimum of 2) by 8.
workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8
For example 8 cores gives us 64 worker threads.
-
Jetty: The javadoc:jetty.Jetty[text=Jetty server] implementation uses the default configuration with
200worker threads.
These are sensible defaults suggested by the server implementation. If you need to increase/decrease worker threads:
{
configureServer(server -> {
server.workerThreads(Number);
});
}{
configureServer { server ->
server.workerThreads(Number)
}
}