Docs

Service Event Bus

Observing session locks, client-to-server RPC invocations, and data provider queries through the VaadinService event bus.

Every VaadinService has an event bus through which the framework reports what it’s doing, and through which an application can fire service-wide events of its own. The events the framework fires cover the machinery that a request passes through: the session lock that serializes all server-side work for a session, the individual client-to-server RPC invocations handled while that lock is held, and the data provider queries those invocations trigger. Listening to them is useful for performance monitoring, distributed tracing, and diagnosing lock contention — without modifying application logic.

The bus is reached with VaadinService.getEventBus(), and a listener is added for one event type:

Source code
Java
Registration registration = service.getEventBus()
        .addListener(SessionLockAcquiredEvent.class,
                event -> handleLockAcquired(event));

The returned Registration removes the listener again with remove().

A few properties of the bus are worth knowing:

  • Events are dispatched by their exact runtime type. A listener registered for a supertype — for instance AbstractDataFetchEvent — is never notified; register for each concrete event type you want.

  • The bus is thread-safe. Listeners can be added and removed while events are being fired from other request threads. Requests belonging to different sessions are handled concurrently, so a listener must expect events from several sessions on several threads at once.

  • An exception thrown by a listener is logged, and the remaining listeners are notified regardless, so that one misbehaving listener can neither disrupt the framework nor hide the event from other listeners.

  • Listeners run on the request or access thread, directly around the operation they report. Implementations must be fast and non-blocking.

Registering Listeners

Listeners are typically added from a VaadinServiceInitListener, which receives the service before it starts handling requests. How that init listener itself is discovered depends on the project type:

  • In Spring and CDI projects, annotate the class with @Component (Spring) or make it a managed bean (CDI); it’s then registered automatically.

  • In plain Java projects, register it through the Java Service Provider Interface by listing its fully qualified class name in META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener.

See Service Init Listener for the full details of each approach.

Session Lock Events

Because all of a session’s server-side work is serialized behind a single lock, the time a thread spends blocked acquiring that lock (the wait time) and the time it then holds it (the hold time) are key performance signals. Three events expose both:

Event When It’s Fired

SessionLockRequestedEvent

Immediately before the thread attempts to acquire the lock.

SessionLockAcquiredEvent

Immediately after the lock has been acquired.

SessionLockReleasedEvent

Immediately after the outermost hold has been released (the hold count reached zero).

All three carry the VaadinService the lock belongs to, through getService().

The same lock instance protects a session whether it’s acquired by the framework while handling a request, or through VaadinSession.lock() — for example from UI.access(). Events are fired for the outermost acquisition only; reentrant re-locks aren’t reported. For a single lock-hold, all three are fired on the same thread, in the order requested → acquired → released. This makes a ThreadLocal a natural place to record timing, as shown in the following example:

Source code
SessionLockMetricsInitListener.java

When several listeners are registered, the requested and acquired events are delivered in registration order, while the released event is delivered in reverse registration order. Listeners therefore nest like try/finally blocks: a listener registered later sees the release before one registered earlier does.

Listeners registered after a session’s lock has already been created are still honored.

RPC Invocation Events

A single client request typically carries several RPC invocations — a DOM event, a synchronized property update, a @ClientCallable or template event handler, a server-side navigation, a return channel message, and so on. One event of each type is fired per invocation, which is useful for emitting a tracing span that shows exactly which invocation consumes the time spent holding the session lock during a request.

Event When It’s Fired

RpcInvocationStartedEvent

Immediately before an invocation is handled.

RpcInvocationFailedEvent

When handling an invocation threw, before the ended event. The throwable is available from getError(). The framework still routes it to the session error handler independently of this event.

RpcInvocationEndedEvent

Once an invocation has been handled, whether it completed normally or threw.

All three expose the same details about the invocation:

Method Description

getUI()

The UI the invocation is handled against. Never null.

getType()

The protocol-level invocation type, such as event, mSync, publishedEventHandler, navigation, or channel. Never null.

getNodeId()

The id of the targeted StateNode, or -1 if the invocation doesn’t target a node.

getName()

A human-readable identifier — the DOM event name, the invoked method name, the navigation location, and so on — or null if none applies. It never carries the data of the invocation, only its identity.

