In WebFlux Reactive controller onErrorResume don't work - spring-webflux

WebFlux
In a GET controller I need to check connectivity to mongo db to verify liveness from Kubernates point of view, but when I disconnect the db, the code doesn't enter in onErrorResume. Why? A possible solution?
#RequestMapping("/liveness-for-kubernates/{id}")
#ResponseBody
public Mono<ResponseEntity<String>> livenessForKubernates(#PathVariable #NotBlank String id) {
return Mono.just(id)
.map(golamService::findById)
.map(result -> new ResponseEntity<String>("UP", HttpStatus.OK))
.onErrorResume(
throwable -> Mono.just(new ResponseEntity<String>("DOWN", HttpStatus.OK)));
}

findById is an asynchronous, non-blocking operation resulting in a Mono publisher. Thus, you should wrap it into a flatmap instead of map:
return Mono.just(id)
.flatmap(golamService::findById)
.map(result -> new ResponseEntity<String>("UP", HttpStatus.OK))
.onErrorResume(throwable -> Mono.just(new ResponseEntity<String>("DOWN", HttpStatus.OK)));
map is for synchronous, non-blocking, 1-to-1 transformations.

Related

Spring Mono<User> as constructor param - how to "cache" object

I'm drawing a blank on how to do this in project reactor with Spring Boot:
class BakerUserDetails(val bakerUser: Mono<BakerUser>): UserDetails {
override fun getPassword(): String {
TODO("Not yet implemented")
// return ???.password
}
override fun getUsername(): String {
TODO("Not yet implemented")
// return ???.username
}
}
How do I make this work? Do I just put bakerUser.block().password and bakerUser.block().username and all, or is there a better way to implement these methods?
Currently, I'm doing something like this but it seems strange:
private var _user: BakerUser? = null
private var user: BakerUser? = null
get() {
if(_user == null){
_user = bakerUser.block()
}
return _user
}
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return mutableSetOf(SimpleGrantedAuthority("USER"))
}
override fun getPassword(): String {
return user!!.password!!
}
im not well versed at Kotlin, but i can tell you that you should not pass in a Monoto the UserDetails object.
A Mono<T> is sort of like a future/promise. Which means that there is nothing in it. So if you want something out of it, you either block which means we wait, until there is something in it, or we subscribe, which basically means we wait async until there is something in it. Which can be bad. Think of it like starting a job on the side. What happens if you start a job and you quit the program, well the job would not be executed.
Or you do something threaded, and the program returns/exits, well main thread dies, all threads die, and nothing happend.
We usually in the reactive world talk about Publishers and Consumers. So a Flux/Mono is a Publisher and you then declare a pipelinefor what to happen when something is resolved. And to kick off the process the consumerneeds to subscribe to the producer.
Usually in a server world, this means that the webpage, that does the request, is the consumer and it subscribes to the server which in this case is the publisher.
So what im getting at, is that you, should almost never subscribe in your application, unless, your application is the one that starts the consumption. For instance you have a cron job in your server that consumes another server etc.
lets look at your problem:
You have not posted your code so im going to do some guesswork here, but im guessing you are getting a user from a database.
public Mono<BakerUserDetails> loadUserByUsername(String username) {
Mono<user> user = userRepository.findByUsername(username);
// Here we declare our pipline, flatMap will map one object to another async
Mono<BakerUserDetails> bakerUser = user.flatMap(user -> Mono.just(new BakerUserDetails(user));
return bakerUser;
}
i wrote this without a compiler from the top of my head.
So dont pass in the Mono<T> do your transformations using different operators like map or flatMap etc. And dont subscribe in your application unless your server is the final consumer.

Flux collectList() on list of WebClient exchanges always empty

I'm trying to execute a list requests using WebClient, then filter them finding the first one that succeed (if any) and return that. Or fall back to a default response if non succeeded.
The problem I'm facing is that when I call .collectList() on a Flux<ServerResponse>, the list is always empty. I would have expected the list to contain N number of ServerResponse based on the number of requests I issued earlier.
public Mono<ServerResponse> retry(ServerRequest request) {
return Flux.fromIterable(request.headers().header(SEQUENCE_HEADER_NAME))
.map(URI::create)
// Build a "list" of responses
.flatMap(uri -> webClientBuilder.baseUrl(uri.toString()).build()
.method(Objects.requireNonNull(request.method()))
.headers(headers -> request.headers().asHttpHeaders().forEach((key, values) -> {
if (!SEQUENCE_HEADER_NAME.equals(key)) {
headers.addAll(key, values);
}
}))
.body(BodyInserters.fromDataBuffers(request.body(BodyExtractors.toDataBuffers())))
.exchange()
.flatMap(clientResponse -> ServerResponse.status(clientResponse.statusCode())
.headers(headers -> headers.addAll(clientResponse.headers().asHttpHeaders()))
.body(BodyInserters.fromDataBuffers(clientResponse.body(BodyExtractors.toDataBuffers()))))
)
// "Wait" for all of them to complete so we can filter
.collectList()
.flatMap(clientResponses -> {
List<ServerResponse> filteredResponses = clientResponses.stream()
.filter(response -> response.statusCode().is2xxSuccessful())
.collect(Collectors.toList());
if (filteredResponses.isEmpty()) {
log.error("No request succeeded; defaulting to {}", HttpStatus.BAD_REQUEST.toString());
return ServerResponse.badRequest().build();
}
if (filteredResponses.size() > 1) {
log.error("Multiple requests succeeded; defaulting to {}", HttpStatus.BAD_REQUEST.toString());
return ServerResponse.badRequest().build();
}
return Mono.just(filteredResponses.get(0));
});
}
Any ideas why .collectList() always returns an empty list?
Well, it seems to me you have a confused requirement in that you want the First Mono that responds but you are trying to put that functionality into a Flux which is meant to process all items in the flow efficiently. Mono in Webflux is meant to create a flow that will perform a series of transformations on the item in the flow efficiently. Nothing in your requirement of testing a bunch of URIs for the first one that succeeds is what WebFlux is good for so I have to question why try to force that into the framework.
You might argue that a Flux is giving you better asynchronous processing but I don't think that's the case when it is a bunch of WebClient calls. WebClient is still HTTP under the hood and so each item in the flow stops and starts around WebClient. If you want to do HTTP asynchronously you should use a ThreadPool and Callable.

Cache the result of a Mono from a WebClient call in a Spring WebFlux web application

I am looking to cache a Mono (only if it is successful) which is the result of a WebClient call.
From reading the project reactor addons docs I don't feel that CacheMono is a good fit as it caches the errors as well which I do not want.
So instead of using CacheMono I am doing the below:
Cache<MyRequestObject, Mono<MyResponseObject>> myCaffeineCache =
Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(Duration.ofSeconds(60))
.build();
MyRequestObject myRequestObject = ...;
Mono<MyResponseObject> myResponseObject = myCaffeineCache.get(myRequestObject,
requestAsKey -> WebClient.create()
.post()
.uri("http://www.example.com")
.syncBody(requestAsKey)
.retrieve()
.bodyToMono(MyResponseObject.class)
.cache()
.doOnError(t -> myCaffeineCache.invalidate(requestAsKey)));
Here I am calling cache on the Mono and then adding it to the caffeine cache.
Any errors will enter doOnError to invalidate the cache.
Is this a valid approach to caching a Mono WebClient response?
This is one of the very few use cases where you'd be actually allowed to call non-reactive libraries and wrap them with reactive types, and have processing done in side-effects operators like doOnXYZ, because:
Caffeine is an in-memory cache, so as far as I know there's no I/O involved
Caches often don't offer strong guarantees about caching values (it's very much "fire and forget)
You can then in this case query the cache to see if a cached version is there (wrap it and return right away), and cache a successful real response in a doOn operator, like this:
public class MyService {
private WebClient client;
private Cache<MyRequestObject, MyResponseObject> myCaffeineCache;
public MyService() {
this.client = WebClient.create();
this.myCaffeineCache = Caffeine.newBuilder().maximumSize(100)
.expireAfterWrite(Duration.ofSeconds(60)).build();
}
public Mono<MyResponseObject> fetchResponse(MyRequestObject request) {
MyResponseObject cachedVersion = this.myCaffeineCache.get(myRequestObject);
if (cachedVersion != null) {
return Mono.just(cachedVersion);
} else {
return this.client.post()
.uri("http://www.example.com")
.syncBody(request.getKey())
.retrieve()
.bodyToMono(MyResponseObject.class)
.doOnNext(response -> this.myCaffeineCache.put(request.getKey(), response));
}
}
Note that I wouldn't cache reactive types here, since there's no I/O involved nor backpressure once the value is returned by the cache. On the contrary, it's making things more difficult with subscription and other reactive streams constraints.
Also you're right about the cache operator since it isn't about caching the value per se, but more about replaying what happened to other subscribers. I believe that cache and replay operators are actually synonyms for Flux.
Actually, you don't have to save errors with CacheMono.
private Cache<MyRequestObject, MyResponseObject> myCaffeineCache;
...
Mono<MyResponseObject> myResponseObject =
CacheMono.lookup(key -> Mono.justOrEmpty(myCaffeineCache.getIfPresent(key))
.map(Signal::next), myRequestObject)
.onCacheMissResume(() -> /* Your web client or other Mono here */)
.andWriteWith((key, signal) -> Mono.fromRunnable(() ->
Optional.ofNullable(signal.get())
.ifPresent(value -> myCaffeineCache.put(key, value))));
When you switch to external cache, this may be usefull. Don't forget using reactive clients for external caches.

WebFlux: Only one item arriving at the backend

On the backend im doing:
#PostMapping(path = "/products", consumes = MediaType.APPLICATION_STREAM_JSON_VALUE)
public void saveProducts(#Valid #RequestBody Flux<Product> products) {
products.subscribe(product -> log.info("product: " + product.toString()));
}
And on the frontend im calling this using:
this.targetWebClient
.post()
.uri(productUri)
.accept(MediaType.APPLICATION_STREAM_JSON)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(this.sourceWebClient
.get()
.uri(uriBuilder -> uriBuilder.path(this.sourceEndpoint + "/id")
.queryParam("date", date)
.build())
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Product.class), Product.class)
.exchange()
.subscribe();
What happens now is that I have 472 products which need to get saved but only one of them is actually saving. The stream closes after the first and I cant find out why.
If I do:
...
.retrieve()
.bodyToMono(Void.class);
instead, the request isnt even arriving at the backend.
I also tried fix amount of elements:
.body(Flux.just(new Product("123"), new Product("321")...
And with that also only the first arrived.
EDIT
I changed the code:
#PostMapping(path = "/products", consumes =
MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> saveProducts(#Valid #RequestBody Flux<Product> products) {
products.subscribe(product -> this.service.saveProduct(product));
return Mono.empty();
}
and:
this.targetWebClient
.post()
.uri(productUri)
.accept(MediaType.APPLICATION_STREAM_JSON)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(this.sourceWebClient
.get()
.uri(uriBuilder -> uriBuilder.path(this.sourceEndpoint + "/id")
.queryParam("date", date)
.build())
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Product.class), Product.class)
.exchange()
.block();
That led to the behaviour that one product was saved twice (because the backend endpoint was called twice) but again only just one item. And also we got an error on the frontend side:
IOException: Connection reset by peer
Same for:
...
.retrieve()
.bodyToMono(Void.class)
.subscribe();
Doing the following:
this.targetWebClient
.post()
.uri(productUri)
.accept(MediaType.APPLICATION_STREAM_JSON)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(this.sourceWebClient
.get()
.uri(uriBuilder -> uriBuilder.path(this.sourceEndpoint + "/id")
.queryParam("date", date)
.build())
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Product.class), Product.class)
.retrieve();
Leads to the behaviour that the backend again isnt called at all.
The Reactor documentation does say that nothing happens until you subscribe, but it doesn't mean you should subscribe in your Spring WebFlux code.
Here are a few rules you should follow in Spring WebFlux:
If you need to do something in a reactive fashion, the return type of your method should be Mono or Flux
Within a method returning a reactive typoe, you should never call block or subscribe, toIterable, or any other method that doesn't return a reactive type itself
You should never do I/O-related in side-effects DoOnXYZ operators, as they're not meant for that and this will cause issues at runtime
In your case, your backend should use a reactive repository to save your data and should look like:
#PostMapping(path = "/products", consumes = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> saveProducts(#Valid #RequestBody Flux<Product> products) {
return productRepository.saveAll(products).then();
}
In this case, the Mono<Void> return type means that your controller won't return anything as a response body but will signal still when it's done processing the request. This might explain why you're seeing that behavior - by the time the controller is done processing the request, all products are not saved in the database.
Also, remember the rules noted above. Depending on where your targetWebClient is used, calling .subscribe(); on it might not be the solution. If it's a test method that returns void, you might want to call block on it and get the result to test assertions on it. If this is a component method, then you should probably return a Publisher type as a return value.
EDIT:
#PostMapping(path = "/products", consumes =
MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> saveProducts(#Valid #RequestBody Flux<Product> products) {
products.subscribe(product -> this.service.saveProduct(product));
return Mono.empty();
}
Doing this isn't right:
calling subscribe decouples the processing of the request/response from that saveProduct operation. It's like starting that processing in a different executor.
returning Mono.empty() signals Spring WebFlux that you're done right away with the request processing. So Spring WebFlux will close and clean the request/response resources; but your saveProduct process is still running and won't be able to read from the request since Spring WebFlux closed and cleaned it.
As suggested in the comments, you can wrap blocking operations with Reactor (even though it's not advised and you may encounter performance issues) and make sure that you're connecting all the operations in a single reactive pipeline.

How to call a private method from reactive subscribe of Mono and return a specific type in Spring 5?

I have a main method whose return type WebClient. In this method I get a Mono object and using subscribe I'm trying to call another method which returns webclient object. Now within subscribe, I have webclient object which I want to return. I'm blocked here as I'm not sure how to return the object and where to put the return keyword.
Main method:-
public WebClient getWebClientWithAuthorization(String t) {
-----
----
Mono<AccessToken> accessToken = authenticationProvider.getUserAccessToken(serviceConnectionDetails, queryParams);
Disposable disposable = accessToken.subscribe(
value -> getWebClientBuilder(t, value.getAccessToken()),
error -> error.printStackTrace(),
() -> System.out.println("completed without a value")
);
}
Below getWebClientBuilder method returns webclient object:-
private WebClient getWebClientBuilder(String tenantDomain, String accessToken) {
//TODO Logic for Saving the Token using Caching/Redis mechanism will be taken up from here and implemented in future Sprints
logger.info("Bearer token received: "+ CommerceConnectorConstants.REQUEST_HEADER_AUTHORIZATION_BEARER +" "+ accessToken);
if (null != proxyHost) {
return utilsbuilder.baseUrl(tenantDomain).filter(oauth2Credentials(accessToken)).clientConnector(getHttpConnector()).build();
} else {
return utilsbuilder
.baseUrl(tenantDomain)
.filter(oauth2Credentials(accessToken))
.build();
}
}
Now in getWebClientWithAuthorization method, where to put the return keyword inside subscribe or outside subscribe.
Think "Reactive" end to end
In my opinion, what is the most important when star building application using Reactive Programming is treating any call as asynchronous hence providing end to end asynchronous and non-blocking communication.
Thus, what I suggest you is providing instead of synchronous type a Mono<WebClient> in the following way:
public Mono<WebClient> getWebClientWithAuthorization(String t) {
-----
----
Mono<AccessToken> accessToken = authenticationProvider.getUserAccessToken(serviceConnectionDetails, queryParams);
return accessToken.map(value -> getWebClientBuilder(t, value.getAccessToken()))
.doOnError(error -> error.printStackTrace())
.doOnComplete(() -> System.out.println("completed without a value"))
);
}
So, now you may easily map value to the WebClient's instance and send it to the downstream. In turn, your downstream may react to that value and transform WebClient to the execution of HTTP call as it is shown in the following example:
getWebClientWithAuthorization("some magic string here")
.flatMapMany(webClient -> webClient.get()
.uri("some uri here")
.retrieve()
.bodyToFlux(MessageResponse.class))
// operate with downstream items somehow
// .map
// .filter
// .etc
// and return Mono or Flux again to just continue the flow
And remember, just continue the flow and everywhere specify reactive types if async communication is supposed. There is no sense to subscribe to the source until you met some network boundary or some logical end of the stream where you do not have to return something back.