Kotest BlockHound Extension

This page introduces the 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.

Latest Release

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.


References