A blocking dialog is a dialog that blocks the code execution until an answer is obtained from the user. They can be a major hindrance to a seamless user experience in web applications. Imagine being engrossed in a task, only to be interrupted by a modal dialog that demands attention and halts your progress.
For example, in Swing, it is possible to write a code block that shows a confirm dialog, waits until the user clicks the "Yes" or "No" button, then resumes its execution on confirmation and finishes afterward. Something akin to the following:
public class TicketPurchasingService {
public static void purchaseTicketForConcert(String singerOrBand) {
List<Concert> concerts = getAvailableConcertsFor(singerOrBand);
Concert soonestConcert = concerts.get(0);
// Waits for the user to click either the "Yes" or "No" button, then returns 0 for yes, 1 for no, 2 for cancel
int input = JOptionPane.showConfirmDialog(null, "Soonest concert available: " + soonestConcert + ". Would you like to continue with the purchase?");
if (input == 0) {
Purchase purchase = doPurchase(soonestConcert);
JOptionPane.showMessageDialog(null, "Concert " + soonestConcert + " purchased, thank you. Purchase details: " + purchase);
} else {
JOptionPane.showMessageDialog(null, "Please come back again");
}
}
}
Is it actually possible to achieve such a thing in Vaadin? The answer to this question depends on which JVM you're running. Read on for more details.
The challenge of blocking dialogs
All Swing apps have exactly one event loop that processes all UI events, such as mouse clicks, tracking the mouse cursor as it moves, and firing hover events. All the UI code is called from this event loop, and it is forbidden to manipulate UI components from outside the event loop. A single thread is dedicated to running the event loop, and when that thread finishes, the app quits.
If the event loop becomes blocked in any way, such as during a long-running network call (which can be simulated by calling Thread.sleep(10000))
, the Swing app freezes. During this time, the application will not respond to any mouse clicks, the windows will not move, and it will appear completely unresponsive.
As its name suggests, the blocking dialog blocks the event loop from executing further code. It waits for the user to press a button before returning a value, such as 0, for 'yes.' This raises a question: if the blocking dialog blocks the event loop, how can the application still respond to clicks on the "Yes"/"No" button?
The solution to this problem is as follows: the JOptionPane.showConfirmDialog()
function runs a nested event loop that processes UI events until one of the buttons is clicked. This is the reason why the application continues to respond to events. The event loop is still running, but it is temporarily running within theshowConfirmDialog()
function.
Blocking dialogs in Vaadin
Consider the following Vaadin click handler:
public class MyView {
public MyView() {
Button deleteButton = new Button("Delete");
deleteButton.addClickListener(e -> {
if (confirmDialog("Are you sure?")) {
entity.delete();
}
});
}
}
Is there an implementation of the confirmDialog()
function that would show a Vaadin Dialog and block until a button is clicked? Well, we know the answer already, so let's focus on the 'no' part first.
The above code cannot be implemented using the regular thread-based approach. The problem arises from the following scenario: assume that the confirmDialog()
function blocks the Vaadin UI thread, waiting for the user to click either the "Yes" or "No" button. However, for the dialog to be displayed in the browser, the Vaadin UI thread must complete and respond to the browser. Unfortunately, the Vaadin UI thread cannot finish because it is blocked within the confirmDialog()
function.
This is a fundamental problem of all web frameworks, not just of Vaadin Flow. For more information on how 'event loops' work in server UI frameworks, please check out my blog post, Event Loop (Session Lock) in Vaadin.
The solution: Using Java 20 Virtual Threads or Project Loom
So, what dark magic are we using to solve the problem above? The answer lies in Java 20 Virtual Threads or Project Loom. Project Loom introduces the concept of a virtual thread, which distinguishes itself from the traditional native OS thread in its ability to be suspended.
When code running in a virtual thread encounters a blocking function, its execution is temporarily halted. The current call stack is stored, and the thread remains idle until the blocking function becomes unblocked. At that point, the call stack is restored, and code execution resumes seamlessly as if no special occurrence had taken place.
This, of course, requires extensive support from the JVM itself. If you're interested, an excellent Oracle article on virtual threads provides an accessible explanation of the concept. The article describes, in layman's terms:
- How the execution of virtual threads suspends and detaches from the carrier thread (a term referring to the traditional native OS thread).
- How the JVM waits until the blocking call completes.
- How the JVM reattaches the virtual thread to a carrier thread (which may or may not be the same thread as before) and resumes execution.
This creates the illusion of blocking code, but in reality, the code is no longer truly blocking. Instead, it remains dormant in the heap until it becomes unblocked.
Virtual threads have the ability to preserve ThreadLocal
values, as well as the value of UI.getCurrent()
, even after the unmount+mount procedure. This indicates that we can utilize virtual threads to execute Vaadin UI code.
Returning to the topic of blocking dialogs, we can leverage this mechanism to unmount the virtual thread from within the confirmDialog()
function. By doing so, we enable the native thread responsible for running Vaadin UI code to complete its execution and send the response to the browser, thereby displaying the dialog as desired. To achieve this, we can utilize CompletableFuture.get()
to block the virtual thread.
Once the user clicks a button, we can simply complete the corresponding Future, causing the CompletableFuture.get()
call to unblock. As a result, the dormant virtual thread can be mounted back onto the Vaadin UI thread, allowing execution to resume seamlessly. The function will look as follows:
public class Dialogs {
public static boolean confirmDialog(@NotNull String message) {
final CompletableFuture<Boolean> responseFuture = new CompletableFuture<>();
final ConfirmDialog dialog = new ConfirmDialog();
dialog.setText(message);
dialog.addConfirmListener(e -> responseFuture.complete(true));
dialog.addCancelListener(e -> responseFuture.complete(false));
dialog.open();
// this is where we'll block until the user clicks a button in the dialog
final boolean response = responseQueue.get();
dialog.close();
return response;
}
}
So, how can we persuade the virtual thread mechanism only to use Vaadin UI threads as the carrier threads?
Unwinding the magic
The whole magic happens in the VirtualThread
class. The source code of that class gets very technical quickly, so I won't go further into it here. The mechanism for suspending virtual threads involves the use of Continuation.park()
to unmount the thread and storing the call stack in a continuation object. When it's time to resume execution, Continuation.unpark()
is utilized.
Virtual threads are constructed via the Thread.ofVirtual().factory()
factory (of type ThreadFactory
), and they behave exactly like regular threads until they're blocked. The default factory uses a dedicated Executor
(a ForkJoinPool, but the difference is not important for this article), which runs tasks in good old native threads. The Executor
basically runs a chunk of the virtual thread execution until the virtual thread blocks.
Now we need two things:
- Create an Executor which runs tasks in the Vaadin UI thread,
- Pass that Executor into the virtual thread factory.
The first item is easy. The Executor
is an interface with just one method: Executor.execute(Runnable)
. It's trivial to create an Executor
that simply runs all submitted tasks in ui.access()
:
private class UIExecutor implements Executor {
private final UI ui;
public UIExecutor(UI ui) {
this.ui = ui;
}
@Override
public void execute(Runnable command) {
ui.access((Command) command::run);
}
}
The latter item is a bit tricky. The virtual thread factory is implemented by the java.lang.ThreadBuilders$VirtualThreadBuilder
class, which is not part of the public API. It introduces a constructor which accepts an Executor
, but the constructor is also not public. That's not a problem, though: we'll use a bit of reflection to create the builder.
Please see the Vaadin Loom example project for more details:
- The
SuspendingExecutor
class runs submitted tasks in virtual threads on a given executor. - The
VaadinSuspendingExecutor
class uses the class above while running the continuations in the Vaadin UI thread.
Blocking dialogs in Kotlin
The Kotlin language offers a solution that works on any JVM version, even on old JVM 8. The mechanism used is the Kotlin Coroutines. With this approach, the Kotlin compiler (rather than the JVM runtime) will break the code into separate code chunks. Basically, the code is transformed into a bunch of callbacks by the compiler, a state machine with the runNext()
method is created, which then keeps track of which callback (or continuation) goes next.
However, there is an inherent problem with this solution: it does not work with Thread.sleep()
and other conventional Java blocking calls. To address this issue, you will need to utilize their corresponding suspending counterparts. Please find more in the Kotlin Coroutines documentation.
Got questions? Comment them below or join our Discord server to chat directly with the Vaadin team!