Spring WebClient: Retry with WebFlux.fn + reactor-addons - spring

I'm trying to add a conditional Retry for WebClient with Kotlin Coroutines + WebFlux.fn + reactor-addons:
suspend fun ClientResponse.asResponse(): ServerResponse =
status(statusCode())
.headers { headerConsumer -> headerConsumer.addAll(headers().asHttpHeaders()) }
.body(bodyToMono(DataBuffer::class.java), DataBuffer::class.java)
.retryWhen {
Retry.onlyIf { ctx: RetryContext<Throwable> -> (ctx.exception() as? WebClientResponseException)?.statusCode in retryableErrorCodes }
.exponentialBackoff(ofSeconds(1), ofSeconds(5))
.retryMax(3)
.doOnRetry { log.error("Retry for {}", it.exception()) }
)
.awaitSingle()
also adding a condition before the retry
if (statusCode().isError) {
body(
BodyInserters.fromPublisher(
Mono.error(StatusCodeError(status = statusCode())),
StatusCodeException::class.java
)
)
} else {
body(bodyToMono(DataBuffer::class.java), DataBuffer::class.java)
}
Call looks like:
suspend fun request(): ServerResponse =
webClient/*...*/
.awaitExchange()
.asResponse()

This spring webclient: retry with backoff on specific error gave me the hint to answer the question:
.awaitExchange() returns the ClientResponse and not Mono<ClientReponse>
This means my retry was acting on bodyToMono instead of the operation of exchange().
The solution now looks like
suspend fun Mono<ClientResponse>.asResponse(): ServerResponse =
flatMap {
if (it.statusCode().isError) {
Mono.error(StatusCodeException(status = it.statusCode()))
} else {
it.asResponse()
}
}.retryWhen(
Retry.onlyIf { ctx: RetryContext<Throwable> ->
(ctx.exception() as? StatusCodeException)?.shouldRetry() ?: false
}
.exponentialBackoff(ofSeconds(1), ofSeconds(5))
.retryMax(3)
.doOnRetry { log.error { it.exception() } }
).awaitSingle()
private fun ClientResponse.asResponse(): Mono<ServerResponse> =
status(statusCode())
.headers { headerConsumer -> headerConsumer.addAll(headers().asHttpHeaders()) }
.body(bodyToMono(DataBuffer::class.java), DataBuffer::class.java)

Related

Kotlin: High-Performance concurrent file I/O best-practices?

What's the most performant way in Kotlin to allow concurrent file I/O in multi-reader, single-writer fashion?
I have the below, but I'm unsure how much overhead is being created by the coroutine facilities:
AsyncFileChannel, with extension functions to use it in a suspend context
Taken from example here: https://github.com/Kotlin/coroutines-examples/blob/master/examples/io/io.kt
DiskManager class, that uses a custom ReadWriteMutex
Searching for examples of this doesn't turn up much (try searching Github for ReadWriteMutex, there are a tiny handful of Kotlin repos implementing this).
class DiskManagerImpl(file: File) : DiskManager {
private val mutex = ReadWriteMutexImpl()
private val channel = AsynchronousFileChannel.open(file.toPath(),
StandardOpenOption.READ, StandardOpenOption.WRITE)
override suspend fun readPage(pageId: PageId, buffer: MemorySegment) = withContext(Dispatchers.IO) {
mutex.withReadLock {
val offset = pageId * PAGE_SIZE
val bytesRead = channel.readAsync(buffer.asByteBuffer(), offset.toLong())
require(bytesRead == PAGE_SIZE) { "Failed to read page $pageId" }
}
}
override suspend fun writePage(pageId: PageId, buffer: MemorySegment) = withContext(Dispatchers.IO) {
mutex.withWriteLock {
val offset = pageId * PAGE_SIZE
val bytesWritten = channel.writeAsync(buffer.asByteBuffer(), offset.toLong())
require(bytesWritten == PAGE_SIZE) { "Failed to write page $pageId" }
}
}
}
class ReadWriteMutexImpl : ReadWriteMutex {
private val read = Mutex()
private val write = Mutex()
private val readers = atomic(0)
override suspend fun lockRead() {
if (readers.getAndIncrement() == 0) {
read.lock()
}
}
override fun unlockRead() {
if (readers.decrementAndGet() == 0) {
read.unlock()
}
}
override suspend fun lockWrite() {
read.lock()
write.lock()
}
override fun unlockWrite() {
write.unlock()
read.unlock()
}
}
suspend inline fun <T> ReadWriteMutex.withReadLock(block: () -> T): T {
lockRead()
return try {
block()
} finally {
unlockRead()
}
}
suspend inline fun <T> ReadWriteMutex.withWriteLock(block: () -> T): T {
lockWrite()
return try {
block()
} finally {
unlockWrite()
}
}

how to cancel the coroutines jobs after timeout

Having a suspend function fetchData(). What it does is to launch a few jobs in the withContext, so that it will only return after the jobs are complete (which are: suspend fun getData(): Boolean).
And also want if it times out then return false from the function.
The problem is when it times out, with withTimeoutOrNull(500) { jobs.joinAll() }, it stuck in the function not exit.
The log shows it times out, also clearly points to last line of the code before exit the function:
E/+++: +++ in fetchData() after null = withTimeoutOrNull(500), jobs.sizs: 3
E/+++: +++ --- exit fetchData(), allFresh: false
But the caller of the fetchData() gets stuck and not return from the fetchData().
This is the caller:
suspend fun caller() {
var allGood = fetchData()
// never return to here
Log.e("+++", "+++ caller(), after allGood: $allGood = fetchData()")
...
}
Below is the code, how to cancel the jobs if timeout?
suspend fun fetchData(): Boolean = withContext(Dispatchers.IO) {
var allFresh = requestHandlertMap.size > 0
if (!allFresh) {
allFresh
} else {
val handlers = requestHandlertMap.values.toList()
val jobs: List<Deferred<Boolean>> = handlers.map {handler->
async(start = CoroutineStart.LAZY) {
if (isActive) handler.getData() else true
.also {
Log.e("+++", "+++ in fetchData():async{} after handler.getData()")
}
}
}
val result = withTimeoutOrNull(500) { jobs.joinAll() }
Log.e("+++", "+++ in fetchData() after $result = withTimeoutOrNull(500), jobs.size: ${jobs.size} ")
if (result != null) {
allFresh = jobs.all { deferred ->
deferred.await()
}
Log.e("+++", "+++ +++ +++ in fetchData() call onDataReady(), allFresh: $allFresh = deferred.await() ")
onDataReady()
} else {
// how to cancel the jobs ???
//jobs.all { deferred ->
//deferred.cancelChildren()
//}
allFresh = false
}
allFresh
.also {
Log.e("+++", "+++ --- exit fetchData(), allFresh: $allFresh ")
}
}
}
After some reading/trying, it seems having a few issues with the implementation.
Somehow the CoroutineStart.LAZY causes a strange behavior, that the async(start = CoroutineStart.LAZY)
start sequentially (expecting they should start to be concurrently), so that when it times out it stuck in the function (guess because it is wrapped in the withContext(Dispatchers.IO) and not all child coroutines are completed -- if there is someone not start yet).
Remove the start = CoroutineStart.LAZY makes it returning from the fun fetchData()
val jobs: List<Deferred<Boolean>> = handlers.map {handler->
async(start = CoroutineStart.LAZY) {
if (isActive) handler.getData() else true
.also {
Log.e("+++", "+++ in fetchData():async{} after handler.getData()")
}
}
}
the suspend fun getData(): Boolean was not implemented cooperate to be cancellable, which may causes it still stay in the function until all children are completed although timeout has already happened.
seems it still need to call the deferred.cancelChildren(), otherwise they are not cancelled by the withTimeoutNotNull(), not sure why, isnt it supposed to cancel the jobs automatically?
the change made
private suspend fun fetchData(): Boolean {
var allFresh: Boolean? = requestHandlertMap.size > 0
if (allFresh == true) {
val handlers = requestHandlertMap.values.toList()
val jobs: List<Deferred<Boolean>> = handlers.map {
serviceScope.async(start = CoroutineStart.DEFAULT) { handler -> if (isActive) handler.getData() else false }
}
allFresh = withTimeoutOrNull(3000) {
try {
jobs.awaitAll().all { it }
} catch (ex: Throwable) {
false
}
}
if (allFresh != null) {
onDataReady()
} else {
jobs.map { deferred -> deferred.cancelChildren() }
}
}
return allFresh == true // allFresh = {null, true, false}
}
ref: here and here

Kotlin Spring Reactive Webflux - Handle WebClient Error

I am having troubles trying to handle different errors from calling Spring webflux's web client.
Below is my current code.
return request
.bodyToMono(InputMessage::class.java)
.flatMap { inputMessage ->
client
.get()
.uri { builder ->
builder.path("/message")
.queryParam("message", inputMessage.message)
.build()
}
.retrieve()
.onStatus({t: HttpStatus -> t.is5xxServerError}, {c: ClientResponse -> Mono.error(Throwable("Internal Server Error - try again later"))})
.bodyToMono(ListOfAddresses::class.java)
}
.flatMap { s -> ServerResponse.ok().syncBody(s) }
If it errors, it is still returning the full error message from the client's call.
I tried something else, like this
return request
.bodyToMono(InputMessage::class.java)
.flatMap { inputMessage ->
client
.get()
.uri { builder ->
builder.path("/message")
.queryParam("message", inputMessage.message)
.build()
}
.retrieve()
.onStatus({t: HttpStatus -> t.is5xxServerError}, {c: ClientResponse -> Mono.error(Throwable("Internal Server Error - try again later"))})
.bodyToMono(ListOfAddresses::class.java)
}
.flatMap { s -> ServerResponse.ok().syncBody(s) }
.onErrorResume { e -> Mono.just("Error " + e.message)
.flatMap { s -> ServerResponse.ok().syncBody(s) } }
It actually works but then I want to handle different Http status codes error (different messages for each Http status code).
How can I modify my code so it will return the custom message I build?
As per WebFlux documentation, you can user the exchangeToMono() or awaitExchange { } in order to have an error handling.
Mono<Object> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else if (response.statusCode().is4xxClientError()) {
// Suppress error status code
return response.bodyToMono(ErrorContainer.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
Code copied from WebFlux link above.
Take a look at 2.3. Exchange
val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Person>()
}
else if (response.statusCode().is4xxClientError) {
return response.awaitBody<ErrorContainer>()
}
else {
throw response.createExceptionAndAwait()
}
}

kotlin connect Timeout

I'm trying to do a task with an Asynctask in kotlin. I want to show a dialog message to user In the case that user has a very slow or spotty data connection at the time of use, I'd like to make the AsyncTask timeout after a period of time 5 second .My point is where should l put dialog ? after finally in doInBackground ?
inner class Arr : AsyncTask<String, String, String>() {
val progressDialog = AlertDialog.Builder(this#MainActivity)
val dialogView = layoutInflater.inflate(R.layout.progress_dialog, null)
val message = dialogView.findViewById<TextView>(R.id.message_id)
val dialog = progressDialog.create()
override fun onPreExecute() {
super.onPreExecute()
dialog.setMessage("please wait")
dialog.setCancelable(false)
dialog.show()
}
// for build connection
override fun doInBackground(vararg url: String?): String {
var text: String
val connection = URL(url[0]).openConnection() as HttpURLConnection
connection.connectTimeout = 300
try {
connection.connect()
text = connection.inputStream.use { it.reader().use { reader -> reader.readText() } }
} finally {
dialog.setMessage("Sorry you dont have proper net connectivity..!\nCheck your internet settings or retry.")
dialog.setCancelable(false)
dialog.show()
}
return text
}
override fun onPostExecute(result: String?) {
super.onPostExecute(result)
handleJson(result)
dialog.dismiss();
}
override fun onProgressUpdate(vararg text: String?) {
}
}

how to use socket IO in kotlin?

I want to initialize socket IO in my kotlin app.
my problem is here :
private var mSocket: Socket? = null
{
try {
mSocket = IO.socket("http://chat.socket.io")
} catch (URISyntaxException e) {
}
}
import com.github.nkzawa.socketio.client.IO
cant recognize
I searched for this some more and found this solution:
You connect your ws like this:
val opts = IO.Options()
opts.path = "/path/to/ws"
opts.transports = arrayOf(WebSocket.NAME) // Set the transfer to 'websocket' instead of 'polling'
val webSocket = IO.socket("http(s)://your.ip.here", opts)
webSocket.connect()
.on(Socket.EVENT_CONNECT) {
// Do your stuff here
}
.on("foo") { parameters -> // do something on recieving a 'foo' event
// 'parameters' is an Array of all parameters you sent
// Do your stuff here
}
If you want to emit an event, you'll call:
webSocket.emit("foo", "bar") // Emits a 'foo' event with 'bar' as a parameter
You will need to use
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
so be sure to add the corresponding libraries to your build.gradle
dependencies {
...
implementation 'com.github.nkzawa:socket.io-client:0.6.0'
}
first import this
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
and then initialize this one
val socket = IO.socket("http://localhost:4000/")
socket.on(Socket.EVENT_CONNECT, Emitter.Listener {
socket.emit("messages", "hi")
});
socket.connect()
It's a static block in java But we can't wirte same as in Kotlin.
We can use its like a companion object.
companion object{
private var mSocket: Socket?=null
init {
try {
mSocket = IO.socket(Constants.Chat_URl)
}
catch (e: Exception){
throw RuntimeException(e)
}
}
}
In Kotlin you can make a Socket Client like the following. All the Exceptions are handled here too.
fun pingYourTCPServerWith(message: String): String{
try {
val socket = Socket("<YOUR IP ADDRESS>", <YOUR PORT HERE>)
socket.use {
var responseString : String? = null
it.getOutputStream().write(message.toByteArray())
val bufferReader = BufferedReader(InputStreamReader(it.inputStream))
while (true) {
val line = bufferReader.readLine() ?: break
responseString += line
if (line == "exit") break
}
println("Received: $responseString")
bufferReader.close()
it.close()
return responseString!!
}
}catch (he: UnknownHostException){
val exceptionString = "An exception occurred:\n ${he.printStackTrace()}"
return exceptionString
}catch (ioe: IOException){
val exceptionString = "An exception occurred:\n ${ioe.printStackTrace()}"
return exceptionString
} catch (ce: ConnectException){
val exceptionString = "An exception occurred:\n ${ce.printStackTrace()}"
return exceptionString
}catch (se: SocketException){
val exceptionString = "An exception occurred:\n ${se.printStackTrace()}"
return exceptionString
}
}
The right syntax is below for anyone who is interested in the future
private lateinit var mSocket:Socket
fun socket(){
try {
mSocket=IO.socket("http://host:port")
}
catch(e: URISyntaxException){
println("Exception"+e)
}
}

Resources