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

Fix concurrency issue for Single definitions #914

Merged
merged 3 commits into from
Oct 8, 2020

Conversation

Sloy
Copy link

@Sloy Sloy commented Oct 8, 2020

The bug

We were having some crashes in our production app when injecting a Segment SDK (analytics tracker). This SDK has the particularity that only one instance can be created with the same tag. (Source for reference)

We are constructing this SDK inside a Koin's single definition. In theory, single only creates one instance of the bean, even in multi-threaded environments. But in our millions of daily sessions we get a few hundred crashes where this assumption fails.

We managed to reproduce the issue consistently in a unit test by simulating concurrent injections a few thousands of times. 🤯
I'm submitting in the PR the most simple version of the test that I could write (our initial test was way more complicated 😂 ). The number of repetitions and threads come from trial and error, with lower numbers the test would sometime give a false positive. I'm not sure if you want to keep the test, or if this is the right place to put it. Let me know.

Screenshot 2020-10-08 at 10 49 17

The cause

After some research, I came to the conclusion that the issue lies in the concurrency code from SingleInstanceFactory.
The synchronization code in the create() function works fine, but there's one little operation that happens outside the synchronized block: the value = assignment.
The bug is so hard to reproduce because the affected code is such a tiny part!
Screenshot 2020-10-08 at 11 17 24

The fix

I surrounded the code in get() with another synchronized, and that seems to fix the issue. 🎉
I'm no concurrency expert myself, but I can't think of any problems with this solution.

@arnaudgiuliani
Copy link
Member

arnaudgiuliani commented Oct 8, 2020

Good catch.

Then we don't need to keep it on create:

 override fun create(context: InstanceContext): T {
        return if (value == null) {
            super.create(context)
        } else value ?: error("Single instance created couldn't return value")
    }

    @Suppress("UNCHECKED_CAST")
    override fun get(context: InstanceContext): T {
        return synchronized(this) {
            if (!isCreated()) {
                value = create(context)
            }
            value ?: error("Single instance created couldn't return value")
        }
    }

Copy link
Member

@arnaudgiuliani arnaudgiuliani left a comment

Choose a reason for hiding this comment

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

Remove sycnhonized from create 👍

@arnaudgiuliani arnaudgiuliani added this to the 2.2.0 milestone Oct 8, 2020
@arnaudgiuliani arnaudgiuliani added status:accepted accepted to be developed type:issue labels Oct 8, 2020
@Sloy
Copy link
Author

Sloy commented Oct 8, 2020

Done!

@arnaudgiuliani
Copy link
Member

Great work 👍

@arnaudgiuliani arnaudgiuliani changed the base branch from master to 2.2.0 October 8, 2020 15:38
@arnaudgiuliani arnaudgiuliani changed the base branch from 2.2.0 to master October 8, 2020 15:39
@arnaudgiuliani arnaudgiuliani merged commit 34dd6f3 into InsertKoinIO:master Oct 8, 2020
@Sloy Sloy deleted the single-concurrency branch October 9, 2020 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status:accepted accepted to be developed type:issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants