Skip to content

Support injection of default context elements #2932

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

Open
OliverO2 opened this issue Sep 13, 2021 · 3 comments
Open

Support injection of default context elements #2932

OliverO2 opened this issue Sep 13, 2021 · 3 comments

Comments

@OliverO2
Copy link
Contributor

OliverO2 commented Sep 13, 2021

CoroutineScope.newCoroutineContext currently sets up coroutine contexts via a hard-wired mechanism:

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = coroutineContext + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}

Here is a use case where a customizable set of default context elements would help:

The Kotest assertions library has a Soft Assertions feature which collects multiple failures and finally reports them in a single exception. It uses a thread-local error-collecting context to achieve this. ThreadLocal<T>.asContextElement makes this work with thread-switching coroutines.

However, the user must either use the Kotest framework (which sets up the coroutine context) or remember to insert a provided errorCollectorContextElement wherever a top-level coroutine context is created:

    @Test
    fun `assertSoftly demonstration`() = runBlocking(Dispatchers.Unconfined + errorCollectorContextElement) {
        // Two intentional failures will be collected reported as one.
        assertSoftly {
            1 + 1 shouldBe 2 + 1
            delay(10)  // the coroutine will switch threads here
            2 + 3 shouldBe 5 + 1
        }
    }

Ideally, the Kotest assertions library would take care of such initialization, allowing the user to just use runBlocking(Dispatchers.Unconfined) { ... } without having to care about the errorCollectorContextElement.

An idea would be to provide a DefaultContextFactory, which could be set up on the JVM via ServiceLoader, analogous to the existing MainDispatcherFactory.

Related:

@dkhalanskyjb
Copy link
Collaborator

dkhalanskyjb commented Jul 10, 2024

Notable: both use cases reported for this involve CopyableThreadContextElement. I think we can easily provide a limited version of default context element injection just for CopyableThreadContextElement. This way, nothing strange should happen.

Question: is there a reason why https://github.com/kotest/kotest/blob/42d571cb22bae6ef60db701615b63c27130223b8/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/ErrorCollector.kt#L40-L62 is not just threadLocalErrorCollector.asContextElement()?

@OliverO2
Copy link
Contributor Author

ErrorCollectorContextElement is a CopyableThreadContextElement which does a deep copy of the underlying CoroutineLocalErrorCollector. If we'd use a plain asContextElement() instead, we'd miss ErrorCollector isolation. The test "concurrent withClue invocations should be isolated from each other" then fails.

@dovchinnikov
Copy link
Contributor

ServiceLoader API will also help us with tracing and other observability. We also wanted to override the default dispatcher but gave up. Since we have a separate fork, we might return to it, so it'd be great to have the flexibility in the library instead. Also we'd like to be able to bind an arbitrary coroutine to our global root job if it does not have a parent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants