Skip to content

Bundle Client

Gabor Galazzo edited this page Jul 25, 2026 · 1 revision

Bundle Client

Package com.evento.application.client.* in evento-bundle. This is the bundle side of the connection: it dials the server, performs the handshake, registers handlers, tracks in-flight requests, and survives reconnects without losing work.


1. Key classes

Class Role
BundleClient Public façade: Builder, start / stop, request(payloadType, byte[], Duration), notify(...), enable / disable, registerRequestHandler / registerNotificationHandler
BundleClientConfig Record + Builder: host/port list, identity, auth token, capabilities, timeouts, transport config, autoEnable, maxInFlightRequests
BundleClientState INITIAL → CONNECTING → HANDSHAKING → REGISTERING → READY → RECONNECTING → CLOSING → CLOSED
ConnectionSupervisor Owns the Netty transport and the reconnect loop. The start() future resolves at the first READY
BundleCorrelationTracker UUID → CompletableFuture<Response>, scheduler-driven expiry
ProcessedRequestCache Inbound dedup: resolveOrClaim → Claimed / InFlight / Replay. LRU + TTL
HandlerRegistry payloadType → RequestHandler / NotificationHandler (pure byte arrays)
HelloFactory Builds Hello from BundleClientConfig
InboundDispatcher Pattern-matches: Response → tracker; Request → dedup, handler, reply; Notification → handler. All on a virtual-thread executor
EventoServerAdapter Implements the EventoServer gateway SPI over BundleClient
BundleInboundDispatcher Bridges HandlerRegistry.RequestHandler to the v2 CBOR byte-array contract for @CommandHandler / @QueryHandler classes
BundleAdminRequestHandler Handles evento:server-admin-request — decodes EventoRequest, dispatches consumer operations, encodes EventoResponse

2. Futures survive reconnect

BundleCorrelationTracker.failAll() runs on shutdown only, never on disconnect. A request that was in flight when the socket dropped keeps its future pending; the server's reconnect buffer (see Server Bus § 4) replays the response after re-handshake, and the future completes normally. Failing futures on every disconnect would turn a transient network blip into an application error for work that in fact completed.


3. Request outcomes are typed

A failed request no longer collapses into a generic IllegalStateException. EventoServerAdapter.request reconstructs the specific exception from ResponseError.exceptionClassName(). Two types in com.evento.transport matter operationally:

Exception Meaning Safe to retry?
RequestTimeoutException The caller stopped waiting. Not a rejection — the handler may have applied the work in full Only if the operation is idempotent. Report it as indeterminate (HTTP 504), not failed (500)
TooManyPendingRequestsException maxInFlightRequests was already outstanding, so nothing was transmitted Always — the refusal is definite

This distinction came out of a real incident: roughly 20 commands reported as failures had in fact been fully applied, and retrying them would have double-applied. See Throughput and Capacity.

BundleClientConfig.Builder.maxInFlightRequests(n) (default 2048, 0 disables) makes a bundle refuse a request outright once n are outstanding, rather than queueing work that will expire unsent.


4. Inbound path

Frame → InboundDispatcher
          ├── Response      → BundleCorrelationTracker.complete(correlationId)
          ├── Request       → ProcessedRequestCache.resolveOrClaim
          │                     ├── Claimed  → run handler, reply, cache response
          │                     ├── InFlight → drop (a reply is already coming)
          │                     └── Replay   → re-send the cached response, do NOT re-run the handler
          └── Notification  → HandlerRegistry notification handler

Everything dispatches on a virtual-thread executor — the Netty EventLoop is never blocked by handler code.


5. Confinement check

Since 2.1.0, ConfinementScanner performs an ASM sweep at registration time over every class in the scanned packages that is not a registered component, and reports each CommandGateway.send / sendAndWait / QueryGateway.query call site it finds there — with class, method, line and kind.

The problem it solves: such "gateway leaks" (say, an injected helper class issuing commands on a handler's behalf) are invisible to static interaction-graph extraction, so the extracted emit set was silently under-approximated and the GUI's interaction graph quietly wrong. AsmInvocationScanner additionally reports gateway calls whose payload is typed as the abstract Command / Query base, since the concrete payload type is then statically unresolvable.

Findings are logged as warnings by default. EventoBundle.Builder.strictConfinement (default false) turns them into an IllegalStateException at registration.


See also

Clone this wiki locally