I'm stuck with understanding of some aspects of Project Reactor.
I have the following list of components:
Validator of input params, returns Mono<Void> or Mono.error()
Service saving data to db, returns Mono<Item>
Logger for successful actions of an user, returns Mono<Void
A business logic is quite simple: validate params (1), save an item to db (2) and log actions (3). The problem is validator (1) returns Mono.empty() if there are no errors with input data and Mono.error() if input params contain some errors.
I would like to achieve the next things:
If validator returns Mono.empty() then continue chain
If validator returns Mono.error() then immediately stop processing and throw error which will be handled by exceptionHanlder
I have tried two options:
First with .then(Mono<Item> item) after validation. It allows me to execute saving operation after validation. Given that .then() ignores any errors, I can't rise an exception.
return inputValidator.validateFields(userId, projectId)
.then(repository.save(item))
.onErrorMap(RepoException.class, ex -> new UnexpectedError("Failed to save item", ex))
.subscribeOn(Schedulers.boundedElastic())
.doOnSuccess(n -> logService.logActivity(new Activity(adminId, n))
.subscribe());
Second with .flatMap(Function<Mono<Void>, <Mono<? extends Item> func) after validation. This approach can rise an exception from validator, but I can't execute saving operation because flatMap() doesn't trigger on empty result.
return inputValidator.validateFields(userId, projectId)
.flatMap(v -> repository.save(item))
.onErrorMap(RepoException.class, ex -> new UnexpectedError("Failed to save item", ex))
.subscribeOn(Schedulers.boundedElastic())
.doOnSuccess(n -> logService.logActivity(new Activity(adminId, n))
.subscribe());
It is also important to have access for created object after saving (step 2), because I need to pass it to logger service.
You can't use flatMap because there is no onNext signal - use then instead. Not sure what do you mean by "called" but there is a difference between Assembly and Subscription time in reactive. Publisher you specified in then will not be resolved in case inputValidator.validateFields returns onError signal.
Here is a test for failed validation and as you may see subscription was not triggered
#Test
void saveWasNotCalledIfValidationFailed() {
var saveMock = PublisherProbe.of(Mono.just("id"));
var repo = mock(Repository.class);
when(repo.save())
.thenReturn(saveMock.mono());
var res = validateFields()
.then(repo.save())
.onErrorMap(IllegalArgumentException.class,
ex -> new IllegalStateException("Failed to save item", ex)
);
StepVerifier.create(res)
.expectError(IllegalStateException.class)
.verify();
saveMock.assertWasNotSubscribed();
}
private Mono<Void> validateFields() {
return Mono.error(new IllegalArgumentException("oops"));
}
public static class Repository {
public Mono<String> save() {
return Mono.just("id");
}
}
and here is a test for passed validation
#Test
void saveIsCalledIfValidationPassed() {
var saveMock = PublisherProbe.of(Mono.just("id"));
var repo = mock(Repository.class);
when(repo.save())
.thenReturn(saveMock.mono());
var res = validateFields()
.then(repo.save())
.onErrorMap(IllegalArgumentException.class,
ex -> new IllegalStateException("Failed to save item", ex)
);
StepVerifier.create(res)
.expectNext("id")
.verifyComplete();
saveMock.assertWasSubscribed();
}
private Mono<Void> validateFields() {
return Mono.empty();
}
Related
I'm totally new to the Java Reactor API.
I use a WebClient to retrieve data from an external webservice, which I then map to a DTO of class "LearnDetailDTO".
But before sending back this DTO, I have to modify it with data I get from another webservice. For this, I chain the calls with flatMap(). I get my data from the second webservice, but my DTO is returned before it is modified with the new data.
My problem is: how to wait until all calls to the second webservice are finished and the DTO is modified before sending it back to the caller?
Here is my code:
class Controller {
#GetMapping(value = "/learn/detail/", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<LearnDetailDTO> getLearnDetail() {
return getLearnDetailDTO();
}
private Mono<LearnDetailDTO> getLearnDetailDTO() {
WebClient client = WebClient.create("https://my_rest_webservice.com");
return client
.get()
.retrieve()
.bodyToMono(LearnDetailDTO.class)
.flatMap(learnDetailDTO -> {
LearnDetailDTO newDto = new LearnDetailDTO(learnDetailDTO );
for (GroupDTO group : newDto.getGroups()) {
String keyCode = group.getKeyCode();
for (GroupDetailDto detail : group.getGroupsDetailList()) {
adeService.getResourcesList(keyCode) // one asynchonous rest call to get resources
.flatMap(resource -> {
Long id = resource.getData().get(0).getId();
return adeService.getEventList(id); // another asynchronous rest call to get an events list with the resource coming from the previous call
})
.subscribe(event -> {
detail.setCreneaux(event.getData());
});
}
}
return Mono.just(newDto);
});
}
I tried to block() my call to adeservice.getEventList() instead of subscribe(), but I get the following error:
block()/blockFirst()/blockLast() are blocking, which is not supported
in thread reactor-http-nio-2
How to be sure that my newDTO object is complete before returning it ?
You should not mutate objects in subscribe. The function passed to subscribe will be called asynchronously in an unknown time in the future.
Subscribe should be considered a terminal operation, and should only serve to connect to other part of your system. It should not modify values inside the scope of your datastream.
What you want, is a pipeline that collects all events, and then map them to a dto with collected events.
As a rule of thumb your pipeline result must be composed of accumulated results in the operation chain. You should never have a "subscribe" in the middle of the operation chain, and you should never mutate an object with it.
I will provide a simplified example so you can take time to analyze the logic that can reach the goal: accumulate new values asynchronously in a single result. In this example, I've removed any notion of "detail" to connect directly groups to events, to simplify the overall code.
The snippet:
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
public class AccumulateProperly {
// Data object definitions
record Event(String data) {}
record Resource(int id) {}
record Group(String keyCode, List<Event> events) {
// When adding events, do not mute object directly. Instead, create a derived version
Group merge(List<Event> newEvents) {
var allEvents = new ArrayList<>(events);
allEvents.addAll(newEvents);
return new Group(keyCode, allEvents);
}
}
record MyDto(List<Group> groups) { }
static Flux<Resource> findResourcesByKeyCode(String keyCode) {
return Flux.just(new Resource(1), new Resource(2));
}
static Flux<Event> findEventById(int id) {
return Flux.just(
new Event("resource_"+id+"_event_1"),
new Event("resource_"+id+"_event_2")
);
}
public static void main(String[] args) {
MyDto dtoInstance = new MyDto(List.of(new Group("myGroup", List.of())));
System.out.println("INITIAL STATE:");
System.out.println(dtoInstance);
// Asynchronous operation pipeline
Mono<MyDto> dtoCompletionPipeline = Mono.just(dtoInstance)
.flatMap(dto -> Flux.fromIterable(dto.groups)
// for each group, find associated resources
.flatMap(group -> findResourcesByKeyCode(group.keyCode())
// For each resource, fetch its associated event
.flatMap(resource -> findEventById(resource.id()))
// Collect all events for the group
.collectList()
// accumulate collected events in a new instance of the group
.map(group::merge)
)
// Collect all groups after they've collected events
.collectList()
// Build a new dto instance from the completed set of groups
.map(completedGroups -> new MyDto(completedGroups))
);
// NOTE: block is here only because we are in a main function and that I want to print
// pipeline output before program extinction.
// Try to avoid block. Return your mono, or connect it to another Mono or Flux object using
// an operation like flatMap.
dtoInstance = dtoCompletionPipeline.block(Duration.ofSeconds(1));
System.out.println("OUTPUT STATE:");
System.out.println(dtoInstance);
}
}
Its output:
INITIAL STATE:
MyDto[groups=[Group[keyCode=myGroup, events=[]]]]
OUTPUT STATE:
MyDto[groups=[Group[keyCode=myGroup, events=[Event[data=resource_1_event_1], Event[data=resource_1_event_2], Event[data=resource_2_event_1], Event[data=resource_2_event_2]]]]]
I'm leaning webflux and reactor. Got three test methods as below. "documentOperations.findById" and "documentOperations.delete" are two database operations. I know test1 is bad as the two db operations are placed in one async method. My question is:
Do test2 and test3 have the same impact to system performace? Or in other words, which one is better?
private Mono<ServerResponse> test1(ServerRequest request, Contexts contexts) {
return request.body(bodyDocumentExtractor)
.flatMap(doc -> {
Document document = documentOperations.findById(doc.getId());
documentOperations.delete(document.getId());
return ServerResponse.noContent().build();
});
}
private Mono<ServerResponse> test2(ServerRequest request, Contexts contexts) {
return request.body(bodyDocumentExtractor)
.flatMap(doc -> {
return Mono.just(documentOperations.findById(doc.getId()))
.flatMap(document -> {
documentOperations.delete(document.getId());
return ServerResponse.noContent().build();
});
});
}
private Mono<ServerResponse> test3(ServerRequest request, Contexts contexts) {
return request.body(bodyDocumentExtractor)
.flatMap(doc -> {
return Mono.just(documentOperations.findById(doc.getId()));
}).flatMap(document -> {
documentOperations.delete(document.getId());
return ServerResponse.noContent().build();
});
}
None of the examples above are good. All your database calls return concrete types which means that they are all blocking calls.
// returns the concrete type
// thread does the call, needs to wait until we get the value (document)
Document document = documentOperations.findById("1");
If it is non blocking it returns a Mono<T> or a Flux<T>.
// Returns a mono, so we know it's not blocking.
// We can chain on actions with for example flatMap etc.
Mono<Document> document = documentOperations.findById("1");
If you have to use a blocking database like for instance oracle database etc. You need to place this on its entire own thread, so that it doesn't block any of the main worker threads. This can be done with a scheduler. So in this example when a client subscribes it will be placed on a separate thread.
Mono<Document> document = Mono.fromCallable(() -> documentOperations.findById("1"))
.subscribeOn(Schedulers.boundedElastic());;
So for your example:
private Mono<ServerResponse> test3(ServerRequest request, Contexts contexts) {
return request.body(bodyDocumentExtractor)
.flatMap(doc -> Mono.fromCallable(() -> documentOperations.findById(doc.getId()))
.flatMap(document -> Mono.fromCallable(() -> documentOperations.delete(document.getId()))
.then(ServerResponse.noContent().build());
).subscribeOn(Schedulers.boundedElastic());
}
Reactor documentation - Wrap blocking calls
I would expect the code to look something along the lines of the code below based on the assumption that DocumentOperations is reactive.
private Mono<ServerResponse> test3(ServerRequest request, Contexts contexts) {
return request.body(bodyDocumentExtractor)
.flatMap(doc -> documentOperations.findById(doc.getId()))
.flatMap(document -> documentOperations.delete(document.getId()))
.then(ServerResponse.noContent().build());
}
I'm having some trouble with reactive streams and based on my code below, the return value of the function should be the value created by the call to myServiceConnector.createSummary(request), whether or not the following call to otherService.postDetails(summary, details, item) throws an exception or not. If it throws an exception I just want to do some logging and otherwise ignore it.
public Mono<Summary> createSummary(final Details details, String authorisation)
{
return myService.doSomething(details.getItemId(), authorisation)
.zipWhen(item -> Mono.just(convertToMyRequest(item, details, myServiceConfig.getBaseRedirectUrl())))
.flatMap(tuple -> {
MyItem item = tuple.getT1();
OrderRequest request = tuple.getT2();
return myServiceConnector.createSummary(request)
.doOnSuccess(summary -> otherService.postDetails(summary, details, item)
.onErrorContinue((o,i) -> {
// Log error
}));
});
}
Currently it seems that the onErrorContinue call is not called (I am forcing an exception to be thrown by otherService.postDetails(summary, details, item) in my test). I have also tried onErrorResume which was called but the exception was still thrown so I got no Summary object returned. Not sure if I have my error handling in the right place.
Updating to include test code below:
#Test
public void returnSummaryWhenOtherServiceFails()
{
Details details = Details.builder()
.itemId(ITEM_ID)
.build();
when(myServiceConfig.getBaseRedirectUrl()).thenReturn(BASE_REDIRECT_URL);
when(myService.doSomething(ITEM_ID, AUTH_STRING)).thenReturn(Mono.just(ITEM));
when(myServiceConnector.createSummary(any())).thenReturn(SUMMARY);
when(otherService.postDetails(any(), any(), any())).thenThrow(WebClientResponseException.class);
summaryService.createSummary(details, AUTH_STRING).block();
verify(myServiceConnector).createSummary(any());
}
The test fails due to:
org.springframework.web.reactive.function.client.WebClientResponseException
If you want to call otherService.postDetails in background and don't care about its result then you can do it like this:
otherService.postDetails(...).subscribe()
or
otherService.postDetails(...).publishOn(Schedulers.elastic()).subscribe()
it's depended on your code.
Or you can change your code like this:
myServiceConnector.createSummary(request)
.flatMap(summary -> otherService.postDetails(summary, details, item)
.onErrorContinue(...)
)
it will run createSummary and then postDetails and if postDetails fails then onErrorContinue will be triggered.
I try to learn Webflux, but im facing problem when i want to validate list id of employee before save the data.
And my qestion
How to catch error, when employeId is doesn't exist and show the error to client?
#PostMapping(path = "/{tenantId}/outlet")
public Mono<OutletEntity> createNewOutlet(#PathVariable String tenantId, #RequestBody OutletEntity outletEntity) {
return Mono.just(outletEntity).map(outletEntity1 -> {
outletEntity.getEmployees().forEach(s -> {
this.employeeService.getRepository().existsById(s).subscribe(aBoolean -> {
System.out.println(aBoolean);
if (!aBoolean) {
/**
* variable s is employeId
* i want to validate every single employee id before save new outlet
*/
throw new ApiExceptionUtils("tenant not found", HttpStatus.UNPROCESSABLE_ENTITY.value(),
StatusCodeUtils.TENANT_NOT_FOUND);
}
});
});
return outletEntity1;
}).flatMap(outletEntity1 -> {
outletEntity.setTenantId(tenantId);
return this.outletRepository.save(outletEntity);
});
Better way run your validation in the same chain without additional subscriber
return Flux.fromIterable(outletEntity.getEmployees()) (1)
.flatMap(this.employeeService.getRepository()::existsById)
.doOnNext(System.out::println)
.map(aBoolean -> {
if (!aBoolean) { (2)
throw new ApiExceptionUtils("tenant not found", HttpStatus.UNPROCESSABLE_ENTITY.value(),
StatusCodeUtils.TENANT_NOT_FOUND);
}
else {
return aBoolean;
}
})
.then(Mono.just(outletEntity)) (3)
.flatMap(outletEntity1 -> {
outletEntity.setTenantId(tenantId);
return this.outletRepository.save(outletEntity);
});
1) Create Flux from employees collection and iterate via reactor streams with a validation;
2) Check if your type false and throw exception, it stops this chain;
3) If everything ran smoothly then() switch to Mono with the outletEntity, saves it and returns;
About error handling.
If you don't handle errors, WebFlux resolve it in DefaultErrorWebExceptionHandler.
You can add your own error handling like in Web MVC or add you custom exception handler in the WebFlux Config.
More details you can read here: web-reactive
I try to combine CompletionStages in play framework and then return a Result like ok(). This is my setup:
AccountDao which has two methods:
public CompletionStage<Account> getUserByEmail(String email) {
return supplyAsync(() -> ebeanServer.find(Account.class).setUseCache(true).where().eq(EMAIL, email).findOne(), executionContext).thenApply(account -> {
return account;
});
}
public CompletionStage<Void> updateAccount(Account account) throws OptimisticLockException{
return runAsync(() -> {
ebeanServer.update(account);
}, executionContext);
}
And then i have my controller with the action:
public CompletionStage<Result> editAccount() {
Map<String, String[]> form_values = request().body().asFormUrlEncoded();
return CompletableFuture.completedFuture(ok());
}
So now in the action i want first to execute getUserByEmail and then i want to set some values and Update this with updateAccount method. How can i combine this two stages without blocking play context? I tried different setups with thenCompose and combine but i dont get it ...
Here one of my tries:
public CompletionStage<Result> editAccount() {
Map<String, String[]> form_values = request().body().asFormUrlEncoded();
accountDao.getUserByEmail(session().get("accountEmail")).thenCompose(x -> accountDao.updateAccount(x).thenApplyAsync(account -> {
return ok("Going to save account edits");
}, httpExecutionContext.current()));
return CompletableFuture.completedFuture(ok("Fehler am Ende"));
}
The problem here is, that i cannot access the account (x) from before because i cannot set this as function ... like this:
public CompletionStage<Result> editAccount() {
Map<String, String[]> form_values = request().body().asFormUrlEncoded();
accountDao.getUserByEmail(session().get("accountEmail")).thenCompose(x -> {
//Update vars in x and then save to database
accountDao.updateAccount(x);
}.thenApplyAsync(account -> {
return ok("Going to save account edits");
}, httpExecutionContext.current()));
return CompletableFuture.completedFuture(ok("Fehler am Ende"));
}
Here i get the error: The target type of this expression must be a functional interface and plays says that i have to include the return statement at the end of the function!
I just dont get it ... Thanks for your help!
#Marimuthu Madasamy Thats no exactly what i want. In your awnser i would update the account twice. On etime in accountDao.updateAccount(account) and in accountDao.saveAccount(account); I want something like this:
return accountDao.getUserByEmail("mail").thenCompose(account -> {
account.setName("NewName");
accountDao.save(account);
} .thenApplyAsync(voidInput -> {
return ok("Account saved");
}, httpExecutionContext.current()));
In this case in only update the account once and only return the result on the httpExecutionContext
If I understand your question correctly, you want to access (to save?) account after updateAccount(account) method call.
Since updateAccount method returns CompletionStage<Void>, when you call thenApplyAsync on this stage, the input type would only be Void which is not Account. But with the following code, you would still have access to the account returned from getUserByEmail assuming updateAccount mutates the account by your text "update vars in x":
public CompletionStage<Result> editAccount() {
return accountDao
.getUserByEmail(email)
.thenCompose(account -> accountDao.updateAccount(account)
.thenApplyAsync(voidInput -> {
// here you still have access to the `account` from `getUserByEmail` method
accountDao.saveAccount(account);
return ok("Account saved");
}, httpExecutionContext.current());
}
Ok i found my own awnser here with the support of Marimuthu Madasamy! Thanks. I trie to explain it. First here is the code:
public CompletionStage<Result> editAccount() {
Map<String, String[]> form_values = request().body().asFormUrlEncoded();
return accountDao.getUserByEmail(session().get("accountEmail")).thenApply(account -> {
System.out.println("Async get Account / "+Thread.currentThread());
account.setCompany(form_values.get("company")[0]);
return accountDao.updateAccount(account);
}).thenApplyAsync(account -> {
System.out.println("Async resutl / "+Thread.currentThread());
return ok("Account saved normal");
}, httpExecutionContext.current()).exceptionally(e ->{
System.out.println("Async exception / "+Thread.currentThread());
System.out.println(e.getLocalizedMessage());
return ok(e.getLocalizedMessage());
});
}
Ok at first i execute accountDao.getUserByEmail() as you can see at top in my awnser this returns CompletionStage and is executed in my database execution context. At next with thenApply i get the result and i execute the next Async mehtod. I use thenApply instand of thenApplyAsync so the next call is also executed with the database execution context without setting it explicitly. After the accountDao.updateAccount() i execute the next stage on the httpExecutionContext to replay a result or to quit exceptionally! I really hope it is clear and helps some one!