How to return single object from Reactive REST API in Kotlin Coroutine style - spring

I am trying to convert a REST service from the Spring 5 Reactive style to an async Kotlin Coroutine style.
I followed several different guides/tutorials on how this should work but I must be doing something wrong.
I get a compile error trying to turn a single object into a Flow, whereas the guides I'm following dont seem to do this at all.
Any pointers or otherwise very appreciated!
Router:
#Bean
fun mainRouter(handler: EobHandler) = coRouter {
GET("/search", handler::search)
GET("/get", handler::get)
}
Handler:
suspend fun search(request: ServerRequest): ServerResponse {
val eobList = service.search()
return ServerResponse.ok().bodyAndAwait(eobList)
}
suspend fun get(request: ServerRequest): ServerResponse
val eob = service.get()
return ServerResponse.ok().bodyAndAwait(eob); // compile error about bodyAndAwait expecting a Flow<T>
}
Service:
override fun search(): Flow<EOB> {
return listOf(EOB()).asFlow()
}
//
override suspend fun get(): EOB? {
return EOB()
}
If curious, here are some of the guides I've based my code on:
https://www.baeldung.com/spring-boot-kotlin-coroutines
https://docs.spring.io/spring/docs/5.2.0.M1/spring-framework-reference/languages.html#how-reactive-translates-to-coroutines
https://medium.com/#hantsy/using-kotlin-coroutines-with-spring-d2784a300bda

I was able to get this to compile by changing
return ServerResponse.ok().bodyAndAwait(eob);
to
return eob?.let { ServerResponse.ok().bodyValueAndAwait(it) } ?: ServerResponse.notFound().buildAndAwait()
guess it's something to do with type-safety of Kotlin - I was not returning a nullable object I think

Related

Why is my data being interpreted as a readable stream instead of a json object?

