Always finish Mono.usingWhen on cancel signal - spring-webflux

we write a reactive WebFlux application that basically gets a resource, does amount of work in closure and at the end unlocks the resource with either initial version or updated one from closure.
Ex:
Mono<ProductLock> lock = service.lock()
Mono.usingWhen(lock,
(ProductLock state) -> service.doLongOperationAsync(state),
(ProductLock state) -> service.unlock(state))
ProductLock is a definition of locked product meaning that we can do operations via HTTP API including multiple microservices.
service.doLongOperationAsync() - calls a few HTTP APIs which ARE EXPECTED to ALWAYS be finished once started (failure - is normal, but if operation started - it needs to be finished, because HTTP call cannot be rolled back)
service.unlock() - operation MUST be called only after successful or failed execution of doLongOperationAsync.
In happy scenarios everything works as expected: on success product unlocked, on failure also unlocked.
The problems come when client, who calls our service (SOAP UI, POSTMAN, any real client), drops connection or times out - the cancel signal is generated and getting up till the above code.
At this point, anything within service.doLongOperationAsync is stopped and service.unlock is called on cancel asynchronously.
Question: how can we prevent this from happening.
The requirements are:
once doLongOperationAsync started - it must finish
service.unlock(state) - must be called ONLY after doLongOperationAsync, even on cancel.
Spring Boot repro
MRE:
#Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions.route().GET("/work", request -> processRequest()
.flatMap(res -> ServerResponse.ok().bodyValue(res))).build();
}
private Mono<String> processRequest() {
//Need unlock to execute exactly after doWorkAsync in any case
return Mono.usingWhen(lock(), this::doWorkAsync, this::unlock)
.doOnNext((id) -> System.out.println("Request processed:" + id))
.doOnCancel(() -> System.out.println("Request cancelled"));
}
private Mono<UUID> lock() {
return Mono.defer(() -> Mono.just(UUID.randomUUID())
.doOnNext(id -> System.out.println("Locked:" + id)));
}
//Need this to finish no matter what
private Mono<String> doWorkAsync(UUID lockID) {
return Mono.just(lockID).map(UUID::toString)
.doOnNext(id -> System.out.println("Start working on:" + lockID))
.delayElement(Duration.ofSeconds(10))
.doOnNext(id -> System.out.println("Finished work on:" + id))
// Should never be called
.doOnCancel(() -> System.out.println("Processing cancelled:" + lockID));
}
private Mono<Void> unlock(UUID lockID) {
return Mono.fromRunnable(() -> System.out.println("Unlocking:" + lockID));
}

Apparently what does work for the usecase is using Mono/Flux create with sink. This way it's initiated when subscribed, but still not cancelled even when Cancellation requested:
Mono.create(sink -> {
doWorkAsync().subscribe(sink::next, sink::error, null, sink.currentContext())
})
In case of using, whole usingWhen should be placed inside create

Related

Reactor framework confusion with Assembly time and subscription time (when to call subscribe)

