Mono timeout does not return the response on RestController after timeout but waits for the whole execution - spring-webflux

I have a rest controller as below.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
#RestController
public class DummyController {
#GetMapping("/get/mono")
public Mono<String> getTest(Integer groupId) {
return Mono.fromCallable(() -> {
Thread.sleep(10000); //Simulating a long running database call.
return "Success";
}).timeout(Duration.ofMillis(50), Mono.fromCallable(() -> {
System.out.println("timed out");
return "timeout";
})).onErrorResume(e -> Mono.just(e.getMessage()));
}
}
Here, I expect the controller to return the response after the timeout of 50ms. But instead, the controller returns the timeout fallback response("timeout") after the execution time of 10000 ms.
The strange thing here is that, after the timeout of 50ms the code inside the timeout fallback gets executed. But it still waits for the Thread.sleep to complete before returning the response.
Is there anyway to make the controller return the response immediately after the timeout and not wait for the whole execution of the callable method.
A minimal reproducable version of the code is here: https://github.com/rahulgul8/MonoTimeout.git Any help is greatly appreciated.

The main thread is blocked on the Thread.sleep() or any long running task. In this case, delegate the task to a different thread to execute the long running task and the main thread takes care of the timeout.
To do this, add subscribeOn(Schedulers.boundedElastic()) to the Mono. This executes the task on the thread provided by the Schedulers.boundedElastic() keeping the main thread available to process the timeout.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
#RestController
public class DummyController {
#GetMapping("/get/mono")
public Mono<String> getTest(Integer groupId) {
return Mono.fromCallable(() -> {
Thread.sleep(10000); //Simulating a long running database call.
return "Success";
}).subscribeOn(Schedulers.boundedElastic()) //Add this to fix the issue.
.timeout(Duration.ofMillis(50), Mono.fromCallable(() -> {
System.out.println("timed out");
return "timeout";
})).onErrorResume(e -> Mono.just(e.getMessage()));
}
}

Related

Ktor modify request on retry not working as expected

I have a custom retry policy on receiving 5XX errors from the server. The idea is to retry until I get a non-5XX error with an exponential delay between each retry request also I would like to update the request body on every retry.
Here is my code
import io.ktor.client.*
import io.ktor.client.engine.java.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds
suspend fun main() {
val serverJob = CoroutineScope(Dispatchers.Default).launch { startServer() }
val client = HttpClient(Java) {
install(HttpTimeout) {
connectTimeoutMillis = 5.seconds.inWholeMilliseconds
}
install(HttpRequestRetry)
}
client.post {
url("http://127.0.0.1:8080/")
setBody("Hello")
retry {
retryOnServerErrors(maxRetries = Int.MAX_VALUE)
exponentialDelay(maxDelayMs = 128.seconds.inWholeMilliseconds)
modifyRequest { it.setBody("With Different body ...") } // It's not working! if I comment this out then my retry logic works as expected
}
}
client.close()
serverJob.cancelAndJoin()
}
suspend fun startServer() {
embeddedServer(Netty, port = 8080) {
routing {
post("/") {
val text = call.receiveText()
println("Retrying exponentially... $text")
call.response.status(HttpStatusCode(500, "internal server error"))
}
}
}.start(wait = true)
}
As you can see, if I comment out modifyRequest { it.setBody("With Different body ...") } line from retry logic then everything works fine. If I include that line it only tries once and stuck there, what I'm doing wrong here? how to change the request body for every retry?
The problem is that rendering (transformation to an OutgoingContent) of a request body happens during the execution of the HttpRequestPipeline, which takes place only once after making an initial request. The HTTP request retrying happens after in the HttpSendPipeline.
Since you pass a String as a request body it needs to be transformed before the actual sending. To solve this problem, you can manually wrap your String into the TextContent instance and pass it to the setBody method:
retry {
retryOnServerErrors(maxRetries = Int.MAX_VALUE)
exponentialDelay(maxDelayMs = 128.seconds.inWholeMilliseconds)
modifyRequest {
it.setBody(TextContent("With Different body ...", ContentType.Text.Plain))
}
}

Spring Cloud Gateway Filter with external configuration service call

I am working on a Spring Cloud Gateway app that has a filter controlling access to certain paths or features based on a configuration held by a different service. So if a path is associated with feature x then only allow access if the configuration service returns that feature x is enabled.
The configuration is returned as a Mono and then flatMapped to check the enabled features. This all appears to work correctly. If the feature is enabled then the request is allowed to proceed through the chain. If the feature is disabled, then the response status is set to forbidden and the request marked as complete. However, this does not appear to stop the filter chain, and the request continues to be processed and eventually returns a 200 response.
If the feature configuration is not returned from an external source and is immediately available then this logic works correctly, but this involves a blocking call and does not seem desirable. I cannot see what is wrong with the first approach. It seems to be similar to examples available elsewhere.
I believe my question is similar to this one:
https://stackoverflow.com/questions/73496938/spring-cloud-api-gateway-custom-filters-with-external-api-for-authorization/75095356#75095356
Filter 1
This is the way I would like to do this:
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
logger.info("Feature Security Filter")
// getFeatures returns Mono<Map<String, Boolean>>
return featureConfigService.getFeatures().flatMap { features ->
val path = exchange.request.path.toString()
val method = exchange.request.method.toString()
if (featureMappings.keys.any { it.matcher(path).matches() }) {
val pathIsRestricted = featureMappings
.filter { it.key.matcher(path).matches() }
.filter { features[it.value.requiresFeature] != true || !it.value.methodsAllowed.contains(method) }
.isNotEmpty()
if (pathIsRestricted) {
logger.warn("Access to path [$method|$path] restricted. ")
exchange.response.statusCode = HttpStatus.FORBIDDEN
exchange.response.setComplete()
// processing should stop here but continues through other filters
}
}
chain.filter(exchange);
}
}
Filter 2
This way works but involves a blocking call in featureService.
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
logger.info("Feature Security Filter")
// this call returns a Map<String, Boolean> instead of a Mono
val features = featureService.getFeatureConfig()
val path = exchange.request.path.toString()
val method = exchange.request.method.toString()
if (featureMappings.keys.any { it.matcher(path).matches() }) {
val pathIsRestricted = featureMappings
.filter { it.key.matcher(path).matches() }
.filter { features[it.value.requiresFeature] != true || !it.value.methodsAllowed.contains(method) }
.isNotEmpty()
if (pathIsRestricted) {
logger.warn("Access to path [$method|$path] restricted. ")
val response: ServerHttpResponse = exchange.response
response.statusCode = HttpStatus.FORBIDDEN;
return response.setComplete()
// this works as this request will complete here
}
}
return chain.filter(exchange)
}
When the tests run I can see that a path is correctly logged as restricted, and the response status is set to HttpStatus.FORBIDDEN as expected, but the request continues to be processed by filters later in the chain, and eventually returns a 200 response.
I've tried returning variations on Mono.error and onErrorComplete but I get the same behaviour. I am new to Spring Cloud Gateway and cannot see what I am doing wrong
After doing a few tests, I figured out that Filters are executed after route filters even if you set high order. If you need to filter requests before routing, you can use WebFilter. Here is a working Java example based on your requirements.
package com.test.test.filters;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Map;
#Configuration
#Slf4j
public class TestGlobalFilter implements WebFilter, Ordered {
private Mono<Map<String, Boolean>> test() {
return Mono.just(Map.of("test", Boolean.TRUE));
}
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("Feature Security Filter");
// getFeatures returns Mono<Map<String, Boolean>>
return test().flatMap(features -> {
final var isRestricted = features.get("test");
if (Boolean.TRUE.equals(isRestricted)) {
log.info("Feature Security stop");
exchange.getResponse().setStatusCode(HttpStatus. FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
});
}
}

How to trigger a websockets message from outside the routing() block in Ktor?

If I have an Kotlin application that wants to trigger outgoing Websocket messages in Ktor, I usually do this from within the relevant routing block. If I have a process outside of the routing block that wants to send a Websocket message, how can I trigger that?
You need to store the session provided by the web socket connection and then you can send messages in that session:
var session: WebSocketSession? = null
try {
client.ws {
session = this
}
} finally {
// clear the session both when the socket is closed normally
// and when an error occurs, because it is no longer valid
session = null
}
// other coroutine
session?.send(/*...*/)
You can use coroutines' channels to send a Websocket session and receive it in a different place:
import io.ktor.application.*
import io.ktor.http.cio.websocket.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.Channel
suspend fun main() {
val sessions = Channel<WebSocketSession>()
val server = embeddedServer(Netty, 5555, host = "0.0.0.0") {
install(WebSockets) {}
routing {
webSocket("/socket") {
sessions.send(this)
}
}
}
server.start()
for (session in sessions) {
session.outgoing.send(Frame.Text("From outside of the routing"))
}
}

Is there any way to make a fake call from Ktor to itself, to make request pass through all pipeline?

I have an ktor web server that successfully responds on http requests. Now there is a need to read data from kafka's topic and process it.
Is there any way send the data I've read to ktor, like this data came from outside, to make it pass through all pipeline, like ContentNegotiation and other features?
Application class has method execute(), which takes ApplicationCall, but I've found zero examples - how can I fill my implementation of this class properly. Especially route - do I need the real one? Would be nice if this route would be private and would be unavailable from the outside.
You can use the withTestApplication function to test your application's modules without making an actual network connection. Here is an example:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class SimpleTest {
#Test
fun test() = withTestApplication {
application.module()
// more modules to test here
handleRequest(HttpMethod.Post, "/post") {
setBody("kafka data")
}.response.let { response ->
assertEquals("I get kafka data", response.content)
}
}
}
fun Application.module() {
routing {
post("/post") {
call.respondText { "I get ${call.receiveText()}" }
}
}
}
I think that #AlekseiTirman answer is great and most probably you should go for it
But I have to mention that it's easy to do it even in "real life" run. Your local machine ip is 0.0.0.0, you can get port from the env variable, so you just can create a simple HttpClient and send a request:
CoroutineScope(Dispatchers.IO).launch {
delay(1000)
val client = HttpClient {
defaultRequest {
// actually this is already a default value so no need to setting it
host = "0.0.0.0"
port = environment.config.property("ktor.deployment.port").getString().toInt()
}
}
val result = client.get<String>("good")
println("local response $result")
}
routing {
get("good") {
call.respond("hello world")
}
}

Timer: Not start my method periodically every N seconds

In my Kotlin project
I want start two independent timer.
First must run every 30 seconds.
Second must run every 20 seconds (start after 5 seconds).
I try this:
import kotlin.concurrent.schedule
import kotlin.concurrent.timerTask
Timer().scheduleAtFixedRate(timerTask { login() }, 1000, 30 * 1000)
Timer().schedule(5_000) { Timer().scheduleAtFixedRate(timerTask { updateState() }, 1000, 20 * 1000) }
First timer success run (login) periodically ever 20 seconds. Nice.
But seconds timer start only ONCE. Not run (updateState) periodically every 30 seconds.
Method login do sync http request. Method updateState also do sync http request.
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
fun login() {
val loginRequestURL =
"${Config.instance.loginApiUrl}/importAPILogist.php"
val requestToc= Request.Builder()
.url(loginRequestURL)
.get()
.build()
val httpClient = OkHttpClient()
val loginResponse: Response = httpClient.newCall(requestToc).execute() // sync request
}
and another method:
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
fun updateOrdersState() {
logger.info("updateOrdersState:")
val someRequestURL = "${Config.instance.someApiUrl}/exportAPI"
val requestOrderStateTocan = Request.Builder()
.url(someRequestURL)
.get()
.build()
val httpClient = OkHttpClient()
val tocanOrderStateResponse: Response = httpClient.newCall(requestOrderStateTocan).execute() // sync request
Method updateState can throw exception(java.net.SocketTimeoutException)
Maybe I need to catch exception when http request is not success? E.g.
Exception in thread "Timer-2" java.net.SocketTimeoutException: timeout