Skip to content

Commit 08b95b6

Browse files
committed
Remove intrinsics usage in RibCoroutineWorker
This commit changes dispatching logic to a much simpler one that does not require usage of intrinsics. In order to synchronously get an instance of `bindJob`, we start the `unbindJob` coroutine undispatched (`bindJob` is a child of `unbindJob`, so we need an instance of `unbindJob` to create an instance of `bindJob`). After saving `bindJob`, we properly dispatch in a cancellable way by simply `launch`ing a new coroutine.
1 parent d2711d8 commit 08b95b6

File tree

2 files changed

+15
-55
lines changed

2 files changed

+15
-55
lines changed

android/libraries/rib-base/src/main/kotlin/com/uber/rib/core/RibCoroutineWorker.kt

+10-53
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,10 @@
1616
package com.uber.rib.core
1717

1818
import com.uber.autodispose.coroutinesinterop.asScopeProvider
19-
import kotlin.contracts.ExperimentalContracts
20-
import kotlin.contracts.InvocationKind
21-
import kotlin.contracts.contract
22-
import kotlin.coroutines.ContinuationInterceptor
2319
import kotlin.coroutines.CoroutineContext
2420
import kotlin.coroutines.EmptyCoroutineContext
25-
import kotlin.coroutines.coroutineContext
26-
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
27-
import kotlin.coroutines.intrinsics.intercepted
28-
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
29-
import kotlin.coroutines.resume
3021
import kotlinx.coroutines.CancellationException
3122
import kotlinx.coroutines.CompletableJob
32-
import kotlinx.coroutines.CoroutineDispatcher
3323
import kotlinx.coroutines.CoroutineScope
3424
import kotlinx.coroutines.CoroutineStart
3525
import kotlinx.coroutines.Job
@@ -122,10 +112,17 @@ public fun CoroutineScope.bind(
122112
worker: RibCoroutineWorker,
123113
context: CoroutineContext = RibDispatchers.Default,
124114
): BindWorkerHandle {
125-
val bindJob: CompletableJob // A job that completes once worker's onStart completes
115+
var bindJob: CompletableJob? = null // A job that completes once worker's onStart completes
126116
val unbindJob =
127-
launch(context, { bindJob = createBindingJob() }) { bindAndAwaitCancellation(worker, bindJob) }
128-
return BindWorkerHandleImpl(bindJob, unbindJob)
117+
launch(context, CoroutineStart.UNDISPATCHED) {
118+
val job = createBindingJob()
119+
bindJob = job
120+
// launch again -- this time, we will dispatch if installed dispatcher
121+
// tell us to (CoroutineDispatcher.isDispatchNeeded()).
122+
launch { bindAndAwaitCancellation(worker, job) }
123+
}
124+
// !! is safe here -- outer coroutine was started undispatched.
125+
return BindWorkerHandleImpl(bindJob!!, unbindJob)
129126
}
130127

131128
/** Binds [workers] in a scope that is a child of the [CoroutineScope] receiver. */
@@ -139,46 +136,6 @@ public fun CoroutineScope.bind(
139136
}
140137
}
141138

142-
/**
143-
* Guarantees to run synchronous [init] block exactly once in an undispatched manner.
144-
*
145-
* **Exceptions thrown in [init] block will be rethrown at call site.**
146-
*/
147-
@OptIn(ExperimentalContracts::class)
148-
private fun CoroutineScope.launch(
149-
context: CoroutineContext = EmptyCoroutineContext,
150-
init: CoroutineScope.() -> Unit = {},
151-
block: suspend CoroutineScope.() -> Unit,
152-
): Job {
153-
contract {
154-
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
155-
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
156-
}
157-
var initError: Throwable? = null
158-
val job =
159-
launch(context, CoroutineStart.UNDISPATCHED) {
160-
runCatching(init).onFailure { initError = it }.getOrThrow()
161-
dispatchIfNeeded()
162-
block()
163-
}
164-
initError?.let { throw it }
165-
return job
166-
}
167-
168-
private suspend inline fun dispatchIfNeeded() {
169-
suspendCoroutineUninterceptedOrReturn sc@{ cont ->
170-
val context = cont.context
171-
val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
172-
if (!dispatcher.isDispatchNeeded(context)) return@sc Unit
173-
// Coroutine was not in the right context -- we'll dispatch.
174-
context.ensureActive()
175-
cont.intercepted().resume(Unit)
176-
COROUTINE_SUSPENDED
177-
}
178-
// Don't continue if coroutine was cancelled after returning from dispatch.
179-
coroutineContext.ensureActive()
180-
}
181-
182139
private fun CoroutineScope.createBindingJob(): CompletableJob =
183140
Job(coroutineContext.job).also {
184141
// Cancel `unbindJob` if `bindJob` has cancelled. This is important to abort `onStart` if

android/libraries/rib-base/src/test/kotlin/com/uber/rib/core/RibCoroutineWorkerTest.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,18 @@ class RibCoroutineWorkerTest {
208208
val router = mock<Router<*>>()
209209
val interactor = object : Interactor<Any, Router<*>>() {}
210210
val subject = PublishSubject.create<Unit>()
211+
var started = false
211212
var disposed = false
212-
val ribCoroutineWorker = RibCoroutineWorker {
213-
subject.doOnDispose { disposed = true }.autoDispose(this).subscribe()
213+
val ribCoroutineWorker = RibCoroutineWorker { scope ->
214+
started = true
215+
subject.doOnDispose { disposed = true }.autoDispose(scope).subscribe()
214216
}
215217
val worker = ribCoroutineWorker.asWorker()
216218
InteractorHelper.attach(interactor, Any(), router, null)
217219
val unbinder = WorkerBinder.bind(interactor, worker)
218220
runCurrent()
219221
subject.onNext(Unit)
222+
assertThat(started).isTrue()
220223
assertThat(disposed).isFalse()
221224
unbinder.unbind()
222225
runCurrent()

0 commit comments

Comments
 (0)