forked from Kotlin/kotlinx.coroutines
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented a rough sketch of
CoroutineStartInterceptor
.
The implementation is only for the JVM runtime. To make entrance from a blocked thread work as expected for `ThreadContextElements`, this draft chooses to modify `runBlocking {}` s.t. its contract is that the `CoroutineContext` stack parameter is always used for interception.
- Loading branch information
1 parent
ab44090
commit 343d478
Showing
5 changed files
with
304 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
kotlinx-coroutines-core/common/src/CoroutineStartInterceptor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines | ||
|
||
import kotlin.coroutines.* | ||
|
||
/** | ||
* Called to construct the `CoroutineContext` for a new coroutine. | ||
* | ||
* When a `CoroutineStartInterceptor` is present in a parent coroutine’s | ||
* `CoroutineContext`, Coroutines will call it to construct a new child coroutine’s | ||
* `CoroutineContext`. | ||
* | ||
* The `CoroutineStartInterceptor` can insert, remove, copy, or modify | ||
* `CoroutineContext.Elements` passed to the new child coroutine. | ||
* | ||
* The “default implementation” of `interceptContext()` used by coroutine builders | ||
* ([coroutineScope], [launch], [async]) when no [CoroutineStartInterceptor] is included, is | ||
* `callingContext + addedContext`. This default folds the the coroutine builder’s `context` | ||
* parameter left onto the parent coroutine's `coroutineContext`. | ||
* | ||
* This API is delicate and performance sensitive. | ||
* | ||
* Since `interceptContext()` is called each time a new coroutine is created, its | ||
* implementation has a disproportionate impact on coroutine performance. | ||
* | ||
* Since `interceptContext` _replaces_ coroutine context inheritance, it can arbitrarily change how | ||
* coroutines inherit their scope. In order for a child coroutine’s `CoroutineContext` | ||
* to be inherited as described in documentation, an override of `interceptContext()` | ||
* __must__ add `callingContext` and `addedContext` to form the return value, with | ||
* `callingContext` to the left of `addedContext` in the sum. | ||
* | ||
* These example statements all preserve "normal" inheritance and modify a custom element: | ||
* | ||
* ``` | ||
* callingContext + addedContext | ||
* callingContext + CustomContextElement() + addedContext | ||
* callingContext + addedContext + CustomContextElement() | ||
* ``` | ||
* | ||
* These examples _break `Job` inheritance_, because they drop or reverse `callingContext` folding: | ||
* | ||
* ``` | ||
* addedContext + callingContext | ||
* CustomContextElement() + addedContext | ||
* ``` | ||
*/ | ||
@ExperimentalCoroutinesApi | ||
@DelicateCoroutinesApi | ||
public interface CoroutineStartInterceptor : CoroutineContext.Element { | ||
|
||
public companion object Key : CoroutineContext.Key<CoroutineStartInterceptor> | ||
|
||
/** | ||
* Called to construct the `CoroutineContext` for a new coroutine. | ||
* | ||
* The `CoroutineContext` returned by `interceptContext()` will be the `CoroutineContext` used | ||
* by the child coroutine. | ||
* | ||
* [callingContext] is the `CoroutineContext` of the coroutine constructing the coroutine. If | ||
* the coroutine is getting constructed by `runBlocking {}` outside of a running coroutine, | ||
* [callingContext] will be the `EmptyCoroutineContext`. | ||
* | ||
* [addedContext] is the `CoroutineContext` passed as a parameter to the coroutine builder. If | ||
* no `CoroutineContext` was passed, [addedContext will be the `EmptyCoroutineContext`. | ||
* | ||
* Consider this example: | ||
* | ||
* ``` | ||
* runBlocking(CustomCoroutineStartInterceptor()) { | ||
* async(CustomContextElement()) { | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* In this arrangement, `CustomCoroutineStartInterceptor.interceptContext()` is called | ||
* to construct the `CoroutineContext` for the `async` coroutine. When | ||
* `interceptContext()` is called, `callingContext` will contain | ||
* `CustomCoroutineStartInterceptor`. `addedContext` will contain the new | ||
* `CustomContextElement`. | ||
*/ | ||
public fun interceptContext( | ||
callingContext: CoroutineContext, | ||
addedContext: CoroutineContext | ||
): CoroutineContext | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
kotlinx-coroutines-core/jvm/test/CoroutineStartInterceptorSimpleTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines | ||
|
||
import org.junit.Test | ||
import kotlin.coroutines.* | ||
import kotlin.test.* | ||
|
||
class CoroutineStartInterceptorSimpleTest: TestBase() { | ||
|
||
/** | ||
* A [CoroutineContext.Element] holding an integer, used to enumerate elements allocated | ||
* during context construction. | ||
*/ | ||
private class IntegerContextElement( | ||
val number: Int | ||
): CoroutineContext.Element { | ||
public companion object Key : CoroutineContext.Key<IntegerContextElement> | ||
override val key = Key | ||
} | ||
|
||
/** | ||
* Inserts a new, unique [IntegerContextElement] into each new coroutine context constructed, | ||
* overriding any present. | ||
*/ | ||
class AddElementInterceptor : CoroutineStartInterceptor, | ||
AbstractCoroutineContextElement(CoroutineStartInterceptor.Key) { | ||
private var integerSource = 0 | ||
|
||
override fun interceptContext( | ||
callingContext: CoroutineContext, | ||
addedContext: CoroutineContext | ||
): CoroutineContext { | ||
integerSource += 1 | ||
return callingContext + addedContext + IntegerContextElement(integerSource) | ||
} | ||
} | ||
|
||
@Test | ||
fun testContextInterceptorOverridesContextElement() = runTest { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
|
||
runBlocking(AddElementInterceptor()) { | ||
|
||
} | ||
} | ||
|
||
@Test | ||
fun testAsyncDoesNotInterceptFromAddedContext() = runTest { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
|
||
async(AddElementInterceptor()) { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
}.join() | ||
} | ||
|
||
@Test | ||
fun testLaunchDoesNotInterceptFromAddedContext() = runTest { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
|
||
launch(AddElementInterceptor()) { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
}.join() | ||
} | ||
|
||
@Test | ||
fun testRunBlockingInterceptsFromAddedContext() = runTest { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
|
||
runBlocking(AddElementInterceptor()) { | ||
assertEquals( | ||
1, | ||
coroutineContext[IntegerContextElement.Key]!!.number | ||
) | ||
} | ||
} | ||
|
||
@Test | ||
fun testChildCoroutineContextInterceptedFromCallingContext() = runTest { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
|
||
launch(AddElementInterceptor()) { | ||
launch { | ||
assertEquals( | ||
1, | ||
coroutineContext[IntegerContextElement.Key]!!.number | ||
) | ||
}.join() | ||
launch { | ||
assertEquals( | ||
2, | ||
coroutineContext[IntegerContextElement.Key]!!.number | ||
) | ||
}.join() | ||
} | ||
} | ||
|
||
@Test | ||
fun testChildCoroutineContextInterceptedFromCallingContextNotAddedContext() = runTest { | ||
assertNull(coroutineContext[IntegerContextElement.Key]) | ||
|
||
launch(AddElementInterceptor()) { | ||
launch {}.join() | ||
|
||
launch(AddElementInterceptor()) {// Count of this interceptor is 0. | ||
assertEquals( | ||
2, | ||
coroutineContext[IntegerContextElement.Key]!!.number | ||
) | ||
launch { | ||
assertEquals( | ||
1, // Parent coroutine's context intercepted. | ||
coroutineContext[IntegerContextElement.Key]!!.number | ||
) | ||
}.join() | ||
}.join() | ||
} | ||
} | ||
} |