Eagerly cancel async bindings (#37)
Make suspendible binding function eagerly cancel child jobs by cancelling wrapping scope
This commit is contained in:
parent
3c5b432b55
commit
c8372a0522
@ -4,6 +4,9 @@ import com.github.michaelbull.result.Err
|
|||||||
import com.github.michaelbull.result.Ok
|
import com.github.michaelbull.result.Ok
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
import kotlinx.coroutines.CancellationException
|
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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
@ -11,16 +14,20 @@ import kotlin.contracts.contract
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Suspending variant of [binding][com.github.michaelbull.result.binding].
|
* 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> {
|
public suspend inline fun <V, E> binding(crossinline block: suspend SuspendableResultBinding<E>.() -> V): Result<V, E> {
|
||||||
contract {
|
contract {
|
||||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
}
|
}
|
||||||
|
|
||||||
val receiver = SuspendableResultBindingImpl<E>()
|
val receiver = SuspendableResultBindingImpl<E>()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
with(receiver) { Ok(block()) }
|
coroutineScope {
|
||||||
|
receiver.coroutineScope = this@coroutineScope
|
||||||
|
with(receiver) { Ok(block()) }
|
||||||
|
}
|
||||||
} catch (ex: BindCancellationException) {
|
} catch (ex: BindCancellationException) {
|
||||||
receiver.internalError
|
receiver.internalError
|
||||||
}
|
}
|
||||||
@ -37,6 +44,7 @@ internal class SuspendableResultBindingImpl<E> : SuspendableResultBinding<E> {
|
|||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
lateinit var internalError: Err<E>
|
lateinit var internalError: Err<E>
|
||||||
|
var coroutineScope: CoroutineScope? = null
|
||||||
|
|
||||||
override suspend fun <V> Result<V, E>.bind(): V {
|
override suspend fun <V> Result<V, E>.bind(): V {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
@ -47,6 +55,7 @@ internal class SuspendableResultBindingImpl<E> : SuspendableResultBinding<E> {
|
|||||||
internalError = this
|
internalError = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
coroutineScope?.cancel(BindCancellationException)
|
||||||
throw BindCancellationException
|
throw BindCancellationException
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import com.github.michaelbull.result.coroutines.runBlockingTest
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class SuspendableBindingTest {
|
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
|
@Test
|
||||||
fun returnsFirstErrIfBindingsOfDifferentTypesFailed() {
|
fun returnsFirstErrIfBindingsOfDifferentTypesFailed() {
|
||||||
suspend fun provideX(): Result<Int, BindingError> {
|
suspend fun provideX(): Result<Int, BindingError> {
|
||||||
|
@ -8,6 +8,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class AsyncSuspendableBindingTest {
|
class AsyncSuspendableBindingTest {
|
||||||
@ -47,7 +48,7 @@ class AsyncSuspendableBindingTest {
|
|||||||
@Test
|
@Test
|
||||||
fun returnsFirstErrIfBindingFailed() {
|
fun returnsFirstErrIfBindingFailed() {
|
||||||
suspend fun provideX(): Result<Int, BindingError> {
|
suspend fun provideX(): Result<Int, BindingError> {
|
||||||
delay(1)
|
delay(3)
|
||||||
return Ok(1)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user