In the following method, I am purposely making the call c.rxCommit() throw an exception during test.
The exception is thrown as expected, but I never land in the onErrorResumeNext block as expected.
I am expecting to land inside here to handle the error and perform some rollback.
Can I get some advice as to why I am not landing within the error block? Thanks.
public Observable<Void> myMethod(Observable<Map<String, String>> records) {
return client.rxGetConnection()
.flatMapCompletable(c -> c.rxSetAutoCommit(false)
.toCompletable()
.andThen(records.flatMapSingle(record -> perform(c, record)).toCompletable())
.andThen(c.rxCommit().toCompletable()) // test makes this c.rxCommit() throw an error on purpose.
.onErrorResumeNext(throwable -> {
// I want to land in here when error but it is not happening.
return c.rxRollback().toCompletable();
})
.andThen(c.rxSetAutoCommit(true).toCompletable())
.andThen(c.rxClose()).toCompletable()
).toObservable();
}
The test
#Test
void test() {
when(sqlConnection.rxCommit()).thenThrow(new RuntimeException("Error on Commit")); // mockito
myClass.myMethod(mockedRecords).subscribe(
success -> System.out.println(),
throwable -> System.out.println("err " + throwable), // I land in here with the new RuntimeException("Error on Commit") exception.
// this is wrong. Error should have been handled in the onErrorResumeNext block and should be getting success here instead.
);
// some verify() to test outcome
}
As akarnokd says, your sqlConnection.rxCommit() mock is wrong, because it throws exception right after method call, not by subscription.
If want to receive error in rx flow, try this:
when(sqlConnection.rxCommit()).thenReturn(Single.error(RuntimeException("Error on Commit")));
But if you really want to throw exceptions in rxCommit(), try to wrap it in Single.defer:
.andThen(
Single.defer(() -> {
return c.rxCommit().toCompletable();
})
Related
I'm trying to get into CompletableFuture class for a project I'm running, and I got to some question here:
There is the following method: it tries to find a conversation by its ID or hash; and, if not found, it throws an exception. So far, so good.
public ConversationOutput getConversationByIdOrHash(String conversationIdOrHash)
throws ConversationNotFoundException {
Conversation conversation = this.conversationRepository.getByIdOrHash(conversationIdOrHash);
if (conversation == null) {
throw new ConversationNotFoundException(conversationIdOrHash);
}
return this.modelMapper.map(conversation, ConversationOutput.class);
}
Note that I am throwing ConversationNotFoundException from my method signature. My SpringBoot controller is reacting to this exception and it's all working fine since the beginning.
What I'm trying to do is to make this to a CompletableFuture return and actually throwing an exception, something similar to:
public CompletableFuture<ConversationOutput> getConversationByIdOrHashAsync(String conversationIdOrHash)
throws ConversationNotFoundException {
return CompletableFuture.supplyAsync(() -> this.getConversationByIdOrHash(conversationIdOrHash));
}
I've seen posts where people use exceptionally to handle exceptions, but what I really want to do is to throw it to my controller and let it handle it. Any suggestions of how can I make it?
Thank you all!
The question is do you care about the result of CompletableFuture.
CompletableFuture is like a special task and it is processed on other thread. If you don't invoke .join() you won't receive the results of CompletableFuture. This method also will propagate the exception if any occured. However it waits for CompletableFuture to finish and blocks the request.
However, there is no way to get exceptions from the inside of the CompletableFuture without waiting, you have to treat it like other task.
You can pass the completed future in case of a success, and failed future along with your custom exception.
public CompletableFuture<ConversationOutput> getConversationByIdOrHashAsync(String conversationIdOrHash) {
try {
return CompletableFuture.completedFuture(this.getConversationByIdOrHash(conversationIdOrHash));
} catch (ConversationNotFoundException e) {
return CompletableFuture.failedFuture(e);
}
}
and then at your controller level you can handle the exception.
final CompletableFuture<ConversationOutput> future = getConversationByIdOrHashAsync("idOrHash");
future.whenComplete((r, e) -> {
if (e != null) {
if (e instanceof ConversationNotFoundException) {
//handling
}
}
});
CompletionStage.whenComplete() block of code is run and its results are ignored even if it throws an exception, but in my test case, I want it to break based on the exception.
How do I test it? I'm able to simulate assertion-based exception from internal code but the exception is ignored and hence test cases do not fail.
return orderService.newOrder(order, request)
.whenComplete((__, throwable) -> {
if (throwable == null) {
eventPublisher.publishEvent(orderSummary.getOrderId());
}
});
I have been tried to find out why the Runtime exception is not propagated back to the client. I have the next piece of code, so when I return a Mono.error this should be handled in the subscribe error section, to throw an exception to the client, but this is not happening. Any idea about I am doing wrong?
public void onmethod(EventDetails eventDetails, String eventType) {
messageConverter.convertAndSendMessage(eventType, eventDetails)
.flatMap(aBoolean -> {
if (aBoolean)
log.debug("Event published");
else {
log.debug("Problem publishing event.");
return Mono.error(new RuntimeException("Problem publishing event."));
}
return Mono.just(true);
})
.doOnError(throwable -> log.error("Failed to consume message", throwable))
.subscribe(
next -> { } ,
error -> {
throw Exceptions.propagate(error);
}
);
}
And this is the test I have to verify the method behaviour. This test fails as any exception is thrown. However, I can see in the logs that the exception happens.
Assertions.assertThrows(RuntimeException.class, () ->
consentsListener.onmethod(
eventDetails, "eventType")
);
19:44:32.463 [main] ERROR events.auth.ConsentsListenerImpl - Failed to consume message
java.lang.RuntimeException: Problem publishing event.
at events.auth.ConsentsListenerImpl.lambda$publishMessage$0(ConsentsListenerImpl.java:121)
at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:152)
at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53)
at reactor.core.publisher.Mono.subscribe(Mono.java:4084)
at reactor.core.publisher.Mono.subscribeWith(Mono.java:4214)
at reactor.core.publisher.Mono.subscribe(Mono.java:4070)
at reactor.core.publisher.Mono.subscribe(Mono.java:4006)
at reactor.core.publisher.Mono.subscribe(Mono.java:3978)
at ...
org.opentest4j.AssertionFailedError: Expected java.lang.RuntimeException to be thrown, but nothing was thrown.
Thank you very much in advance.
Best regards.
I have been tried to find out why the Runtime exception is not propagated back to the client.
Because you're subscribing to it, which is almost certainly the wrong thing to do. The framework (Webflux in this case) should be what controls the subscription to your publisher.
If you remove your subscribe() call on that chain, change your method to return Mono<Boolean> and then return the entire chain in that method, it should work as expected.
When I remove the try/catch it works but cannot test negative test
public class TileCombinationSetsTest {
#Test public void testTileCombinations() {
new TileCombinationSets();
assertEquals(TileCombinationSets.tileCombinations(1).size(), 7);
assertEquals(TileCombinationSets.tileCombinations(2).size(), 42);
assertEquals(TileCombinationSets.tileCombinations(3).size(), 210);
assertEquals(TileCombinationSets.tileCombinations(4).size(), 840);
assertEquals(TileCombinationSets.tileCombinations(5).size(), 2520);
assertEquals(TileCombinationSets.tileCombinations(6).size(), 5040);
assertEquals(TileCombinationSets.tileCombinations(7).size(), 5040);
try {
TileCombinationSets.tileCombinations(4);
fail("Exceptions expected");
}
catch(Throwable e) {}
}
}
In JUnit a test fails when the test method throws an exception (or an other Throwable). JUnit's test runner catches the exception and reports the test as failed. On the other hand the test runner considers a test to be successful when the test method finishes without throwing an exception. Statements like assertEquals and fail throw an AssertionError.
In your test fail("Exceptions expected") is throwing an AssertionError. which is immediately caught and and therefore the test method testTileCombinations doesn't throw an exception. Now for JUnit it looks like the method was executed successfully and therefore it considers the test to be successful.
If you want to test that TileCombinationSets.tileCombinations(4) throws an exception then you can use JUnit's assertThrows
assertThrows(
Exception.class, // you can be more specific here
() -> TileCombinationSets.tileCombinations(4)
);
If you want to test that TileCombinationSets.tileCombinations(4) doesn't throw an exception then you simply execute it. (JUnit will report the test as failed if it throws an exception and successful otherwise.)
#Test
public void testTileCombinations() {
...
TileCombinationSets.tileCombinations(4);
}
I assume the part
TileCombinationSets.tileCombinations(4)
throws an exception the second time it is executed.
You could print the stacktrace of the exception to see if an exception is thrown.
Without knowing what TileCombinationSets.tileCombinations(int x) does its hard to say.
If possible could you post the method?
You should generally not catch Throwable but Exception instead. Throwable also includes subclasses of Error, which are critical errors concerning that Java Runtime Environment itself (like OutOfMemoryError), from which a program should not attempt to recover because it puts the program into an undefined state.
As it happens, the AssertionError that is thrown by the fail() method in your example
try {
TileCombinationSets.tileCombinations(4);
fail("Exceptions expected");
}
catch(Throwable e) {}
also extends Error.
So by changing your code to
try {
TileCombinationSets.tileCombinations(4);
fail("Exceptions expected");
}
catch(Exception e) {}
it should behave as intended, as long as the exceptions thrown by your tileCombinations() method only extend Exception (which they should, as it is bad practice to extend Error yourself).
I am using io.projectreactor 3 (reactor-core 3.2.6.RELEASE) and I have noticed some discrepancies regarding error handling. Unfortunately, official documentation does not provide enough details to solve my problems.
I have following 4 snippets. In some cases exception will be ignored and in other cases it will thrown further. What is the way to actually produce and consume exceptions?
Snippet 1
In this case, exception will be ignored and main() will complete without receiving exception.
import reactor.core.publisher.Flux;
class Scratch {
public static void main(String[] args) throws Throwable {
Flux.push(sink -> {
sink.next(1);
sink.next(2);
}).doOnNext(e -> {
throw new RuntimeException("HELLO WORLD");
}).subscribe(System.out::println, e -> {
throw new RuntimeException(e);
});
System.out.println("DONE");
}
}
Output:
DONE
Snippet 2
Is similar as example from above, except that we don't use Flux.push but Flux.just. Main() will receive exception.
import reactor.core.publisher.Flux;
class Scratch {
public static void main(String[] args) throws Throwable {
Flux.just(
1
).doOnNext(e -> {
throw new RuntimeException("HELLO WORLD");
}).subscribe(System.out::println, e -> {
throw new RuntimeException(e);
});
System.out.println("DONE");
}
}
Output:
Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: HELLO WORLD
at Scratch.lambda$main$1(scratch_15.java:10)
...
Snippet 3
We signal exception by calling sink.error. Main() will not receive exception.
import reactor.core.publisher.Flux;
class Scratch {
public static void main(String[] args) throws Throwable {
Flux.push(sink -> {
sink.next(1);
sink.next(2);
sink.error(new RuntimeException("HELLO WORLD"));
}).subscribe(System.out::println, e -> {
throw new RuntimeException(e);
});
System.out.println("DONE");
}
}
Output:
1
2
DONE
Snippet 4
We throw exception directly. Main() will receive exception.
import reactor.core.publisher.Flux;
class Scratch {
public static void main(String[] args) throws Throwable {
Flux.push(sink -> {
sink.next(1);
sink.next(2);
throw new RuntimeException("HELLO WORLD");
}).subscribe(System.out::println, e -> {
throw new RuntimeException(e);
});
System.out.println("DONE");
}
}
Output
1
2
Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: HELLO WORLD
at Scratch.lambda$main$1(scratch_15.java:10)
...
What is the correct way to handle exception when working with reactive-core? The only reliable way seems not to use error callback at all, and instead surround flux.subscribe with try/catch. But in that case I always receive UnsupportedOperationException instead of original exception, and then I need to use Exceptions.isErrorCallbackNotImplemented to check if it comes from reactive, extract nested exception and then throw it.
This can be done of course, but it needs to be done consistently on every place where we use Flux is being subscribed. That does not looks nice to me. What I'm missing here?
In all your examples the problem is re-throwing from the .subscribe(...) error-handling lambda.
If you want the exception to be thrown in a main block, use block() variants.
If you want to test that the error is propagated throughout the pipeline, use StepVerifier.create(pipeline).expectError(...).verify().
.subscribe in general is to get notified of "terminal" state, not intended to recover or re-throw errors (use onError* operators upstream for that).
The just-based examples seem to correctly propagate the exception because they execute no user-provided code at subscription, so no try/catch is in place during subscribe(Consumer<Throwable>).
push, like generate/create/defer and compose, execute some user-defined logic (the Consumer<FluxSink>), at subscription. They guard against the whole Consumer throwing exceptions and try to propagate that (as an onError signal) rather than directly throwing it.
But if the failure in the Consumer is caused while executing one of the sink's methods, it can be problematic if the subscriber re-throws: we enter a recursion where calling the sink calls the sink. We protect against this infinite case by exiting when we detect recursive draining of the sink.
This is why push-based examples that trigger error after sink.next or in sink.error (examples 1 and 3) fail to produce the exception in the main:
Consumer is applied
sink.next is called and next operator creates exception 1, or sink.error is called
exception 1 reaches subscribe and is re-thrown as exception 2
this short-circuits the Consumer.apply, exception 2 is passed to sink.error
the sink is already being called so we break out to avoid infinite recursion
exception 2 is never seen
In example 4 on the other hand, we're no longer in the middle of calling the sink's methods, and the original exception doesn't reach the subscriber first:
Consumer is applied
it directly throws exception 1
this short-circuits the Consumer.apply and exception 1 is passed to sink.error
which propagates to the subscribe
which re-throws it as exception 2
exception 2 is seen in the main method