Skip to content

Conversation

@Ladicek
Copy link
Member

@Ladicek Ladicek commented Oct 21, 2025

Fixes #859

@Ladicek Ladicek added this to the CDI 5.0 milestone Oct 21, 2025
@Ladicek Ladicek requested review from Azquelt and manovotn October 21, 2025 11:01
@Ladicek
Copy link
Member Author

Ladicek commented Oct 21, 2025

This is an initial proposal. I assume you will have some questions about certain aspects of the API.

Here are my questions that I don't have a full answer to at the moment:

  1. Currently, we specify that withAsync() must not be called for non-async methods. We could allow calling withAsync(false); should we?
  2. Currently, we specify that "it is recommended that completion is signalled before it is propagated to the caller of Invoker.invoke()"; should we make that mandatory? I think it's straightforward to signal completion before propagating it to the caller for all types except JAX-RS AsyncResponse. I don't know what happens if I register an InvocationCallback on the AsyncResponse, so I went with a recommendation for now.
  3. Similarly to the previous question, I'm also not sure about async methods throwing synchronously. Again, it seems straightforward for all types except JAX-RS AsyncResponse.

----

An _async handler_ is a service provider of `jakarta.enterprise.invoke.AsyncHandler`.
An async handler must not declare a provider method; it must declare a provider constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specified in java.util.ServiceLoader.

I'm actually not sure if we need this provision, but if we allow provider methods, the service provider doesn't have to implement AsyncHandler and figuring out the async type would be harder. I'm not sure if anyone actually uses provider methods; the other parts of the CDI spec ignore them completely...

@Ladicek
Copy link
Member Author

Ladicek commented Oct 21, 2025

One other question I don't currently have an answer for: what if there's multiple async handlers for the same async type?

@Azquelt
Copy link
Member

Azquelt commented Oct 21, 2025

1. Currently, we specify that `withAsync()` must not be called for non-async methods. We could allow calling `withAsync(false)`; should we?

I think we should allow withAsync(false) to state that the method should be treated non-asynchronously.

2. Currently, we specify that "it is recommended that completion is signalled before it is propagated to the caller of `Invoker.invoke()`"; should we make that _mandatory_? I _think_ it's straightforward to signal completion before propagating it to the caller for all types except JAX-RS `AsyncResponse`. I don't know what happens if I register an `InvocationCallback` on the `AsyncResponse`, so I went with a recommendation for now.

I think a recommendation makes sense. If you have a listener model, I don't think you can guarantee that.

3. Similarly to the previous question, I'm also not sure about async methods throwing synchronously. Again, it seems straightforward for all types except JAX-RS `AsyncResponse`.

It's straightforward when the return type represents the async task. If it's an argument, then I don't think there's an obvious answer as to whether it'll call the completion task or not.

I would suggest that we say that cleanup will happen immediately if the invocation throws an exception, and that the container must tolerate the completion being run as well.

@Ladicek
Copy link
Member Author

Ladicek commented Oct 21, 2025

3. Similarly to the previous question, I'm also not sure about async methods throwing synchronously. Again, it seems straightforward for all types except JAX-RS `AsyncResponse`.

It's straightforward when the return type represents the async task. If it's an argument, then I don't think there's an obvious answer as to whether it'll call the completion task or not.

I would suggest that we say that cleanup will happen immediately if the invocation throws an exception, and that the container must tolerate the completion being run as well.

Hmm, that makes sense. So something like the following? The first sentence is copied, the second is new.

If an asynchronous target method throws synchronously, instances of @Dependent looked up beans are destroyed before invoke() rethrows the exception.
In this case, the async handler is permitted to call completion.run(), but the CDI container must ignore that.

@Azquelt
Copy link
Member

Azquelt commented Oct 21, 2025

One other question I don't currently have an answer for: what if there's multiple async handler for the same async type?

I'm a bit concerned about this too since I think it's likely that multiple libraries will provide an async handler for common async types where support from the container is not required.

We could say that any of them may be called, which is a bit unpredictable, but in theory fine as long as all the deployed async handlers are correct.

We could say that it's a deployment error, though that might be difficult for the user to resolve if the handlers are provided by libraries.

I'm also a bit concerned about the scope of an async handler in a multi-module scenario. If I deploy an async handler in a module, is it available for use by the whole application, or only invocations of beans in modules which have visibility of the async handler module.

@Azquelt
Copy link
Member

Azquelt commented Oct 21, 2025

Hmm, that makes sense. So something like the following? The first sentence is copied, the second is new.

If an asynchronous target method throws synchronously, instances of @Dependent looked up beans are destroyed before invoke() rethrows the exception.
In this case, the async handler is permitted to call completion.run(), but the CDI container must ignore that.

That looks good to me. Maybe tweak it slightly:

In this case, the async handler is still permitted to call completion.run(), but the CDI container must ignore it.

@Ladicek
Copy link
Member Author

Ladicek commented Oct 21, 2025

I was thinking about the issue with multiple async handlers existing for the same async type. As we discussed on the CDI call today, we could add withAsyncHandler(Class<? extends AsyncHandler), but:

  1. this would make withAsync(boolean) quite useless (not necessarily bad),
  2. if done naively, this would prevent detecting the situation when an async target method exists but withAsync*() isn't called for it.

