I have a simple controller as below in my spring-webflux application,
@GetMapping("/ping")
suspend fun ping(): ResponseEntity<Map<String, Boolean>> {
println(coroutineContext)
waitforIO()
return ResponseEntity(mapOf("success" to true), HttpStatus.OK)
}
suspend fun waitforIO() {
println(coroutineContext)
delay(1000)
}
The output looks like
[Context1{micrometer.observation={name=http.server.requests(null), error=null, context=name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[exception='none', method='GET', outcome='SUCCESS', status='200', uri='UNKNOWN'], highCardinalityKeyValues=[http.url='/ping'], map=[class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.066692583, duration(nanos)=6.6692583E7, startTimeNanos=475384383429625}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@4b6502a5'], parentObservation=null}}, MonoCoroutine{Active}@33bfeb7d, Dispatchers.Unconfined]
[Context1{micrometer.observation={name=http.server.requests(null), error=null, context=name='http.server.requests', contextualName='null', error='null', lowCardinalityKeyValues=[exception='none', method='GET', outcome='SUCCESS', status='200', uri='UNKNOWN'], highCardinalityKeyValues=[http.url='/ping'], map=[class io.micrometer.core.instrument.LongTaskTimer$Sample='SampleImpl{duration(seconds)=0.068251166, duration(nanos)=6.8251166E7, startTimeNanos=475384383429625}', class io.micrometer.core.instrument.Timer$Sample='io.micrometer.core.instrument.Timer$Sample@4b6502a5'], parentObservation=null}}, MonoCoroutine{Active}@33bfeb7d, Dispatchers.Unconfined]
Can someone elaborate me why this is using Dispatchers.Unconfined
instead of Dispatchers.Default
? AFAIK, Default should be picked by Default (obvious!).
I am using the below configurations if that would play some role in debugging this,
OpenJDK Runtime Environment JBR-17.0.7+7-985.2-nomod (build 17.0.7+7-b985.2)
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
id("org.springframework.boot") version "3.1.5"
id("io.spring.dependency-management") version "1.1.3"
springCloudVersion="2022.0.4"
All dependencies are managed by spring-cloud-dependencies - 2022.0.4.
I find this question very very interesting. I just made some code to test this:
@SpringBootApplication
class CarPartsReactiveApplication
fun main(args: Array<String>) {
runApplication<CarPartsReactiveApplication>(*args)
}
@RestController
@RequestMapping
class CarController(){
@GetMapping("/parts")
suspend fun getParts(): List<String> {
println(coroutineContext)
return listOf("breaks", "keys", "lights")
}
}
And I'm forcing Spring to use netty:
spring.main.web-application-type=reactive
So everything checks-out. Yet, this is the log result when making the rest call:
[Context2{micrometer.observation=io.micrometer.observation.NoopObservation@255832b8, reactor.onDiscard.local=reactor.core.publisher.Operators$$Lambda/0x00007f1a18581a00@5aec54a6}, MonoCoroutine{Active}@1c0cfb28, Dispatchers.Unconfined]
And so it is really using Unconfined by default. Unconfined is, afaik, a dispatcher that doesn't care about which thread a coroutine can use. That's where the name comes. It is not confined to any type of thread. And this is why it is mostly used for testing or when we want to just use any other dispatcher for any specific reason. Processing a request, is, however, part of IO operations and so I would have actually expected Dispatchers.IO to be the default one. The Dispatchers.Default is probably best to use for CPU bound operations only. Having said all of this, making a REST request, implies not only IO operations, but can also imply, for most cases, the usage of CPU bound operations, which could be the reason behind making the default dispatchers to be Unconfined. Maybe kind of averaging out things for better performance without thinking too much about coroutine paradigms? It's just my theory at the moment any way.