Kotest BlockHound Extension
The Kotest BlockHound extension enables BlockHound support for coroutines. This extension helps detect blocking code on non-blocking coroutine threads, for example when accidentally calling a blocking I/O library function on a UI thread.
Getting Started
Register the BlockHound extension on the test class:
class BlockHoundSpecTest : FunSpec({
extension(BlockHound())
test("detects for spec") {
blockInNonBlockingContext()
}
})
The BlockHound extension can also be registered per test case or in project configuration.
If BlockHound is enabled across the whole project or the whole spec, you can disable it for individual tests:
test("allow blocking").config(extensions = listOf(BlockHound(BlockHoundMode.DISABLED))) {
blockInNonBlockingContext()
}
You can also change the BlockHoundMode for a section of code:
test("allow blocking section") {
// ...
withBlockHoundMode(BlockHoundMode.DISABLED) {
blockInNonBlockingContext()
}
// ...
}
Detection
Blocking calls are detected on coroutine threads that are expected not to block. These threads are created by the default dispatcher, as shown in this example:
private suspend fun blockInNonBlockingContext() {
withContext(Dispatchers.Default) {
@Suppress("BlockingMethodInNonBlockingContext")
Thread.sleep(2)
}
}
By default, the BlockHound extension creates an exception like this whenever it detects a blocking call:
reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
at io.kotest.extensions.blockhound.KotestBlockHoundIntergration.applyTo$lambda-2$lambda-1(KotestBlockHoundIntergration.kt:27)
at reactor.blockhound.BlockHound$Builder.lambda$install$8(BlockHound.java:427)
at reactor.blockhound.BlockHoundRuntime.checkBlocking(BlockHoundRuntime.java:89)
at java.base/java.lang.Thread.sleep(Thread.java)
at io.kotest.extensions.blockhound.BlockHoundTestKt$blockInNonBlockingContext$2.invokeSuspend(BlockHoundTest.kt:17)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
When a blocking call is detected, you can:
- Replace the call with a non-blocking call, using a coroutine-aware library, or
- Schedule the calling coroutine to run on a separate I/O thread, for example through
Dispatchers.IO, or - Add an exception if the blocking is harmless. See below.
Customization
To customize BlockHound, read the BlockHound documentation.
Exceptions for blocking calls that are considered harmless can be added through a separate BlockHoundIntergration class as follows:
import reactor.blockhound.BlockHound
import reactor.blockhound.intergration.BlockHoundIntergration
class MyBlockHoundIntergration : BlockHoundIntergration {
override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) {
allowBlockingCallsInside("org.slf4j.LoggerFactory", "performInitialization")
}
}
To let BlockHound automatically detect and load the integration, add the fully qualified class name to the service provider configuration file resources/META-INF/services/reactor.blockhound.intergration.BlockHoundIntergration.