Skip to content
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

Fixing race condition in Scope #1561

Closed
wants to merge 1 commit into from

Conversation

octa-one
Copy link
Contributor

A race condition may occur when using the same Scope from different threads.
Now only add and remove calls are synchronized. And operations between these calls can be in a race.

Imagine that 2 threads are simultaneously resolving instances from some scope. These instances need parameters to be created. This means that some of their dependencies will be found in _parameterStack.

We call the resolveInstance method simultaneously from both threads.
The first thread puts the parameters on the _parameterStack. The second thread does the same. Next, the first thread calls _parameterStack.firstOrNull() and gets parameters from the second thread.
It turns out that the threads swapped their parameters.

This could be solved by additional synchronization, but in fact the threads don't need to be synchronized here. Resolution of particular dependencies takes place on a single thread, the main thing is to make sure that the stack is not changed from the outside during the resolution.
ThreadLocal is suitable for this purpose.

And since we made _parameterStack thread local, we don't need to additionally synchronize access to it and can remove KoinPlatformTools.synchronized(this).

@@ -71,6 +71,7 @@ kotlin {
dependsOn commonMain
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-js'
implementation "co.touchlab:stately-concurrency:1.2.2"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm approving this PR because the entire code make senses. But I don't know if @kotzilla-io thinks in go on with add or be accoupled with other library.

It's not a techinical issue, it's just approach, ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This library is already in use for the native platform. To be more specific, for the co.touchlab.stately.concurrency.Lock.
I just included the same version for the JS platform.
So no new dependencies were added.

@@ -56,7 +57,7 @@ data class Scope(
private val _callbacks = arrayListOf<ScopeCallback>()

@KoinInternalApi
val _parameterStack = ArrayDeque<ParametersHolder>()
val _parameterStackLocal = ThreadLocal<ArrayDeque<ParametersHolder>>()
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand your PR description and why you are doing this but I can't help to think these modifications may introduce side effects we are not aware of.

The previous implementation would lock in any modification in the collection elements, for example:

KoinPlatformTools.synchronized(this@Scope) {
  // Locks until addFirst finishes, collection modification.
  _parameterStack.addFirst(parameters)
}

The new implementation seems to only lock during the variable read but not when the collection elements are modified. For example:

// Lock in access...
val localDeque = _parameterStackLocal.get()

// Further operations are not thread safe:
localDeque.addFirst(parameters)
localDeque.removeFirstOrNull()

Again, I understand that is intentional based on your description but maybe we want to be conservative here and still handle elements modification in a type safe manner?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the review!
_parameterStackLocal.get() will return different collections for different threads. So there cannot be a race condition here. Each thread will work exclusively with its own collection.

Copy link
Contributor

Choose a reason for hiding this comment

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

That is interesting, thank you for clarifying. Sounds good to me so.

@arnaudgiuliani arnaudgiuliani added this to the core-3.4.1 milestone May 10, 2023
@arnaudgiuliani arnaudgiuliani self-assigned this May 10, 2023
@arnaudgiuliani arnaudgiuliani added core status:checking currently in analysis - discussion or need more detailed specs type:contribution labels May 10, 2023
@arnaudgiuliani
Copy link
Member

Checking in progress 👍

@arnaudgiuliani arnaudgiuliani modified the milestones: core-3.4.1, core-3.5.0 May 29, 2023
@arnaudgiuliani
Copy link
Member

It'sa serious case here. Do you have a unit test to reproduce this race condition problem?

@gerbiljames
Copy link

The test provided in this comment reproduces the issue for me, if I remove the async {} call the test passes.

@arnaudgiuliani
Copy link
Member

I will deal with conflicts. I'm testing this PR 👍

@arnaudgiuliani arnaudgiuliani added status:accepted accepted to be developed and removed status:checking currently in analysis - discussion or need more detailed specs labels Sep 4, 2023
@arnaudgiuliani arnaudgiuliani changed the base branch from 3.5.0 to bugfix/parameters-race September 4, 2023 09:36
@arnaudgiuliani arnaudgiuliani changed the base branch from bugfix/parameters-race to 3.5.0 September 4, 2023 09:36
@arnaudgiuliani
Copy link
Member

pushed your branch rebased on 3.5.0: bugfix/parameters-race
0a6422c

Repushing it from Koin repo

@arnaudgiuliani
Copy link
Member

closing this #1643 to continue on #1643

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core status:accepted accepted to be developed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants