> Markdown version of [Service Event Bus](https://vaadin.com/docs/v25/flow/advanced/session-lock-and-rpc-listeners). Section index: [llms.txt](https://vaadin.com/docs/v25/flow/llms.txt)

# Service Event Bus (new in V25.3)

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:

```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.

## <a id="registering-listeners"></a>Registering Listeners

Listeners are typically added from a [`VaadinServiceInitListener`](https://vaadin.com/docs/v25/flow/advanced/service-init-listener.md), 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](https://vaadin.com/docs/v25/flow/advanced/service-init-listener.md) for the full details of each approach.

## <a id="session-lock-events"></a>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()`](https://vaadin.com/docs/v25/flow/advanced/server-push.md). 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:

`SessionLockMetricsInitListener.java`

```java
public class SessionLockMetricsInitListener
        implements VaadinServiceInitListener {

    private final ThreadLocal<Long> requestedAt = new ThreadLocal<>();
    private final ThreadLocal<Long> acquiredAt = new ThreadLocal<>();

    @Override
    public void serviceInit(ServiceInitEvent event) {
        VaadinServiceEventBus eventBus = event.getSource().getEventBus();

        eventBus.addListener(SessionLockRequestedEvent.class,
                lockEvent -> requestedAt.set(System.nanoTime()));

        eventBus.addListener(SessionLockAcquiredEvent.class, lockEvent -> {
            long acquired = System.nanoTime();
            acquiredAt.set(acquired);
            LoggerFactory.getLogger(getClass()).debug(
                    "Session lock wait: {} ms",
                    (acquired - requestedAt.get()) / 1_000_000.0);
        });

        eventBus.addListener(SessionLockReleasedEvent.class, lockEvent -> {
            long holdNanos = System.nanoTime() - acquiredAt.get();
            LoggerFactory.getLogger(getClass())
                    .debug("Session lock hold: {} ms", holdNanos / 1_000_000.0);
            requestedAt.remove();
            acquiredAt.remove();
        });
    }
}
```

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.

## <a id="rpc-invocation-events"></a>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.

## <a id="data-provider-query-events"></a>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.

## <a id="other-framework-events"></a>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.

## <a id="firing-your-own-events"></a>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`:

```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.

## <a id="deprecated-listener-interfaces"></a>Deprecated Listener Interfaces (deprecated since V25.3)

`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`