To avoid issue 2, we'd have to still require that async handlers are service providers of the AsyncHandler interface, so that the CDI container can find all of them and hence be able to figure out what methods are async.

There are some usability issues with this, but they are minor compared to the issue we'd solve. I'll think more about it.

@Ladicek
Copy link
Member Author

Ladicek commented Oct 27, 2025

I've adjusted this PR to what I described in my last comment. Async handlers must still be service providers, and they are selected explicitly using withAsync(). This allows cross-checking during deployment.

I'm not super happy about this proposal, but it isn't bad either.

@Ladicek
Copy link
Member Author

Ladicek commented Nov 7, 2025

As I mentioned on the CDI call earlier this week, I wanted to list all possible issues the current design is supposed to prevent. Here goes.

  1. If the user forgets to call withAsync() but the async handler is registered, we can detect that.
  2. If the user calls withAsync() but forgets to register the async handler, we can detect that.
  3. I don't think we can detect a situation when the user forgets to call withAsync() and forgets to register the async handler. This is not a problem in case of the built-in async types (CompletionStage, CompletableFuture, Flow.Publisher) and shouldn't be a big deal in case of platforms that include async handlers for their common async types. It is problematic in other situations.

If we removed the need to register async handlers (via the service loader mechanism), we would rely on users to never forget to call withAsync(). Alternatively, we could treat all async handlers passed to withAsync() as registered and check all built invokers against those, but that doesn't prevent the problem, it just makes it less likely.

If we removed the [need to call the] withAsync() method, we would rely on users always registering async handlers. This is not a terribly huge price to pay, but there would be a bigger problem: which async handler should we choose if multiple are registered for the same async type?

All in all, I think there's no better alternative to the current design.

@Ladicek
Copy link
Member Author

Ladicek commented Nov 7, 2025

I slightly adjusted the API (renamed AsyncHandler.CompletionStage to AsyncHandler.ForCompletionStage and similar) and I believe this is ready for final review and merging.

Comment on lines +97 to +98
* This method must be called when the target method is asynchronous and must not be called
* when the target method is not asynchronous.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to restrict calling withAsync(null) for non-asynchronous methods?

I would change this to "must not be called with a non-null value when the target method is not asynchronous"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep it, because it's nicely symmetric: if the target method is async, this method must be called, and if the target method is not async, this method may not be called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that, but it does mean that if another library is added which adds an async handler for your return type, you need to call withAsync(null) if that library is present and not call it if it isn't present.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that situation is weird enough that users want to know about it instantly. (Also it seems quite unlikely. There's not that many async types out there.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem unlikely, but I really dislike the idea of having to call the method if an unrelated library is present and not call it if the library is not present and having no way of writing the code that works in both situations.

I guess you could argue that the developer can use reflection to detect whether the other library is present, but that seems more nasty and error prone.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say if you found yourself in such situation, you want know about it and you need to think about it. Using a null handler may be a valid solution, but I'd argue that more often than not, there will be other things you want to do.

* This method must be called when the target method is asynchronous and must not be called
* when the target method is not asynchronous.
*
* @param asyncHandler the {@link AsyncHandler} to use; may be {@code null}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param asyncHandler the {@link AsyncHandler} to use; may be {@code null}
* @param asyncHandler the {@link AsyncHandler} to use, or {@code null} to indicate that the invoker is synchronous

----

If the target method is asynchronous, the `withAsync()` method on `InvokerBuilder` must be called, otherwise deployment problem occurs.
If the target method is not asynchronous, the `withAsync()` method on `InvokerBuilder` must not be called, otherwise deployment problem occurs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If the target method is not asynchronous, the `withAsync()` method on `InvokerBuilder` must not be called, otherwise deployment problem occurs.
If the target method is not asynchronous, the `withAsync()` method on `InvokerBuilder` must not be called with a non-`null` value, otherwise deployment problem occurs.

If we make the corresponding change in the Javadoc.

Comment on lines +240 to +241
An async handler may not declare a provider method; it must declare a provider constructor.
An async handler must have a direct superinterface type that is a parameterized type whose generic interface is `AsyncHandler` and its sole type argument is a class type, interface type or parameterized type, otherwise deployment problem occurs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pair of restrictions seem unusual for a service provider. Is this necessary so that we can reliably find out what T is, or is there some other reason?

The first restriction seems difficult to detect or enforce if the handler was loaded with ServiceLoader.load, so should I infer that you're not expecting implementations to do that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do very much expect implementations to use ServiceLoader.load(), which is not an issue in runtime-oriented implementations, which will find async handlers at runtime and may easily just hold onto the implementations obtained from the service loader. Build-time oriented implementations will call ServiceLoader.load() during build (at deployment time), and if we allowed provider methods, build-time oriented implementations would have a hard time matching between what they obtained at build time and what they obtained again at runtime. If we only allow provider constructors, build-time oriented implementations can call ServiceLoader.load() just once, during build, and then just call the provider constructor at runtime at suitable place, making the matching trivial.

I actually think all other places in CDI spec that rely on ServiceLoader also expect only provider constructors and no provider methods, but I don't think anyone has ever tested this.

The second provision is to prevent situations such as class Foo implements AsyncHandler or class Bar<T> implements AsyncHandler<T>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invoker support for asynchronous methods

2 participants