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:
- A callback/completion mechanism from the dispatch (e.g., using the
Future returned by executeBlocking)
- Making
dispatch() responsible for calling sendResponse() when it knows the handler has completed
- Exposing a public API on
MessageImpl to read/clear the trace object, so frameworks like Quarkus don't need reflection
Environment
Problem
In
InboundDeliveryContext.execute(), for fire-and-forget EventBus messages (send/publish with noreplyAddress),tracer.sendResponse()is called synchronously immediately afterdispatch()returns: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. ThesendResponse()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.tracebeforedispatch()returns, preventingInboundDeliveryContext.execute()from callingsendResponse(). We then callsendResponse()ourselves in the worker thread'sfinallyblock after the handler completes.This works but is fragile since it depends on the internal
MessageImpl.tracefield.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:Futurereturned byexecuteBlocking)dispatch()responsible for callingsendResponse()when it knows the handler has completedMessageImplto read/clear the trace object, so frameworks like Quarkus don't need reflectionEnvironment