diff --git a/api/pom.xml b/api/pom.xml index 88e33a0b..585a266c 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -336,6 +336,16 @@ jakarta.annotation-api 2.0.0 + + jakarta.enterprise + jakarta.enterprise.cdi-api + 4.0.0.Alpha2 + + + jakarta.interceptor + jakarta.interceptor-api + 2.0.0 + 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: + *

+ *

+ * 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.