Skip to content

Commit 74875ed

Browse files
authored
Adding Task<>.recover(String, Class<> exceptionFilter, Function1<>) method to provide recovery with failure filter (#299)
1 parent c51f3a2 commit 74875ed

5 files changed

Lines changed: 109 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
v5.1.3
22
------
3-
* Fix for multiple javadoc warnings coming from Task.java
4-
3+
* Fix for multiple javadoc warnings coming from Task.java
4+
* Add recover() method with exception filter
5+
56
v5.1.2
67
------
78
- Migrate the ParSeq release process from Bintray to JFrog Artifactory.

subprojects/parseq/src/main/java/com/linkedin/parseq/Task.java

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -637,11 +637,46 @@ default <R> Task<R> andThen(final Task<R> task) {
637637
*/
638638
default Task<T> recover(final String desc, final Function1<Throwable, T> func) {
639639
ArgumentUtil.requireNotNull(func, "function");
640+
return recover(desc, Throwable.class, func);
641+
}
642+
643+
/**
644+
* Equivalent to {@code recover("recover", func)}.
645+
* @see #recover(String, Function1)
646+
*/
647+
default Task<T> recover(final Function1<Throwable, T> func) {
648+
return recover("recover: " + _taskDescriptor.getDescription(func.getClass().getName()), Throwable.class, func);
649+
}
650+
651+
/**
652+
* Creates a new task that will handle failure of this task.
653+
*
654+
* Early completion due to cancellation is not considered to be a failure and the recovery function is not called.
655+
* If this task competed with failure, and failure type matches the exception class the provided, that failure recovery
656+
* function is called.
657+
*
658+
* If this task completes successfully, then recovery function is not called.
659+
*
660+
* Note that the first call to <code>recover()</code> with matching exception class provided would result in a recovered Task.
661+
* For example, observe the code below:
662+
* <pre><code>
663+
* Task&lt;String&gt; task = Task.failure(new RuntimeException())
664+
* .recover("try recovering RuntimeException", RuntimeException.class, e -&gt; "recovered") // results in recovery, as exception class matches
665+
* .recover("try recovering Throwable", Throwable.class, e -&gt; "no action"); // no action, as already recovered at previous step
666+
* </code></pre>
667+
* @param desc description of a recovery function, it will show up in a trace
668+
* @param exceptionClass exception class, defines which types of failures would be attempted to recover with {@code func}
669+
* @param func recovery function which can complete task with a value depending on failure of this task
670+
* @return a new task which can recover from failure of this task
671+
*/
672+
default <X extends Throwable> Task<T> recover(final String desc, final Class<X> exceptionClass, final Function1<? super X, ? extends T> func) {
673+
ArgumentUtil.requireNotNull(func, "function");
674+
ArgumentUtil.requireNotNull(exceptionClass, "exception class");
640675
return apply(desc, (src, dst) -> {
641676
if (src.isFailed()) {
642-
if (!(Exceptions.isCancellation(src.getError()))) {
677+
if (!(Exceptions.isCancellation(src.getError())) && exceptionClass.isInstance(src.getError())) {
643678
try {
644-
dst.done(func.apply(src.getError()));
679+
dst.done(func.apply(exceptionClass.cast(src.getError())));
645680
} catch (Throwable t) {
646681
dst.fail(t);
647682
}
@@ -655,13 +690,14 @@ default Task<T> recover(final String desc, final Function1<Throwable, T> func) {
655690
}
656691

657692
/**
658-
* Equivalent to {@code recover("recover", func)}.
659-
* @see #recover(String, Function1)
693+
* Equivalent to {@code recover("recover", exceptionClass, func)}.
694+
* @see #recover(String, Class, Function1)
660695
*/
661-
default Task<T> recover(final Function1<Throwable, T> func) {
662-
return recover("recover: " + _taskDescriptor.getDescription(func.getClass().getName()), func);
696+
default <X extends Throwable> Task<T> recover(Class<X> exceptionClass, final Function1<? super X, ? extends T> func) {
697+
String description = "recover: " + _taskDescriptor.getDescription(func.getClass().getName());
698+
return recover(description, exceptionClass, func);
663699
}
664-
700+
665701
/**
666702
* Creates a new task which applies a consumer to the exception this
667703
* task may fail with. It is used in situations where consumer needs

subprojects/parseq/src/test/java/com/linkedin/parseq/AbstractTaskTest.java

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
package com.linkedin.parseq;
22

3-
import static org.testng.Assert.assertEquals;
4-
import static org.testng.Assert.assertFalse;
5-
import static org.testng.Assert.assertNull;
6-
import static org.testng.Assert.assertSame;
7-
import static org.testng.Assert.assertTrue;
8-
import static org.testng.Assert.fail;
9-
103
import java.util.concurrent.Callable;
114
import java.util.concurrent.TimeUnit;
125
import java.util.concurrent.atomic.AtomicReference;
136

7+
import com.linkedin.parseq.trace.Trace;
148
import org.testng.annotations.Test;
159

1610
import com.linkedin.parseq.function.Failure;
1711
import com.linkedin.parseq.function.Function1;
1812
import com.linkedin.parseq.function.Success;
1913
import com.linkedin.parseq.function.Try;
2014

15+
import static org.testng.Assert.*;
16+
2117

2218
/**
2319
* @author Jaroslaw Odzga (jodzga@linkedin.com)
@@ -98,6 +94,56 @@ public void testRecover(int expectedNumberOfTasks) {
9894
assertEquals(countTasks(failure.getTrace()), expectedNumberOfTasks);
9995
}
10096

97+
public void testRecoverWithExceptionFilter(int expectedNumberOfTasks) {
98+
Task<Integer> recoverd = getFailureTask()
99+
.map("strlen", String::length)
100+
.recover("", RuntimeException.class, e -> -1);
101+
runAndWait("AbstractTaskTest.recoverd", recoverd);
102+
assertEquals((int) recoverd.get(), -1);
103+
assertEquals(countTasks(recoverd.getTrace()), expectedNumberOfTasks);
104+
105+
Task<Integer> notRecovered = getFailureTask()
106+
.map("strlen", String::length)
107+
.recover("", ArithmeticException.class, e -> -1);
108+
runAndWaitException("AbstractTaskTest.testRecoverWithExceptionFilterFailure", notRecovered, RuntimeException.class);
109+
assertTrue(notRecovered.isFailed());
110+
assertEquals(countTasks(notRecovered.getTrace()), expectedNumberOfTasks);
111+
112+
Task<String> recoveredExceptionSubclassesForward = getFailureTask() // Failed with RuntimeError
113+
.recover("", Throwable.class, e -> "expected-value") // expected to recover, as RuntimeError is subclass of Throwable
114+
.recover("", RuntimeException.class, e -> "unexpected-value"); // no action, as already recovered at previous step
115+
runAndWait("AbstractTaskTest.recoveredExceptionSubclassesForward", recoveredExceptionSubclassesForward);
116+
assertEquals(recoveredExceptionSubclassesForward.get(), "expected-value");
117+
assertEquals(countTasks(recoveredExceptionSubclassesForward.getTrace()), expectedNumberOfTasks);
118+
119+
Task<String> recoveredExceptionSubclassesBackwards = getFailureTask() // Failed with RuntimeError
120+
.recover("", RuntimeException.class, e -> "expected-value") // expected to recover, as exception class matches
121+
.recover("", Throwable.class, e -> "unexpected-value"); // no action, as already recovered at previous step
122+
runAndWait("AbstractTaskTest.recoveredExceptionSubclassesBackwards", recoveredExceptionSubclassesBackwards);
123+
assertEquals(recoveredExceptionSubclassesBackwards.get(), "expected-value"); // recovered with expected value
124+
assertEquals(countTasks(recoveredExceptionSubclassesBackwards.getTrace()), expectedNumberOfTasks);
125+
126+
Task<String> recoveredUnrelatedExceptionFirst = getFailureTask() // Failed with RuntimeError
127+
.recover("", Error.class, e -> "unexpected-value") // no action, as the failed state is not of type Error
128+
.recover("", Throwable.class, e -> "expected-value"); // expected to recover, as exception class matches
129+
runAndWait("AbstractTaskTest.recoveredUnrelatedExceptionFirst", recoveredUnrelatedExceptionFirst);
130+
assertEquals(recoveredUnrelatedExceptionFirst.get(), "expected-value"); // recovered with expected value
131+
assertEquals(countTasks(recoveredUnrelatedExceptionFirst.getTrace()), expectedNumberOfTasks);
132+
133+
Task<String> recoveredRecoveryThrows = getFailureTask() // Failed with RuntimeError
134+
.recover("", RuntimeException.class, e -> {throw new IllegalArgumentException();}) // exception class matches, but recovery throws another exception
135+
.recover("", IllegalArgumentException.class, e -> "expected-value"); // expected to recover, as exception class matches (see above)
136+
runAndWait("AbstractTaskTest.recoveredRecoveryThrows", recoveredRecoveryThrows);
137+
assertEquals(recoveredRecoveryThrows.get(), "expected-value"); // recovered with expected value
138+
assertEquals(countTasks(recoveredRecoveryThrows.getTrace()), expectedNumberOfTasks);
139+
140+
Task<String> failedAfterRecoveryThrows = getFailureTask() // Failed with RuntimeError
141+
.recover("", RuntimeException.class, e -> {throw new IllegalArgumentException();}); // throws another exception while recovering
142+
IllegalArgumentException ex = runAndWaitException(failedAfterRecoveryThrows, IllegalArgumentException.class);
143+
assertNotNull(ex);
144+
assertEquals(countTasks(recoveredRecoveryThrows.getTrace()), expectedNumberOfTasks);
145+
}
146+
101147
public void testCancelledRecover(int expectedNumberOfTasks) {
102148
Task<Integer> cancelled = getCancelledTask().map("strlen", String::length).recover(e -> -1);
103149
try {

subprojects/parseq/src/test/java/com/linkedin/parseq/TestFusionTask.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public void testRecover() {
6565
testRecover(4);
6666
}
6767

68+
@Test
69+
public void testRecoverWithExceptionFilter() {
70+
testRecoverWithExceptionFilter(4);
71+
}
72+
6873
@Test
6974
public void testCancelledRecover() {
7075
testCancelledRecover(4);

subprojects/parseq/src/test/java/com/linkedin/parseq/TestTask.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public void testRecover() {
5454
testRecover(5);
5555
}
5656

57+
@Test
58+
public void testRecoverWithExceptionFilter() {
59+
testRecoverWithExceptionFilter(5);
60+
}
61+
5762
@Test
5863
public void testCancelledRecover() {
5964
testCancelledRecover(5);

0 commit comments

Comments
 (0)