context propagation API design

17 views
Skip to first unread message

Ladislav Thon

unread,
Mar 27, 2026, 9:59:57 AM (7 days ago) Mar 27
to Quarkus Development mailing list
Hi,

I don't want to clutter Bill's thread, but I feel obliged to reply to Stef. Sorry Stef :-)

čt 26. 3. 2026 v 17:08 odesílatel Stephane Epardaud <stephane...@gmail.com> napsal:
On Thu, 26 Mar 2026 at 17:02, Ladislav Thon <lad...@gmail.com> wrote:
Without responding to the rest of this thread, I would like to highlight that our Mutiny + context propagation integration is wrong, because it captures contexts at the time the `Uni`/`Multi` is created, which may very well happen (and relatively often does happen) outside of the actual request handler. Naively, contexts should be captured at the moment the `Uni`/`Multi` is subscribed to, but that typically happens in a framework, so again outside of the actual request handler. I might have some ideas for other context propagation issues, but for this one, I don't even have an idea.

I disagree with this. It was meant to capture the context that is in the lexical scope of the lambda that captures it, precisely like lambdas capture variables from their lexical scopes, which is what is intuitive, and happens to be correct in both the request and session context cases.

Now, it is incorrect in the case of at least tracing, which changes contexts in ways that are not lexical, and probably other types of contexts. But I don't think that's a problem here for these contexts. 

Looks like your intuition works very differently from mine, then. Contextual values are not in lexical scope, so arguing that context capturing behaves just like lambdas makes no sense to me. The very purpose, the fundamental property, the defining characteristic, the raison d’être of contexts [in Quarkus] is that they are dynamic scopes (and contextual values are dynamically scoped). If you feel like this is pointless bikeshedding, well, I can only apologize.

Now, this is not the only issue. The MP Context Propagation API was designed with JDK `CompletionStage` in mind, and JDK `CompletionStage` (as well as e.g. Vert.x `Future`) represent values (albeit possibly not yet existing) The Mutiny types, `Uni` and `Multi`, do not represent values -- they represent computations. The difference is typically explained using the words eager and lazy, but they don't really make it justice -- the terms value and computation make the difference much easier to see. A computation has to be started to produce the value, and contexts need to be captured [roughly] when the computation is started, not when it is created.

Some might say that vast majority of `Uni`s and `Multi`s are created at the very place they are subscribed to (or not far -- subscriptions typically happens in the framework that called the application method that created the `Uni`/`Multi`), so there's no difference, but that's just ignoring the problem. At the very least, SmallRye Reactive Messaging creates all streams at application startup, which is very far from when the message handlers are called. If you use a hot publisher, you're typically also creating it far from the use site. I know SmallRye Health creates a `Uni` once and reuses it multiple times, again far from the creation site. (Far in time, not necessarily in space.) These usages might be a minority, but they are not insignificant, and they all cause troubles with context propagation.

I might be completely stupid in thinking that we need a new API for context propagation (it would be our 3rd I think?) and ideally concurrency as well, and I invite everyone to tell me that, if you can back it with arguments :-)

I don't feel ready to show the API I currently have, because I don't yet have an implementation and so I have zero experience with it. Since the Quarkus F2F, I had very little time to think about it and work on it; I hope I'll get back to it relatively soon.

LT

Stephane Epardaud

unread,
Mar 27, 2026, 10:54:15 AM (7 days ago) Mar 27
to quark...@googlegroups.com
In my opinion, this sort of context definition that you have of contexts being dynamic scopes depends on the context we're talking about.

To me, at least the request and transaction contexts are definitely lexical, exactly like variables. Take this example:

Uni<Void> m(Request r, Transaction t) {
 // here I'm using the request and transaction from the lexical scope
 doSomething(r, t);
 // here I'm returning a Uni that captures the lexical contexts and does something with them
 return Uni.createFrom().item(() -> doSomething(r, t));
}

Now, this works because I know my lambda captured the lexical scope and I can still access the same request and transaction as the method. MP-CP was created, so that the same intuition as to "what the hell is the current context being captured here?" can be taken from the lexical scope of variables:

@Transactional
Uni<Void> m() {
 // here I'm using the request and transaction from the lexical "implicit" scope
 doSomething(CDI.currentContext(), Transaction.current());
 // here I'm returning a Uni that captures the lexical contexts and does something with them
 return Uni.createFrom().item(() -> doSomething(CDI.currentContext(), Transaction.current()));
}

Nobody in their right mind would expect that, given context propagation, the request or transaction scopes in use within that last example in the Uni lambda, would be different from the ones active at the time of the m() method call. If you switch those contexts at subscription time, people will get very confused. Now, perhaps you're not suggesting that, and merely that you would capture the contexts in place at the Uni creation, and pass them on to the Uni from the starting point of the subscription, so in practice, both ways of looking at things end up executing equally.

But my intuition, and the design of MP-CP, is so that contexts are captured lexically, at lambda creation time, so that their contexts are the same as the outer scope of the lambda.