I'm actually confused on assembly time and subscription time. I know mono's are lazy and does not get executed until its subscribed. Below is a method.
public Mono<UserbaseEntityResponse> getUserbaseDetailsForEntityId(String id) {
GroupRequest request = ImmutableGroupRequest
.builder()
.cloudId(id)
.build();
Mono<List<GroupResponse>> response = ussClient.getGroups(request);
List<UserbaseEntityResponse.GroupPrincipal> groups = new CopyOnWriteArrayList<>();
response.flatMapIterable(elem -> elem)
.toIterable().iterator().forEachRemaining(
groupResponse -> {
groupResponse.getResources().iterator().forEachRemaining(
resource -> {
groups.add(ImmutableGroupPrincipal
.builder()
.groupId(resource.getId())
.name(resource.getDisplayName())
.addAllUsers(convertMemebersToUsers(resource))
.build());
}
);
}
);
log.debug("Response Object - " + groups.toString());
ImmutableUserbaseEntityResponse res = ImmutableUserbaseEntityResponse
.builder()
.userbaseId(id)
.addAllGroups(groups)
.build();
Flux<UserbaseEntityResponse.GroupPrincipal> f = Flux.fromIterable(res.getGroups())
.parallel()
.runOn(Schedulers.parallel())
.doOnNext(groupPrincipal -> getResourcesForGroup((ImmutableGroupPrincipal)groupPrincipal, res.getUserbaseId()))
.sequential();
return Mono.just(res);
}
This gets executed Mono<List<GroupResponse>> response = ussClient.getGroups(request); without calling subscribe, however below will not get executed unless I call subscribe on that.
Flux<UserbaseEntityResponse.GroupPrincipal> f = Flux.fromIterable(res.getGroups())
.parallel()
.runOn(Schedulers.parallel())
.doOnNext(groupPrincipal -> getResourcesForGroup((ImmutableGroupPrincipal)groupPrincipal, res.getUserbaseId()))
.sequential();
Can I get some more input on assembly time vs subscription?
"Nothing happens until you subscribe" isn't quite true in all cases. There's three scenarios in which a publisher (Mono or Flux) will be executed:
You subscribe;
You block;
The publisher is "hot".
Note that the above scenarios all apply to an entire reactive chain - i.e. if I subscribe to a publisher, everything upstream (dependent on that publisher) also executes. That's why frameworks can, and should call subscribe when they need to, causing the reactive chain defined in a controller to execute.
In your case it's actually the second of these - you're blocking, which is essentially a "subscribe and wait for the result(s)". Usually the methods that block are clearly labelled, but again that's not always the case - in your case it's the toIterable() method on Flux doing the blocking:
Transform this Flux into a lazy Iterable blocking on Iterator.next() calls.
But ah, you say, I'm not calling Iterator.next() - what gives?!
Well, implicitly you are by calling forEachRemaining():
The default implementation behaves as if:
while (hasNext())
action.accept(next());
...and as per the above rule, since ussClient.getGroups(request) is upstream of this blocking call, it gets executed.

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.

webflux with reactor does not print exception in bodyToMono

I expect either map or doOnError prints the log, but the fact is neither of them does so, making it seem like both fail to run. If bodyToMono throws exception, how to print it?
public Mono<ServerResponse> tryWebflux(ServerRequest request) {
log.info("start controller");
Mono bodyMono = request.bodyToMono(HashMap.class)
.doOnError(e -> log.error("something wrong", e))
.map(body -> {
log.info("in map");
return body;
});
bodyMono.log().subscribe(System.out::println);
return ServerResponse.ok().build();
}
Result:
[reactor-http-nio-2] reactor.util.Loggers$Slf4JLogger: onSubscribe(FluxMap.MapSubscriber)
[reactor-http-nio-2] reactor.util.Loggers$Slf4JLogger: request(unbounded)
[reactor-http-nio-2] reactor.util.Loggers$Slf4JLogger: onComplete()
my guess it is because of this line here:
bodyMono.log().subscribe(System.out::println);
You see in most cases you never subscribe in your application. The one that subscribes is the one whom initiated the call, and in most cases this is the calling client (front end, caller to the api, etc. etc.) Your server is the publisher, and in this server you want to build an event chain that basically does things one after the other.
By subscribing in the middle you are breaking the event chain.
Try:
public Mono<ServerResponse> tryWebflux(ServerRequest request) {
return request.bodyToMono(HashMap.class)
.doOnError(e -> log.error("something wrong", e))
.map(body -> {
log.info("in map");
return body;
}).log() // We log, chaining on as you did before
.then(ServerResponse.ok().build());
// Here we use the then() keyword to still keep chaining on the mono
}
It is very important to never break the event chain. If you see yourself subscribing in the middle, you have probably done something wrong, unless your application is the initiator of something, or the "consumer".

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.