Skip to content

Instantly share code, notes, and snippets.

@pyricau
Created June 4, 2021 20:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pyricau/e3063be10afc06c34ff5a7dd50290808 to your computer and use it in GitHub Desktop.
Save pyricau/e3063be10afc06c34ff5a7dd50290808 to your computer and use it in GitHub Desktop.
A test coroutine dispatcher to reproduce race conditions
// Here's an example based on a test that was meant to reproduce a race condition,
// where we were launching several coroutines in a specific order without realizing
// that a different order would introduce race conditions. The test code here was used to
// reproduce the exact bad order.
// Note: there are probably better approaches to doing this.
val dispatcher = QueueDelegatingTestDispatcher()
// ... TODO pass in the dispatcher to the tested code launching coroutines
dispatcher += Immediate
val delayedDispatch = Delayed()
dispatcher += delayedDispatch
dispatcher += Immediate
// At this point the dispatcher can dispatch 3 separate coroutines.
// The first one will execute immediately / synchronously
// The second one will not execute at all and wait.
// The third one will execute immediately / synchronously
// ... TODO Have the code under test perform operations that launch coroutines.
// At this point, the 1st and 3rd coroutines have executed.
// Now execute the 2nd coroutine, BOOM.
delayedDispatch.dispatchNow()
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Runnable
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.CoroutineContext
/**
* A dispatcher that must be set up ahead of dispatching with one [QueueWorker] for each dispatch,
* where that [QueueWorker] defines how the work will be dispatched (either [QueueWorker.Immediate]
* or [QueueWorker.Delayed]).
*/
@OptIn(ExperimentalCoroutinesApi::class)
class QueueDelegatingTestDispatcher : CoroutineDispatcher() {
private val workers = CopyOnWriteArrayList<QueueWorker>()
override fun dispatch(context: CoroutineContext, block: Runnable) {
val worker = workers.removeFirst()
worker.dispatch(block)
}
operator fun plusAssign(worker: QueueWorker) {
workers.add(worker)
}
}
sealed class QueueWorker {
abstract fun dispatch(block: Runnable)
object Immediate : QueueWorker() {
override fun dispatch(block: Runnable) {
block.run()
}
}
class Delayed : QueueWorker() {
private var storedBlock: Runnable? = null
override fun dispatch(block: Runnable) {
check(storedBlock == null)
storedBlock = block
}
fun dispatchNow() {
val block = storedBlock!!
storedBlock = null
block.run()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment