Skip to content

Commit

Permalink
feat(SwallowedExceptionHandleUtils): add attachments parameter for …
Browse files Browse the repository at this point in the history
…handle methods; add test cases
  • Loading branch information
oldratlee committed Jan 25, 2025
1 parent 11dd415 commit e608c3f
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 9 deletions.
12 changes: 11 additions & 1 deletion cffu-core/src/main/java/io/foldright/cffu/eh/ExceptionInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import edu.umd.cs.findbugs.annotations.Nullable;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;


/**
* Exception info of exceptions, used as argument of {@link ExceptionHandler}.
Expand All @@ -11,11 +14,13 @@
public final class ExceptionInfo {
/**
* The location where the exception occurs.
* <p>
* The location is provided through the {@code where} parameter of the handle methods in {@link SwallowedExceptionHandleUtils}.
*/
public final String where;

/**
* The <strong>0-based</strong> index of the {@code CompletionStage} that throws the exception.
* The <strong>0-based</strong> index of the input {@code CompletionStage} that throws the exception.
*/
public final int index;

Expand All @@ -26,6 +31,11 @@ public final class ExceptionInfo {

/**
* An optional attachment object that may contain additional context; can be {@code null}.
* <p>
* The attachment object is provided through the attachments parameter of the handle methods in {@link SwallowedExceptionHandleUtils}.
*
* @see SwallowedExceptionHandleUtils#handleAllSwallowedExceptions(String, Object[], ExceptionHandler, CompletionStage[])
* @see SwallowedExceptionHandleUtils#handleSwallowedExceptions(String, Object[], ExceptionHandler, CompletableFuture, CompletionStage[])
*/
@Nullable
public final Object attachment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import java.util.concurrent.CompletionStage;

import static io.foldright.cffu.CompletableFutureUtils.unwrapCfException;
import static io.foldright.cffu.internal.CommonUtils.requireArrayAndEleNonNull;
import static io.foldright.cffu.internal.ExceptionLogger.logException;
import static io.foldright.cffu.internal.ExceptionLogger.logUncaughtException;
import static java.util.Objects.requireNonNull;


/**
Expand All @@ -29,7 +31,7 @@
public final class SwallowedExceptionHandleUtils {
/**
* Handles all exceptions from multiple input {@code CompletionStage}s as swallowed exceptions,
* using {@link #cffuSwallowedExceptionHandler()}.
* using {@link #cffuSwallowedExceptionHandler()} and callback it with null attachment.
*
* @param where the location where the exception occurs
*/
Expand All @@ -38,23 +40,42 @@ public static void handleAllSwallowedExceptions(String where, CompletionStage<?>
}

/**
* Handles all exceptions from multiple input {@code CompletionStage}s as swallowed exceptions.
* Handles all exceptions from multiple input {@code CompletionStage}s as swallowed exceptions,
* callback the exceptionHandler with null attachment.
*
* @param where the location where the exception occurs
* @param exceptionHandler the exception handler
*/
public static void handleAllSwallowedExceptions(
String where, ExceptionHandler exceptionHandler, CompletionStage<?>... inputs) {
handleAllSwallowedExceptions(where, null, exceptionHandler, inputs);
}

/**
* Handles all exceptions from multiple input {@code CompletionStage}s as swallowed exceptions.
*
* @param where the location where the exception occurs
* @param attachments the attachment objects
* @param exceptionHandler the exception handler
*/
public static void handleAllSwallowedExceptions(
String where, @Nullable Object[] attachments, ExceptionHandler exceptionHandler,
CompletionStage<?>... inputs) {
requireNonNull(where, "where is null");
requireNonNull(exceptionHandler, "exceptionHandler is null");
requireArrayAndEleNonNull("input", inputs);

for (int i = 0; i < inputs.length; i++) {
final int idx = i;
// argument ex of method `exceptionally` is never null
inputs[idx].exceptionally(ex -> safeHandle(new ExceptionInfo(where, idx, ex, null), exceptionHandler));
inputs[idx].exceptionally(ex -> safeHandle(new ExceptionInfo(
where, idx, ex, safeGet(attachments, idx)), exceptionHandler));
}
}

/**
* Handles swallowed exceptions from multiple input {@code CompletionStage}s that are discarded (not propagated)
* by the output {@code CompletionStage}, using {@link #cffuSwallowedExceptionHandler()}.
* by the output {@code CompletionStage}, using {@link #cffuSwallowedExceptionHandler()} and callback it with null attachment.
*
* @param where the location where the exception occurs
*/
Expand All @@ -63,15 +84,34 @@ public static void handleSwallowedExceptions(
handleSwallowedExceptions(where, cffuSwallowedExceptionHandler(), output, inputs);
}

/**
* Handles swallowed exceptions from multiple input {@code CompletionStage}s that are discarded (not propagated)
* by the output {@code CompletionStage},callback the exceptionHandler with null attachment.
*
* @param where the location where the exception occurs
* @param exceptionHandler the exception handler
*/
public static void handleSwallowedExceptions(
String where, ExceptionHandler exceptionHandler, CompletableFuture<?> output, CompletionStage<?>... inputs) {
handleSwallowedExceptions(where, null, exceptionHandler, output, inputs);
}

/**
* Handles swallowed exceptions from multiple input {@code CompletionStage}s
* that are discarded (not propagated) by the output {@code CompletionStage}.
*
* @param where the location where the exception occurs
* @param attachments the attachment objects
* @param exceptionHandler the exception handler
*/
public static void handleSwallowedExceptions(
String where, ExceptionHandler exceptionHandler, CompletableFuture<?> output, CompletionStage<?>... inputs) {
String where, @Nullable Object[] attachments, ExceptionHandler exceptionHandler,
CompletableFuture<?> output, CompletionStage<?>... inputs) {
requireNonNull(where, "where is null");
requireNonNull(exceptionHandler, "exceptionHandler is null");
requireNonNull(output, "output is null");
requireArrayAndEleNonNull("input", inputs);

// uses unreferenced cfs to prevent memory leaks, in case that
// some inputs complete quickly and retain large memory while other inputs or output continue running
CompletionStage<Void>[] unreferencedInputs = unreferenced(inputs);
Expand All @@ -87,7 +127,7 @@ public static void handleSwallowedExceptions(
cf.exceptionally(ex -> {
// if ex is returned to output cf(aka. not swallowed ex), do NOTHING
if (unwrapCfException(ex) == outputBizEx) return null;
return safeHandle(new ExceptionInfo(where, idx, ex, null), exceptionHandler);
return safeHandle(new ExceptionInfo(where, idx, ex, safeGet(attachments, idx)), exceptionHandler);
});
}
});
Expand Down Expand Up @@ -118,10 +158,15 @@ private static CompletionStage<Void>[] unreferenced(CompletionStage<?>[] css) {
});
}

