InsertKoinIO / koin

Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform
https://insert-koin.io
Apache License 2.0
8.98k stars 710 forks source link

Breaking change between koin-ktor versions 3.5.1 and 3.5.3 : can only install Koin in one embeddedServer instance #1738

Closed volkert-fastned closed 8 months ago

volkert-fastned commented 9 months ago

Describe the bug Right up until version 3.5.1 of koin-ktor, it was never a problem to install Koin in multiple embeddedServer instances in a multi-server Ktor application. But since version 3.5.3, this results in a KoinAppAlreadyStartedException.

To Reproduce Steps to reproduce the behavior:

  1. Create a Ktor project that launches multiple embeddedServer instances.
  2. Make sure that only the last embeddedServer instance has wait set to true in the .start() function
  3. Run the application with version 3.5.1 of koin-ktor
  4. Run the application with version 3.5.3 of koin-ktor

Expected behavior The application should start up and run fine with either version, since a patch-level version update should not break compatibility. What actually happens is that this only runs well with version 3.5.1, but with version 3.5.3, the following exception is thrown:

Exception in thread "main" org.koin.core.error.KoinAppAlreadyStartedException: A Koin Application has already been started
    at org.koin.core.context.GlobalContext.register(GlobalContext.kt:44)
    at org.koin.core.context.GlobalContext.startKoin(GlobalContext.kt:56)
    at org.koin.core.context.DefaultContextExtKt.startKoin(DefaultContextExt.kt:34)
    at org.koin.ktor.plugin.KoinPluginKt$Koin$2.invoke(KoinPlugin.kt:37)
    at org.koin.ktor.plugin.KoinPluginKt$Koin$2.invoke(KoinPlugin.kt:35)
    at io.ktor.server.application.CreatePluginUtilsKt.setupPlugin(CreatePluginUtils.kt:301)
    at io.ktor.server.application.CreatePluginUtilsKt.createPluginInstance(CreatePluginUtils.kt:270)
    at io.ktor.server.application.CreatePluginUtilsKt.access$createPluginInstance(CreatePluginUtils.kt:1)
    at io.ktor.server.application.CreatePluginUtilsKt$createApplicationPlugin$2.install(CreatePluginUtils.kt:90)
    at io.ktor.server.application.CreatePluginUtilsKt$createApplicationPlugin$2.install(CreatePluginUtils.kt:83)
    at io.ktor.server.application.ApplicationPluginKt.install(ApplicationPlugin.kt:100)
    at org.example.MainKt$main$2.invoke(Main.kt:35)
    at org.example.MainKt$main$2.invoke(Main.kt:34)
    at io.ktor.server.engine.internal.CallableUtilsKt.executeModuleFunction(CallableUtils.kt:51)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:332)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$launchModuleByName$1.invoke(ApplicationEngineEnvironmentReloading.kt:331)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartupFor(ApplicationEngineEnvironmentReloading.kt:356)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.launchModuleByName(ApplicationEngineEnvironmentReloading.kt:331)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.access$launchModuleByName(ApplicationEngineEnvironmentReloading.kt:32)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:319)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:310)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartup(ApplicationEngineEnvironmentReloading.kt:338)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.instantiateAndConfigureApplication(ApplicationEngineEnvironmentReloading.kt:310)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.createApplication(ApplicationEngineEnvironmentReloading.kt:150)
    at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.start(ApplicationEngineEnvironmentReloading.kt:277)
    at io.ktor.server.netty.NettyApplicationEngine.start(NettyApplicationEngine.kt:216)
    at org.example.MainKt.main(Main.kt:44)
    at org.example.MainKt.main(Main.kt)

Koin module and version: koin-ktor:3.5.3

Snippet or Sample project to help reproduce

Below is an example Ktor application that reproduces the issue. the behavior can simply be compared by changing the value of koinKtorVersion from 3.5.1 to 3.5.3 in gradle.properties. I'll also attach a ZIP file of the entire example project.

package org.example

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import org.koin.logger.SLF4JLogger
import java.math.BigDecimal
import java.time.Instant

private val koinModules = listOf(
    module {
        single { Instant.now() }
        single { BigDecimal("1.0") }
    }

)
fun main() {
    embeddedServer(Netty, port = 8080) {
        install(Koin) {
            SLF4JLogger()
            modules(koinModules)
        }
        routing {
            get("/") {
                call.respondText("Hello world from server 1!")
            }
        }
    }.start(wait = false)

    embeddedServer(Netty, port = 8081) {
        install(Koin) {
            SLF4JLogger()
            modules(koinModules)
        }
        routing {
            get("/") {
                call.respondText("Hello world from server 2!")
            }
        }
    }.start(wait = true)
}
volkert-fastned commented 9 months ago

Here is the entire example project that reproduces the issue, if that helps:

reproduce-koin-breakage-2023-12.zip

volkert-fastned commented 9 months ago

What I also noticed with that same version upgrade (to version 3.5.3) was that suddenly, tests started to fail with the same exception, requiring me to add something like this to get them to work again:

    @AfterEach
    fun tearDown() {
        stopKoin()
    }

I saw some documentation referring to this, but it's still weird that it was never necessary (not even in 3.5.1) until I tried upgrading to version 3.5.3. It seems related to this issue, so I'm mentioning this here as well.

volkert-fastned commented 8 months ago

Any news on this? I'm concerned that such a behavioral change in a patch update might also manifest itself at runtime somehow.

arnaudgiuliani commented 8 months ago

Thanks for all your feedback and content. Did you check to use KoinIsolated plugin instead of normal Koin plugin? Here is some doc: https://insert-koin.io/docs/reference/koin-ktor/ktor-isolated

It would help run instances isolated

alturkovic commented 8 months ago

@arnaudgiuliani I can confirm that using KoinIsolated solved all the issues I had with Koin in my ktor tests, thanks for the tip!

arnaudgiuliani commented 8 months ago

cool :)

volkert-fastned commented 8 months ago

@arnaudgiuliani Thanks for the suggestion, I'll look into it. However, my primary concern that led me to open this issue in the first place was the fact that there is compatibility breakage between 3.5.1 and 3.5.3. Was this an exceptional occurrence, or can we expect such breakage to occur more often with future patch-level version updates?

It worked fine without KoinIsolated in 3.5.1, but not in 3.5.3.

Sorry if I'm being a bit nit-picky about it. But the way how version numbers (with an implied SemVer structure) are updated does raise certain expectations in terms of stability of behavior. I mean: if this breaks, what else might break, especially at run-time? Thanks.

volkert-fastned commented 8 months ago

Sorry, I didn't read your sorry for the break answer in issue https://github.com/InsertKoinIO/koin/issues/1750#issuecomment-1908150608 until just now. From that, I take it that this was an unintended breaking patch change.

Thanks. And please understand that I absolutely appreciate your hard work on this important project. 🙂

arnaudgiuliani commented 8 months ago

Sure; It was an attempt to isolate context from Ktor perspective, but it was breaking a lot. I should have tested feedback with RC releases. It should be as breaking in next versions.

volkert-fastned commented 7 months ago

@arnaudgiuliani What if I want to continue sharing the same Koin context between multiple embeddedServer instances in the same Ktor application, when using Koin version 3.5.3 and onwards? I don't want separate singleton instances in different contexts, since that would be a waste of resources. Thanks.

volkert-fastned commented 7 months ago

@arnaudgiuliani Oh wait. If I pass the same Koin module with singletons to different isolated Koin contexts, will those separate Koin contexts share the same singleton instances?

volkert-fastned commented 7 months ago

@arnaudgiuliani Oh wait. If I pass the same Koin module with singletons to different isolated Koin contexts, will those separate Koin contexts share the same singleton instances?

I answered my own question here. TL;DR: yes, singletons will remain singletons (and thus will not have duplicate instances), even between isolated Koin contexts. ✅

arnaudgiuliani commented 7 months ago

ok good :)