Ktor respondTextWriter complaining about inappropriate blocking method call - kotlin-coroutines

I'm implementing a server using Ktor, and one of the calls is to get a (potentially large) list of users that will come from a database query. I would like to chunk my response, so my user call looks like this:
fun users() =
flow {
emit("A")
emit("B")
emit("C")
}
Where the emit calls will eventually be replaced with data from a result set. The route looks like this:
route("Users") {
get() {
call.respondTextWriter {
users().collect { write(it) }
flush()
}
}
// other calls, e.g. post(), delete(), etc.
However, both calls to write and flush show a warning: "Inappropriate blocking method call". What am I missing?
Thanks.

Eh, a little digging provided the answer:
private suspend fun <T> flowResponse(writer: Writer, flow: Flow<T>) {
val gson = Gson()
flow.collect { data ->
withContext(Dispatchers.IO) {
writer.write(gson.toJson(data))
}
}
withContext(Dispatchers.IO) { writer.flush() }
}
And then:
get() {
call.respondTextWriter {
flowResponse(this, Users.all())
}
}
Slight change from the question, as my Users.all() call returns a User object that then gets converted to JSON. Perhaps there is a way to simply stream the objects and let the content negotiator do the translation, but I haven't gotten there yet.

Related

Spring webflux with multiple sequential API call and convert to flux object without subscribe and block

I am working on spring reactive and need to call multiple calls sequentially to other REST API using webclient. The issue is I am able to call multiple calls to other Rest API but response am not able to read without subscribe or block. I can't use subscribe or block due to non reactive programming. Is there any way, i can merge while reading the response and send it as flux.
Below is the piece of code where I am stuck.
private Flux<SeasonsDto> getSeasonsInfo(List<HuntsSeasonsMapping> l2, String seasonsUrl) {
for (HuntsSeasonsMapping s : l2)
{
List<SeasonsJsonDto> list = huntsSeasonsProcessor.appendSeaosonToJson(s.getSeasonsRef());
for (SeasonsJsonDto sjdto:list)
{
Mono<SeasonsDto> mono =new SeasonsAdapter("http://localhost:8087/").callToSeasonsAPI(sjdto.getSeasonsRef());
//Not able to read stream without subscribe an return as Flux object
}
public Mono<SeasonsDto> callToSeasonsAPI(Long long1) {
LOGGER.debug("Seasons API call");
return this.webClient.get().uri("hunts/seasonsInfo/"
+long1).header("X-GoHunt-LoggedIn-User",
"a4d4b427-c716-458b-9bb5-9917b6aa30ff").retrieve().bodyToMono(SeasonsDto.class);
}
Please help to resolve this.
You need to combine the reactive streams using operators such as map, flatMap and concatMap.
private Flux<SeasonsDto> getSeasonsInfo(List<HuntsSeasonsMapping> l2, String seasonsUrl) {
List<Mono<SeasonsDto>> monos = new ArrayList<>();
for (HuntsSeasonsMapping s : l2) {
List<SeasonsJsonDto> list = huntsSeasonsProcessor.appendSeaosonToJson(s.getSeasonsRef());
for (SeasonsJsonDto sjdto:list) {
Mono<SeasonsDto> mono =new SeasonsAdapter("http://localhost:8087/").callToSeasonsAPI(sjdto.getSeasonsRef());
//Not able to read stream without subscribe an return as Flux object
monos.add(mono);
}
}
return Flux.fromIterable(monos).concatMap(mono -> mono);
}
This can further be improved using the steam API, which I suggest you look into, but I didn't want to change too much of your existing code.
I have figured how to do this. I have completely rewrite the code and change in reactive. It means all the for loop has been removed. Below is the code for the same and may be help for others.
public Flux<SeasonsDto> getAllSeasonDetails(String uuid) {
return hunterRepository.findByUuidAndIsPrimaryAndDeleted(uuid, true, false).next().flatMapMany(h1 -> {
return huntsMappingRepository.findByHunterIdAndDeleted(h1.getId(), false).flatMap(k -> {
return huntsMappingRepository.findByHuntReferrenceIdAndDeleted(k.getHuntReferrenceId(), false)
.flatMap(l2 -> {
return huntsSeasonsProcessor.appendSeaosonToJsonFlux(l2.getSeasonsDtl()).flatMap(fs -> {
return seasonsAdapter.callSeasonsAPI(fs.getSeasonsRef(), h1.getId(), uuid).map(k->{
return k;
});
});
});
});
});
}

Returning values into GlobalScope launch using Spring

I have an endpoint exposed, that is launching a coroutine:
val apiCall = ApiCall()
#GetMapping("/example")
fun example(#RequestParam paramExample:String):Int{
GlobalScope.launch{
return apiCall.callApi(paramExample)
}
}
This function is calling another external API, using Retrofit:
suspend fun callApi(param:String):Int{
var tot_records =0
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(appProperties.sampleUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create<ResponseService>(ResponseService::class.java)
service.getResponse().enqueue(object : Callback<Response> {
override fun onFailure(call: Call<Response>, throwable: Throwable) {
println("Error")
println(throwable.stackTrace)
}
override fun onResponse(call: Call<Response>, response: Response<Response>) {
println("OK")
println(response.body())
println("Tot records")
tot_records = response.body()?.tot_records!!
}
})
return tot_records
}
The problem is that I can't launch this, the error is: 'return' is not allowed here
Any idea how to fix it and whats is happening?
Thanks for your help
It seems like you can't decide if you want your code to be synchronous (so code waits for its subtasks to finish before continuing) or asynchronous (it launches operations in the background). You intend to return a result from example(), so you need it to be synchronous, but you immediately use launch() to invoke callApi() asynchronously. The same in callApi() - you intend to return from it (so synchronous), but you invoke Retrofit using callbacks (so asynchronous). Note that callApi() has exactly the same problem as example(). Even if it compiles, it still does not really work properly. It always returns 0, because tot_records is returned before being set.
You have to decide between asynchronous and synchronous and stick to it. If you want to go fully asynchronous, then you need to redesign both callApi() and example() to return their results either with callbacks or futures.
However, I suggest going fully synchronous, utilizing Kotlin suspend functions. Make all functions suspend: example(), callApi() (it is already) and ResponseService.getResponse(). The last one will look something like:
suspend fun getResponse(): Response
Then remove GlobalScope.launch(), and almost everything inside enqueue(). Instead, service.getResponse() will return Response object directly, so you can just return its tot_records property.
Also note that in your original code you ignored failures. After above change service.getResponse() will throw exceptions on failures, so you have to handle them.
This solution seems that works:
This is the endpoint declaration:
#GetMapping("/example")
suspend fun example(#RequestParam param:String):CustomResponse{
return coroutineScope {
val job = async{apiCall.callApi(param)}
job.await()
}
}
And this is my function that is calling an external API:
suspend fun callApi(param:String):CustomResponse{
var responseCustom = CustomResponse()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(appProperties.reservationUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create<CustomResponseService>(CustomResponseService::class.java)
responseCustom = service.getResponse(appProperties.token, param).execute().body()!!
return responseCustom
}

How to handle empty event in Spring reactor

Well, this sounds counter-intuitive to what reactive programming is, but I am unable to comprehend a way to handle nulls/exceptions.
private static class Data {
public Mono<String> first() {
return Mono.just("first");
}
public Mono<String> second() {
return Mono.just("second");
}
public Mono<String> empty() {
return Mono.empty();
}
}
I understand that fundamentally unless a publisher publishes an event, a subscriber will not act. So a code like this would work.
Data data = new Data();
data.first()
.subscribe(string -> Assertions.assertThat(string).isEqualTo("first"));
And if the first call returns empty, I can do this.
Data data = new Data();
data.empty()
.switchIfEmpty(data.second())
.subscribe(string -> Assertions.assertThat(string).isEqualTo("second"));
But how do I handle a case when both the calls return empty (typically this is an exception scenario that would need to be propagated to the user).
Data data = new Data();
data.empty()
.switchIfEmpty(data.empty())
.handle((string, sink) -> Objects.requireNonNull(string))
.block();
The handle is not called in the above example since no event was published.
as JB Nizet pointed out, you can chain in a second switchIfEmpty with a Mono.error.
Or, if you're fine with a NoSuchElementException, you could chain in single(). It enforces a strong contract of exactly one element, otherwise propagating that standard exception.

Spring Web-Flux: How to return a Flux to a web client on request?

We are working with spring boot 2.0.0.BUILD_SNAPSHOT and spring boot webflux 5.0.0 and currently we cant transfer a flux to a client on request.
Currently I am creating the flux from an iterator:
public Flux<ItemIgnite> getAllFlux() {
Iterator<Cache.Entry<String, ItemIgnite>> iterator = this.getAllIterator();
return Flux.create(flux -> {
while(iterator.hasNext()) {
flux.next(iterator.next().getValue());
}
});
}
And on request I am simply doing:
#RequestMapping(value="/all", method=RequestMethod.GET, produces="application/json")
public Flux<ItemIgnite> getAllFlux() {
return this.provider.getAllFlux();
}
When I now locally call localhost:8080/all after 10 seconds I get a 503 status code. Also as at client when I request /all using the WebClient:
public Flux<ItemIgnite> getAllPoducts(){
WebClient webClient = WebClient.create("http://localhost:8080");
Flux<ItemIgnite> f = webClient.get().uri("/all").accept(MediaType.ALL).exchange().flatMapMany(cr -> cr.bodyToFlux(ItemIgnite.class));
f.subscribe(System.out::println);
return f;
}
Nothing happens. No data is transferred.
When I do the following instead:
public Flux<List<ItemIgnite>> getAllFluxMono() {
return Flux.just(this.getAllList());
}
and
#RequestMapping(value="/allMono", method=RequestMethod.GET, produces="application/json")
public Flux<List<ItemIgnite>> getAllFluxMono() {
return this.provider.getAllFluxMono();
}
It is working. I guess its because all data is already finished loading and just transferred to the client as it usually would transfer data without using a flux.
What do I have to change to get the flux streaming the data to the web client which requests those data?
EDIT
I have data inside an ignite cache. So my getAllIterator is loading the data from the ignite cache:
public Iterator<Cache.Entry<String, ItemIgnite>> getAllIterator() {
return this.igniteCache.iterator();
}
EDIT
adding flux.complete() like #Simon Baslé suggested:
public Flux<ItemIgnite> getAllFlux() {
Iterator<Cache.Entry<String, ItemIgnite>> iterator = this.getAllIterator();
return Flux.create(flux -> {
while(iterator.hasNext()) {
flux.next(iterator.next().getValue());
}
flux.complete(); // see here
});
}
Solves the 503 problem in the browser. But it does not solve the problem with the WebClient. There is still no data transferred.
EDIT 3
using publishOn with Schedulers.parallel():
public Flux<ItemIgnite> getAllFlux() {
Iterator<Cache.Entry<String, ItemIgnite>> iterator = this.getAllIterator();
return Flux.<ItemIgnite>create(flux -> {
while(iterator.hasNext()) {
flux.next(iterator.next().getValue());
}
flux.complete();
}).publishOn(Schedulers.parallel());
}
Does not change the result.
Here I post you what the WebClient receives:
value :[Item ID: null, Product Name: null, Product Group: null]
complete
So it seems like he is getting One item (out of over 35.000) and the values are null and he is finishing after.
One thing that jumps out is that you never call flux.complete() in your create.
But there's actually a factory operator that is tailored to transform an Iterable to a Flux, so you could just do Flux.fromIterable(this)
Edit: in case your Iterator is hiding complexity like a DB request (or any blocking I/O), be advised this spells trouble: anything blocking in a reactive chain, if not isolated on a dedicated execution context using publishOn, has the potential to block not only the entire chain but other reactive processes has well (as threads can and will be used by multiple reactive processes).
Neither create nor fromIterable do anything in particular to protect from blocking sources. I think you are facing that kind of issue, judging from the hang you get with the WebClient.
The problem was my Object ItemIgnite which I transfer. The system Flux seems not to be able to handle this. Because If I change my original code to the following:
public Flux<String> getAllFlux() {
Iterator<Cache.Entry<String, ItemIgnite>> iterator = this.getAllIterator();
return Flux.create(flux -> {
while(iterator.hasNext()) {
flux.next(iterator.next().getValue().toString());
}
});
}
Everything is working fine. Without publishOn and without flux.complete(). Maybe someone has an idea why this is working.

In Kotlin, how do I integrate a Kovenant promise with Elasticsearch async responses?

I use Kovenant in my Kotlin application, and I'm calling Elasticsearch which has its own async API. I would rather use promises but the best I can come up with is something like:
task {
esClient.prepareSearch("index123")
.setQuery(QueryBuilders.matchAllQuery())
.execute().actionGet()
} then {
...
} success {
...
} fail {
...
}
Which makes an Kovenant async task thread, then Elasticsearch uses a thread from its pool, and then actionGet() synchronously blocks Elasticsearch to get back a result. It seems silly to spawn new threads while blocking others. Is there an approach to integrate the thread dispatching more closely?
Note: this question is intentionally written and answered by the author (Self-Answered Questions), so that solutions for interesting problems are shared in SO.
You can use the Kovenant Deferred class to create a promise without dispatching via an async task as you did in your sample. The model is basically:
create a deferred instance
hook up to the async handlers and resolve or reject the deferred based on async callbacks
return the deferred.promise to the caller
In code, this would look like:
fun doSearch(): Promise<SearchResponse, Throwable> {
val deferred = deferred<Response, Throwable>()
esClient.prepareSearch("index")
.setQuery(QueryBuilders.matchAllQuery())
.execute(object: ActionListener<T> {
override fun onResponse(response: T) {
deferred.resolve(response)
}
override fun onFailure(e: Throwable) {
deferred.reject(e)
})
return deferred.promise
}
A re-usable way to do this is to first create an adapter that can just adapt Elasticsearch's desire for an ActionListener to work generically work with a promise:
fun <T: Any> promiseResult(deferred: Deferred<T, Exception>): ActionListener<T> {
return object: ActionListener<T> {
override fun onResponse(response: T) {
deferred.resolve(response)
}
override fun onFailure(e: Throwable) {
deferred.reject(wrapThrowable(e))
}
}
}
class WrappedThrowableException(cause: Throwable): Exception(cause.message, cause)
fun wrapThrowable(rawEx: Throwable): Exception = if (rawEx is Exception) rawEx else WrappedThrowableException(rawEx)
Note: the wrapThrowable() method is there to change a Throwable into an Exception because current versions (3.3.0) of Kovenant have some methods that expect the rejection type of the promise to descend from Exception (for example bind()) and you can stay with Throwable if you use unwrap() instead for nested promises.
Now use this adapter function to generically extend Elasticsearch ActionRequestBuilder which is pretty much the only thing you ever will call execute() on; creating a new promise() extension function:
fun <Request: ActionRequest<*>, Response: ActionResponse, RequestBuilder: ActionRequestBuilder<*, *, *, *>, Client: ElasticsearchClient<*>>
ActionRequestBuilder<Request, Response, RequestBuilder, Client>.promise(): Promise<Response, Exception> {
val deferred = deferred<Response, Exception>()
this.execute(promiseResult(deferred))
return deferred.promise
}
Now you can call promise() instead of execute():
esClient.prepareSearch("index")
.setQuery(QueryBuilders.matchAllQuery())
.promise()
And start chaining your promises...
esClient.admin().indices().prepareCreate("index1").setSettings("...").promise()
.bind {
esClient.admin().cluster().prepareHealth()
.setWaitForGreenStatus()
.promise()
} bind {
esClient.prepareIndex("index1", "type1")
.setSource(...)
.promise()
} bind {
esClient.prepareSearch("index1")
.setQuery(QueryBuilders.matchAllQuery())
.promise()
} then { searchResults ->
// ... use searchResults
}.success {
// ...
}.fail {
// ...
}
}
You should be familiar with bind() and unwrap() when you have nested promises you want to chain without nesting deeper. You can use unwrap().then in place of bind in the above cases if you did not want to include kovenant-functional.
Every call you have in Elasticsearch will be able to use promise() instead of execute() due to the consistent nature of all request objects in the Elasticsearch client.

Resources