@Nullable
private static @Nullable Object safeGet(@Nullable Object[] attachments, int index) {
if (attachments == null) return null;
else if (index < attachments.length) return attachments[index];
else return null;
}

@Contract("_, _ -> null")
@SuppressWarnings("SameReturnValue")
private static <T> T safeHandle(ExceptionInfo exceptionInfo, ExceptionHandler exceptionHandler) {
private static <T> @Nullable T safeHandle(ExceptionInfo exceptionInfo, ExceptionHandler exceptionHandler) {
try {
exceptionHandler.handle(exceptionInfo);
} catch (Throwable e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package io.foldright.cffu.eh

import io.foldright.cffu.CompletableFutureUtils.failedFuture
import io.foldright.cffu.eh.SwallowedExceptionHandleUtils.handleAllSwallowedExceptions
import io.foldright.cffu.eh.SwallowedExceptionHandleUtils.handleSwallowedExceptions
import io.kotest.assertions.throwables.shouldThrowExactly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
import java.util.concurrent.CompletableFuture.completedFuture
import java.util.concurrent.CopyOnWriteArrayList

@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
class SwallowedExceptionHandleUtilsTest : FunSpec({

test("handleAllSwallowedExceptions") {
val eiList = CopyOnWriteArrayList<ExceptionInfo>()
val eh = ExceptionHandler { eiList.add(it) }

shouldThrowExactly<NullPointerException> {
handleAllSwallowedExceptions(null, eh, null, null)
}.message shouldBe "where is null"
shouldThrowExactly<NullPointerException> {
handleAllSwallowedExceptions("shouldThrowExactly", arrayOf(), null)
}.message shouldBe "exceptionHandler is null"
shouldThrowExactly<NullPointerException> {
handleAllSwallowedExceptions("shouldThrowExactly", null, eh, null)
}.message shouldBe "input1 is null"
shouldThrowExactly<NullPointerException> {
handleAllSwallowedExceptions("shouldThrowExactly", null, eh, completedFuture(42), null)
}.message shouldBe "input2 is null"

val where = "test handleAllSwallowedExceptions"
handleAllSwallowedExceptions(where, eh)
eiList shouldHaveSize 0
handleAllSwallowedExceptions(where, eh, completedFuture(0))
eiList shouldHaveSize 0

val rte = RuntimeException("Bang...")
handleAllSwallowedExceptions(where, eh, failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 0
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()

eiList.clear()
handleAllSwallowedExceptions(where, eh, completedFuture(42), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()

eiList.clear()
handleAllSwallowedExceptions(where, arrayOf("a0"), eh, completedFuture(42), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()

eiList.clear()
handleAllSwallowedExceptions(where, arrayOf("a0", "a1", "a2"), eh, completedFuture(42), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment shouldBe "a1"

eiList.clear()
handleAllSwallowedExceptions(
where, arrayOf("a0", "a1", "a2", "a3"), eh,
failedFuture<Int>(rte), completedFuture(42), failedFuture<Int>(rte)
)
eiList shouldHaveSize 2
eiList[0].where shouldBe where
eiList[0].index shouldBe 0
eiList[0].exception shouldBe rte
eiList[0].attachment shouldBe "a0"
eiList[1].where shouldBe where
eiList[1].index shouldBe 2
eiList[1].exception shouldBe rte
eiList[1].attachment shouldBe "a2"
}

test("handleSwallowedExceptions") {
val eiList = CopyOnWriteArrayList<ExceptionInfo>()
val eh = ExceptionHandler { eiList.add(it) }

shouldThrowExactly<NullPointerException> {
handleSwallowedExceptions(null, eh, null, null)
}.message shouldBe "where is null"
shouldThrowExactly<NullPointerException> {
handleSwallowedExceptions("shouldThrowExactly", arrayOf(), null, null)
}.message shouldBe "exceptionHandler is null"
shouldThrowExactly<NullPointerException> {
handleSwallowedExceptions("shouldThrowExactly", null, eh, null)
}.message shouldBe "output is null"
shouldThrowExactly<NullPointerException> {
handleSwallowedExceptions("shouldThrowExactly", null, eh, completedFuture(42), null)
}.message shouldBe "input1 is null"
shouldThrowExactly<NullPointerException> {
handleSwallowedExceptions("shouldThrowExactly", null, eh, completedFuture(42), completedFuture(43), null)
}.message shouldBe "input2 is null"

val where = "test handleSwallowedExceptions"
handleSwallowedExceptions(where, eh, completedFuture(42))
eiList shouldHaveSize 0
handleSwallowedExceptions(where, eh, completedFuture(42), completedFuture(0))
eiList shouldHaveSize 0


val rte = RuntimeException("Bang!")
handleSwallowedExceptions(where, eh, completedFuture(42), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 0
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()

eiList.clear()
handleSwallowedExceptions(where, eh, completedFuture(42), completedFuture(1), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()

eiList.clear()
handleSwallowedExceptions(
where, arrayOf("a0"), eh,
completedFuture(42),
completedFuture(42), failedFuture<Int>(rte)
)
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()

eiList.clear()
handleSwallowedExceptions(
where, arrayOf("a0", "a1", "a2"), eh,
completedFuture(42),
completedFuture(42), failedFuture<Int>(rte)
)
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment shouldBe "a1"

eiList.clear()
handleSwallowedExceptions(
where, arrayOf("a0", "a1", "a2", "a3"), eh,
completedFuture(42),
failedFuture<Int>(rte), completedFuture(42), failedFuture<Int>(rte)
)
eiList shouldHaveSize 2
eiList[0].where shouldBe where
eiList[0].index shouldBe 0
eiList[0].exception shouldBe rte
eiList[0].attachment shouldBe "a0"
eiList[1].where shouldBe where
eiList[1].index shouldBe 2
eiList[1].exception shouldBe rte
eiList[1].attachment shouldBe "a2"
}

test("handleSwallowedExceptions, wont report exception of output") {
val eiList = CopyOnWriteArrayList<ExceptionInfo>()
val eh = ExceptionHandler {
if (it.index == 0) throw RuntimeException("intend exception in test cases")
eiList.add(it)
}

val rte = RuntimeException("Bang...")
val outRte = RuntimeException("exception of output")
val where = "exceptionHandler"
handleSwallowedExceptions(where, eh, failedFuture<Int>(outRte), failedFuture<Int>(outRte), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()
}

test("exception handler throws uncaught exception") {
val eiList = CopyOnWriteArrayList<ExceptionInfo>()
val eh = ExceptionHandler {
if (it.index == 0) throw RuntimeException("intend exception in test cases")
eiList.add(it)
}

val rte = RuntimeException("Bang...")
val where = "exceptionHandler"
handleAllSwallowedExceptions(where, eh, failedFuture<Int>(rte), failedFuture<Int>(rte))
eiList shouldHaveSize 1
eiList.first().where shouldBe where
eiList.first().index shouldBe 1
eiList.first().exception shouldBe rte
eiList.first().attachment.shouldBeNull()
}
})

0 comments on commit e608c3f

Please sign in to comment.