flow icon indicating copy to clipboard operation
flow copied to clipboard

Callable event dispatcher

Open Izum-si opened this issue 3 years ago • 7 comments

Describe your motivation

We need a small upgrade for existing feature (EventBus), because we want to implement waiting for specific Vaadin events.

Really is no big deal to implement, yet it would have a huge impact upon simplification of the Java code using Vaadin. Now we (and every other developer using Vaadin) have to use asynchronous callbacks (lambda methods) for each and every event. Which is okay(ish) in most cases, but really complicates code when you need to sequentially interact with user (say in a wizard-like code, opening one dialog after another).

Most typical example is a modal dialog in form of a function, that blocks current execution until the dialog is closed and returns which option user chose as a function value. All of the modal code should execute in the same thread as the caller (which is blocked meanwhile).

Having call-with-wait would thus allow us to create functions, which open UI dialogs and return the value user entered as the function value. (Classic real modal dialogs.) This is practically impossible to implement now, as each dialog needs a callback for reading the result. And these callbacks complicate the code, disrupt its natural flow (esp. if a callback need to open new dialogs) and worsen its clarity/readability.

But that's just one example. Call-with-wait isn't limited to dialogs, but simplifies in-line waiting for any specific event, without disrupting the code-flow with unnecessary callbacks.

Java Swing had InvokeAndWait method for this purpose. Delphi and C++ builder had a method called ProcessMessages. Even ancient Visual Basic had the DoEvents method for this.

Describe the solution you'd like

This immensely useful feature is quite simple to implement: All it needs is that the Event Dispatcher (ED in further text - though you call it EventBus in Vaadin) is written in re-entrant manner. That is: ED could be called recursively from inside an event.

Normally, ED is the method that controls the whole application. It takes an event message form the event message queue and processes it (by calling all registered event handlers). Then it takes the next message from queue and so on. If the queue is empty, it just stops and waits, until the next message arrives in the queue. That's how all EDs work - including Vaadin's EventBus…

All that is needed (and Vaadin's EventBus lacks) is a way for the event handler to block its own execution when it needs to wait for some event (like waiting for a modal dialog to close), by calling ED back.

In other words, event handler in this case doesn't finish immediately (and thus doesn't return control to ED as it usually would), but explicitly calls ED recursively. When called this way, ED does what it normally would (processes all event messages), but when the queue is empty, it doesn't stop and wait for more, but instead simply returns to the calling event handler. And that's it.

To show more graphically what I mean, lets say we have such execution diagram (queue message events are shown in square brackets, nested execution is shown indented to the right, curly braces represent blocks of code and double slash is comment):

[_Queue empty_]
  ED (Event Dispatcher) waiting
[_Keypress message arrives in queue_]
  ED executing // Processing the message by calling the registered ShortcutListener for example
    Listener executing:
    Open new dialog
[_Events related with displaying dialog message_]
    While(not expected event) { // Waiting for events that indicate that dialog was closed
      Call ED back // Listener blocking itself and passing control to ED in alternative manner
        ED processing // Show dialog stuff, process keyboard and mouse events ...
[_Queue empty_]
        ED returns to Listener
      Listener waits small amount of time (say 10ms) in order not to consume CPU needlessly
[_New events potentially arrive in queue_]
    }
	// While loop exits since dialog is closed
    Listener processing dialog result
    Listener finishes // returns control to ED in usual manner
  ED waiting for next message // Return to top of the diagram

I hope it's clear what we mean.

As I said: all it needs is a small change to existing ED to make it callable from inside event handlers (ED needs to be recursive and have an alternate entry point, which event handlers can call).

Describe alternatives you've considered

One alternative is using threads and Vaadin Push to update UI. It works, but it's quite cumbersome. Biggest problem is that if the programmer isn't VERY meticulous with threads synchronization, object corruption and other impossible-to-repeat bugs can occur.

Not to mention how much more difficult is to debug multi-threaded UI than single threaded. And for no good reason: as you know very well yourself, UI code should always be single-threaded.

Another alternative is what we (and every other Vaadin user) are doing now: whenever code needs to ask for user's response, the programmer provides an asynchronous piece of callback code to read the values. In cases when you need multiple sequential responses (e.g. in a "wizard-like" series of dialogs or other sequential procedures, requiring user's input every now and then), these callbacks need to open new dialogs, needing new callbacks. This breaks the simple streamlined code into separate callback chunks - each chunk again providing new callbacks. And before you know, you are ten-level-deep in callbacks, which makes exception handling very tedious and garbage collection quite inefficient (calls pile up on the stack, while objects pile on the heap and nothing can be freed until the very last nested callback finishes).

