Skip to content

Support yield in immediate dispatchers #1667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 26, 2019
Merged

Support yield in immediate dispatchers #1667

merged 3 commits into from
Nov 26, 2019

Conversation

elizarov
Copy link
Contributor

@elizarov elizarov commented Nov 20, 2019

Fixes #1474

Copy link
Contributor

@JakeWharton JakeWharton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@elizarov elizarov requested a review from qwwdfsad November 20, 2019 15:22
@elizarov
Copy link
Contributor Author

elizarov commented Nov 20, 2019

@JakeWharton Note, that if somebody creates a wrapper around Unconfined dispatcher or writes their own dispatcher like this:

object MyUnconfined : CoroutineDispatcher() {
    override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
    override fun dispatch(context: CoroutineContext, block: Runnable) = block.run() // !!!
}

Then yield() in such an MyUnconfined dispatcher implementation would never work. It will not be able to actually "yield". So, using yield() to work-around java.lang.reflect.Proxy checked exception problem is not 100% fool-proof. I wonder if we can figure out a solution that works all the time.

@JakeWharton
Copy link
Contributor

The first thing I reached for was delay(1), but it made me sad.

@elizarov
Copy link
Contributor Author

The first thing I reached for was delay(1), but it made me sad.

The other potential problematic case would be coroutine running without a dispatcher at all (a truly unconfined coroutine). The existing and updated version of yield is implemented as "nop" in this case. It does not do anything. So, I see two conceptual ways to solve it:

  1. Retrofit-like libraries that use Proxy can rely on their own thread-pool to schedule continuation.resumeWithException to happen later in their own thread.

  2. We can update implementation of yield in such a way that it ether use dispatcher-provided ability to yield thread or if there is no dispatcher or dispatcher is unconfined, then yield() would use our built-in DefaultExecutor to yield --- that same executor that is used as a fallback to implement delay(...) for dispatchers that do not support it natively.

@elizarov
Copy link
Contributor Author

elizarov commented Nov 20, 2019

@JakeWharton I gave a bit more thought to the second idea. It is not really good. The change in this PR (support yield for immediate) is good. But if we provide a hard guarantee that "yield always suspends" then it means that in some simple situations without a dispatcher execution after yield() must resume in another thread. This would be surprising.

However, it is not surprising if some code that uses a remote-call library like retrofit without a dispatcher gets resumed in another thread. So, the first option to work around Proxy exception handling seems better to me. I'd suggest to copy and use the following function instead of yield() as an exception work-around:

import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*

suspend fun ensureSuspension() = suspendCoroutineUninterceptedOrReturn<Unit> {
    Dispatchers.Default.dispatch(it.context, Runnable { it.intercepted().resume(Unit) })
    COROUTINE_SUSPENDED
}

UPDATE: Corrected to use suspendCoroutineUninterceptedOrReturn.

You'd use just like you use yield() now inside your Proxy:

try { 
   // do something that might throw exception w/o suspending
} catch (e: Throwable) {
   ensureSuspension()
   throw e // rethrow exception after guaranteed suspension 
}

Copy link
Member

@qwwdfsad qwwdfsad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done, please squash before merging

So yield now checks for "Unconfined" dispatcher instead of
"isDispatchNeeded" and works properly for immediate dispatchers.

Fixes #1474
@elizarov
Copy link
Contributor Author

I've found a problem with this change. We try to dispatch to dispatchers that return false from isDispatchNeed to support immediate dispatcher. Now consider a user that had accidentally wrapped Unconfined dispatcher for whatever reason. Maybe they have some tracking framework and need their own CorouteDispatcher wrapper or whatever. With such a wrapper on top of Unconfined calling yield from a loop would cause StackOverflowError. I've added a test to demonstrate it.

As a fix, yield now adds a special coroutine context element that is detected by Unconfined dispatcher so they still work correctly in tandem even when Unconfined dispatcher is wrapped. I've also updated docs for CoroutineDispatcher.dispatch.

The downside of this change is that a few extra objects are created on each yield call. I have another implementation strategy based on ThreadLocal which I've tried and it also works, but I've found the resulting code to be somewhat more cryptic.

Please, review.

@elizarov elizarov requested a review from qwwdfsad November 23, 2019 15:29
See ImmediateYieldTest.testWrappedUnconfinedDispatcherYieldStackOverflow

This commit changes the way Unconfined dispatcher is detected.
Instead of equality check we try to dispatch to the dispatcher with
a specially updated coroutine context that has YieldContext element
that is processed by the Unconfied dispatcher in a special way.
@elizarov elizarov force-pushed the immediate-yield branch 2 times, most recently from b6b5136 to 2808f09 Compare November 26, 2019 08:38
Also, as it was before, throw UnsupportedOperationException from the
Dispatcher.Unconfined.dispatch method in case some code wraps
the Unconfined dispatcher but fails to delegate isDispatchNeeded
properly.
@elizarov elizarov merged commit 69d1d41 into develop Nov 26, 2019
@elizarov elizarov deleted the immediate-yield branch November 26, 2019 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants