Skip to content

Conversation

@sergeys-opera
Copy link

Scope was incorrectly using ThreadLocal leading to a race condition. The actual issue was not putting ArrayDeque in a thread-local variable but creating the thread-local variable itself. Several concurrent threads could have created several instances of the parameterStack thread-local variable and only the last one would have used.
The correct usage of ThreadLocal is to instantiate it directly and let the ThreadLocal machinery to deal with synchronization.

This commit also adds a test that was failing prior to the fix. I've also added iosSimulatorArm64 to the :core:koin-test-coroutines to test the fix on iOS.

This change fixes #2305.

`Scope` was incorrectly using `ThreadLocal` leading to a race
condition. The actual issue was not putting `ArrayDeque` in a
thread-local variable but creating the thread-local variable itself.
Several concurrent threads could have created several instances
of the `parameterStack` thread-local variable and only the last one
would have used.
The correct usage of `ThreadLocal` is to instantiate it directly
and let the `ThreadLocal` machinery to deal with synchronization.

This commit also adds a test that was failing prior to the fix.
I've also added `iosSimulatorArm64` to the `:core:koin-test-coroutines`
to test the fix on iOS.

This change fixes InsertKoinIO#2305.

private fun getOrCreateParameterStack(): ArrayDeque<ParametersHolder> {
return parameterStack?.get() ?: ArrayDeque<ParametersHolder>().let { parameterStack = ThreadLocal(); parameterStack?.set(it) ; it }
val oldStack = parameterStack.get()
Copy link
Member

Choose a reason for hiding this comment

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

You remove ThreadLocal capacity to set those parameters?

Copy link
Author

Choose a reason for hiding this comment

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

I don't. If the ThreadLocal parameterStack holds a parameters list already, it's returned.
If it's not, a new list is created and the existing parameterStack is updated.
The code works similarly to how it worked before (albeit not a one liner, I found it confusing to read) with one difference - instead of creating a new instance of a ThreadLocal variable we only create a list instance and update the existing ThreadLocal variable. This is how ThreadLocal is supposed to work.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a race condition in Scope’s use of thread-local parameters when resolving dependencies with parametersOf and adds a regression test that previously failed. It also enables iosSimulatorArm64 for the koin-test-coroutines module to validate the fix on iOS.

  • Replace racy recreation of ThreadLocal with a single, stable ThreadLocal field managed via get/set/remove.
  • Add a multi-threaded test ensuring parametersOf works concurrently.
  • Enable iosSimulatorArm64 target for test coverage on iOS.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
projects/core/koin-core/src/commonMain/kotlin/org/koin/core/scope/Scope.kt Fix race by instantiating a single ThreadLocal<ArrayDeque> and managing per-thread stacks via get/set/remove; minor import cleanup.
projects/core/koin-test-coroutines/src/commonTest/kotlin/org/koin/test/ParametersOfMultiThreadedTest.kt New regression test that launches concurrent resolution using parametersOf and verifies both components resolve.
projects/core/koin-test-coroutines/build.gradle.kts Add iosSimulatorArm64() target to run the new test on iOS simulator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@arnaudgiuliani arnaudgiuliani added this to the 4.2.0 milestone Dec 2, 2025
It is not reset anymore, no need for it to be a var
Not that it matters, as the scope is being closed and the memory
will be released anyway, but it's more accurate and robust to
call `remove` instead of clearing the list manually.
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.

Can't invoke factories with parameters from different threads

2 participants