InsertKoinIO / koin

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

Koin 3.0.1 upgrade from 2.2.2 causes weird ClassCastException of identical class identifier in KTOR #1083

Closed niom closed 3 years ago

niom commented 3 years ago

Describe the bug After I tried to upgrade our koin Ktor project from 2.2.2 to 3.0.1 I get really weird exception from some of my tests.

java.lang.ClassCastException: class foo.bar.FooBar cannot be cast to class foo.bar.FooBar (foo.bar.FooBar is in unnamed module of loader io.ktor.server.engine.OverridingClassLoader$ChildURLClassLoader @240f6c41; foo.bar.FooBar is in unnamed module of loader 'app')

This is a multimodule project and if I transfer the class FooBar to a different module the tests start to act normal but not in all cases. Smells like race condition or some how the ClassLoaderContext gets changed or corrupted.

Also smells a lot that something has happened in KTOR 1.5.x upgrade to coroutine handling and that's not handled correctly in KTOR. I have this hunch because running tests through gradle gives development mode method missing exception and after upgrading our KTOR dependencies to 1.5.4 I get this weird ClassCastException again.

To Reproduce I have no idea how to reproduce this in any easy manner.

Expected behavior Class loading would work as expected

Koin project used and used version (please complete the following information): Koin 3.0.1 produces this weird exception. With 2.2.2 everything works as expected

Additional moduleDefinition I start the koin with Ktor server instance an try to override some of the modules with test versions at the koin context star up. I chain the module configurations from the multimodule configuration with using modules() chaining.

Really would like to get new ideas how to poke this around. I feel really stuck with this one.

niom commented 3 years ago

Ok, update on this one.

I poked around a little bit more. I get rid of the errors if I don't inject my dependencies as class variables but doing the inject inside the test function it self. Tests work at intellij level but then again when running all the tests with gradle the problem is at hand again.

class SomeTest : KoinTest {
   private val someDependency by inject<Dependency>()

   @Test
   fun doTest() {
      someDependency.doSomething() //this will fail
   }
}
class SomeTest : KoinTest {

   @Test
   fun doTest() {
      val someDependency by inject<Dependency>()
      someDependency.doSomething() //this works ok in idea but fails in gradle
   }
}
niom commented 3 years ago

I poked this even further and now I can confirm that this is KTOR 1.5.x problem starting from KTOR 1.5.0 update.

I started from the 2.2.2 tag and worked my way first to upgrade Kotlin and coroutines to latest 1.4.x and everything worked. Then I upgraded the KTOR to 1.5.x and my tests started to fail.

So something has fundamentally changed in KTOR 1.5.x

niom commented 3 years ago

I have to admit that this was a really painful to solve but I have some good news on this one. The problem is in KTOR server as I confirmed before. This happens due ktor for some very random reason enables development mode automatically and due that the ClassLoader gets mixed up in tests. My best guess is that it has something to do with their autoreload functionality.

I also found a bug report from ktor that helped me to solve this: https://youtrack.jetbrains.com/issue/KTOR-2306

ktor {
    development = false
}

Hopefully that bug is some day fixed.

I would keep this bug open until the KTOR one is fixed.

Oh during by poking around I also tried to upgrade to Kotlin 1.5.0. That didn't end well. Got KoinAlreadyStarted errors and also ClassCastException from tests. Just for heads up for the future.

arnaudgiuliani commented 3 years ago

I'm updating to Ktor 1.6.0, let me know if you can reproduce it

niom commented 3 years ago

Tried to upgrade to ktor 1.6.0 my self. Our tests failed: java.lang.reflect.InvocationTargetException at NativeMethodAccessorImpl.java:-2 Caused by: org.koin.core.error.KoinAppAlreadyStartedException at GlobalContext.kt:44

niom commented 3 years ago

Confirmed that upgrading to koin 3.0.2 this issue was fixed and I could upgrade to both KTOR 1.6.0 and to Kotlin 1.5.10.

arnaudgiuliani commented 3 years ago

great 👍

Marek00Malik commented 2 years ago

Hello, so I have the same issue but with the new Ktor 2.0.1 using Koin 3.2. The problem is when I run tests that inject dependencies (for example in the Route function).

This is the injection that is failing.

fun Route.artworkRouting() {

    val idService by inject<IdService>()

    route("/artworks") {
        get { 
        }
    }
 }

this is the test:


class AuthenticationTest : WordSpec(), KoinTest {
    val dataSource = install(JdbcTestContainerExtension(container)) {
        poolName = "myconnectionpool"
        maximumPoolSize = 8
        idleTimeout = 10000
    }

    private val testDbModule = module {
        single { dataSource }
        single<Jdbi> {
            Jdbi.create(get<DataSource>()).installPlugins().registerColumnMapper(LocalDateTimeMapper())
                .registerArgument(LocalDateTimeArgumentFactory())
        }
    }

    private val applicationConfig = ApplicationConfig("test-application.conf")

    private val testHttpModule = module {
        single { setupClient(mockEnginePB, applicationConfig) }
    }

    override fun extensions(): List<Extension> =
        listOf(KoinExtension(listOf(testDbModule, testHttpModule, RepositoryModule)))

    lateinit var jwt: JwtToken

    init {
        "JWT Authorization" should {
            "Be authorized on correct token" {
                // when
                testApplication {
                    application { Application::testModule }
                    environment {
                        config = ApplicationConfig("test-application.conf")
                    }
                    val client = createClient { install(ContentNegotiation) { json() } }

                    val response = client.get("/api/artworks?artistName=%25Picasso") {
                        header(HttpHeaders.Authorization, "Bearer ${jwt.token}")
                    }

                    // then
                    response shouldHaveStatus HttpStatusCode.NotFound
                }
            }     
      }
 }

This is happening only with the new testFramework. When I run the old withApplication there is no issue.