Additional context

Different problems require different solutions and some times different approaches to coding. Callback event handlers are of course essential for event-driven programming, but aren't the magical silver-bullet, best suited for every case.

They work well when UI reacts immediately to end-user's actions (like mouse-clicks or keyboard presses - that is in typical event-driven programming), but not so well when code needs to wait in-line for some response, to be able to proceed. (E.g. wait for user's decision to further execute either branch A or branch B.)

In such cases, calling ED inside event handlers to temporary yield control and proceed again when events are handled is a well-known and long-proven alternative to strict callback approach. Very useful and what's more not difficult to implement inside well-constructed Event Dispatcher. Thus I can't imagine, why Vaadin doesn't offer it.

Izum-si avatar Aug 29 '22 12:08 Izum-si

Thanks for the issue. Moving it to Flow repository as it's related to the Flow framework API.

web-padawan avatar Aug 29 '22 12:08 web-padawan

I'm afraid the solution isn't as easy as you describe it here (or then I'm missing some very essential detail). The restriction here isn't the the event dispatcher itself but rather the fact that Vaadin works with HTTP requests sent from the browser.

The first problem comes from the way HTTP requests are handled. Outside of @Push usage, Vaadin uses a simple non-asynchronous HTTP servlet approach which means that an incoming HTTP request triggers dispatching one or several application events and then the response is sent to the client only when the event dispatching logic returns execution back to the original low-level request handling logic. If the event handler doesn't return, then there won't be any response sent to the client to ever show the dialog to the user. This can in theory be worked around by dispatching responses early in specific cases, but that's far from a trivial change with the current architecture. Also, thread handling in servlet containers is typically based on the assumption that request handling threads aren't blocking for too long. I would expect problems with scaling if many of those request dispatch threads would end up blocking while waiting for users to click further. We could compensate for this by instead giving each user session its own dedicated event dispatch thread, but I wouldn't want to do that until Project Loom gives us cheaper threads.

The second problem is related to locking. One of the first things that happen when handling a request is that the request dispatching logic identifies the correct session and acquires a lock for it to prevent concurrent access. If another thread is blocking in application logic, then that thread currently holds the lock which means that the other thread will not be able to make any progress. To deal with this, we would thus have to make the locking protocol more complicated so that the second request thread could somehow detect that the target session is currently in a blocking state and then somehow hand over the events from the request for processing on the other thread instead of processing them directly. This is surely doable, but it does again add a whole bunch of complexity.

Finally, there is the problem that web applications might do unexpected things that you don't have in desktop applications. In particular, there's the reload and back buttons in browsers that would need to somehow trigger a graceful rollback of the blocking call stack. This is of course also technically doable by throwing an exception that application logic has no business catching, but that's again adding even more complexity to the whole solution.

All-in-all, what you're describing is conceptually a relatively simple thing to do but actually making it work in practice is far more complicated because of the realities of HTTP requests and web browsers.

Legioth avatar Aug 29 '22 16:08 Legioth

Thanks for your quick response.

True, your last point could be tricky. Afraid I don't know enough about the internal working of your dispatcher and how it handles exceptions, so I can't really respond meaningfully to that. If the internals of the ED are available somewhere, I'll gladly study them and try to be more specific...

The first two points however, shouldn't be that problematic IMHO. (Again I don't know much about the internal workings, so I might be wrong of course - I'm only commenting on general principles here.)

If the event handler doesn't return, then there won't be any response sent to the client to ever show the dialog to the user.

Yes. And no.

Not sure if you understand what I meant quite the way I meant it: as you say, event dispatcher (ED) has to remain in control of the server most of the time - just as it is now.

But in those special cases when an event handler (EH) doesn't return (quickly), EH has to (quickly) explicitly call ED back instead (via its alternate entry point - let's call this ProcessMessages). When ED regains control in this way, it does exactly what it does normally, except waiting for new events once the queue is empty. When it's empty, it just returns back to event handler. EH at this point can again either:

  • finish whatever its doing and return back to ED or
  • decide that it has to wait further - and call ED back via the ProcessMessages once more.

So if ED either gets control back or if it's called explicitly, it can do its thing unhindered and there really shouldn't be any unhandled/undispatched events piling up. Nor should there be any need to handle them in different order (that is, worked around by dispatching responses early in specific cases, as you said).