For one invocation, the started event, the optional failed event, and the ended event are fired on the same thread, in that order. The ended event is always fired after the started one, regardless of outcome, so timing state can again be kept in a ThreadLocal. Within one request the events don’t nest: those of one invocation are all fired before those of the next.

Being reported doesn’t mean the invocation had an effect: an RPC targeting a node that’s detached, disabled, or inert is reported and only then discarded unhandled.

Data Provider Query Events

Every fetch and count query a data-bound component issues is reported on the bus, so that slow backend queries can be measured where they happen instead of being inferred from overall request timings.

Event When It’s Fired

DataFetchStartedEvent

Before a page of items is requested from a data provider.

DataFetchFailedEvent

When the fetch threw, before the ended event. The throwable is available from getError().

DataFetchEndedEvent

Once the items have been loaded and consumed, or the fetch threw. getRowsReturned() gives the number of items the data provider actually returned — possibly fewer than requested — or -1 if it threw.

DataCountStartedEvent

Before a count query is issued.

DataCountFailedEvent

When the count query threw, before the ended event.

DataCountEndedEvent

Once the count query has returned or thrown. getCount() gives the reported number of items, or -1 if it threw.

The events describe the query and where it came from:

Method Description

getUI()

The UI the component belongs to. Never null.

getComponent()

An Optional with the component whose data is being loaded, so that a query can be attributed to a view. Empty when the component couldn’t be resolved.

getOffset() and getLimit()

The index of the first item requested, and how many were requested. Fetch events only.

isFiltered()

Whether the query carried a filter, which distinguishes, for example, a Combo Box search from its initial page load.

Because a data provider may return a lazily evaluated Stream, the ended event is fired only after the returned items have been consumed, so the measured duration covers the backend round-trip rather than only the call that started it. The started and ended events of a query are fired on the same thread, and the ended event is fired in reverse registration order, so listeners nest around the started one. Queries made for a component that isn’t attached to a live UI aren’t reported.

Note
Data fetches triggered by push updates run on the executor given to DataCommunicator.enablePushUpdates(), so these events aren’t always fired on a request thread.

Other Framework Events

The service lifecycle events are fired through the same bus, and the dedicated addSessionInitListener(), addSessionDestroyListener(), addServiceDestroyListener(), and addUIInitListener() methods on VaadinService are thin wrappers that register on it. Either style works: use the listener interface when it exists, or listen for SessionInitEvent, SessionDestroyEvent, ServiceDestroyEvent, and UIInitEvent directly on the bus.

Firing Your Own Events

Any component of an application that needs to notify service-wide listeners can define an event type of its own and fire it on the bus, instead of maintaining its own listener collection. An event type only has to extend EventObject:

Source code
Java
public class MaintenanceModeEvent extends EventObject {
    public MaintenanceModeEvent(VaadinService service) {
        super(service);
    }
}

// Elsewhere, to notify the listeners:
service.getEventBus().fireEvent(new MaintenanceModeEvent(service));

Three firing methods are available:

Method Description

fireEvent(event)

Notifies the listeners of the event type in registration order, logging an exception from a listener and continuing with the rest.

fireEvent(event, errorHandler)

The same, but hands a listener that threw to the given error handler instead of logging it.

fireEventInReverseOrder(event)

Notifies the listeners in reverse registration order. Use it for the closing half of a pair of events, so that listeners nest around the opening one.

On a hot code path, hasListener(MaintenanceModeEvent.class) tells whether building the event is worth it at all.

Deprecated Listener Interfaces

SessionLockListener and RpcInvocationListener, together with the VaadinService.addSessionLockListener() and VaadinService.addRpcInvocationListener() methods that register them, are deprecated for removal. They still work and are still delivered from the bus, but new code should listen for the events directly:

Deprecated Callback Event

SessionLockListener.lockRequested()

SessionLockRequestedEvent

SessionLockListener.lockAcquired()

SessionLockAcquiredEvent

SessionLockListener.lockReleased()

SessionLockReleasedEvent

RpcInvocationListener.invocationStarted()

RpcInvocationStartedEvent

RpcInvocationListener.invocationFailed()

RpcInvocationFailedEvent

RpcInvocationListener.invocationEnded()

RpcInvocationEndedEvent

The SessionLockEvent and RpcInvocationEvent classes the callbacks receive are deprecated with them; the new events carry the same information.

970a4b45-bc87-4381-9ab9-7c3e34e97b26

Updated