I'm building a simple REST API in Kotlin and front-end with React. I'm pretty new to Kotlin and Spring, but I've managed to get a simple service working with Postman. I'm able to return all objects in an SQL table using JpaRepository interface. When I make the call through Postman, my output is as expected (looks like a normal Json object).
However, when I make the call through a standard react fetch the body is produced as a ReadableStream {locked: false}. I did some research on readable streams and they appear to mostly be for images. I tried body.getReader() from https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams, but that did not work either.
Here is my Kotlin code:
import org.springframework.web.bind.annotation.*
#RestController
#RequestMapping("/api/catalog")
class ArtworkController (
private val artworkService: ArtworkService
) {
#CrossOrigin
#GetMapping("")
#ResponseBody
fun getData(): MutableList<Artwork> {
// println(artworkService.getArt().javaClass.kotlin)
return artworkService.getArt()
}
}
#Service
class ArtworkService(
private val artworkRepository: ArtworkRepository
){
fun getArt(): MutableList<Artwork> {
return artworkRepository.findAll()
}
}
These are in separate files - just joined them here for brevity.
Any help would be greatly appreciated!
It turns out I was immediately returning a promise and had to set up an async function to handle the request.
const { isLoading, error, data } = useQuery("repoData",
async () => {
const data = await fetch("https://url_here")
.then(function(response) {
return response.json()
}
)
setData(data)
}
For context, this request is wrapped in react-query but works without it as well.

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 use a mono in another mono that have circular dependency

I have a spring boot reactive app. Where I want to implement to create a user if it doesn't already exists. Like this:
fun userAlreadyExist() = Mono.error<UserDTO>(UsernameAlreadyExistException())
fun create(userDTO: Mono<UserDTO>): Mono<Void> {
return userDTO.filter { userRepository.existsByNameIgnoreCase(it.username).block() == false }
.switchIfEmpty(userAlreadyExist())
.flatMap { createNewUser(it).then() }
What I really dislike is that I need to use .block() inside the filter. Is there a better way to do this?
The big issue is the circular dependency both have, since UserRepository needs to know the username and userDTO stream needs to know wether this already exists which returns a mono.
The logic here looks a bit strange - you can probably do something like:
fun create(userDTO: Mono<UserDTO>): Mono<Void> {
return userDTO.flatMap {
userRepository.findByNameIgnoreCase(it.username)
.flatMap(user -> userAlreadyExist())
.switchIfEmpty(createNewUser(it))
}.then()
The issue was that I used filter() instead what I should have used is filterWhen() which evaluates async Monos.
What I did:
fun create(userDTO: Mono<UserDTO>): Mono<Void> {
return userDTO.filterWhen { innerUserDTO ->
userRepository.existsByNameIgnoreCase(innerUserDTO.username).map { !it }
}
.flatMap { createNewUser(it) }
.switchIfEmpty(userAlreadyExist())
.then()
}

Reactor switchifempty does not behave as expected in junit test

I am writing tests for the method provide below.
`
class ScrapedRecipeCache #Autowired constructor(private val cache: RecipeScrapingCacheService,
private val recipeService: RecipeService) : ScrapedRecipeProvider {
override fun provide(request: ScrapingRequest): Flux<ScrapedRecipe> =
cache.retrieve(request.link)
.doOnNext { println(it) }
.flatMap { (link, _, recipeHash, error) ->
recipeService.findByHash(recipeHash)
.map { ScrapedRecipe(it, link, error)}
.switchIfEmpty(cache.remove(request.link).then(Mono.empty()))
}
.flux()
}
`
The test looks as follows:
private val recipeFetched = Recipe("Tortellini", RecipeDifficulty.EASY, 15.0)
val cacheContents = RecipeScrapingResource("www.google.com", ScrapingOrigin.JAMIE_OLIVER, recipeFetched.hash,
mutableListOf(
pl.goolash.core.Exception("aa", ErrorType.WARNING, LocalDateTime.MIN)
))
val request = ScrapingRequest("www.google.com", ScrapingOrigin.JAMIE_OLIVER, 4)
#BeforeEach
fun setUp() {
given(cache.retrieve("www.google.com")).willReturn(Mono.just(cacheContents))
given(recipeService.findByHash(recipeFetched.hash)).willReturn(Mono.just(recipeFetched))
}
#Test
#DisplayName("Then return data fetched from service and don't delete cache")
fun test() {
cacheFacade.provide(request)
.test()
.expectNext(ScrapedRecipe(recipeFetched, "www.google.com", cacheContents.error!!))
.expectComplete()
.verify()
BDDMockito.verify(cache, BDDMockito.never()).remove(request.link)
}
The test fails because cache.remove(request.link) is called. To my understanding (or from what I managed to gather from documentation) switchIfEmpty, should only be fired when recipeService.findByHash returns Mono.empty(). However the debugger shows that it returns mocked value of Mono.just(fetchedRecipe).
The interesting thing is that when I replace
.switchIfEmpty(cache.remove(request.link).then(Mono.empty()))
with
.switchIfEmpty(Mono.just(1).doOnNext{println("weeee")}.then(Mono.empty()))
Then weee is not printed hence it behaves as expected, that is switchIfEmpty is not fired.
Furthermore the tested issue runs properly in integration test and does not clear the cache.
Reactor version : 3.1.0-RC1
Other notable details: Spring Boot 2.0.0-M4, Mockito-core:2.10, junit 5, project is written in kotlin
The question is, does anybody see anything wrong with this? Because I have spent two days over and still have no clue why this behaves so bizzarely.
Finally I found out how to make this work.
In order to remedy it:
override fun provide(request: ScrapingRequest): Flux<ScrapedRecipe> =
cache.retrieve(request.link)
.flatMap { (link, _, recipeHash, error) ->
recipeService.findByHash(recipeHash)
.map { ScrapedRecipe(it, link, error) }
.switchIfEmpty(Mono.just(1)
.flatMap { cache.remove(request.link) }
.then(Mono.empty()))
}
.flux()
You can see how using flatMap to execute the asynch work does the job, even if this is not the neatest implementation, it revealed to me quite an interesting mechanism hidden here.

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