context propagation API design

26 views
Skip to first unread message

Ladislav Thon

unread,
Mar 27, 2026, 9:59:57 AMMar 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 AMMar 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 PMMar 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 PMMar 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.

Yoann Rodiere

unread,
Apr 9, 2026, 4:25:06 AM (3 days ago) Apr 9
to quark...@googlegroups.com
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.

This is one thing I do not understand.

Let's leave aside the current implementation of context propagation for a minute.

To me the fact that a Uni is a computation almost makes the "context" problem easier. It no longer matters when the Uni was created, you just need to make sure that clients subscribe to a "wrapping" Uni that wraps the computation with some context setting/unsetting primitives. And those will automatically be executed on each subscription/execution.

If the method is this:

```
@WithSomeContext
Uni<Void> doSomething() {
    ...;
}
```

Then we can ensure the context can be available upon execution (which necessarily happens on subscription after the method call, so that's great!) by wrapping any call to this method that way:

```
Uni<Void> uniWithContext = Uni.createFrom().deferred(() -> { setSomethingInVertxContext(); return doSomething(); } )
    .eventually(() -> unsetSomethingInVertxContext());
```

My Mutiny fu might not be top-notch, and I do agree with have a very bad Vert.x "stacking"/"nesting" problem, but apart from that... you get the idea? Uni doesn't make things worse.

I assume I'm wrong or misunderstood. Can you please help me see what I'm missing?

Yoann Rodière
Hibernate team


--

Ladislav Thon

unread,
Apr 9, 2026, 4:49:39 AM (3 days ago) Apr 9
to quark...@googlegroups.com
čt 9. 4. 2026 v 10:25 odesílatel 'Yoann Rodiere' via Quarkus Development mailing list <quark...@googlegroups.com> napsal:
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.

This is one thing I do not understand.

Let's leave aside the current implementation of context propagation for a minute.

To me the fact that a Uni is a computation almost makes the "context" problem easier. It no longer matters when the Uni was created, you just need to make sure that clients subscribe to a "wrapping" Uni that wraps the computation with some context setting/unsetting primitives. And those will automatically be executed on each subscription/execution.

If the method is this:

```
@WithSomeContext
Uni<Void> doSomething() {
    ...;
}
```

This is exactly what I'm suggesting in my 2nd email in this thread: an interceptor binding annotation that says "when this method returns, transform the resulting `Uni` or `Multi` so that is has the currently active contexts attached". There could be a member that defines which contexts are captured, but that's a detail.

This is certainly possible (and currently I don't have a better idea), my biggest worry is that it is super easy for users to forget about this. But since both creation time and subscription time are fundamentally wrong, and there's no other place where we can do something implicitly, I'm afraid there's no other option.

LT
 

Yoann Rodiere

unread,
Apr 9, 2026, 8:45:06 AM (3 days ago) Apr 9
to quark...@googlegroups.com
Understood, thanks. So what I suggested, and what I did there, does make sense: https://github.com/quarkusio/quarkus/pull/53315

> my biggest worry is that it is super easy for users to forget about this

To me "this" boils down to "unis only get executed on subscription", and while I agree it's easy to forget about it, there are good reasons for it, and as long as we attach contexts as explained ^, I can't imagine it frequently causing problems, at least not for simple/entry-level use cases.
So... good enough?

Yoann Rodière
Hibernate team


William Burke

unread,
Apr 9, 2026, 10:55:03 AM (3 days ago) Apr 9
to quark...@googlegroups.com
On Fri, Mar 27, 2026 at 12:09 PM Ladislav Thon <lad...@gmail.com> wrote:
pá 27. 3. 2026 v 15:54 odesílatel Stephane Epardaud <stephane...@gmail.com> napsal:
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.)


Isn't at `return result` time exactly how @ActivateRequestContext [1] annotation works?   As Stef(?) said, the context will be gone by subscription time if I understand correctly how mutiny works. 

I'm running into this at `return result` time problem with the new CDI scope types I'm defining/creating and I'm having to write an interceptor to make the scopes work.

For example I'm creating an invocation scope which runs for the duration of the top-level method call.  Only way to support mutiny return results is to weave in an interceptor to do this and to wrap the mutiny return result like the ActiveRequestContextInterceptor [1] does.



Ladislav Thon

unread,
Apr 9, 2026, 1:12:08 PM (3 days ago) Apr 9
to quark...@googlegroups.com
čt 9. 4. 2026 v 16:55 odesílatel 'William Burke' via Quarkus Development mailing list <quark...@googlegroups.com> napsal:
On Fri, Mar 27, 2026 at 12:09 PM Ladislav Thon <lad...@gmail.com> wrote:
pá 27. 3. 2026 v 15:54 odesílatel Stephane Epardaud <stephane...@gmail.com> napsal:

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


Isn't at `return result` time exactly how @ActivateRequestContext [1] annotation works?

Yes and no. Yes in that it's an interceptor that transforms the return value. No in that what I'm thinking about is using Mutiny contexts (https://smallrye.io/smallrye-mutiny/latest/guides/context-passing/), instead of leaving whatever contexts active even if no Mutiny callback with that context is currently executing. (Which is often not an issue at all with the CDI request context, because that is stored in the Vert.x duplicated context, but it might be an issue when relying on thread locals. At least I think it might be an issue; I didn't bother trying to write a reproducer yet. It's a weird situation and whenever I try to think about it, my head hurts.)
 
As Stef(?) said, the context will be gone by subscription time if I understand correctly how mutiny works. 

Yeah, the contexts are gone at subscription time, because most of the time, it is a Quarkus framework that does the subscription. It could possibly subscribe when the contexts are still active, but that's only possible if it does activate those contexts, which is ... I don't know how often, but certainly not always.
 
I'm running into this at `return result` time problem with the new CDI scope types I'm defining/creating and I'm having to write an interceptor to make the scopes work.

For example I'm creating an invocation scope which runs for the duration of the top-level method call.  Only way to support mutiny return results is to weave in an interceptor to do this and to wrap the mutiny return result like the ActiveRequestContextInterceptor [1] does.

I hope that we can provide SPIs so that frameworks don't have to bother implementing this over and over again, but we're not there yet, by far. Maybe by the end of the year, we can have something usable, but don't hold your breath.

LT
 

--
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.
Reply all
Reply to author
Forward
0 new messages