jakarta.transaction
jakarta.transaction-api
diff --git a/api/src/main/java/jakarta/enterprise/concurrent/Asynchronous.java b/api/src/main/java/jakarta/enterprise/concurrent/Asynchronous.java
new file mode 100644
index 00000000..e5d0721b
--- /dev/null
+++ b/api/src/main/java/jakarta/enterprise/concurrent/Asynchronous.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (c) 2021 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.enterprise.concurrent;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.concurrent.CompletableFuture;
+
+import jakarta.enterprise.util.Nonbinding;
+import jakarta.interceptor.InterceptorBinding;
+
+/**
+ * Annotates a CDI managed bean method to run asynchronously.
+ * The CDI managed bean must not be a Jakarta Enterprise Bean,
+ * and neither the method nor its class can be annotated with
+ * the MicroProfile Asynchronous annotation.
+ *
+ * The Jakarta EE Product Provider runs the method on a {@link ManagedExecutorService}
+ * and returns to the caller a {@link java.util.concurrent.CompletableFuture CompletableFuture}
+ * that is backed by the same ManagedExecutorService
+ * to represent the execution of the method. The ManagedExecutorService
+ * is the default asynchronous execution facility for the CompletableFuture
+ * and and all dependent stages that are created from those, and so on,
+ * as defined by the ManagedExecutorService
JavaDoc API.
+ * The Jakarta EE Product Provider makes this CompletableFuture
available
+ * to the asynchronous method implementation via the
+ * {@link Result#getFuture Asynchronous.Result.getFuture} and
+ * {@link Result#complete Asynchronous.Result.complete} methods.
+ *
+ * For example,
+ *
+ *
+ * {@literal @}Asynchronous
+ * public CompletableFuture{@literal } hoursWorked(LocalDate from, LocalDate to) {
+ * // Application component's context is made available to the async method,
+ * try (Connection con = ((DataSource) InitialContext.doLookup(
+ * "java:comp/env/jdbc/timesheetDB")).getConnection()) {
+ * ...
+ * return Asynchronous.Result.complete(total);
+ * } catch (NamingException | SQLException x) {
+ * throw new CompletionException(x);
+ * }
+ * }
+ *
+ *
+ * with usage,
+ *
+ *
+ * hoursWorked(mon, fri).thenAccept(total {@literal ->} {
+ * // Application component's context is made available to dependent stage actions,
+ * DataSource ds = InitialContext.doLookup(
+ * "java:comp/env/jdbc/payrollDB");
+ * ...
+ * });
+ *
+ *
+ * When the asynchronous method implementation returns a different
+ * CompletableFuture
instance, the Jakarta EE Product Provider
+ * uses the completion of that instance to complete the CompletableFuture
+ * that the Jakarta EE Product Provider returns to the caller,
+ * completing it with the same result or exception.
+ *
+ * For example,
+ *
+ *
+ * {@literal @}Asynchronous
+ * public CompletableFuture{@literal >} findSingleLayoverFlights(Location source, Location dest) {
+ * try {
+ * ManagedExecutorService executor = InitialContext.doLookup(
+ * "java:comp/DefaultManagedExecutorService");
+ *
+ * return executor.supplyAsync(source::flightsFrom)
+ * .thenCombine(executor.completedFuture(dest.flightsTo()),
+ * Itinerary::sourceMatchingDest);
+ * } catch (NamingException x) {
+ * throw new CompletionException(x);
+ * }
+ * }
+ *
+ *
+ * with usage,
+ *
+ *
+ * findSingleLayoverFlights(RST, DEN).thenApply(Itinerary::sortByPrice);
+ *
+ *
+ *
+ * Methods with the following return types can be annotated to be
+ * asynchronous methods:
+ *
+ * - {@link java.util.concurrent.CompletableFuture CompletableFuture}
+ * - {@link java.util.concurrent.CompletionStage CompletionStage}
+ * void
+ *
+ *
+ * The Jakarta EE Product Provider raises
+ * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}
+ * if other return types are used or if the annotation is placed at the class
+ * level. The injection target of ElementType.TYPE
is to be used only
+ * by the CDI extension that is implemented by the Jakarta EE Product Provider to
+ * register the asynchronous method interceptor. Applications must only use the
+ * asynchronous method annotation at method level.
+ *
+ * Exceptions that are raised by asynchronous methods are not raised directly
+ * to the caller because the method runs asynchronously to the caller.
+ * Instead, the CompletableFuture
that represents the result
+ * is completed with the raised exception. Asynchronous methods are
+ * discouraged from raising checked exceptions because checked exceptions
+ * force the caller to write exception handling code that is unreachable.
+ * When a checked exception occurs, the asynchronous method implementation
+ * can flow the exception back to the resulting CompletableFuture
+ * either by raising a
+ * {@link java.util.concurrent.CompletionException CompletionException}
+ * with the original exception as the cause, or it can take the equivalent
+ * approach of exceptionally completing the CompletableFuture
, using
+ * {@link java.util.concurrent.CompletableFuture#completeExceptionally completeExceptionally}
+ * to supply the original exception as the cause.
+ *
+ * Except where otherwise stated, the Jakarta EE Product Provider raises
+ * {@link java.util.concurrent.RejectedExecutionException RejectedExecutionException}
+ * upon invocation of the asynchronous method if evident upfront that it cannot
+ * be accepted, for example if the JNDI name is not valid or points to something
+ * other than a managed executor resource. If determined at a later point that the
+ * asynchronous method cannot run (for example, if unable to establish thread context),
+ * then the Jakarta EE Product Provider completes the CompletableFuture
+ * exceptionally with {@link java.util.concurrent.CancellationException CancellationException},
+ * and chains a cause exception if there is any.
+ *
+ * The Jakarta EE Product Provider must assign the interceptor for asynchronous methods
+ * to have priority of Interceptor.Priority.PLATFORM_BEFORE + 5
.
+ * Interceptors with a lower priority, such as Transactional
, must run on
+ * the thread where the asynchronous method executes, rather than on the submitting thread.
+ * When an asynchronous method is annotated as Transactional
,
+ * the transactional types which can be used are:
+ * TxType.REQUIRES_NEW
, which causes the method to run in a new transaction, and
+ * TxType.NOT_SUPPORTED
, which causes the method to run with no transaction.
+ * All other transaction attributes must result in
+ * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}
+ * upon invocation of the asynchronous method.
+ *
+ * @since 3.0
+ */
+// TODO the above restrictions on Transactional interceptors could be eliminated
+// if transaction context propagation is later added to the spec.
+@Documented
+@Inherited
+@InterceptorBinding
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.TYPE })
+public @interface Asynchronous {
+ /**
+ * JNDI name of a {@link ManagedExecutorService} or {@link ManagedScheduledExecutorService}
+ * upon which to run the asynchronous method.
+ *
+ * The default value is the JNDI name of the built-in ManagedExecutorService
+ * that is provided by the Jakarta EE platform provider:
+ * java:comp/DefaultManagedExecutorService
+ *
+ * @return managed executor service JNDI name.
+ */
+ @Nonbinding
+ String executor() default "java:comp/DefaultManagedExecutorService";
+
+ /**
+ * Mechanism by which the Jakarta EE Product Provider makes available
+ * to the asynchronous method implementation the same
+ * {@link java.util.concurrent.CompletableFuture CompletableFuture}
+ * instance that the Jakarta EE Product Provider supplies to the caller
+ * of the asynchronous method.
+ *
+ * Before invoking the asynchronous method implementation on a thread,
+ * the Jakarta EE Product Provider invokes the {@link #setFuture} method
+ * which makes available to the asynchronous method implementation
+ * the same CompletableFuture
that the Jakarta EE Product Provider
+ * returns to the caller.
+ *
+ * The asynchronous method implementation invokes the {@link #getFuture} method
+ * to obtain the same CompletableFuture
that the
+ * Jakarta EE Product Provider returns to the caller.
+ * The asynchronous method implementation can choose to complete
+ * this future (normally or exceptionally) or otherwise arrange for its
+ * completion, for example upon completion of a pipeline of completion stages.
+ * Having this same CompletableFuture
also enables the asynchronous
+ * method implementation to determine if the caller has forcibly completed
+ * (such as by cancellation or any other means) the CompletableFuture
,
+ * in which case the asynchronous method implementation could decide to end
+ * immediately rather than continue processing.
+ *
+ * For example,
+ *
+ *
+ * {@literal @}Asynchronous
+ * public CompletableFuture{@literal } hoursWorked(LocalDateTime from, LocalDateTime to) {
+ * CompletableFuture{@literal } future = Asynchronous.Result.getFuture();
+ * if (future.isDone())
+ * return future;
+ *
+ * try (Connection con = ((DataSource) InitialContext.doLookup(
+ * "java:comp/env/jdbc/timesheetDB")).getConnection()) {
+ * ...
+ * for (ResultSet result = stmt.executeQuery(); result.next() {@literal &&} !future.isDone(); )
+ * ...
+ * future.complete(total);
+ * } catch (NamingException | SQLException x) {
+ * future.completeExceptionally(x);
+ * }
+ * return future;
+ * }
+ *
+ *
+ * After the asynchronous method completes, the Jakarta EE Product Provider
+ * invokes the {@link #setFuture} method with a null
value
+ * to clear it from the thread.
+ *
+ * @since 3.0
+ */
+ public static class Result {
+ private static final ThreadLocal> FUTURES = new ThreadLocal>();
+
+ /**
+ * Completes the {@link java.util.concurrent.CompletableFuture CompletableFuture}
+ * instance that the Jakarta EE Product Provider supplies to the caller of the
+ * asynchronous method.
+ *
+ * This method must only be invoked by the asynchronous method implementation.
+ *
+ * @param type of result returned by the asynchronous method's CompletableFuture
.
+ * @param result result with which to complete the asynchronous method's CompletableFuture
.
+ * @return the same CompletableFuture
that the container returns to the caller.
+ * @throws IllegalStateException if the CompletableFuture
for an asynchronous
+ * method is not present on the thread.
+ */
+ public static CompletableFuture complete(final T result) {
+ @SuppressWarnings("unchecked")
+ CompletableFuture future = (CompletableFuture) FUTURES.get();
+ if (future == null) {
+ throw new IllegalStateException();
+ }
+ future.complete(result);
+ return future;
+ }
+
+ /**
+ * Obtains the same {@link java.util.concurrent.CompletableFuture CompletableFuture}
+ * instance that the Jakarta EE Product Provider supplies to the caller of the
+ * asynchronous method.
+ *
+ * This method must only be invoked by the asynchronous method implementation.
+ *
+ * @param type of result returned by the asynchronous method's CompletableFuture
.
+ * @return the same CompletableFuture
that the container returns to the caller.
+ * @throws IllegalStateException if the CompletableFuture
for an asynchronous
+ * method is not present on the thread.
+ */
+ public static CompletableFuture getFuture() {
+ @SuppressWarnings("unchecked")
+ CompletableFuture future = (CompletableFuture) FUTURES.get();
+ if (future == null) {
+ throw new IllegalStateException();
+ }
+ return future;
+ }
+
+ /**
+ * Before invoking the asynchronous method implementation on a thread,
+ * the Jakarta EE Product Provider invokes this method to make available
+ * to the asynchronous method implementation the same CompletableFuture
+ * that the Jakarta EE Product Provider returns to the caller.
+ *
+ * After the asynchronous method completes, the Jakarta EE Product Provider
+ * invokes this method with a null
value
+ * to clear it from the thread.
+ *
+ * This method must only be invoked by the Jakarta EE Product Provider.
+ *
+ * @param type of result returned by the asynchronous method's CompletableFuture
.
+ * @param future CompletableFuture
that the container returns to the caller,
+ * or null
to clear it.
+ */
+ public static void setFuture(final CompletableFuture future) {
+ if (future == null) {
+ FUTURES.remove();
+ } else {
+ FUTURES.set(future);
+ }
+ }
+ }
+}
diff --git a/api/src/test/java/jakarta/enterprise/concurrent/AsynchronousTest.java b/api/src/test/java/jakarta/enterprise/concurrent/AsynchronousTest.java
new file mode 100644
index 00000000..6a842cd3
--- /dev/null
+++ b/api/src/test/java/jakarta/enterprise/concurrent/AsynchronousTest.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2021 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.enterprise.concurrent;
+
+import static org.junit.Assert.*;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+import org.junit.Test;
+
+/**
+ * This test includes the code examples from the specification and JavaDoc
+ * for asychronous methods, verifying that they compile.
+ * The Asynchronous.Result class is also tested.
+ */
+public class AsynchronousTest {
+ /**
+ * Example from the section on Asynchronous Methods in the Concurrency spec.
+ */
+ @Asynchronous(executor = "java:module/env/concurrent/myExecutorRef")
+ public CompletableFuture> findSimilar(Cart cart, History h) {
+ Set- combined = new LinkedHashSet
- ();
+ for (Item item : cart.items())
+ combined.addAll(item.similar());
+ for (Item item : h.recentlyViewed(3))
+ combined.addAll(item.similar());
+ combined.removeAll(cart.items());
+
+ try (Connection con = ((DataSource) InitialContext.doLookup(
+ "java:comp/env/jdbc/ds1")).getConnection()) {
+ PreparedStatement stmt = con.prepareStatement("...");
+ for (Item item : combined) {
+ // ... Remove if the similar item is unavailable
+ }
+ } catch (NamingException | SQLException x) {
+ throw new CompletionException(x);
+ }
+ return Asynchronous.Result.complete(combined);
+ }
+
+ /**
+ * Second example from the Asynchronous class JavaDoc.
+ */
+ @Asynchronous
+ public CompletableFuture
> findSingleLayoverFlights(Location source, Location dest) {
+ try {
+ ManagedExecutorService executor = InitialContext.doLookup(
+ "java:comp/DefaultManagedExecutorService");
+
+ return executor.supplyAsync(source::flightsFrom)
+ .thenCombine(executor.completedFuture(dest.flightsTo()),
+ Itinerary::destMatchingSource);
+ } catch (NamingException x) {
+ throw new CompletionException(x);
+ }
+ }
+
+ /**
+ * First example from the Asynchronous class JavaDoc.
+ */
+ @Asynchronous
+ public CompletableFuture hoursWorked(LocalDate from, LocalDate to) {
+ // Application component's context is made available to the async method,
+ try (Connection con = ((DataSource) InitialContext.doLookup(
+ "java:comp/env/jdbc/timesheetDB")).getConnection()) {
+ // ...
+ double total = 40.0; // added to make the example compile
+ return Asynchronous.Result.complete(total);
+ } catch (NamingException | SQLException x) {
+ throw new CompletionException(x);
+ }
+ }
+
+ /**
+ * Example from the Asynchronous.Result class JavaDoc.
+ */
+ @Asynchronous
+ public CompletableFuture hoursWorked(LocalDateTime from, LocalDateTime to) {
+ CompletableFuture future = Asynchronous.Result.getFuture();
+ if (future.isDone())
+ return future;
+
+ try (Connection con = ((DataSource) InitialContext.doLookup(
+ "java:comp/env/jdbc/timesheetDB")).getConnection()) {
+ PreparedStatement stmt = con.prepareStatement("SQL"); // added to make the example compile
+ // ...
+ for (ResultSet result = stmt.executeQuery(); result.next() && !future.isDone(); )
+ ; // ...
+ double total = 40.0; // added to make the example compile
+ future.complete(total);
+ } catch (NamingException | SQLException x) {
+ future.completeExceptionally(x);
+ }
+ return future;
+ }
+
+ /**
+ * Used by Concurrency spec example code under Asynchronous Methods section.
+ * This code doesn't do anything; we only need the method signatures to verify compilation of the example.
+ */
+ static class Cart {
+ public List- items() {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Used by Concurrency spec example code under Asynchronous Methods section.
+ * This code doesn't do anything; we only need the method signatures to verify compilation of the example.
+ */
+ static class Customer {
+ public Cart getCart() {
+ return new Cart();
+ }
+
+ public History getHistory() {
+ return new History();
+ }
+ }
+
+ /**
+ * Used by Concurrency spec example code under Asynchronous Methods section.
+ * This code doesn't do anything; we only need the method signatures to verify compilation of the example.
+ */
+ static class History {
+ public Set
- recentlyViewed(int max) {
+ return Collections.emptySet();
+ }
+ }
+
+ /**
+ * Used by Concurrency spec example code under Asynchronous Methods section.
+ * This code doesn't do anything; we only need the method signatures to verify compilation of the example.
+ */
+ static class Item {
+ public Set
- similar() {
+ return Collections.emptySet();
+ }
+ }
+
+ /**
+ * Used by example from Asynchronous class JavaDoc.
+ * This code doesn't do anything; we only need the method signatures to verify compilation of the example.
+ */
+ static class Itinerary {
+ static List destMatchingSource(List toDest, List fromSource) {
+ return Collections.emptyList();
+ }
+ static List sortByPrice(List list) {
+ return list;
+ }
+ }
+
+ /**
+ * Used by example from Asynchronous class JavaDoc.
+ * This code doesn't do anything; we only need the method signatures to verify compilation of the example.
+ */
+ static class Location {
+ List flightsFrom() {
+ return Collections.emptyList();
+ }
+ List flightsTo() {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Verify that the first usage example in the Asynchronous class JavaDoc compiles.
+ */
+ @Test
+ public void testAsynchronousJavaDocUsageExample1() {
+ LocalDateTime mon = LocalDateTime.of(2021, 9, 13, 0, 0, 0);
+ LocalDateTime fri = LocalDateTime.of(2021, 9, 17, 23, 59, 59);
+
+ // Normally, the Jakarta EE Product Provider would do this on the same
+ // thread where the asynchronous method executes, just before it starts.
+ CompletableFuture future = new CompletableFuture();
+ Asynchronous.Result.setFuture(future);
+
+ // There is no interceptor when running these tests, so this won't actually run async.
+ try {
+ hoursWorked(mon, fri).thenAccept(total -> {
+ try {
+ DataSource ds = InitialContext.doLookup(
+ "java:comp/env/jdbc/payrollDB");
+ // ...
+ } catch (NamingException x) {
+ throw new CompletionException(x);
+ }
+ });
+ } finally {
+ // Normally, the Jakarta EE Product Provider would do this on the same
+ // thread where the asynchronous method executes, just after it ends.
+ Asynchronous.Result.setFuture(null);
+ }
+
+ // Naming lookup will have failed,
+ assertTrue(future.isCompletedExceptionally());
+ }
+
+ /**
+ * Verify that the second usage example in the Asynchronous class JavaDoc compiles.
+ */
+ @Test
+ public void testAsynchronousJavaDocUsageExample2() {
+ Location RST = new Location();
+ Location DEN = new Location();
+
+ try {
+ CompletableFuture
> leastToMostExpensive =
+ findSingleLayoverFlights(RST, DEN).thenApply(Itinerary::sortByPrice);
+ } catch (CompletionException x) {
+ // Expected in these tests because this is no interceptor to run the async method on another thread
+ }
+ }
+
+ /**
+ * Verify that the Asynchronous annotation can be configured at method level
+ * and that its executor field defaults to the JNDI name of the
+ * built-in ManagedExecutorService that is provided by the Jakarta EE platform.
+ */
+ @Test
+ public void testAsynchronousMethodLevelAnnotation() throws Exception {
+ Asynchronous anno = AsynchronousTest.class
+ .getMethod("hoursWorked", LocalDate.class, LocalDate.class)
+ .getAnnotation(Asynchronous.class);
+ assertNotNull(anno);
+ assertEquals("java:comp/DefaultManagedExecutorService", anno.executor());
+ }
+
+ /**
+ * Invoke all of the static methods of Asynchronous.Result.
+ */
+ @Test
+ public void testAsynchronousResult() {
+ try {
+ fail("Must not be present by default: " + Asynchronous.Result.getFuture());
+ } catch (IllegalStateException x) {
+ // Pass - detected that this was not invoked from an async method
+ }
+
+ CompletableFuture stringFuture = new CompletableFuture();
+ Asynchronous.Result.setFuture(stringFuture);
+ assertEquals(stringFuture, Asynchronous.Result.getFuture());
+ Asynchronous.Result.setFuture(null);
+ try {
+ fail("Must not be present after removing: " + Asynchronous.Result.getFuture());
+ } catch (IllegalStateException x) {
+ // Pass - detected that this was not invoked from an async method
+ }
+
+ CompletableFuture intFuture = new CompletableFuture();
+ Asynchronous.Result.setFuture(intFuture);
+ assertEquals(intFuture, Asynchronous.Result.complete(100));
+ Asynchronous.Result.setFuture(null);
+ try {
+ fail("Must not be present after removing: " + Asynchronous.Result.getFuture());
+ } catch (IllegalStateException x) {
+ // Pass - detected that this was not invoked from an async method
+ }
+
+ CompletableFuture booleanFuture = new CompletableFuture();
+ Asynchronous.Result.setFuture(booleanFuture);
+ assertEquals(booleanFuture, Asynchronous.Result.getFuture());
+ assertEquals(booleanFuture, Asynchronous.Result.complete(true));
+ Asynchronous.Result.setFuture(null);
+ try {
+ fail("Must not be present after removing: " + Asynchronous.Result.getFuture());
+ } catch (IllegalStateException x) {
+ // Pass - detected that this was not invoked from an async method
+ }
+
+ CompletableFuture unusedFuture = new CompletableFuture();
+ Asynchronous.Result.setFuture(unusedFuture);
+ Asynchronous.Result.setFuture(null);
+ try {
+ fail("Must not be present after removing: " + Asynchronous.Result.getFuture());
+ } catch (IllegalStateException x) {
+ // Pass - detected that this was not invoked from an async method
+ }
+ }
+
+ /**
+ * Verify that the usage example in the Asynchronous Methods section of the
+ * Concurrency spec compiles.
+ */
+ @Test
+ public void testAsynchronousUsageExampleFromSpec() throws Exception {
+ Customer cust = new Customer();
+
+ try {
+ findSimilar(cust.getCart(), cust.getHistory())
+ .thenAccept(recommended -> {
+ // ... Update page with recommendations
+ }).join();
+ } catch (CompletionException x) {
+ // Naming lookup will have failed because this doesn't run in a real server.
+ }
+ }
+}
diff --git a/specification/src/main/asciidoc/jakarta-concurrency.adoc b/specification/src/main/asciidoc/jakarta-concurrency.adoc
index 8f5fa8ee..536bcd0b 100644
--- a/specification/src/main/asciidoc/jakarta-concurrency.adoc
+++ b/specification/src/main/asciidoc/jakarta-concurrency.adoc
@@ -2302,3 +2302,164 @@ ends.
See section 3.1.8.2.1 for an example of using a `UserTransaction` within a
task.
+
+== Asynchronous Methods
+
+The `jakarta.enterprise.concurrent.Asynchronous` annotation annotates a
+CDI managed bean method to run asynchronously. The CDI managed bean must
+not be a Jakarta Enterprise Bean, and neither the method nor its class
+can be annotated with the MicroProfile Asynchronous annotation.
+
+Each asynchronous method execution corresponds to a managed
+`java.util.concurrent.CompletableFuture` instance that is backed by a
+`jakarta.enterprise.concurrent.ManagedExecutorService` as its default
+asynchronous execution facility. Its dependent stages
+(and all dependent stages that are created from those, and so on)
+continue to be backed by the managed executor service,
+which also manages the propagation of context to completion stage actions.
+Application code, including from within the asynchronous method, can query
+the status of the completable future instance and can choose to complete
+it at any time and by any means.
+
+==== Application Component Provider’s Responsibilities
+
+Application Component Providers (application developers) (EE2.11.2)
+specify the `jakarta.enterprise.concurrent.Asynchronous` annotation on
+CDI managed bean methods with return type of
+`java.util.concurrent.CompletableFuture`,
+`java.util.concurrent.CompletionStage`, or `void` to designate them for
+asynchronous execution.
+
+The Application Component Provider supplies the implementation of the
+asynchronous method. If the method has return type of
+`java.util.concurrent.CompletableFuture` or
+`java.util.concurrent.CompletionStage`, its implementation arranges for the
+completion of a completable future or completion stage, which it returns
+as the method result. The asynchronous method can create or obtain a new
+completion stage for this purpose, or it can use the
+`jakarta.enterprise.concurrent.Asynchronous.Result` API to obtain the same
+instance that is being returned to the caller of the asynchronous method.
+
+===== Usage Example
+
+In this example, an application component wants to asynchronously identify
+similar items that a customer might wish to purchase.
+
+====== Asynchronous Method Definition
+
+To check if the recommended similar items are currently available for
+purchase, this asynchronous method relies on an external database that
+is accessible via a resource reference that is defined in the application
+component's java:comp namespace, which must be made available to the
+asynchronous method.
+
+[source,java]
+----
+public class ProductRecommendations {
+ @Asynchronous(executor = "java:module/env/concurrent/myExecutorRef")
+ public CompletableFuture> findSimilar(Cart cart, History h) {
+ Set- combined = new LinkedHashSet
- ();
+ for (Item item : cart.items())
+ combined.addAll(item.similar());
+ for (Item item : h.recentlyViewed(3))
+ combined.addAll(item.similar());
+ combined.removeAll(cart.items());
+
+ try (Connection con = ((DataSource) InitialContext.doLookup(
+ "java:comp/env/jdbc/ds1")).getConnection()) {
+ PreparedStatement stmt = con.prepareStatement(CHECK_AVAILABILITY);
+ for (Item item : combined) {
+ ... Remove if the similar item is unavailable
+ }
+ } catch (NamingException | SQLException x) {
+ throw new CompletionException(x);
+ }
+ return Asynchronous.Result.complete(combined);
+ }
+}
+----
+====== Asynchronous Method Invocation
+
+The CDI managed bean with the asynchronous method is injected into a
+a `Servlet`, which uses it to asynchronously determine the product
+recommendations.
+
+[source,java]
+----
+public class CheckoutServlet extends HttpServlet {
+ @Inject
+ ProductRecommendations recommendations;
+
+ @Resource(name=”java:comp/env/jdbc/ds1”, lookup="jdbc/ds1")
+ DataSource ds;
+
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException {
+ ...
+ recommendations.findSimilar(cust.getCart(), cust.getHistory())
+ .thenAccept(recommended -> {
+ ... Update page with recommendations
+ });
+ ...
+ }
+}
+----
+
+==== Jakarta EE Product Provider’s Responsibilities
+
+The Jakarta EE Product Provider’s responsibilities are as defined in
+EE.5.8.3.
+
+The Jakarta EE Product Provider registers a CDI interceptor
+to arrange for the invocation of asynchronous methods on the
+`jakarta.enterprise.concurrent.ManagedExecutorService` or
+`jakarta.enterprise.concurrent.ManagedScheduledExecutorService`
+that is specified by the `jakarta.enterprise.concurrent.Asynchronous`
+annotation.
+
+The Jakarta EE Product Provider creates a
+`java.util.concurrent.CompletableFuture` instance to associate with each
+asynchronous method invocation, returning this same instance to the caller
+of the asynchronous method, and providing it to the asynchronous method
+implementation by means of the
+`jakarta.enterprise.concurrent.Asynchronous.Result` API. The Jakarta EE Product
+Provider completes this instance upon completion of the completion stage
+that is returned by the asynchronous method implementation, which is a
+no-op when the asynchronous method implementation chooses to return the
+same instance. If the asynchronous method return type is `void` or if the
+asynchronous method implementation raises an error or exception, the
+Jakarta EE Product Provider completes this instance upon return from the
+asynchronous method implementation. The Jakarta EE Product Provider
+raises `java.util.concurrent.RejectedExecutionException` to the caller of
+the asynchronous method if it cannot accept a method for asynchronous
+execution, for example if supplied with an invalid JNDI name.
+If the Jakarta EE Product Provider cannot start the asynchronous method
+for any reason after this point, it completes the
+`java.util.concurrent.CompletableFuture` instance with a
+`java.util.concurrent.CancellationException`.
+
+==== Transaction Management
+
+When an asynchronous method is also annotated with
+`jakarta.transaction.Transactional`, the transactional types which can be
+used are:
+
+* `jakarta.transaction.Transactional.TxType.REQUIRES_NEW` -
+which causes the method to run in a new transaction
+* `jakarta.transaction.Transactional.TxType.NOT_SUPPORTED` -
+which causes the method to run with no transaction
+
+All other transaction attributes must result in
+`java.lang.UnsupportedOperationException` upon invocation of the
+asynchronous method.
+
+When an asynchronous method is not annotated as
+`jakarta.transaction.Transactional` or the transaction type is set to
+`TxType.NOT_SUPPORTED`, the Jakarta EE Product Provider
+must support user-managed global transaction demarcation using the
+`jakarta.transaction.UserTransaction` interface, which is described in the
+Jakarta Transactions specification. User-managed transactions allow
+components to manually control global transaction demarcation
+boundaries. Task implementations may optionally begin, commit, and
+roll-back a transaction. See EE.4 for details on transaction management
+in Jakarta EE.