InsertKoinIO / koin-annotations

Koin Annotations - About Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform insert-koin.io
https://insert-koin.io
Apache License 2.0
123 stars 30 forks source link

Generated modules use global state and break context isolation. #102

Open Jadarma opened 7 months ago

Jadarma commented 7 months ago

Describe the bug Module code generation forces instantiating the module class twice and breaks context isolation. When using annotations, it is impossible to run Koin in parallel, such as in the case of unit / integration tests, as the same one global module will be shared across Koin instances.

To Reproduce Consider the following module:

@Module
class MyModule {

    @Named("Seed")
    @Single(createdAtStart = true)
    fun randomSeed() = SecureRandom.getInstanceStrong().nextLong()
}

Which I then install in a Ktor Application:

fun Application.configureDI() {
    val moduleInstance = MyModule()

    install(Koin) {
        modules(moduleInstance.module)
    }

    val applicationInstance = this
    val koinInstance = applicationInstance.getKoin()
    val seedInstance = koinInstance.get<Long>(named("Seed"))

    println("Ktor $applicationInstance, uses koin $koinInstance, and module $moduleInstance has seed $seedInstance")
}

If I run two Ktor test applications in parallel, I get the following logged:

Ktor io.ktor.server.application.Application@2e7d7e03 uses koin org.koin.core.Koin@5b54ae0e, and module com.example.MyModule@66e53ab8 has seed -4596009551291032851
Ktor io.ktor.server.application.Application@3a3a4af6 uses koin org.koin.core.Koin@2efa5d9d, and module com.example.MyModule@6ceeaad3 has seed -4596009551291032851

So, two different Ktor instances, with two different Koin instances, with two different module instances, ended up generating the same seed... hmm.

Looking at the generated code, I see:

public val io_example_MyModule : Module = module {
    val moduleInstance = io.example.MyModule()
    single(qualifier=org.koin.core.qualifier.StringQualifier("Seed"),createdAtStart=true) { moduleInstance.randomSeed() } bind(kotlin.Long::class)
}
public val io.example.MyModule.module : org.koin.core.module.Module get() = io_example_MyModule

Even though the module is an extension over an already created instance of MyModule, it resolves to a global variable which instantiates it itself once more, and that instance is then used to provide the values, regardless of module or Koin instance they were defined in.

Expected behavior The .module extension should get a module based on the instance it was called on instead of global state.

Koin project used and used version: io.insert-koin:koin-bom:3.5.1 io.insert-koin:koin-annotations-bom:1.3.0

Extra notes: This is a more trivial example for reproducibility, how I originally found this bug is that I was trying to execute parallel integration tests with different databases, but I got constraint violation exceptions, which made no sense unless the same connection was reused. I was able to trace back the culprit to this. If I instead declare the module with the normal DSL, then the issue goes away.

arnaudgiuliani commented 5 months ago

yes, this is clearly a limitation in the current design. It needs to be followed-up for next milestone, as it's an important change

stale[bot] commented 3 days ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Jadarma commented 2 days ago

Bad stale bot, bad!