Skip to content

EventBus: tracer.sendResponse() called prematurely for fire-and-forget messages dispatched to blocking handlers #6021

@zhfeng

Description

@zhfeng

Problem

In InboundDeliveryContext.execute(), for fire-and-forget EventBus messages (send/publish with no replyAddress), tracer.sendResponse() is called synchronously immediately after dispatch() returns:

// InboundDeliveryContext.execute() pseudocode:
message.trace = tracer.receiveRequest(ctx, ...);  // 1. sets up trace context
dispatch(message, ctx, handler);                    // 2. for blocking handlers, submits to worker pool and returns immediately
Object trace = message.trace;
if (replyAddress == null && trace != null) {
    tracer.sendResponse(ctx, null, trace, ...);     // 3. closes trace context BEFORE worker thread runs
}

When the handler is a blocking handler (e.g., Quarkus @ConsumeEvent(blocking = true)), dispatch() submits the work to a worker thread pool and returns immediately. The sendResponse() call at step 3 then removes the tracing context (e.g., OpenTelemetry span/scope) from the Vert.x context locals before the worker thread has a chance to execute the handler.

This causes the trace context to be lost inside the blocking handler — for example, io.opentelemetry.context.Context.current() returns the root context instead of the expected trace context, breaking distributed tracing.

Expected Behavior

For blocking/async dispatch, tracer.sendResponse() should be deferred until after the handler has completed execution on the worker thread, so that the trace context remains available throughout the handler's lifecycle.

Current Workaround

In quarkusio/quarkus#53056, we work around this by using reflection to null out MessageImpl.trace before dispatch() returns, preventing InboundDeliveryContext.execute() from calling sendResponse(). We then call sendResponse() ourselves in the worker thread's finally block after the handler completes.

This works but is fragile since it depends on the internal MessageImpl.trace field.

Proposed Solution

InboundDeliveryContext.execute() could be made aware of whether the dispatch is synchronous or asynchronous. For asynchronous dispatch (blocking handlers, virtual threads), sendResponse() should be deferred — either via:

  1. A callback/completion mechanism from the dispatch (e.g., using the Future returned by executeBlocking)
  2. Making dispatch() responsible for calling sendResponse() when it knows the handler has completed
  3. Exposing a public API on MessageImpl to read/clear the trace object, so frameworks like Quarkus don't need reflection

Environment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions