Webflux Webclient re-try with different URL - java

I am using webclient for the rest call and what i need is, if the primary URL is failing for the n'th time do the next re-try on Secondary URL . Please find below sample code for the logic which i am using. But it seems we cannot change the URL once he client is created and even if i change the URL its not getting effected and still requests are been fired to the initial URL.
ClientHttpConnector connector;//initiate
WebClient webClient = WebClient.builder().clientConnector(connector).build();
WebClient.RequestBodyUriSpec client = webClient.post();
client.uri("http://primaryUrl/").body(BodyInserters.fromObject("hi")).retrieve().bodyToMono(String.class).retryWhen(Retry.anyOf(Exception.class)
.exponentialBackoff(Duration.ofSeconds(2), Duration.ofSeconds(10)).doOnRetry(x ->
{
if (x.iteration() == 2) {
client.uri("http://fail_over_url/");//this does not work
}
})
.retryMax(2)).subscribe(WebClientTest::logCompletion, WebClientTest::handleError);
Is there any way to change the URL at the middle of re-try cycle ?

But it seems we cannot change the URL once he client is created
You cannot - it's immutable.
even if i change the URL its not getting effected
You're not actually changing the URL. Take a look at the uri() method - it's returning a new instance with a URI set. Since you're not doing anything with that new instance, nothing happens (as expected.)
The route I'd probably suggest is to create a separate method to form & return your basic WebClient publisher:
private Mono<String> fromUrl(String url) {
return WebClient.builder().clientConnector(connector).build()
.post()
.body(BodyInserters.fromValue("hi"))
.uri(url)
.retrieve()
.bodyToMono(String.class);
}
...and then do something like:
fromUrl("https://httpstat.us/400").retryWhen(Retry.backoff(2, Duration.ofSeconds(1)))
.onErrorResume(t -> Exceptions.isRetryExhausted(t), t -> fromUrl("https://httpstat.us/500").retryWhen(Retry.backoff(5, Duration.ofSeconds(1))))
.onErrorResume(t -> Exceptions.isRetryExhausted(t), t -> fromUrl("https://httpstat.us/200").retryWhen(Retry.backoff(7, Duration.ofSeconds(1))))
...which will try /400 3 times, then try /500 5 times, then /200 up to 7 times (but unless it's down, that will of course return on the first try.)
Note that the above example uses the latest version of reactor-core which has the retry functionality built in, rather than the retry functionality in reactor addons. Translating it to the reactor addons functionality should be reasonably straightforward.
This doesn't not strictly changing the URL in the same retry cycle, but instead chaining requests together with configurable retries per request. This then allows you to set different retry strategies on different URLs, which is advantageous if you don't necessarily want the retry to "carry on" from its previous point (It could make sense to set the backoff back to one second for a fresh URL, for example.)

Related

logging an encoded (Bcrypt) password Mono<String> with org.slf4j.Logger from a db call

I'm trying to figure out how to log a Mono<String> password with slf4j but it would always return a Monotype.
logger.info(login.getPassword()+" "+userRepository.findPasswordByUsername(login.getUsername()));
and
logger.info(login.getPassword()+" "+userRepository.findPasswordByUsername(login.getUsername()).toString());
the first 2 logging tries above
return the literal password (from the request) and MonoNext
and ofc you cant use .block()
which just throws
"block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3"
yes i'm aware that i can pass a Subscriber / Consumer for onNext() like:
.subscribe(x -> System.out.println( x.toString()))
to get some output but how would i do that with a logger only, is there even a way ?
login represents a user retrieved from a request.
The password is properly stored and encoded (Bycrypt) beforehand ofc (doesn't seem to be the issue).
Edit: to give more context
userRepository.findPasswordByUsername("username")
will return a Mono which i want to compare to another password as in:
passwordEncoder.matches( "userinputPW", userRepository.findPasswordByUsername("username") )
which is how you use a ByCryptEncoder in Spring and i can't .map() the Mono to a String (will obviously always return an Object and not a String as explained here https://stackoverflow.com/a/47180610/6414816 )
Using spring-boot-starter-webflux, spring-boot-starter-data-r2dbc, spring-boot-starter-security
What am i missing ? Thanks in Advance.
that is correct you should not block in a reactive application, neither should you subscribe in this usercase, as your application is most likely a producer, and the calling client is the consumer that subscribes.
what you are looking for is the doOn operators, that handles side effects. Logging is a side effect, its something you want to do on the side without disturbing the current flow. For instance update something, increment something, or in your case write to a log.
what you want os probably the doOnSuccess operator
example (i have not chacked against a compiler since im on mobile), but something like this.
function Mono<Login> foobar(Login login) {
return userRepository.findPasswordByUsername(login.getUsername)
.doOnSuccess(pwd -> {
logger.info(login.getPassword() + " " + pwd);
}).thenReturn(login);
}
Since nobody came up with an answer yet,
logger.info("userName :" +userMono.map(user -> user.getUsername()).share().block());
.share() will allow to .block()
from the docs:
"Prepare a Mono which shares this Mono result similar to Flux.shareNext().This will effectively turn this Mono into a hot task when the first Subscriber subscribes using subscribe() API. Further Subscriber will share the same Subscription and therefore the same result.It's worth noting this is an un-cancellable Subscription."
works for me (even though it incorporates .block() )
userMono.subscribe(user -> logger.info("username: "+user.getUsername()));
will return a Disposable
Both will output the String value for the logger.
on a sidenote (which i wasn't aware of) there is an operator .log("callerForADefinedLogger").

Retrofit Kotlin - make an API request followed by two more concurrent ones

I want to make an api request, then I need to make two more requests after I receive the data. I found a great SO answer that uses rxjava2 to make two concurrent requests here:
How to make multiple request and wait until data is come from all the requests in retrofit 2.0 - android
I suppose I could just chain the logic for this after the first request, but my intuition tells me thats a bad idea because I'd be duplicating some code (I'd have separate logic for the first request in a function, then some similar logic for the second two requests in a function)
Is there a better way to accomplish this? I'd prefer Kotlin, but Java is ok.
Here is the code for concurrent requests from the SO answer.
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
val backendApi = retrofit.create(MyBackendAPI::class.java)
val requests = ArrayList<Observable<*>>()
requests.add(backendApi.getUser())
requests.add(backendApi.listPhotos())
requests.add(backendApi.listFriends())
Observable
.zip(requests) {
// do something with those results and emit new event
Any() // <-- Here we emit just new empty Object(), but you can emit anything
}
// Will be triggered if all requests will end successfully (4xx and 5xx also are successful requests too)
.subscribe({
//Do something on successful completion of all requests
}) {
//Do something on error completion of requests
}
Thanks

Spring Webflux endpoint working as a topic

I have an Flux endpoint that I provide to clients (subscribers) to receive updated prices. I'm testing it accessing the URL (http://localhost:8080/prices) though the browser and it works fine. The problem I'm facing (I'm maybe missing some concepts here) is when I open this URL in many browsers and I expect to receive the notification in all of them, but just one receives. It is working as a queue instead of a topic (like in message Brokers). Is that correct behavior?
#GetMapping(value = "prices", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<Collection<Price>>> prices() {
return Flux.interval(Duration.ofSeconds(5))
.map(sec -> pricesQueue.get())
.filter(prices -> !prices.isEmpty())
.map(prices -> ServerSentEvent.<Collection<Price>> builder()
.event("status-changed")
.data(prices)
.build());
}
get isn't a standard queue operation, but this is almost certainly because your pricesQueue.get() method isn't idempotent. With every request (with every browser window you open in this case), you'll get a new flux that calls pricesQueue.get() every 5 seconds. Now if pricesQueue.get() just retrieves the latest item in the queue and does nothing with it, all is good - all your subscribers receive the same item, and the same item is displayed. But if it acts more like a poll() where it removes the item in the queue after it's retrieved it, then only the first flux will get that value - the rest won't, as by that point it will have been removed.
You've really two main options here:
Change your get() implementation (or implement a new method) so that it doesn't mutate the queue, only retrieves a value.
Turn the flux into a hot flux. Store Flux.interval(Duration.ofSeconds(5)).map(sec -> pricesQueue.get()).publish().autoConnect() somewhere as a field (let's say as queueFlux), then just return queueFlux.filter(prices -> !prices.isEmpty()).map(...) in your controller method.

Vertx.io GET silently fails

I'm writing a POC using vertx, looking for alternatives when we have to migrate Spring Web from 4.x to 5 to be java 9 compliant.
I've written a simple client, just a GET towards a publicly available server just to get something working but it silently fails me.
public List<String> pull() {
Vertx vertx = Vertx.vertx();
HttpClientOptions options = new HttpClientOptions().setLogActivity(true);
HttpClient hc = vertx.createHttpClient(options);
hc.getNow(80, "http://sunet.se", "/",r -> {
System.out.println("\n****** Handler called! ***\n");
});
return new ArrayList<>();
}
This will silently fail and I cannot understand why.
As far as I can tell, I do exactly as in the examples given in the docs.
In desperation I fired up wire shark and according to WS, there is no actual call (when I use the browser WS captures that). So, it seems my call is never actually done. I don't get any exceptions or anything. Setting the log level to debug gives nothing noteworthy other than
Failed to get SOMAXCONN from sysctl and file /proc/sys/net/core/somaxconn. Default: 128
And that should not fail the call.
I've also tried using vertx.io WebClient but that fails also, in the same manner.
UPDATE
I've managed to get it to work but with a caveat.
As #tsegismont states in his answer, the protocol part of the URI shouldn't be there, that was not in the examples, I just missed it myself.
I ran my example as a stand-alone and then it worked.
My original example was run as a junit test (it's an easy way to test code and I usually try to write the test code first) and when it's run as a junit test it still doesn't work. Why that is, I have no idea. I would greatly appreciate if someone could tell me how to get that to work.
The getNow variant you use expects the server host, not a URL. It should be:
hc.getNow(80, "sunet.se", "/",r -> {
System.out.println("\n****** Handler called! ***\n");
}
If you found a snippet like this in the Vert.x docs it's a bug. Would you mind to report it?
Now a few comments.
1/ The HttpClient is a low-level client.
Most users should prefer the Vert.x Web Client
Here's an example for your use case:
WebClient client = WebClient.create(vertx);
client
.get(80, "sunet.se", "/")
.send(ar -> {
if (ar.succeeded()) {
// Obtain response
HttpResponse<Buffer> response = ar.result();
System.out.println("Received response with status code" + response.statusCode());
} else {
System.out.println("Something went wrong " + ar.cause().getMessage());
}
});
2/ Create a single Vert.x and WebClient instance
Do not create a Vert.x and WebClient instance on every method call.
It wastes resources and is inefficient.

How to elegantly recover from network failure & repeat request to different endpoint?

I have a few servers that I make REST requests to from my program. They will all have the same response to a particular request.
I accept one ip as argument and make my requests to that server. If I wish to now accept a List<ip>, how do I elegantly switch to the next server in the list when one fails? Looping through the list on every network call seems silly.
Unfortunately I cannot make a REST call with the catch-repeat_to_next_server extracted to one function that accepts an HttpClient with the rest of the request because I'm using a 3rd part SDK to talk to the servers and every request is a chain of method calls.
I can't do this (pseudo-code):
def doRequest(HttpClient client)
for ip in list_of_ips:
try:
client.host = ip
return client.execute()
catch exp:
// move failed ip to end of list or something
throw "None of them worked"
HttpClient c
c.method = "GET /api/employees"
doRequest(c)
c.method = "GET /api/department/:id"
doRequest(c)
Are there any standard ways to solve this in clean way?
I'm using Java and Spring so I am constrained by static typing but may have some sort of Spring annotation magic that I can use that I am not aware of.
How you are making the REST call is not important.
Your psuedo code should be correct even if you need to make a million method calls per REST call.
for ip in ip_list
do_stuff_to_make_the_rest_call
perhaps_note_the_ip_that_was_used
indicate_success
break_out_of_for_loop
catch some_exception
perhaps_note_the_ip_that_failed
end for
if !success
do all_ip_failed_stuff.
procedure do_stuff_to_make_the_rest_call
make a million method calls to get one REST call attempt.

Categories