The second problem is related to locking. One of the first things that happen when handling a request is that the request dispatching logic identifies the correct session and acquires a lock for it to prevent concurrent access. If another thread is blocking in application logic, then that thread currently holds the lock which means that the other thread will not be able to make any progress.

Yes, if you assume that the EH would truly block. EH must never do that however! (That's the responsibility of the app programmer - just as it is now.) It must either return quickly or call ED back via ProcessMessages (also quickly).

Anyway, as EH normally executes in the same thread as its caller (ED), it shouldn't happen that another thread would hog a lock. I agree with you, that a bit different processing of locks inside ED is required though:

Now the normal sequence (as I understand it) is: ED acquires [Session X] lock -> ED calls EH -> EH returns -> ED releases [Session X] lock.

When ED is called back via ProcessMessages, it would be: ED acquires [Session X] lock -> ED calls EH -> EH calls ED back -> ED releases [Session X] lock -> ED acquires other locks and calls other EHs (for the same or other sessions)until event queue is empty -> ED acquires [Session X] lock again -> ED returns to EH -> EH returns to ED -> ED releases [Session X] lock

I also agree that if an EH would indeed block (for a long(er) time), the application would indeed hang and events would pile up. But the same happens now, if EHs don't return quickly, doesn't it?

The same also happens in any other GUI framework if the app programmer is sloppy. It is hir responsibility to write EHs to respond in expedite manner. If s/he doesn't, it isn't the framework's (in this case Vaadin's) fault...

Anyway, app programmer should never use call-with-wait (ProcessMessages) unnecessarily. It is very useful in specific cases, but most EHs (say 99% of them) should still be written to do their thing as fast as possible and return to ED.

Izum-si avatar Aug 30 '22 13:08 Izum-si

Most of your assumptions are based on the traditional event loop that is used in most desktop GUI frameworks and game engines. In simplified terms, the traditional event loop looks like this.

public void eventLoop() {
  while (true) {
    sleep();
    runEventListeners();
    render();
  }
}

We don't have this kind of event loop because Vaadin Flow is a web framework where the rendered user interface and the application logic are separated from each other by the asynchronous request-response cycle that drives everything on the web. In simplified terms, it looks like this:

public Response handleHttpRequest(Request request) {
  Session session = request.getSession();
  synchronized(session) {
    runEventListeners(request, session);
    UiStateDiff update = collectChanges(session);
    return new Response(update);
  }
}

There's then a separate structure when using @Push where, again very much simplified, UI::access does something along these lines:

public void access(Runnable task) {
  Session session = this.getSession();
  synchronized(session) {
    task.run();
    UiStateDiff update = collectChanges(session);
    websocket.send(update);
  }
}

Hopefully, this overview helps you understand how most of your assumptions based on a traditional event loop cannot be directly applied in this case. I'm not saying that it would be impossible, but it's certainly more than what one would assume if Vaadin Flow would use a traditional event loop.

Legioth avatar Aug 31 '22 11:08 Legioth

If the internals of the ED are available somewhere, I'll gladly study them and try to be more specific...

It's all open source software, so feel free to go on studying.

There are lots of indirections for various practical reasons, but the main entry point for the request-based update logic is these two lines of code: https://github.com/vaadin/flow/blob/9b82032915646878679a28107bfee0c783128d17/flow-server/src/main/java/com/vaadin/flow/server/communication/UidlRequestHandler.java#L115-L116

The control flow when using @Push and UI::access is even more spread out but the core of it centers around https://github.com/vaadin/flow/blob/9b82032915646878679a28107bfee0c783128d17/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java#L962-L977 combined with the logic to push out changes to the browser when the session the session is ultimately unlocked: https://github.com/vaadin/flow/blob/9b82032915646878679a28107bfee0c783128d17/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java#L715-L726

Legioth avatar Aug 31 '22 12:08 Legioth

Thanks. I'll give it a go and try to figure it out. :) Might take a bit though, since I'm just leaving on vacation. We'd really appreciate if you could give it some thought in the future as well. As I said, it would be very useful for any developer using Vaadin. BTW: Is there any documentation available about this? (Besides the sources themselves.) Best regards.

Izum-si avatar Sep 05 '22 06:09 Izum-si

BTW: Is there any documentation available about this?

Unfortunately not.

Legioth avatar Sep 06 '22 07:09 Legioth