Skip to content

Commit f220fc1

Browse files
Konstantin PavlovKonstantin Pavlov
Konstantin Pavlov
authored and
Konstantin Pavlov
committed
More tests, refactoring and javadocs, short-circuit done futures
1 parent fdf0257 commit f220fc1

File tree

11 files changed

+269
-103
lines changed

11 files changed

+269
-103
lines changed

.github/workflows/maven-publish.yml

-34
This file was deleted.

.github/workflows/maven.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
distribution: 'temurin'
2929
cache: maven
3030
- name: Build with Maven
31-
run: mvn -B package
31+
run: mvn -B verify javadoc:jar source:jar
3232

3333
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
3434
- name: Update dependency graph

src/main/java/me/kpavlov/await4j/Async.java

+158-36
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,40 @@
44
import java.util.concurrent.*;
55
import java.util.concurrent.atomic.AtomicReference;
66

7+
/**
8+
* The {@code Async} class provides utilities to execute code asynchronously using virtual threads.
9+
* It allows running {@link ThrowingRunnable} and {@link Callable} tasks asynchronously, handling exceptions,
10+
* and returning results in a synchronous manner. It leverages the new virtual threads feature introduced in Java
11+
* to provide lightweight concurrency.
12+
* <p>
13+
* This class offers methods to:
14+
* <ul>
15+
* <li>Run a block of code asynchronously and wait for its completion.</li>
16+
* <li>Handle both checked and unchecked exceptions in asynchronous tasks.</li>
17+
* <li>Retrieve results from {@link Future} and {@link CompletableFuture} objects.</li>
18+
* </ul>
19+
*/
720
public class Async {
821

922
private static final Thread.Builder virtualThreadBuilder = Thread.ofVirtual()
1023
.name("async-virtual-", 0);
1124

12-
public static void await(ThrowingRunnable block) {
25+
private Async() {
26+
// hide public constructor
27+
}
28+
29+
/**
30+
* Executes a block of code asynchronously and waits for its completion.
31+
*
32+
* @param block The code to be executed asynchronously
33+
* @param millis The maximum time to wait for the block to complete, in milliseconds
34+
* @throws CompletionException if the virtual thread is interrupted
35+
* @throws RuntimeException if the block throws an exception
36+
* @throws Error if the block throws an Error
37+
* @throws IllegalStateException if an unexpected Throwable is encountered
38+
*/
39+
@SuppressWarnings("java:S1181")
40+
public static void await(ThrowingRunnable block, long millis) {
1341
Objects.requireNonNull(block, "Block should not be null");
1442
try {
1543
final var failureHolder = new AtomicReference<Throwable>();
@@ -18,44 +46,71 @@ public static void await(ThrowingRunnable block) {
1846
ThrowingRunnable
1947
.toRunnable(block)
2048
.run();
21-
} catch (RuntimeException | Error e) {
49+
} catch (Error e) {
2250
failureHolder.set(e);
51+
} catch (Exception e) {
52+
failureHolder.set(toRuntimeException(e));
2353
}
2454
})
25-
.join();
55+
.join(millis);
2656
final Throwable throwable = failureHolder.get();
27-
if (throwable instanceof Error e) {
28-
throw e;
29-
} else if (throwable instanceof RuntimeException re) {
30-
throw re;
31-
} else {
32-
throw new IllegalStateException("Unexpected Throwable: " + throwable, throwable);
57+
switch (throwable) {
58+
case null -> {
59+
// success
60+
}
61+
case Error e -> throw e;
62+
case RuntimeException re -> throw re;
63+
default -> throw new IllegalStateException("Unexpected Throwable: " + throwable, throwable);
3364
}
3465
} catch (InterruptedException e) {
3566
Thread.currentThread().interrupt();
36-
throw new RuntimeException("Interrupted virtual thread", e);
67+
throw new CompletionException("Interrupted virtual thread", e);
3768
}
3869
}
3970

40-
public static <T> T await(Callable<T> block) throws RuntimeException {
71+
/**
72+
* Executes a block of code asynchronously and waits indefinitely for its completion.
73+
*
74+
* @param block The code to be executed asynchronously
75+
* @throws CompletionException if the virtual thread is interrupted
76+
* @throws RuntimeException if the block throws an exception
77+
* @throws Error if the block throws an Error
78+
* @throws IllegalStateException if an unexpected Throwable is encountered
79+
*/
80+
public static void await(ThrowingRunnable block) {
81+
await(block, 0);
82+
}
83+
84+
/**
85+
* Executes a callable block asynchronously and returns its result.
86+
*
87+
* @param <T> The type of the result
88+
* @param block The callable block to be executed asynchronously. <strong>Block should not execute code
89+
* that contains synchronized blocks or invokes synchronized methods to avoid scalability issues.</strong>
90+
* @param millis The maximum time to wait for the callable block to complete, in milliseconds
91+
* @return The result of the callable block
92+
* @throws RuntimeException if the virtual thread is interrupted or if the block throws an exception
93+
* @throws Error if the block throws an Error
94+
* @throws IllegalStateException if an unexpected throwable is encountered in the call result
95+
*/
96+
public static <T> T await(Callable<T> block, long millis) {
4197
Objects.requireNonNull(block, "Callable should not be null");
4298
try {
4399
final var resultHolder = new AtomicReference<Result<T>>();
44100
virtualThreadBuilder.start(() -> {
45101
final Result<T> result = callWithErrorHandling(block);
46102
resultHolder.set(result);
47-
}).join();
103+
}).join(millis);
48104
final Result<T> result = resultHolder.get();
49105
if (result.isSuccess()) {
50-
return result.getOrThrow();
106+
return result.getOrNull();
51107
} else {
52108
final Throwable failure = result.failure();
53-
if (failure instanceof RuntimeException re) {
54-
throw re;
55-
} else if (failure instanceof Error e) {
56-
throw e;
57-
} else {
58-
throw new IllegalStateException("Unexpected throwable in call Result:" + failure, failure);
109+
switch (failure) {
110+
case RuntimeException re -> throw re;
111+
case Error e -> throw e;
112+
case null, default ->
113+
throw new IllegalStateException("Unexpected throwable in call Result:" + failure, failure);
59114
}
60115
}
61116
} catch (InterruptedException e) {
@@ -64,6 +119,62 @@ public static <T> T await(Callable<T> block) throws RuntimeException {
64119
}
65120
}
66121

122+
/**
123+
* Executes a callable block asynchronously and returns its result.
124+
*
125+
* @param <T> The type of the result
126+
* @param block The callable block to be executed asynchronously
127+
* @return The result of the callable block
128+
* @throws RuntimeException if the virtual thread is interrupted or if the block throws an exception
129+
* @throws Error if the block throws an Error
130+
* @throws IllegalStateException if an unexpected throwable is encountered in the call result
131+
*/
132+
public static <T> T await(Callable<T> block) {
133+
return await(block, 0);
134+
}
135+
136+
/**
137+
* Waits for the completion of a Future and returns its result.
138+
*
139+
* @param <T> The type of the result
140+
* @param future The Future to await
141+
* @return The result of the Future
142+
* @throws RuntimeException if the Future completes exceptionally
143+
*/
144+
public static <T> T await(Future<T> future) {
145+
if (shortCircuitDoneFuture(future)) return future.resultNow();
146+
return await(() -> future.get());
147+
}
148+
149+
150+
/**
151+
* Waits for the completion of a Future and returns its result.
152+
*
153+
* @param <T> The type of the result
154+
* @param future The Future to await
155+
* @param millis The maximum time to wait for the future to complete, in milliseconds
156+
* @return The result of the Future
157+
* @throws RuntimeException if the Future completes exceptionally
158+
*/
159+
public static <T> T await(Future<T> future, long millis) {
160+
if (shortCircuitDoneFuture(future)) return future.resultNow();
161+
return await(() -> future.get(millis, TimeUnit.MILLISECONDS));
162+
}
163+
164+
/**
165+
* Waits for the completion of a CompletableFuture and returns its result.
166+
*
167+
* @param <T> The type of the result
168+
* @param completableFuture The CompletableFuture to await
169+
* @return The result of the CompletableFuture
170+
* @throws RuntimeException if the CompletableFuture completes exceptionally
171+
*/
172+
public static <T> T await(CompletableFuture<T> completableFuture) {
173+
if (shortCircuitDoneFuture(completableFuture)) return completableFuture.resultNow();
174+
return await(completableFuture::join);
175+
}
176+
177+
67178
/**
68179
* Executes a block of code with error handling.
69180
* <p>
@@ -79,38 +190,49 @@ public static <T> T await(Callable<T> block) throws RuntimeException {
79190
* @throws RuntimeException if an {@link InterruptedException},
80191
* {@link ExecutionException}, or any other exception occurs
81192
*/
193+
@SuppressWarnings("java:S1181")
82194
private static <T> Result<T> callWithErrorHandling(Callable<T> block) {
83195
try {
84196
return Result.success(block.call());
85197
} catch (InterruptedException e) {
86198
Thread.currentThread().interrupt(); // Restore the interrupted status
87199
return Result.failure(
88-
new RuntimeException("Can't execute async task: interrupted", e)
200+
new CompletionException("Can't execute async task: interrupted", e)
89201
);
90-
} catch (ExecutionException | CompletionException e) {
202+
} catch (Error e) {
203+
return Result.failure(e);
204+
} catch (CompletionException | ExecutionException e) {
91205
final Throwable cause = e.getCause();
92-
if (cause instanceof RuntimeException re) {
93-
// re-throw RuntimeException as it is
94-
return Result.failure(re);
95-
} else if (cause instanceof Error error) {
96-
// re-throw Error as it is
97-
return Result.failure(error);
206+
if (cause instanceof Error) {
207+
return Result.failure(cause);
98208
} else {
99-
return Result.failure(new RuntimeException("Can't execute async task: exception", cause));
209+
return Result.failure(toRuntimeException(cause));
100210
}
101-
} catch (RuntimeException | Error e) {
102-
return Result.failure(e);
103-
} catch (Exception e) {
104-
return Result.failure(new RuntimeException("Can't execute async task: exception", e));
211+
} catch (Throwable e) {
212+
return Result.failure(toRuntimeException(e));
105213
}
106214
}
107215

108-
public static <T> T await(Future<T> future) {
109-
return await(() -> future.get());
216+
private static <T> boolean shortCircuitDoneFuture(Future<T> future) {
217+
if (future.state() == Future.State.SUCCESS) {
218+
return true;
219+
} else if (future.state() == Future.State.FAILED) {
220+
Throwable throwable = future.exceptionNow();
221+
if (throwable instanceof Error e) {
222+
throw e;
223+
}
224+
throw toRuntimeException(throwable);
225+
}
226+
return false;
110227
}
111228

112-
public static <T> T await(CompletableFuture<T> completableFuture) {
113-
return await(completableFuture::join);
229+
private static RuntimeException toRuntimeException(Throwable cause) {
230+
if (cause instanceof RuntimeException re) {
231+
// re-throw RuntimeException as it is
232+
return re;
233+
} else {
234+
return new CompletionException("Can't execute async task: exception", cause);
235+
}
114236
}
115237

116238
}

src/main/java/me/kpavlov/await4j/Result.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package me.kpavlov.await4j;
22

33
import java.util.function.Function;
4+
import java.util.function.UnaryOperator;
45

56
/**
67
* Represents the result of an operation that can either succeed with a value or fail with a throwable.
@@ -83,7 +84,15 @@ public <R> Result<R> map(Function<T, R> function) {
8384
return success(function.apply(value));
8485
}
8586

86-
public <R> Result<R> mapThrowable(Function<Throwable, Throwable> function) {
87+
/**
88+
* Maps the failure throwable using the provided function.
89+
*
90+
* @param <R> the type parameter for the new Result
91+
* @param function the mapping function for the throwable
92+
* @return a new Result instance with the mapped throwable
93+
* @throws IllegalStateException if this instance represents success
94+
*/
95+
public <R> Result<R> mapThrowable(UnaryOperator<Throwable> function) {
8796
if (throwable == null) {
8897
throw new IllegalStateException("Can't map empty Throwable. Use map if needed.");
8998
}

src/test/java/me/kpavlov/await4j/AsyncCallableTest.java

+13-6
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
import org.junit.jupiter.params.provider.MethodSource;
77

88
import java.util.concurrent.Callable;
9+
import java.util.concurrent.CompletionException;
910
import java.util.concurrent.atomic.AtomicBoolean;
1011

1112
import static org.assertj.core.api.Assertions.assertThat;
1213
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1314

1415
class AsyncCallableTest extends AbstractAsyncTest {
1516

17+
static Object[][] awaitCallableToCompleteSuccessfully() {
18+
return TestUtils.combine(threadBuilders(), "OK", null);
19+
}
20+
1621
@ParameterizedTest
17-
@MethodSource("threadBuilders")
18-
void awaitCallableToCompleteSuccessfully(Thread.Builder threadBuilder) throws InterruptedException {
22+
@MethodSource("awaitCallableToCompleteSuccessfully")
23+
void awaitCallableToCompleteSuccessfully(final Thread.Builder threadBuilder,
24+
final String expectedResult) throws InterruptedException {
1925
final var completed = new AtomicBoolean();
2026
threadBuilder.start(() -> {
2127
final var originalThread = Thread.currentThread();
@@ -27,17 +33,18 @@ void awaitCallableToCompleteSuccessfully(Thread.Builder threadBuilder) throws In
2733
checkVirtualThreadInvariants(originalThread, threadLocal);
2834
completed.compareAndSet(false, true);
2935
// return result
30-
return "Supplier Completed";
36+
return expectedResult;
3137
});
3238
// then
33-
assertThat(result).isEqualTo("Supplier Completed");
39+
assertThat(result).isEqualTo(expectedResult);
3440
}).join();
3541
assertThat(completed).isTrue();
3642
}
3743

44+
3845
@ParameterizedTest
3946
@MethodSource("threadBuilders")
40-
void awaitSupplierReThrowsRuntimeException(Thread.Builder threadBuilder) throws InterruptedException {
47+
void awaitSupplierReThrowsRuntimeException(final Thread.Builder threadBuilder) throws InterruptedException {
4148
// given
4249
final var runtimeException = new RuntimeException("Failure");
4350
final Callable<String> callable = () -> {
@@ -60,7 +67,7 @@ void awaitHandlesThrowable(Exception throwable) {
6067
Assertions.fail("Expected to fail with exception: %s", (Object) throwable);
6168
} catch (Exception e) {
6269
assertThat(e)
63-
.isInstanceOf(RuntimeException.class)
70+
.isInstanceOf(CompletionException.class)
6471
.hasCause(throwable);
6572
}
6673
}

0 commit comments

Comments
 (0)