Webflux data check with mongo reactive spring - java

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

Related

Use flatMap after method returning Mono<Void>

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();
}

Resilience4j circuit breaker does not switch back to closed state from half open after the downstream system is up again

I have two micro-services, order-service, and inventory-service. The order-service makes a call to inventory-service to check if ordered items are in stock. An order is placed only if all the items in the order request are in stock. If the inventory-service is down or slow, the circuit breaker part triggers. The case of an order not being placed because of a missing item is not a failure case for the circuit breaker. This works well as intended only until the circuit has never switched from closed state to half open state.
What I mean by that is if more than 5 consecutive orders cannot be placed because of a missing item, the circuit does not switch to open state. This is as expected. If the inventory-service is brought down, 3 more failed requests cause the circuit to move to open and subsequently to half open state. This also is as expected. However when the inventory-service comes up again, and the only requests that are made are requests containing one or more items not in stock, the circuit remains in half_open state continuously. This is not ok. A missing item in an order is a success case and should increment the successful buffered call count, but it doesn't. Looking at the actuator info, it looks like these calls are not counted either as failure or as success cases.
What am I doing wrong.
Note -- if I make sufficient number of calls where order gets placed, then the circuit switches to closed again. That's ok but shouldn't the case of ignored exception count as a success case even if the only calls that are made are those with one or missing items.
Following are the properties of my circuit breaker in the calling microservice.
resilience4j.circuitbreaker.instances.inventory.register-health-indicator=true
resilience4j.circuitbreaker.instances.inventory.event-consumer-buffer-size=10
resilience4j.circuitbreaker.instances.inventory.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.inventory.sliding-window-size=5
resilience4j.circuitbreaker.instances.inventory.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.inventory.wait-duration-in-open-state=5s
resilience4j.circuitbreaker.instances.inventory.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.inventory.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.inventory.ignore-exceptions=com.mayrevision.orderservice.exception.OrderItemNotFoundException
I have a custom exception handler for OrderItemNotFoundException.
#ControllerAdvice
#ResponseStatus
public class OrderItemNotFoundExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(OrderItemNotFoundException.class)
public ResponseEntity<ErrorResponse> getErrorMessage(OrderItemNotFoundException exception) {
ErrorResponse response = new ErrorResponse(HttpStatus.NOT_ACCEPTABLE, exception.getMessage());
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(response);
}
}
The controller code is --
#PostMapping
#ResponseStatus(HttpStatus.CREATED)
#CircuitBreaker(name = "inventory", fallbackMethod = "fallBackMethod")
public String placeOrder(#RequestBody OrderRequest orderRequest) throws OrderItemNotFoundException {
orderService.placeOrder(orderRequest);
return "Order placed successfully";
}
public String fallBackMethod(OrderRequest orderRequest, RuntimeException runtimeException) {
return "The order could not be placed. Please try back after some time.";
}
Edit -- Edited resilience4j.circuitbreaker.instances.inventory.ignore-exceptions[0]=com.mayrevision.orderservice.exception.OrderItemNotFoundException to resilience4j.circuitbreaker.instances.inventory.ignore-exceptions=com.mayrevision.orderservice.exception.OrderItemNotFoundException in application.properties.
Looks like this is how the exception mechanism in Resilience4j is designed to work. If I want the exception to be treated as a success case in all the cases (including the case mentioned above), I should catch it. So I changed the code as follows --
#PostMapping
#CircuitBreaker(name = "inventory", fallbackMethod = "fallBackMethod")
public CompletableFuture<ResponseEntity<StatusResponse>> placeOrder(#RequestBody OrderRequest orderRequest) {
return CompletableFuture.supplyAsync(() -> {
try {
String message = orderService.placeOrder(orderRequest);
StatusResponse response = new StatusResponse(HttpStatus.CREATED, message);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (OrderItemNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE)
.body(new StatusResponse(HttpStatus.NOT_ACCEPTABLE, e.getMessage()));
}
});
}
public CompletableFuture<ResponseEntity<StatusResponse>> fallBackMethod(OrderRequest orderRequest, RuntimeException e) {
return CompletableFuture.supplyAsync(() -> {
//some logic
});
}
This is working close to expected.

Exceptions thrown by Mono inside flatmap not being handled by Spring webflux reactive stream

I am working on reactive streams application using Spring webflux. I have a usecase where I do a webclient request to get some data and once I get the response data, I validate it and if the validation fails, I want to throw an exception which should be handled by the main reactive pipeline. I'm using webclient call within a flatmap to use the value in the next operator of my pipeline. I have something similar to the following code:
public class Example {
public String getData(String name) {
return Mono.just(name)
.map(name -> name.toLowerCase())
.flatMap(name ->
// Webclient Request that returns a
// Mono<String> for example
.doOnSuccess(Validator::validateData); // The webclient request is chained to this doOnSuccess
)
.doOnError(ex -> log.error("Got an error, {}", er))
.onErrorMap(ex -> new AnotherCustomException(ex.getMessage()));
}
}
public class Validator {
public void validateData(String data) {
if(data.length() < 5) throw new CustomException("Invalid data received."); // public CustomException extends RuntimeException {...}
}
}
Currently this code isn't using the doOnError() & onErrorMap() operators and I'm directly receiving the CustomException stacktrace on my console. I believe the reason being the code inside flatMap itself is a publisher Mono so it should have its own doOnError(), onErrorMap() operators. How do I make this Webclient's response i.e., Mono<String> be able to use the main pipeline that's using the WebClient?
This is what you want
SomeWebclientCall().flatMap(value -> {
Validate.validate(value);
return Mono.just(value);
});
This might look wierd and that is because writing void functions that either return void or an exception is wierd and should be avoided.
Such functions are ineffective (throwing exceptions are expensive and should not be a natural flow of the program) and hard to test.
Thats why validation functions usually returns booleans as in it passed validation yes or no, true or false. Not void or exception.
Functional programming does not like void functions, only pure functions.

Handle exception in reactive streams and return value from previous call

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.

Filter a List and return the response from a CompletableFuture java async operation

Hello I have to filter and return the result of a CompletableFuture and store it in an object variable to work with this object after the filter, the Completable method which extract the list from the database is and is located in the salonService is :
public CompletableFuture<List<SalonDTO>> listAllSalons() {
return salonRepository.findAllAsync()
.thenApply(salonList -> ObjectMapperUtils.mapAll(salonList, salonDTO.class));
}
Then I'm trying to filter the info in the next way:
public CompletableFuture<List<SalonDTO>> listKidsByGuardian1() {
return salonService.listAll()
.thenApply(salonDTOList -> {
findsalonByChildAge(salonDTOList);
return salonDTOList;
});
}
private SalonDTO findsalonByChildAge(List<SalonDTO> salonDTOList) {
salonDTOList.stream()
.filter(salon -> salon.getMinAge() > 13);
}
I'm not pretty familiar with the CompletableFuture class, so I don't understand how Can I get a simple object from this async operation. Besides that it is not easy to debug these async methods. Any advice?
Thanks!

Categories

Resources