├── .gitignore ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── augustnagro │ └── jaa │ ├── Async.java │ └── Coroutine.java └── test └── java └── UnitTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | .DS_Store 3 | 4 | *.iml 5 | .idea 6 | target 7 | nbproject 8 | nb-configuration.xml 9 | 10 | .class 11 | .log 12 | .jar 13 | .war 14 | .ear 15 | .zip 16 | .tar.gz 17 | .rar 18 | 19 | hs_err_pid* 20 | 21 | *.log 22 | 23 | .bsp 24 | .metals 25 | .bloop 26 | .vscode 27 | metals.sbt 28 | 29 | *.sc 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Java Async-Await 2 | 3 | Async-Await support for Java [CompletionStage](https://download.java.net/java/early_access/loom/docs/api/java.base/java/util/concurrent/CompletionStage.html). 4 | 5 | ```java 6 | import com.augustnagro.jaa.AsyncContext; 7 | import static com.augustnagro.jaa.Async.async; 8 | import static com.augustnagro.jaa.Async.await; 9 | 10 | CompletableFuture pdfForUsers = async(() -> { 11 | List userIds = await(userIdsFromDb()); 12 | 13 | List userNames = userIds.stream() 14 | .map(id -> await(userNamesFromSomeApi(id))) 15 | .toList(); 16 | 17 | byte[] pdf = await(buildPdf(userNames)); //buildPdf returns CompletableFuture 18 | 19 | System.out.println("Generated pdf for user ids: " + userIds); 20 | return pdf; 21 | }); 22 | ``` 23 | 24 | vs. 25 | 26 | ```java 27 | CompletionStage userPdf = userIdsFromDb().thenCompose(userIds -> { 28 | 29 | CompletionStage> userNamesFuture = 30 | CompletableFuture.supplyAsync(ArrayList::new); 31 | 32 | for (Long userId : userIds) { 33 | userNamesFuture = userNamesFuture.thenCompose(list -> { 34 | return userNamesFromSomeApi(userId) 35 | .thenApply(userName -> { 36 | list.add(userName); 37 | return list; 38 | }); 39 | }); 40 | } 41 | 42 | return userNamesFuture.thenCompose(userNames -> { 43 | return buildPdf(userNames).thenApply(pdf -> { 44 | System.out.println("Generated pdf for user ids: " + userIds); 45 | return pdf; 46 | }); 47 | }); 48 | }); 49 | ``` 50 | 51 | ## Maven Coordinates 52 | 53 | ```xml 54 | 55 | com.augustnagro 56 | java-async-await 57 | 0.3.0 58 | 59 | ``` 60 | 61 | This library requires [JDK 20](https://jdk.java.net/20/) with `--enable-preview`, and has no dependencies. 62 | 63 | ## Docs: 64 | 65 | Within an `async` scope, you can `await` CompletionStages and program in an imperative style. Most Future apis implement CompletionStage, or provide conversions. `async` and `await` calls can be nested to any depth. 66 | 67 | ## Why Async-Await vs the higher-order Future API? 68 | 69 | Abstractions like Future, Rx, ZIO, Uni, etc, are great. They provide convenient functions, like handling timeout and retries. 70 | 71 | However, there are serious downsides to implementing business logic in a fully 'monadic' style: 72 | 73 | * Often it's difficult to express something with Futures, when it is trivial with simple blocking code. 74 | * `flatMap` and its aliases like `thenCompose` are generally not stack-safe, and will StackOverflow if you recurse far enough (see unit tests for example). 75 | * It's hard to debug big Future chains in IDEs 76 | * Stack traces are often meaningless. 77 | * Future is 'viral', infecting your codebase. 78 | 79 | Project Loom solves all five issues, although Async-Await only solves the first 3. Stack traces are significantly better then using higher-order functions, but can still lack detail in certain cases. Since `async` returns `CompletableFuture`, it retains the virility of Future-like apis. 80 | 81 | ## Why Async-Await vs synchronous APIs on Virtual Threads and dropping async entirely? 82 | 83 | * That's not a bad idea. 84 | * You lose the concurrency features like timeout and retry offered by Rx, Uni, ZIO, etc. 85 | * Maybe you're already using Async libraries; the effort to migrate back to sync is gigantic, whereas introducing Async-Await can be done incrementally. 86 | 87 | ## Alternative Approaches 88 | 89 | * Java bytecode manipulation: https://github.com/electronicarts/ea-async 90 | * Scala 3 Async-Await macro: https://github.com/rssh/dotty-cps-async 91 | * Scala 3 Monadic Reflection using Loom: https://github.com/lampepfl/monadic-reflection 92 | * Kotlin Coroutines (another form of bytecode manipulation) 93 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.augustnagro 8 | java-async-await 9 | 0.3.1-SNAPSHOT 10 | 11 | Java Async-Await 12 | Async-Await support for Java 13 | https://github.com/AugustNagro/java-async-await 14 | 15 | 16 | UTF-8 17 | UTF-8 18 | 19 | 20 | 21 | 22 | augustnagro@gmail.com 23 | August Nagro 24 | augustnagro@gmail.com 25 | https://augustnagro.com 26 | 27 | 28 | 29 | 30 | 31 | Apache License, Version 2.0 32 | http://www.apache.org/licenses/LICENSE-2.0.txt 33 | repo 34 | 35 | 36 | 37 | 38 | https://github.com/AugustNagro/java-async-await 39 | scm:git:git@github.com:augustnagro/java-async-await.git 40 | scm:git:git@github.com:augustnagro/java-async-await.git 41 | 42 | 43 | 44 | 45 | ossrh 46 | https://oss.sonatype.org/content/repositories/snapshots 47 | 48 | 49 | ossrh 50 | https://oss.sonatype.org/service/local/staging/deploy/maven2 51 | 52 | 53 | 54 | 55 | 56 | junit 57 | junit 58 | 4.13.2 59 | test 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-compiler-plugin 68 | 3.8.1 69 | 70 | 20 71 | 72 | --enable-preview 73 | 74 | 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-surefire-plugin 80 | 2.22.2 81 | 82 | --enable-preview 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-deploy-plugin 89 | 2.8.2 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-source-plugin 95 | 3.2.0 96 | 97 | 98 | attach-sources 99 | 100 | jar-no-fork 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-javadoc-plugin 109 | 3.3.0 110 | 111 | 112 | attach-javadocs 113 | 114 | jar 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-gpg-plugin 123 | 3.0.1 124 | 125 | 126 | sign-artifacts 127 | verify 128 | 129 | sign 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/main/java/com/augustnagro/jaa/Async.java: -------------------------------------------------------------------------------- 1 | package com.augustnagro.jaa; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.Callable; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.CompletionStage; 7 | 8 | public class Async { 9 | 10 | // todo refactor to use ScopeLocal when it's no longer a draft JEP. 11 | // https://openjdk.java.net/jeps/8263012 12 | private static final ThreadLocal AWAIT_CONTEXT = new ThreadLocal<>(); 13 | 14 | public static CompletableFuture async(Callable fn) { 15 | CompletableFuture promise = new CompletableFuture<>(); 16 | Thread.startVirtualThread(() -> { 17 | try { 18 | AWAIT_CONTEXT.set(new Coroutine()); 19 | promise.complete(fn.call()); 20 | } catch (Throwable t) { 21 | promise.completeExceptionally(t); 22 | } finally { 23 | AWAIT_CONTEXT.remove(); 24 | } 25 | }); 26 | return promise; 27 | } 28 | 29 | public static A await(CompletionStage stage) { 30 | Coroutine coroutine = Objects.requireNonNull( 31 | AWAIT_CONTEXT.get(), 32 | "await must be called within an async block" 33 | ); 34 | 35 | return coroutine.await(stage); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/augustnagro/jaa/Coroutine.java: -------------------------------------------------------------------------------- 1 | package com.augustnagro.jaa; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | import java.util.concurrent.locks.Condition; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | /** 8 | * Must be run in a Virtual Thread, so that blocking until the 9 | * CompletionStage completes does not degrade performance. 10 | */ 11 | class Coroutine { 12 | 13 | private final ReentrantLock lock = new ReentrantLock(); 14 | private final Condition cond = lock.newCondition(); 15 | 16 | public A await(CompletionStage stage) { 17 | lock.lock(); 18 | try { 19 | 20 | stage.whenCompleteAsync((A res, Throwable ex) -> { 21 | lock.lock(); 22 | try { 23 | cond.signal(); 24 | } finally { 25 | lock.unlock(); 26 | } 27 | }); 28 | 29 | cond.await(); 30 | 31 | return stage.toCompletableFuture().join(); 32 | 33 | } catch (InterruptedException e) { 34 | throw new RuntimeException(e); 35 | 36 | } finally { 37 | lock.unlock(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/UnitTests.java: -------------------------------------------------------------------------------- 1 | import org.junit.Ignore; 2 | import org.junit.Test; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.CompletionStage; 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.Future; 10 | 11 | import static com.augustnagro.jaa.Async.async; 12 | import static com.augustnagro.jaa.Async.await; 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class UnitTests { 16 | 17 | CompletionStage> userIdsFromDb() { 18 | return CompletableFuture.completedFuture(List.of(1L, 2L, 3L)); 19 | } 20 | 21 | CompletionStage userNamesFromSomeApi(Long userId) { 22 | return CompletableFuture.supplyAsync(() -> "User " + userId); 23 | } 24 | 25 | CompletionStage buildPdf(List userNames) { 26 | return CompletableFuture.completedFuture(new byte[0]); 27 | } 28 | 29 | @Test 30 | public void testSimpleAsyncAwait() { 31 | CompletableFuture future = async(() -> { 32 | List userIds = await(userIdsFromDb()); 33 | 34 | List userNames = userIds.stream() 35 | .map(id -> await(userNamesFromSomeApi(id))) 36 | .toList(); 37 | 38 | byte[] pdf = await(buildPdf(userNames)); 39 | 40 | System.out.println("Generated pdf for user ids: " + userIds); 41 | return pdf; 42 | }); 43 | 44 | assertEquals(0, future.join().length); 45 | } 46 | 47 | @Test 48 | public void testSimple() { 49 | CompletionStage userPdf = userIdsFromDb().thenCompose(userIds -> { 50 | 51 | CompletionStage> userNamesFuture = 52 | CompletableFuture.supplyAsync(ArrayList::new); 53 | 54 | for (Long userId : userIds) { 55 | userNamesFuture = userNamesFuture.thenCompose(list -> { 56 | return userNamesFromSomeApi(userId) 57 | .thenApply(userName -> { 58 | list.add(userName); 59 | return list; 60 | }); 61 | }); 62 | } 63 | 64 | return userNamesFuture.thenCompose(userNames -> { 65 | return buildPdf(userNames).thenApply(pdf -> { 66 | System.out.println("Generated pdf for user ids: " + userIds); 67 | return pdf; 68 | }); 69 | }); 70 | }); 71 | 72 | assertEquals(0, userPdf.toCompletableFuture().join().length); 73 | } 74 | 75 | 76 | @Test 77 | public void testAwaitNesting() throws ExecutionException, InterruptedException { 78 | Future future = async(() -> { 79 | List ids1 = await(userIdsFromDb()); 80 | List ids2 = await(async(() -> await(userIdsFromDb()))); 81 | 82 | return ids1.size() + ids2.size(); 83 | }); 84 | 85 | assertEquals(6L, future.get().longValue()); 86 | } 87 | 88 | @Test 89 | public void testAwaitForLoop() throws ExecutionException, InterruptedException { 90 | Future future = async(() -> { 91 | ArrayList userNames = new ArrayList<>(); 92 | for (long userId = 1; userId <= 1000; ++userId) { 93 | userNames.add(await(userNamesFromSomeApi(userId))); 94 | } 95 | 96 | return userNames.get(userNames.size() - 1); 97 | }); 98 | 99 | assertEquals("User 1000", future.get()); 100 | } 101 | 102 | private static final long RECURSE_ITERATIONS = 20_000; 103 | 104 | @Test 105 | public void testRecursiveAwait() { 106 | CompletableFuture future = recurseAsync(RECURSE_ITERATIONS, 0); 107 | 108 | assertEquals(RECURSE_ITERATIONS, future.join().longValue()); 109 | } 110 | 111 | private CompletableFuture recurseAsync(long iterations, long result) { 112 | return async(() -> { 113 | if (iterations == 0) { 114 | return result; 115 | } else { 116 | Long newResult = await(calculateNewResult(result)); 117 | return await(recurseAsync(iterations - 1, newResult)); 118 | } 119 | }); 120 | } 121 | 122 | private CompletionStage calculateNewResult(long oldResult) { 123 | return CompletableFuture.completedFuture(oldResult + 1); 124 | } 125 | 126 | 127 | @Test 128 | @Ignore 129 | public void testRecursiveFlatMap() { 130 | CompletionStage future = recurseFlatMap(RECURSE_ITERATIONS, 0); 131 | assertEquals(RECURSE_ITERATIONS, future.toCompletableFuture().join().longValue()); 132 | } 133 | 134 | private CompletionStage recurseFlatMap(long iterations, long result) { 135 | CompletionStage newResult = calculateNewResult(result); 136 | return newResult.thenCompose(nr -> recurseFlatMap(iterations - 1, nr)); 137 | } 138 | 139 | 140 | } 141 | --------------------------------------------------------------------------------