Eagerly cancel async bindings (#37)

Make suspendible binding function eagerly cancel child jobs by cancelling wrapping scope
This commit is contained in:
Tristan H 2020-11-28 22:50:10 +00:00 committed by GitHub
parent 3c5b432b55
commit c8372a0522
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 3 deletions

View File

@ -4,6 +4,9 @@ import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.contracts.InvocationKind
@ -11,16 +14,20 @@ import kotlin.contracts.contract
/**
* Suspending variant of [binding][com.github.michaelbull.result.binding].
* Wraps the suspendable block in a new coroutine scope.
* This scope is cancelled once a failing bind is encountered, allowing deferred child jobs to be eagerly cancelled.
*/
public suspend inline fun <V, E> binding(crossinline block: suspend SuspendableResultBinding<E>.() -> V): Result<V, E> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val receiver = SuspendableResultBindingImpl<E>()
return try {
coroutineScope {
receiver.coroutineScope = this@coroutineScope
with(receiver) { Ok(block()) }
}
} catch (ex: BindCancellationException) {
receiver.internalError
}
@ -37,6 +44,7 @@ internal class SuspendableResultBindingImpl<E> : SuspendableResultBinding<E> {
private val mutex = Mutex()
lateinit var internalError: Err<E>
var coroutineScope: CoroutineScope? = null
override suspend fun <V> Result<V, E>.bind(): V {
return when (this) {
@ -47,6 +55,7 @@ internal class SuspendableResultBindingImpl<E> : SuspendableResultBinding<E> {
internalError = this
}
}
coroutineScope?.cancel(BindCancellationException)
throw BindCancellationException
}
}

View File

@ -7,6 +7,7 @@ import com.github.michaelbull.result.coroutines.runBlockingTest
import kotlinx.coroutines.delay
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class SuspendableBindingTest {
@ -100,6 +101,48 @@ class SuspendableBindingTest {
}
}
@Test
fun returnsStateChangedUntilFirstBindFailed() {
var xStateChange = false
var yStateChange = false
var zStateChange = false
suspend fun provideX(): Result<Int, BindingError> {
delay(1)
xStateChange = true
return Ok(1)
}
suspend fun provideY(): Result<Int, BindingError> {
delay(10)
yStateChange = true
return Err(BindingError)
}
suspend fun provideZ(): Result<Int, BindingError> {
delay(1)
zStateChange = true
return Err(BindingError)
}
runBlockingTest {
val result = binding<Int, BindingError> {
val x = provideX().bind()
val y = provideY().bind()
val z = provideZ().bind()
x + y + z
}
assertTrue(result is Err)
assertEquals(
expected = BindingError,
actual = result.error
)
assertTrue(xStateChange)
assertTrue(yStateChange)
assertFalse(zStateChange)
}
}
@Test
fun returnsFirstErrIfBindingsOfDifferentTypesFailed() {
suspend fun provideX(): Result<Int, BindingError> {

View File

@ -8,6 +8,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class AsyncSuspendableBindingTest {
@ -47,7 +48,7 @@ class AsyncSuspendableBindingTest {
@Test
fun returnsFirstErrIfBindingFailed() {
suspend fun provideX(): Result<Int, BindingError> {
delay(1)
delay(3)
return Ok(1)
}
@ -76,4 +77,46 @@ class AsyncSuspendableBindingTest {
)
}
}
@Test
fun returnsStateChangedForOnlyTheFirstAsyncBindFailWhenEagerlyCancellingBinding() {
var xStateChange = false
var yStateChange = false
var zStateChange = false
suspend fun provideX(): Result<Int, BindingError> {
delay(20)
xStateChange = true
return Ok(1)
}
suspend fun provideY(): Result<Int, BindingError.BindingErrorA> {
delay(10)
yStateChange = true
return Err(BindingError.BindingErrorA)
}
suspend fun provideZ(): Result<Int, BindingError.BindingErrorB> {
delay(1)
zStateChange = true
return Err(BindingError.BindingErrorB)
}
runBlocking {
val result = binding<Int, BindingError> {
val x = async { provideX().bind() }
val y = async { provideY().bind() }
val z = async { provideZ().bind() }
x.await() + y.await() + z.await()
}
assertTrue(result is Err)
assertEquals(
expected = BindingError.BindingErrorB,
actual = result.error
)
assertFalse(xStateChange)
assertFalse(yStateChange)
assertTrue(zStateChange)
}
}
}