Skip to content

Commit 343d478

Browse files
committed
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.
1 parent ab44090 commit 343d478

File tree

5 files changed

+304
-26
lines changed

5 files changed

+304
-26
lines changed

kotlinx-coroutines-core/common/src/CoroutineContext.common.kt

+13-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ package kotlinx.coroutines
77
import kotlin.coroutines.*
88

99
/**
10-
* Creates a context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher or
11-
* [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on).
10+
* Creates a new [CoroutineContext] for a new coroutine constructed in [this] [CoroutineScope].
11+
*
12+
* When a [CoroutineStartInterceptor] is present in [this] [CoroutineScope],]
13+
* [newCoroutineContext] calls it to construct the new context.
14+
*
15+
* Otherwise, it uses `this.coroutineContext + context` to create the new context.
16+
*
17+
* Before returning the new context, [newCoroutineContext] adds [Dispatchers.Default] to if no
18+
* [ContinuationInterceptor] was present in either [this] scope's [CoroutineContext] or in
19+
* [context].
20+
*
21+
* [newCoroutineContext] also adds debugging facilities to the returned context when debug features
22+
* are enabled.
1223
*/
1324
public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext
1425

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import kotlin.coroutines.*
8+
9+
/**
10+
* Called to construct the `CoroutineContext` for a new coroutine.
11+
*
12+
* When a `CoroutineStartInterceptor` is present in a parent coroutine’s
13+
* `CoroutineContext`, Coroutines will call it to construct a new child coroutine’s
14+
* `CoroutineContext`.
15+
*
16+
* The `CoroutineStartInterceptor` can insert, remove, copy, or modify
17+
* `CoroutineContext.Elements` passed to the new child coroutine.
18+
*
19+
* The “default implementation” of `interceptContext()` used by coroutine builders
20+
* ([coroutineScope], [launch], [async]) when no [CoroutineStartInterceptor] is included, is
21+
* `callingContext + addedContext`. This default folds the the coroutine builder’s `context`
22+
* parameter left onto the parent coroutine's `coroutineContext`.
23+
*
24+
* This API is delicate and performance sensitive.
25+
*
26+
* Since `interceptContext()` is called each time a new coroutine is created, its
27+
* implementation has a disproportionate impact on coroutine performance.
28+
*
29+
* Since `interceptContext` _replaces_ coroutine context inheritance, it can arbitrarily change how
30+
* coroutines inherit their scope. In order for a child coroutine’s `CoroutineContext`
31+
* to be inherited as described in documentation, an override of `interceptContext()`
32+
* __must__ add `callingContext` and `addedContext` to form the return value, with
33+
* `callingContext` to the left of `addedContext` in the sum.
34+
*
35+
* These example statements all preserve "normal" inheritance and modify a custom element:
36+
*
37+
* ```
38+
* callingContext + addedContext
39+
* callingContext + CustomContextElement() + addedContext
40+
* callingContext + addedContext + CustomContextElement()
41+
* ```
42+
*
43+
* These examples _break `Job` inheritance_, because they drop or reverse `callingContext` folding:
44+
*
45+
* ```
46+
* addedContext + callingContext
47+
* CustomContextElement() + addedContext
48+
* ```
49+
*/
50+
@ExperimentalCoroutinesApi
51+
@DelicateCoroutinesApi
52+
public interface CoroutineStartInterceptor : CoroutineContext.Element {
53+
54+
public companion object Key : CoroutineContext.Key<CoroutineStartInterceptor>
55+
56+
/**
57+
* Called to construct the `CoroutineContext` for a new coroutine.
58+
*
59+
* The `CoroutineContext` returned by `interceptContext()` will be the `CoroutineContext` used
60+
* by the child coroutine.
61+
*
62+
* [callingContext] is the `CoroutineContext` of the coroutine constructing the coroutine. If
63+
* the coroutine is getting constructed by `runBlocking {}` outside of a running coroutine,
64+
* [callingContext] will be the `EmptyCoroutineContext`.
65+
*
66+
* [addedContext] is the `CoroutineContext` passed as a parameter to the coroutine builder. If
67+
* no `CoroutineContext` was passed, [addedContext will be the `EmptyCoroutineContext`.
68+
*
69+
* Consider this example:
70+
*
71+
* ```
72+
* runBlocking(CustomCoroutineStartInterceptor()) {
73+
* async(CustomContextElement()) {
74+
* }
75+
* }
76+
* ```
77+
*
78+
* In this arrangement, `CustomCoroutineStartInterceptor.interceptContext()` is called
79+
* to construct the `CoroutineContext` for the `async` coroutine. When
80+
* `interceptContext()` is called, `callingContext` will contain
81+
* `CustomCoroutineStartInterceptor`. `addedContext` will contain the new
82+
* `CustomContextElement`.
83+
*/
84+
public fun interceptContext(
85+
callingContext: CoroutineContext,
86+
addedContext: CoroutineContext
87+
): CoroutineContext
88+
}
89+

kotlinx-coroutines-core/jvm/src/Builders.kt

+39-16
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,44 @@
88

99
package kotlinx.coroutines
1010

11-
import java.util.concurrent.locks.*
1211
import kotlin.contracts.*
1312
import kotlin.coroutines.*
1413

1514
/**
16-
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
17-
* This function should not be used from a coroutine. It is designed to bridge regular blocking code
18-
* to libraries that are written in suspending style, to be used in `main` functions and in tests.
15+
* Creates a new coroutine and **blocks** the current thread _interruptibly_ to immediately execute
16+
* it.
1917
*
20-
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
21-
* in this blocked thread until the completion of this coroutine.
22-
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
18+
* [runBlocking] allows regular blocking code to call libraries that are written using coroutines.
19+
* [runBlocking] should be called by a test case or in a program's `main()` to "boostrap" into
20+
* coroutines.
21+
*
22+
* [runBlocking] should never be called from _in_ a coroutine. Blocking a thread is unnecessary
23+
* and inefficient. When a function is a `suspend` function, call [coroutineScope] rather than
24+
* [runBlocking] in order to introduce parallelism.
25+
*
26+
* [runBlocking] uses its input [context] as though it were the [CoroutineContext] that
27+
* constructed the blocking coroutine. Unlike a child coroutine built with [launch] or [async],
28+
* a [runBlocking] coroutine gets its [CoroutineStartInterceptor] and [ContinuationInterceptor]
29+
* from its parameter, rather than from the running context.
2330
*
2431
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
2532
* the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
2633
* then this invocation uses the outer event loop.
2734
*
28-
* If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and
29-
* this `runBlocking` invocation throws [InterruptedException].
35+
* If [context] does not contain a [CoroutineDispatcher], [runBlocking] will include add an event
36+
* loop [CoroutineDispatcher] to the [CoroutineContext], and execute continuations using the
37+
* blocked thread until [block] returns.
38+
*
39+
* When a [CoroutineStartInterceptor] is explicitly specified in the [context], it intercepts the
40+
* construction of _this_ coroutine. This is a special case that allows the thread calling
41+
* `runBlocking` to intercept the coroutine start before it blocks the thread.
42+
*
43+
* If the blocked thread is interrupted (see [Thread.interrupt]), this coroutine's job will be
44+
* cancelled. If the cancellation by interrupt succeeds, the running [runBlocking] function call
45+
* will complete by throwing [InterruptedException].
3046
*
31-
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
32-
* for a newly created coroutine.
47+
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging
48+
* facilities that are available for a newly created coroutine.
3349
*
3450
* @param context the context of the coroutine. The default value is an event loop on the current thread.
3551
* @param block the coroutine code.
@@ -40,19 +56,26 @@ public actual fun <T> runBlocking(context: CoroutineContext, block: suspend Coro
4056
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
4157
}
4258
val currentThread = Thread.currentThread()
43-
val contextInterceptor = context[ContinuationInterceptor]
59+
val continuationInterceptor = context[ContinuationInterceptor]
60+
val coroutineStartInterceptor = context[CoroutineStartInterceptor]
4461
val eventLoop: EventLoop?
4562
val newContext: CoroutineContext
46-
if (contextInterceptor == null) {
63+
if (continuationInterceptor == null) {
4764
// create or use private event loop if no dispatcher is specified
4865
eventLoop = ThreadLocalEventLoop.eventLoop
49-
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
66+
newContext = newCoroutineContext(
67+
callingContext = context + eventLoop,
68+
addedContext = EmptyCoroutineContext
69+
)
5070
} else {
5171
// See if context's interceptor is an event loop that we shall use (to support TestContext)
5272
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
53-
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
73+
eventLoop = (continuationInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
5474
?: ThreadLocalEventLoop.currentOrNull()
55-
newContext = GlobalScope.newCoroutineContext(context)
75+
newContext = newCoroutineContext(
76+
callingContext = context,
77+
addedContext = EmptyCoroutineContext
78+
)
5679
}
5780
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
5881
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)

kotlinx-coroutines-core/jvm/src/CoroutineContext.kt

+42-8
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,51 @@ import kotlin.coroutines.*
99
import kotlin.coroutines.jvm.internal.CoroutineStackFrame
1010

1111
/**
12-
* Creates context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher nor
13-
* [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on).
12+
* Creates a new [CoroutineContext] for a new coroutine constructed in [this] [CoroutineScope].
1413
*
15-
* See [DEBUG_PROPERTY_NAME] for description of debugging facilities on JVM.
14+
* When a [CoroutineStartInterceptor] is present in [this] [CoroutineScope],]
15+
* [newCoroutineContext] calls it to construct the new context.
16+
*
17+
* Otherwise, it uses `this.coroutineContext + context` to create the new context.
18+
*
19+
* Before returning the new context, [newCoroutineContext] adds [Dispatchers.Default] to if no
20+
* [ContinuationInterceptor] was present in either [this] scope's [CoroutineContext] or in
21+
* [context].
22+
*
23+
* [newCoroutineContext] also adds debugging facilities to the returned context when debug features
24+
* are enabled.
25+
*/
26+
@ExperimentalCoroutinesApi
27+
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext =
28+
newCoroutineContext(coroutineContext, context)
29+
30+
/**
31+
* An overload of [CoroutineScope.newCoroutineContext] that accepts both the calling context and
32+
* the context overlay as plain function parameters. This saves using `GlobalScope` or allocating
33+
* a new anonymous `CoroutineScope` when `runBlocking {}` constructs a coroutine.
1634
*/
1735
@ExperimentalCoroutinesApi
18-
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
19-
val combined = coroutineContext.foldCopiesForChildCoroutine() + context
20-
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
21-
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
22-
debug + Dispatchers.Default else debug
36+
public fun newCoroutineContext(
37+
callingContext: CoroutineContext,
38+
addedContext: CoroutineContext
39+
): CoroutineContext {
40+
val interceptor = callingContext[CoroutineStartInterceptor]
41+
42+
val childContext: CoroutineContext =
43+
if (interceptor != null) {
44+
interceptor.interceptContext(
45+
callingContext = callingContext.foldCopiesForChildCoroutine(),
46+
addedContext = addedContext
47+
)
48+
} else {
49+
// Default context inheritance: fold left.
50+
callingContext.foldCopiesForChildCoroutine() + addedContext
51+
}
52+
53+
val withDebug = if (DEBUG) childContext + CoroutineId(COROUTINE_ID.incrementAndGet()) else childContext
54+
55+
return if (childContext !== Dispatchers.Default && childContext[ContinuationInterceptor] == null)
56+
withDebug + Dispatchers.Default else withDebug
2357
}
2458

2559
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import org.junit.Test
8+
import kotlin.coroutines.*
9+
import kotlin.test.*
10+
11+
class CoroutineStartInterceptorSimpleTest: TestBase() {
12+
13+
/**
14+
* A [CoroutineContext.Element] holding an integer, used to enumerate elements allocated
15+
* during context construction.
16+
*/
17+
private class IntegerContextElement(
18+
val number: Int
19+
): CoroutineContext.Element {
20+
public companion object Key : CoroutineContext.Key<IntegerContextElement>
21+
override val key = Key
22+
}
23+
24+
/**
25+
* Inserts a new, unique [IntegerContextElement] into each new coroutine context constructed,
26+
* overriding any present.
27+
*/
28+
class AddElementInterceptor : CoroutineStartInterceptor,
29+
AbstractCoroutineContextElement(CoroutineStartInterceptor.Key) {
30+
private var integerSource = 0
31+
32+
override fun interceptContext(
33+
callingContext: CoroutineContext,
34+
addedContext: CoroutineContext
35+
): CoroutineContext {
36+
integerSource += 1
37+
return callingContext + addedContext + IntegerContextElement(integerSource)
38+
}
39+
}
40+
41+
@Test
42+
fun testContextInterceptorOverridesContextElement() = runTest {
43+
assertNull(coroutineContext[IntegerContextElement.Key])
44+
45+
runBlocking(AddElementInterceptor()) {
46+
47+
}
48+
}
49+
50+
@Test
51+
fun testAsyncDoesNotInterceptFromAddedContext() = runTest {
52+
assertNull(coroutineContext[IntegerContextElement.Key])
53+
54+
async(AddElementInterceptor()) {
55+
assertNull(coroutineContext[IntegerContextElement.Key])
56+
}.join()
57+
}
58+
59+
@Test
60+
fun testLaunchDoesNotInterceptFromAddedContext() = runTest {
61+
assertNull(coroutineContext[IntegerContextElement.Key])
62+
63+
launch(AddElementInterceptor()) {
64+
assertNull(coroutineContext[IntegerContextElement.Key])
65+
}.join()
66+
}
67+
68+
@Test
69+
fun testRunBlockingInterceptsFromAddedContext() = runTest {
70+
assertNull(coroutineContext[IntegerContextElement.Key])
71+
72+
runBlocking(AddElementInterceptor()) {
73+
assertEquals(
74+
1,
75+
coroutineContext[IntegerContextElement.Key]!!.number
76+
)
77+
}
78+
}
79+
80+
@Test
81+
fun testChildCoroutineContextInterceptedFromCallingContext() = runTest {
82+
assertNull(coroutineContext[IntegerContextElement.Key])
83+
84+
launch(AddElementInterceptor()) {
85+
launch {
86+
assertEquals(
87+
1,
88+
coroutineContext[IntegerContextElement.Key]!!.number
89+
)
90+
}.join()
91+
launch {
92+
assertEquals(
93+
2,
94+
coroutineContext[IntegerContextElement.Key]!!.number
95+
)
96+
}.join()
97+
}
98+
}
99+
100+
@Test
101+
fun testChildCoroutineContextInterceptedFromCallingContextNotAddedContext() = runTest {
102+
assertNull(coroutineContext[IntegerContextElement.Key])
103+
104+
launch(AddElementInterceptor()) {
105+
launch {}.join()
106+
107+
launch(AddElementInterceptor()) {// Count of this interceptor is 0.
108+
assertEquals(
109+
2,
110+
coroutineContext[IntegerContextElement.Key]!!.number
111+
)
112+
launch {
113+
assertEquals(
114+
1, // Parent coroutine's context intercepted.
115+
coroutineContext[IntegerContextElement.Key]!!.number
116+
)
117+
}.join()
118+
}.join()
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)