How to Implement Synchronous Dialogs, if Possible on Event Thread?

I am trying to find the best way for implementing synchronous dialogs in Vaadin,
i.e. opening a prompt dialog forcing the user to provide some input, and blocking
until the dialog is closed; similar to how prompt() works in JavaScript.

I understand that blocking is inefficient, as the blocked thread cannot do anything
useful for the whole lifespan of the dialog.
However, we are migrating a huge Swing codebase which uses such a synchronous
programming model. Changing everything to an asynchronous programming model would
take a lot of time and effort.
Also, there is Project Loom, which will introduce Fibers, allowing to write
synchronous code that is executed asynchronously. I don’t know when we will get fibers
in Java, but probably by the time we changed everything from synchronous to asynchronous,
we can already start changing it back.

So much for motivation - now to the technical details.

I found that it is possible to implement synchronous dialogs in background threads with Push enabled.
The basic idea is the following:

  1. lock the Session
  2. show the dialog
  3. unlock the Session
  4. wait until the dialog is closed
  5. proceed with the result

The trick here is to make sure you dont’t hold a lock on the session while waiting for
the answer, so the closing event of the dialog can be processed normally.

The downside of this approach is that it only works in background threads. This means that
for every event that possibly shows a synchronous dialog, the whole logic must be scheduled
in another thread. That can be tedious and easily forgotten. If you forget it, waiting for
the answer will cause a deadlock.

Therefore, I tried to find another approach that also works in event handler threads.
The idea is to only temporarily unlock the session while waiting for an answer, and then
lock the session again and continue with the event handling.

The following class shows the two approaches. It creates a page with two buttons which both open a
dialog asking the user’s name. When the user confirms the dialog, the greeting text is adjusted
accordingly.

@Route("")
@Push
public class MainView extends VerticalLayout {

	private final static ExecutorService executor = Executors.newCachedThreadPool();

	@Override
	protected void onAttach(AttachEvent attachEvent) {
		UI ui = attachEvent.getUI();
		Text greeting = new Text("Hello <please enter your name>");
		Button backgroundThreadButton = new Button("Ask in background thread", e -> {
			// Schedule the event handler logic on a background thread, so we can respond to the
			// current request immediately and block on the background thread without blocking
			// anything else.
			executor.execute(() -> {
				// Set the current UI for the background thread.
				UI.setCurrent(ui);
				// Ask the user's name and block while waiting for an answer.
				String name = askNameInBackroundThread(ui);
				// Update the greeting text, which needs access to the UI.
				ui.access(() -> greeting.setText("Hello " + name));
			});
		});
		Button eventThreadButton = new Button("Ask in event thread", e -> {
			// Just ask the name and update the greeting directly.
			// Doesn't work, since the client seems to wait for the response of the current
			// request before sending another one when clicking OK. That response is only send
			// when this Listener terminates. But since we wait for the answer before terminating,
			// we have a deadlock.
			greeting.setText("Hello " + askNameInEventThread(ui));
		});
		add(greeting, backgroundThreadButton, eventThreadButton);
	}

	/**
	 * Asks and waits for the user's name. Assumes to be running a background thread, and
	 * that the current VaadinSession is not locked.
	 */
	private static String askNameInBackroundThread(UI ui) {
		// No problem to block on join since we are running in a background thread.
		return askNameAsync(ui).join();
	}

	/**
	 * Asks and waits for the user's name. Assumes to be running an EventListener thread,
	 * and that the current VaadinSession is locked.
	 */
	private static String askNameInEventThread(UI ui) {
		// Show dialog, which needs access to the UI.
		CompletableFuture<String> asyncName = askNameAsync(ui);
		VaadinSession session = ui.getSession();

		// Temporarily unlock Session while waiting for user input,
		// so we can immediately show dialog to the user and be ready to process
		// the answer.
		session.unlock();
		try {
			return asyncName.join();
		}
		finally {
			// Lock again to restore the previous state.
			session.lock();
		}
	}

	/**
	 * Asks the user's name and returns a CompletableFuture which is completed with the user's
	 * input when clicking OK.
	 */
    private static CompletableFuture<String> askNameAsync(UI ui) {
    	CompletableFuture<String> result = new CompletableFuture<>();
    	Dialog dialog = new Dialog();
    	TextField nameField = new TextField("Name");
    	Button okButton = new Button("OK", e -> {
    		result.complete(nameField.getValue());
    		dialog.close();
    	});
    	dialog.add(new H1("What's your name?"), nameField, okButton);
    	dialog.addDialogCloseActionListener(e -> {
    		result.completeExceptionally(new CancellationException());
    		dialog.close();
    	});
    	ui.access(dialog::open);
    	return result;
    }
}

The goal is to have a method that can be called to ask the users input and return it synchronously.
Here, we have one such method for each approach, namely askNameInBackroundThread() and askNameInEventThread().
These methods can be called from the event listener to perform the logic.

One button uses the first approach to perform the event handling logic in another thread,
which works fine.

The other button tries to do everything in the thread of the event listener,
temporarily unlocking the session while waiting for an answer, but this doesn’t work.
Even though the dialog shows up, nothing happens when the user clicks OK.
[Apparently]
(https://github.com/vaadin/flow/blob/master/flow-client/src/main/java/com/vaadin/client/communication/RequestResponseTracker.java#L61-L68), the client prevents sending new requests as long as it hasn’t received a response to the previous request.

Since we block the thread that opens the dialog, we never finish processing that request and so never send a response.
The client waits for this response, and the server waits for the client before sending the response.
Thus, we have a deadlock.

I tried committing the response manually by calling VaadinService.requestEnd() or UidlRequestHandler.commitJsonResponse(),
but it didn’t help.

As you can see, the event listener of the second approach would be much simpler.
There is no need to use an executor, the current UI is already set, and we automatically have access to the UI.
Therefore, I would prefer to stick with that approach, but first I have to get it working, if that’s possible at all.

So finally to my questions:

  1. Is it possible to block inside an event listener without causing deadlocks?
  2. Are there any other ways to solve this, e.g. telling Vaadin to call the event handler without locking the session?
  3. If the answer to both questions above is No, do you see any problems with my first approach, i.e. handling events in another thread for being able to block?

Thanks,
Matthias

Unfortunately, having Vaadin blocking dialogs is fundamentally impossible: https://mvysny.github.io/vaadin-blocking-dialogs/

Regarding 3: the problem with the solution is that it’s complex and error-prone. The problem is that the UI components must not be manipulated outside of the Vaadin UI thread - for example the Dialog dialog = new Dialog(); must only be called in the UI thread. The reason is that Vaadin components are not thread-safe. To make them appear as thread-safe, you must make sure to publish them safely at all times, in order to have correct memory visibility of the changes performed to the components. Such code can only be written by a developer which completely understands the happens-before relationship of the Java Memory Model, and trust me - there aren’t many of those.

Mistakes will happen, resulting in unreproducible exceptions (e.g. ConcurrentModificationExceptions) in production, originating from the fact that the code will have race conditions, since it will be written by developers not understanding JMM. This should scare you enough to abandon the idea of having blocking dialogs in Vaadin.

If you’re not scared enough, please see this solution which uses Java virtual threads + Project Loom: https://github.com/mvysny/vaadin-loom