I am struggling to make sense of Reactive process flow. My understanding is that, when manipulating data within a Mono or Flux, type consistency must be maintained. However, when problems occur within that process, something resembling an exception must be thrown, and that breaks the type consistency.
Currently, I have an imperative process for taking a verification token ID and processing it. In two instances, it throws exceptions (InvalidTokenException is 404 response; ExpiredTokenException is a 410), and in the last modifies and saves the account, and deletes the token. I would like to convert this to a reactive process:
public AccountDto verifyAccountByToken (UUID tokenId) {
VerifyToken vToken = verifyTokenRepository.findByToken(tokenId);
if (vToken == null) {
throw new InvalidTokenException();
}
if (vToken.isExpired()) {
verifyTokenRepository.delete(vToken);
throw new ExpiredTokenException();
}
Account account = vToken.getAccount();
account.addRole(AccountRole.VERIFIED);
account.deleteRole(AccountRole.UNVERIFIED);
accountRepository.save(account);
verifyTokenRepository.delete(vToken);
return new AccountDto(account);
}
I believe the below is essentially correct for the data-manipulation process when everything is as it should be, but how do I handle the conditional branching? – How do I say: do-this-if-empty? (I figure I can use .filter() as a way of capturing the expired case.) And how do I convert it to an error type?
public Mono<AccountDto> verifyAccountByToken (UUID tokenId) {
return verifyTokenRepository.findByToken(tokenId)
// the bits I can't figure out to deal with Invalid and Expired tokens;
.flatMap(vToken -> {
verifyTokenRepository.delete(vToken);
return vToken.getAccount();
})
.flatMap(account -> {
account.addRole(AccountRole.VERIFIED);
account.deleteRole(AccountRole.UNVERIFIED);
return accountRepository.save(account);
})
.map(account -> new AccountDto(account));
}
How do I say: do-this-if-empty?
There's an operator for that:
.switchIfEmpty(Mono.error(new InvalidTokenException()));
Have a look at comments below:
public Mono<AccountDto> verifyAccountByToken(UUID tokenId) {
return verifyTokenRepository.findByToken(tokenId) //Mono<VerifyToken> or Mono<Void>
.switchIfEmpty(Mono.error(new InvalidTokenException())); //token does not exist, empty Mono is returned, switch to an error signal
.flatMap(this::validateToken)
.map(vToken -> vToken.getAccount())
.flatMap(account -> {
//token is valid...
account.addRole(AccountRole.VERIFIED);
account.deleteRole(AccountRole.UNVERIFIED);
return accountRepository.save(account);
})
.map(account -> new AccountDto(account));
}
private Mono<VerifyToken> validateToken(VerifyToken vToken) {
if (vToken.isExpired()) {
//Token is expired... delete it and then signal an error
return verifyTokenRepository.delete(vToken)
.then(Mono.error(new ExpiredTokenException()));
}
return Mono.just(vToken);//token is valid...
}
Related
I know that my question has been asked already multiple times, but I feel like there is still no satisfying answer to it.
Basically I have two downstream services which I want to call (in parallel) and then I want to combine the results and return it (as Json). Both calls can fail but both results are not mandatory, so also an empty combined response is possible:
class FirstResponse {...}
class SecondResponse {...}
class CombinedResponse {
private FirstResponse first;
private SecondResponse second;
}
class FirstService {
Mono<FirstResponse> get(){
return webclient.get(...)
.bodyToMono(FirstResponse.class)
.onErrorResume(throwable -> Mono.empty);
}
}
class SecondService {
Mono<SecondResponse> get(){
return webclient.get(...)
.bodyToMono(SecondResponse.class)
.onErrorResume(throwable -> Mono.empty);
}
}
#RestController(...)
class CombinationController {
#GetMapping(...)
Mono<CombinedResponse> getCombined() {
Mono.zip(firstService.get(), secondService.get(), (first, second) -> {
return new CombinedResponse(first, second);
})
}
}
Now in case the calls to firstService fails, also the response from secondService gets ignored. But what I actually would like to have, is that CombinedResponse still gets (partially populated).
As a disclaimer I have to say, that I am currently migrating my code from rxjava1 and there in case of downstream errors I just return Single.just(null). This allows me to zip both results and just sets the values to null.
About Mono.zip() :
An error or empty completion of any source will cause other sources to
be cancelled and the resulting Mono to immediately error or complete,
respectively.
Also, reactor does not allow null values, so you should do some workaround in your case. In some simple cases it is easy to define some default value in case of error (for example, empty String), but for custom types it would be weird to create an empty object.
As an alternative for such cases I would suggest to use Optional.
This solution adds some boilerplate code, though.
First service:
class FirstService {
Mono<Optional<FirstResponse>> get(){
return webclient.get(...)
.bodyToMono(FirstResponse.class)
.map(Optional::of)
.onErrorReturn(Optional.empty());
}
}
Second service:
class SecondService {
Mono<Optional<SecondResponse>> get(){
return webclient.get(...)
.bodyToMono(SecondResponse.class)
.map(Optional::of)
.onErrorReturn(Optional.empty());
}
}
And "combiner" :
#GetMapping(...)
Mono<CombinedResponse> getCombined() {
Mono.zip(firstService.get(), secondService.get())
.map(tuple -> {
// check optionals here from tuple.getT1() and tuple.getT2()
// and do whatever you want
})
...
}
I have a DTO class like this :
public class User {
#Field("id")
private String id;
private String userName;
private String emailId;
}
I have to provide an update and delete feature through API.
I have written the following code to delete the record:
public Mono<String> userData(User body) {
repo.removeUserDetails(userObj).subscribe();
return Mono.just("Remove Successful");
}
RemoveUserDetails method is something like this :
public Mono<User> removeUserDetails(User userObj) {
return findByUsername(userObj.getUsername())
.flatMap(existingUser -> {
// logic to delete the data from database which working as expected
}).switchIfEmpty(
Mono.defer(() -> {
return Mono.error(new Exception("User Name " + userObj.getUsername() + " doesn't exist."));
})
);
}
The problem with this code is even if the user is not existing, it is not showing the Mono error I'm returning. In every case, this always returns "Remove Successful".
How can I change my service layer method so that it can return whatever is received by the repo method? I'm new to Reactor code, so unable to figure out how to write it.
Whenever you call subscribe, consider it an immediate red flag. Subscription is something that should be handled by the framework you're using (Webflux in this case.)
If you subscribe yourself, such as in this example:
public Mono<String> userData(User body) {
repo.removeUserDetails(userObj).subscribe();
return Mono.just("Remove Successful");
}
...then you've essentially created a "fire and forget" type subscription, where you have no way of knowing if that publisher completed successfully, if it caused an error, how long it took to complete, whether it completed at all, or whether it emitted an element. So in this case, you're saying "send a request to remove user details, forget you sent it, and then before waiting for any kind of result, always return 'Remove successful'." This is almost never what you want.
You could use something like:
public Mono<String> userData(User body) {
return repo.removeUserDetails(userObj)
.then(Mono.just("Remove Successful"));
}
...which is much better as it includes everything as part of the reactive chain. In this case, you'll either get an error signal, or you'll get "Remove Successful".
However, chances are you don't need that String to be returned at all - you just need to know if it's successful or not. The standard way of doing that (I just need to know that it's completed successfully or not, I don't need it to return a value) is to use Mono<Void> as the return type and then(), something like:
public Mono<Void> userData(User body) {
return repo.removeUserDetails(userObj).then();
}
...which will give you a standard completion if the deletion was successful, and an error signal otherwise.
A common pattern you find when using reactive java code is handling nulls when collecting a list.
The following code is a simple example showing how to handle nulls returned by a Location by wrapping getLocation in a Mono.defer then handling a null using onErrorReturn.
The test code
List<String> items = inventory.testList().block();
items.forEach(System.out::println);
USA
Not Found
SPAIN
private List<Integer> clusters;
private List<Mono<Location>> locations;
private List<String> countryCodes;
public Mono<List<String>> testList() {
clusters = Arrays.asList(0, 1, 2);
locations = Arrays.asList(Mono.just(new Location(0)), null, Mono.just(new Location(2)));
countryCodes = Arrays.asList("USA", "FRANCE", "SPAIN");
return Flux.fromIterable(clusters)
.flatMap(cluster -> getLocation(cluster))
.collectList();
}
public Mono<String> getLocation(int clusterID) {
return Mono.defer(() -> locations.get(clusterID))
.flatMap(location -> Mono.just(location.id))
.flatMap(id -> Mono.just(countryCodes.get(id)))
.onErrorReturn(Exception.class, "Not Found");
}
I am trying to get my head around Spring Boot and Reactors (having been away from Java for almost two decades). The resource I am learning from gives too-basic examples, and no search is returning any meaningful information: only convoluted documentation that doesn't answer anything.
What I am trying to achieve is - at least in principle - very simple.
I have a function to delete an image stored within UPLOAD_ROOT. On upload, the image had a name and was assigned an id. That image was then stored as image.id + "-" + image.name, (allowing upload of multiple images with the same source filename). The Image class provides the association of the id and name values, with that being stored in MongoDB.
The database Image is accessed through public interface ImageRepository extends ReactiveCrudRepository <Image, String>.
Deletion is by id. My code at the moment (derived from a version that deleted by filename, and didn't cope with name conflicts):
public Mono<Void> deleteImage(String fileId) {
return Mono.fromRunnable(() -> {
imageRepository.findById(fileId)
.map(image -> {
Mono<Void> deleteFile = Mono.fromRunnable(() -> {
try {
Files.deleteIfExists(Paths.get(UPLOAD_ROOT, image.getId() + "-" + image.getName()));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Mono<Void> deleteRecord = Mono.fromRunnable(() -> {
imageRepository.delete(image);
});
return Mono.when(deleteFile, deleteRecord).then();
});
}).then();
}
The problem is that, wrapped inside the imageRepository.findById(fileId).map(image -> { ... });, deleteFile and deleteRecord never happen. Nor the Mono.when().
The other approach, which makes more sense to me, would be to use an alternate function to .map that is non-transformative, operating on the supplied Mono<Image>'s elements and returning the same Mono<Image> so that more could be done with it. But I can't find any reference to anything that enables this.
How do I get all the nested functions to actually happen? (I have tried various things with .then() and .subscribe() on the end of each, but nothing gave consisten and fully-functional results.) Or is there some secret function that will allow me to perform a non-transformative chain?
There is something wrong with the use of Mono.fromRunnable.
Try this code:
public Mono<Void> deleteImage(String fileId) {
return imageRepository.findById(fileId)
.flatMap(image -> Mono.when(deleteFile(image), deleteRecord(image)).then());
}
private Mono<Void> deleteRecord(Image image) {
return imageRepository.delete(image);
}
private Mono<Void> deleteFile(Image image) {
return Mono.fromRunnable(() -> {
try {
Files.deleteIfExists(Paths.get(UPLOAD_ROOT, image.getId() + "-" + image.getName()));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
}
Here, I am trying to make asynchronous and non-blocking calls using reactor and for each request, I may have to call two services in sequence (in my case below, getAccountInfoFromAAA and getAccountInfoFromBBB).
Here is my ItemRequest object:
public class ItemRequest {
private Account account;
private Result firstServiceResult;
private Result secondServiceResult;
private PostingParameterCode postingParameterCode; //enum
//...
//...
//getters and setters
}
So, my request input will contain multiple itemRequests and for each itemRequest, I am doing asynchronous calls as:
public void getAccountData(List<ItemRequest> itemRequests) {
ImmutableList<ItemRequest> list = ImmutableList.copyOf(itemRequests);
Flux.fromIterable(list).flatMap(this::callBothSors).blockLast();
}
public Mono<ItemRequest> callBothSors(ItemRequest itemRequest) {
return getAccountDataService.getAccountDataFromAAAandBBB(itemRequest);
//here, it will enter into a sequential call for each itemRequest
}
This is my first service call interface:
public Mono<ItemRequest> getAccountDataFromAAA(ItemRequest itemRequest);
This is my second service call interface:
public Mono<ItemRequest> getAccountDataFromBBB(ItemRequest itemRequest);
This method will have upto two calls in sequence based on the condition:
public Mono<ItemRequest> getAccountDataFromAAAandBBB(ItemRequest itemRequest){
Mono<ItemRequest> firstCallResult = Mono.empty();
Mono<ItemRequest> secondCallResult = Mono.empty();
if(isFirstServiceCallRequired(itemRequest)){
firstCallResult = this.firstServiceCallImpl.getAccountDataFromAAA(itemRequest);
//basically, firstService call will update the accountKey information and
//will also set the result status to OK which is required to decide
//whether to make secondService call.
} else {
//Account key is already present, so just update the result status which I need later.
Result result = new Result();
result.setStatus(Result.Status.OK);
result.setMessageText("First call not required as account info is set for item request");
itemRequest.setFirstServiceResult(result);
}
//Now, before calling the second service, I need to check the following:
if(null!= itemRequest.getFirstServiceResult() &&
itemRequest.getFirstServiceResult().getStatus().equals(Result.Status.OK) &&
itemRequest.getPostingParameterCode().equals(PostingParameterCode.MOBILECREDIT)){
secondCallResult = this.secondServiceCallImpl.getAccountDataFromBBB(itemRequest);
}
return firstCallResult.then(secondCallResult); //attaching the
//firstCallResult and secondCallResult to produce a single Mono
}
This is working fine when firstCallResult is not required. But when the first call is required, this condition check will not pass since I won't have first call result object updated:
if(null != itemRequest.getFirstServiceResult() &&
itemRequest.getFirstServiceResult().getStatus().equals(Result.Status.OK) &&
itemRequest.getPostingParameterCode().equals(PostingParameterCode.MOBILECREDIT))) { ... }
//this condition check will not pass because first service call is not actually executing
Both cases works fine if I put the following statement:
if(isFirstServiceCallRequired(itemRequest)){
firstCallResult = this.firstServiceCallImpl.getAccountDataFromAAA(itemRequest);
firstCallResult.block(); //adding this case will work on both cases
}
But, I don't think I will get the reactors benefit this way.
I was thinking to have the logic like this:
Mono<ItemRequest> result = firstService.call(...)
.doOnNext(/*do something */)
.then( ... secondService.call())
But couldn't figure out the way to chain the secondService with firstService to get the mono result and have those condition checks too.
Condition check is important since I don't always want to execute the second service. Is there any way to chain the secondService with firstService to get the result and have those condition checks too?
Apologies for the long question. Any suggestions/help would be greatly appreciated.
After offering the bounty points to this question, I was really excited and expecting some answers.
But anyways, I am able to improve my initial solution and have those condition checks too.
I did the following:
I changed the return type from Mono<ItemRequest> to Mono<Void> in both service calls since I am basically updating the data to ItemRequest list:
Handling the parallel call here (each parallel call has a sequential call):
public void getAccountData(List<ItemRequest> itemRequests) {
ImmutableList<ItemRequest> list = ImmutableList.copyOf(itemRequests);
Flux.fromIterable(list).flatMap(this::callBothSors).blockLast();
}
public Mono<Void> callBothSors(ItemRequest itemRequest) {
return getAccountDataService.getAccountDataFromAAAandBBB(itemRequest);
//here, it will enter into a sequential call for each itemRequest
}
and these are my firstServiceCall and secondServiceCall interface changes:
public Mono<Void> getAccountDataFromAAA(ItemRequest itemRequest);
public Mono<Void> getAccountDataFromBBB(ItemRequest itemRequest);
and I chained the secondServiceCall with firstServiceCall to get the mono result and have those condition checks too as:
public Mono<Void> getAccountDataFromAAAandBBB(ItemRequest itemRequest){
Mono<Void> callSequence = Mono.empty();
if(isFirstServiceCallRequired(itemRequest)){
callSequence = this.firstServiceCallImpl.getAccountDataFromAAA(itemRequest);
} else {
//Account key is already present, so just update the result status which I need later.
Result result = new Result();
result.setStatus(Result.Status.OK);
result.setMessageText("First call not required as account info is set for item request");
itemRequest.setFirstServiceResult(result);
}
return callSequence.thenEmpty(Mono.defer(() -> {
//note: Mono.defer ==>> Create a Mono provider that will supply a target Mono to subscribe to
//for each subscriber downstream.
//only if the firstServiceCall result is successful & other condition check successful,
// I am calling secondServiceCall:
if(shouldCallSecondService(itemRequest)){
return this.secondServiceCallImpl.getAccountDataFromAAAandBBB(itemRequest);
} else {
return Mono.empty();
}
}))
Here are some news: A Reactor is not a silver bullet! :)
Whenever you need the response of a call to determine if you need to do something else, this will never be able to be fully parallelized. E.g. you could always do you last suggestion. However, this doesn't mean that using the Reactor doesn't give you any benefits!
Some of the benefits you get:
You are using Netty under the hood instead of Servlet, which helps to avoid locking on I/O operations. This can lead to better allocation of resources, making your system more resilient.
You can do other operations while waiting for a response. If you have things to do where the order doesn't matter, you can always put them there (e.g. auditing, logging etc).
I hope this answers your question :)
public Mono<ItemRequest> getAccountDataFromAAAandBBB(ItemRequest itemRequest) {
Mono<ItemRequest> firstCallResult = Mono.empty();
Mono<ItemRequest> secondCallResult = Mono.empty();
if (isFirstServiceCallRequired(itemRequest)) {
firstCallResult = this.firstServiceCallImpl.getAccountDataFromAAA(itemRequest);
//basically, firstService call will update the accountKey information and
//will also set the result status to OK which is required to decide
//whether to make secondService call.
} else {
/*Account key is already present, so just update the result status which I need
later.*/
firstCallResult = Mono.defer(() -> {
Result result = new Result();
result.setStatus(Result.Status.OK);
result.setMessageText("First call not required as account info is set for item request");
itemRequest.setFirstServiceResult(result);
return Mono.just(itemRequest);
});
}
return firstCallResult.flatMap(itReq -> {
//Now, before calling the second service, I need to check the following:
if (null != itemRequest.getFirstServiceResult() &&
itemRequest.getFirstServiceResult().getStatus().equals(Result.Status.OK) &&
itemRequest.getPostingParameterCode().equals(PostingParameterCode.MOBILECREDIT)) {
return secondCallResult = this.secondServiceCallImpl.getAccountDataFromBBB(itemRequest);
} else {
return itReq;
}
});
}
The next simple example can help you with flatMap understanding:
public static void main(String[] args) {
callExternalServiceA.flatMap(response -> {
if(response.equals("200")){
return Mono.just(response);
} else {
return callExtertnalServiceB();
}
}).block();
}
public static Mono<String> callExtertnalServiceA() {
return Mono.defer(() -> {
System.out.println("Call external service A");
return Mono.just("400");
});
}
public static Mono<String> callExtertnalServiceB() {
return Mono.defer(() -> {
System.out.println("Call external service B");
return Mono.just("200");
});
}
I am new to Java and am using CompletableFutures to perform async operations such as below:
public CompletionStage<Either<ErrorResponse, Response>> insertOrUpdate(String actor, String key) {
return this.objectDAO.getByKey(key)
.thenApply(mapDOToContainer(key))
.thenApply(mergeContainerToDO(key, actor))
.thenComposeAsync(this.objectDAO.UpdateFn())
.thenApply(DBResult::finished)
.thenApply(finished -> {
if (finished) {
Response response = Response.ok().build();
return Either.right(response);
} else {
return Either.left(ErrorResponse.create("Error", 400));
}
});
}
Now I need to modify this so that if the get fails then I perform the above chain, but if it succeeds then I need to break this chain and return from the function with an Either object containing an ErrorResponse.
How can I break this processing chain? I know I can pass a flag to each function in the chain and achieve this by performing the actions in the functions based on the value of the flag. I was hoping there is a better way to do this.
Thanks!!
I would rewrite your code.
Don't use Either for errors, Java has exception
Don't return a CompletionStage from your DAO
Use exceptionally from CompletableFuture, it is designed for this
Then do this:
public CompletionStage<Response> insertOrUpdate(String actor, String key) {
return CompletableFuture.supplyAsync(() -> this.objectDAO.getByKey(key))
.thenApply(mapDOToContainer(key))
.thenApply(mergeContainerToDO(key, actor))
.thenComposeAsync(this.objectDAO.UpdateFn())
.thenApply(DBResult::finished)
.thenApply(finished -> {
Response response = Response.ok().build();
return response;
})
.exceptionally(e -> ErrorResponse.create("Error", 400));
}
The DAO should be something like this:
class ObjectDAO {
public Object getByKey(String key) {
if (keyNotFound) {
throw new NoSuchElementException();
}
return new Object();
}
}
You may have to make sure that ErrorResponse is a subclass of Response to make this work.