Skip to content

Commit c937e9b

Browse files
committed
core(utils): Added utility class for methods related to CompletableFuture
1 parent 719be51 commit c937e9b

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.shortthirdman.primekit.essentials.common.util;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.Future;
7+
import java.util.concurrent.ScheduledFuture;
8+
import java.util.concurrent.ScheduledThreadPoolExecutor;
9+
import java.util.concurrent.ThreadFactory;
10+
import java.util.concurrent.TimeUnit;
11+
import java.util.concurrent.TimeoutException;
12+
import java.util.function.BiConsumer;
13+
14+
/**
15+
* A utility class to add JDK 9+ timeout functionality to CompletableFuture in JDK.
16+
* Inspired by JDK 9's CompletableFuture implementation.
17+
*
18+
* @author ShortThirdMan
19+
*/
20+
public final class CompletableFutureUtils {
21+
22+
private CompletableFutureUtils() {}
23+
24+
/**
25+
* If not already completed, causes this CompletableFuture to be
26+
* completed exceptionally with a {@link TimeoutException} after the
27+
* given timeout.
28+
*
29+
* @param future the CompletableFuture to apply the timeout to
30+
* @param timeout how long to wait before completing exceptionally
31+
* @param unit the time unit of the timeout argument
32+
* @return the original CompletableFuture
33+
*/
34+
public static <T> CompletableFuture<T> orTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) {
35+
if (unit == null) {
36+
throw new NullPointerException("Time unit cannot be null");
37+
}
38+
if (future == null) {
39+
throw new NullPointerException("CompletableFuture cannot be null");
40+
}
41+
// If the future is already done, just return it.
42+
if (future.isDone()) {
43+
return future;
44+
}
45+
// Schedule a task to complete the future exceptionally after the timeout.
46+
// The Canceller will cancel this scheduled task if the future completes normally.
47+
return future.whenComplete(new Canceller(Delayer.delay(new Timeout(future), timeout, unit)));
48+
}
49+
50+
/**
51+
* Inner class to handle the actual timeout logic
52+
*/
53+
static final class Timeout implements Runnable {
54+
final CompletableFuture<?> future;
55+
Timeout(CompletableFuture<?> future) {
56+
this.future = future;
57+
}
58+
public void run() {
59+
if (future != null && !future.isDone()) {
60+
future.completeExceptionally(new TimeoutException());
61+
}
62+
}
63+
}
64+
65+
/**
66+
* Inner class to handle cancellation of the timeout task
67+
*/
68+
static final class Canceller implements BiConsumer<Object, Throwable> {
69+
final Future<?> future;
70+
Canceller(Future<?> future) {
71+
this.future = future;
72+
}
73+
public void accept(Object ignore, Throwable ex) {
74+
// If the original future completed (ex is null) and the timeout task
75+
// hasn't run yet, cancel the timeout task.
76+
if (ex == null && future != null && !future.isDone()) {
77+
future.cancel(false);
78+
}
79+
}
80+
}
81+
82+
/**
83+
* Singleton delayed scheduler
84+
*/
85+
static final class Delayer {
86+
static ScheduledFuture<?> delay(Runnable command, long delay, TimeUnit unit) {
87+
return delayer.schedule(command, delay, unit);
88+
}
89+
private static final ScheduledThreadPoolExecutor delayer;
90+
static {
91+
delayer = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());
92+
delayer.setRemoveOnCancelPolicy(true);
93+
}
94+
static final class DaemonThreadFactory implements ThreadFactory {
95+
public Thread newThread(@NotNull Runnable r) {
96+
Thread t = new Thread(r);
97+
t.setDaemon(true);
98+
t.setName("CompletableFutureUtilsDelayScheduler");
99+
return t;
100+
}
101+
}
102+
}
103+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.shortthirdman.primekit.essentials.common.util;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.CompletionException;
7+
import java.util.concurrent.TimeUnit;
8+
import java.util.concurrent.TimeoutException;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
class CompletableFutureUtilsTest {
13+
14+
@Test
15+
void completesNormallyBeforeTimeout_cancelsTimeoutTask() {
16+
CompletableFuture<String> f = new CompletableFuture<>();
17+
CompletableFutureUtils.orTimeout(f, 200, TimeUnit.MILLISECONDS);
18+
19+
// Complete before 200ms
20+
f.complete("ok");
21+
22+
assertTrue(f.isDone());
23+
assertFalse(f.isCompletedExceptionally());
24+
assertEquals("ok", f.join());
25+
26+
// Sleep past the timeout to ensure the scheduled timeout would have fired if not cancelled
27+
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
28+
// If the timeout wasn't cancelled, the future would be completed exceptionally already, which is not the case.
29+
assertFalse(f.isCompletedExceptionally());
30+
}
31+
32+
@Test
33+
void timesOutWhenNotCompletedInTime_completesExceptionallyWithTimeoutException() {
34+
CompletableFuture<String> f = new CompletableFuture<>();
35+
CompletableFutureUtils.orTimeout(f, 50, TimeUnit.MILLISECONDS);
36+
37+
// Do not complete the future; wait to let timeout trigger
38+
try { Thread.sleep(120); } catch (InterruptedException ignored) {}
39+
40+
assertTrue(f.isCompletedExceptionally());
41+
CompletionException ex = assertThrows(CompletionException.class, f::join);
42+
assertInstanceOf(TimeoutException.class, ex.getCause());
43+
}
44+
45+
@Test
46+
void handlesNullArgumentsAndAlreadyCompletedFuture() {
47+
// Null unit
48+
CompletableFuture<String> f1 = new CompletableFuture<>();
49+
assertThrows(NullPointerException.class, () -> CompletableFutureUtils.orTimeout(f1, 10, null));
50+
51+
// Null future
52+
assertThrows(NullPointerException.class, () -> CompletableFutureUtils.orTimeout(null, 10, TimeUnit.MILLISECONDS));
53+
54+
// Already completed future should be returned as is and not be modified
55+
CompletableFuture<Integer> done = CompletableFuture.completedFuture(42);
56+
CompletableFuture<Integer> returned = CompletableFutureUtils.orTimeout(done, 10, TimeUnit.MILLISECONDS);
57+
assertSame(done, returned);
58+
assertEquals(42, returned.join());
59+
// Sleep beyond timeout to confirm nothing changes
60+
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
61+
assertFalse(returned.isCompletedExceptionally());
62+
}
63+
}

0 commit comments

Comments
 (0)