Again, this doesn't work for things like tracing, I suppose, because perhaps in this case, the contexts should be dynamic and not lexical.

--
You received this message because you are subscribed to the Google Groups "Quarkus Development mailing list" group.
To unsubscribe from this group and stop receiving emails from it, send an email to quarkus-dev...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/quarkus-dev/CALbocOkvBERUoaEORB_SPCobvhVyeZo_f%2BaEdsJxBXjFPPXPWA%40mail.gmail.com.


--
Stéphane Épardaud

Ladislav Thon

unread,
Mar 27, 2026, 12:09:55 PM (7 days ago) Mar 27
to quark...@googlegroups.com
pá 27. 3. 2026 v 15:54 odesílatel Stephane Epardaud <stephane...@gmail.com> napsal:
In my opinion, this sort of context definition that you have of contexts being dynamic scopes depends on the context we're talking about.

To me, at least the request and transaction contexts are definitely lexical, exactly like variables. Take this example:

Uni<Void> m(Request r, Transaction t) {
 // here I'm using the request and transaction from the lexical scope
 doSomething(r, t);
 // here I'm returning a Uni that captures the lexical contexts and does something with them
 return Uni.createFrom().item(() -> doSomething(r, t));
}

Now, this works because I know my lambda captured the lexical scope and I can still access the same request and transaction as the method. MP-CP was created, so that the same intuition as to "what the hell is the current context being captured here?" can be taken from the lexical scope of variables:

@Transactional
Uni<Void> m() {
 // here I'm using the request and transaction from the lexical "implicit" scope
 doSomething(CDI.currentContext(), Transaction.current());
 // here I'm returning a Uni that captures the lexical contexts and does something with them
 return Uni.createFrom().item(() -> doSomething(CDI.currentContext(), Transaction.current()));
}

This example can also look like this:

```java
@Transactional
Uni<Void> foo() {
    return bar();
}

Uni<Void> bar() {
    return baz();
}

Uni<Void> baz() {
    return Uni.createFrom().item(() -> doSomething(CDI.currentContext(), Transaction.current()));
}
```

The methods can be in different classes, possibly in different libraries. This is dynamic scoping at its purest.

Nobody in their right mind would expect that, given context propagation, the request or transaction scopes in use within that last example in the Uni lambda, would be different from the ones active at the time of the m() method call. If you switch those contexts at subscription time, people will get very confused. Now, perhaps you're not suggesting that, and merely that you would capture the contexts in place at the Uni creation, and pass them on to the Uni from the starting point of the subscription, so in practice, both ways of looking at things end up executing equally.

I unfortunately don't have a good name for the place I want the contexts to be captured. I can confidently say it must not be at `Uni`/`Multi` creation time, because that can happen far away from when its used. You can imagine the previous example to look like this:

```java
Uni<Void> result = Uni.createFrom().item(() -> doSomething(CDI.currentContext(), Transaction.current()));

Uni<Void> baz() {
    return result;
}
```

This is entirely valid and code like this exists out there (again, SmallRye Health). Contexts also must not be captured at subscription time, because at that time, they are gone. I think that contexts must be captured at `return result` and they must be "scoped" to the single subscription that's gonna happen later on (just like Mutiny's subscription-bound contexts.)

How do we express this is a big open question for me. We might add a method to `Uni` (ignoring that Julien would never approve, this is just a thought experiment :-) ), but people would easily forget to call it. We might add an interceptor binding `@CaptureContextsHere`, but that has the same problem. I don't have any other idea.

But my intuition, and the design of MP-CP, is so that contexts are captured lexically, at lambda creation time, so that their contexts are the same as the outer scope of the lambda.

Yeah, I believe capturing at creation time works well for `CompletionStage`, but it doesn't work for `Uni` or `Multi`, because of that fundamental difference.

Again, this doesn't work for things like tracing, I suppose, because perhaps in this case, the contexts should be dynamic and not lexical.

I hope I convinced you that all contexts are dynamic :-) I think my latest idea that involves explicit delineation of concurrency boundaries (with forking and joining) can solve this, but I only have an API sketch and no implementation yet, so I don't really know. I need to get back to this soon.

LT

Stephane Epardaud

unread,
Mar 27, 2026, 12:25:13 PM (7 days ago) Mar 27
to quark...@googlegroups.com
On Fri, 27 Mar 2026 at 17:09, Ladislav Thon <lad...@gmail.com> wrote:
This example can also look like this:

```java
@Transactional
Uni<Void> foo() {
    return bar();
}

Uni<Void> bar() {
    return baz();
}

Uni<Void> baz() {
    return Uni.createFrom().item(() -> doSomething(CDI.currentContext(), Transaction.current()));
}
```

The methods can be in different classes, possibly in different libraries. This is dynamic scoping at its purest.

OK, this is not pure lexical scoping, but it's still capturing things that is in the stack. My point is that both in `baz()` and in the lambda, the contexts in use are the same. This is the important part that users expect. But OK, this isn't lexical scoping, you're right.
Reply all
Reply to author
Forward
0 new messages