Skip to content

Commit

Permalink
Better exception handling (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
emil-bar authored Jan 22, 2025
1 parent 6a3eea9 commit 5d5dbfc
Show file tree
Hide file tree
Showing 29 changed files with 444 additions and 359 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,15 +422,41 @@ public class Demo {

* an exception thrown from the scope's body, or from any of the forks, causes the scope to end
* any forks that are still running are then interrupted
* once all forks complete, an `ExecutionException` is thrown by the `supervised` method
* the cause of the `ExecutionException` is the original exception
* once all forks complete, an `JoxScopeExecutionException` is thrown by the `supervised` method
* the cause of the `JoxScopeExecutionException` is the original exception
* any other exceptions (e.g. `InterruptedExceptions`) that have been thrown while ending the scope, are added as
suppressed

Jox implements the "let it crash" model. When an error occurs, the entire scope ends, propagating the exception higher,
so that it can be properly handled. Moreover, no detail is lost: all exceptions are preserved, either as causes, or
suppressed exceptions.

As `JoxScopeExecutionException` is unchecked, we introduced utility method called `JoxScopeExecutionException#unwrapAndThrow`.
If the wrapped exception is instance of any of passed classes, this method unwraps original exception and throws it as checked exception, `throws` signature forces exception handling.
If the wrapped exception is not instance of any of passed classes, **nothing happens**.
All suppressed exceptions are rewritten from `JoxScopeExecutionException`

**Note** `throws` signature points to the closest super class of passed arguments.
Method does **not** rethrow `JoxScopeExecutionException` by default.
So it is advised to manually rethrow it after calling `unwrapAndThrow` method.

e.g.
```java
import com.softwaremill.jox.structured.JoxScopeExecutionException;
import com.softwaremill.jox.structured.Scopes;

...
try {
Scopes.supervised(scope -> {
throw new TestException("x");
});
} catch (JoxScopeExecutionException e) {
e.unwrapAndThrow(OtherException.class, TestException.class, YetAnotherException.class);
throw e;
}
...
```

#### Other types of scopes & forks

There are 4 types of forks:
Expand Down
128 changes: 65 additions & 63 deletions flows/src/main/java/com/softwaremill/jox/flows/Flow.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import com.softwaremill.jox.Channel;
import com.softwaremill.jox.ChannelDone;
import com.softwaremill.jox.ChannelError;
import com.softwaremill.jox.SelectClause;
import com.softwaremill.jox.Source;
import com.softwaremill.jox.structured.JoxScopeExecutionException;
import com.softwaremill.jox.structured.Scopes;
import com.softwaremill.jox.structured.ThrowingFunction;
import com.softwaremill.jox.structured.UnsupervisedScope;

class GroupByImpl<T, V, U> {
Expand Down Expand Up @@ -48,10 +51,10 @@ public ChildDone(V v) {

private final Flow<T> parent;
private final int parallelism;
private final Function<T, V> predicate;
private final ThrowingFunction<T, V> predicate;
private final Flow.ChildFlowTransformer<T, V, U> childFlowTransform;

public GroupByImpl(Flow<T> parent, int parallelism, Function<T, V> predicate, Flow.ChildFlowTransformer<T, V, U> childFlowTransform) {
public GroupByImpl(Flow<T> parent, int parallelism, ThrowingFunction<T, V> predicate, Flow.ChildFlowTransformer<T, V, U> childFlowTransform) {
this.parent = parent;
this.parallelism = parallelism;
this.predicate = predicate;
Expand Down
24 changes: 3 additions & 21 deletions flows/src/test/java/com/softwaremill/jox/flows/FlowAlsoToTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,7 @@ void alsoToTap_shouldSendToBothSinksWhenOtherIsFaster() throws Exception {
Flow<Integer> flow = Flows
.fromValues(1, 2, 3)
.alsoToTap(other)
.tap(v -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
.tap(_ -> Thread.sleep(50));

// when & then
assertEquals(List.of(1, 2, 3), flow.runToList());
Expand Down Expand Up @@ -193,13 +187,7 @@ void alsoTapTo_shouldNotFailTheFlowWhenTheOtherSinkFails() throws Exception {
List<Integer> result = Flows
.iterate(1, i -> i + 1)
.take(10)
.tap(v -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})
.tap(_ -> Thread.sleep(10))
.alsoToTap(other)
.runToList();

Expand All @@ -225,13 +213,7 @@ void alsoTapTo_shouldNotCloseTheFlowWhenTheOtherSinkCloses() throws Exception {
List<Integer> result = Flows
.iterate(1, i -> i + 1)
.take(10)
.tap(v -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})
.tap(_ -> Thread.sleep(10))
.alsoToTap(other)
.runToList();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.softwaremill.jox.flows;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.atomic.AtomicBoolean;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class FlowCompleteCallbacksTest {
@Test
Expand All @@ -27,7 +29,7 @@ void ensureOnCompleteRunsInCaseOfError() {
//given
AtomicBoolean didRun = new AtomicBoolean(false);
Flow<Integer> f = Flows.fromValues(1, 2, 3)
.tap(i -> {throw new RuntimeException();})
.tap(_ -> {throw new RuntimeException();})
.onComplete(() -> didRun.set(true));
assertFalse(didRun.get());

Expand Down Expand Up @@ -57,7 +59,7 @@ void ensureOnDoneDoesNotRunInCaseOfError() {
// given
AtomicBoolean didRun = new AtomicBoolean(false);
Flow<Integer> f = Flows.fromValues(1, 2, 3)
.tap(i -> {throw new RuntimeException();})
.tap(_ -> {throw new RuntimeException();})
.onDone(() -> didRun.set(true));
assertFalse(didRun.get());

Expand All @@ -73,7 +75,7 @@ void ensureOnErrorDoesNotRunInCaseOfSuccess() throws Exception {
// given
AtomicBoolean didRun = new AtomicBoolean(false);
Flow<Integer> f = Flows.fromValues(1, 2, 3)
.onError(e -> didRun.set(true));
.onError(_ -> didRun.set(true));
assertFalse(didRun.get());

// when
Expand All @@ -88,8 +90,8 @@ void ensureOnErrorRunsInCaseOfError() {
// given
AtomicBoolean didRun = new AtomicBoolean(false);
Flow<Integer> f = Flows.fromValues(1, 2, 3)
.tap(i -> {throw new RuntimeException();})
.onError(e -> didRun.set(true));
.tap(_ -> {throw new RuntimeException();})
.onError(_ -> didRun.set(true));
assertFalse(didRun.get());

// when
Expand Down
Loading

0 comments on commit 5d5dbfc

Please sign in to comment.