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

[core] Ensure onClose callbacks are invoked in reversed initialization order #2020

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import org.koin.core.Koin
interface KoinExtension {

/**
* register from Koin instance
* Register from Koin instance
*/
fun onRegister(koin : Koin)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package org.koin.core.instance

import co.touchlab.stately.concurrency.AtomicBoolean
import co.touchlab.stately.concurrency.AtomicLong
import org.koin.core.definition.BeanDefinition
import org.koin.core.error.InstanceCreationException
import org.koin.core.parameter.ParametersHolder
Expand All @@ -28,8 +30,12 @@ import org.koin.mp.Lockable
/**
* Koin Instance Holder
* create/get/release an instance of given definition
*
* Implements [Comparable] to provide factories soring by instance creation order
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: sorting

*/
abstract class InstanceFactory<T>(val beanDefinition: BeanDefinition<T>) : Lockable() {
abstract class InstanceFactory<T>(val beanDefinition: BeanDefinition<T>) : Lockable(), Comparable<InstanceFactory<*>> {
private val instanceCreationOrderSet = AtomicBoolean(false)
private var instanceCreationOrderPosition = Long.MAX_VALUE
Copy link
Contributor

@marcellogalhardo marcellogalhardo Oct 18, 2024

Choose a reason for hiding this comment

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

The current design ties a child instance natural comparison to its insertion ordering within a parent collection. A Comparable is documented as a way to define a class's natural order, not their insertion order. See Comparable and Natural Sort Order.

Instead, what if the InstanceRegistry would handle the instances collection and closing ordering?

Here's a possible approach:

  • The InstanceRegistry could use a ConcurrentLinkedDeque to store keys, ensuring thread-safe ordering of factory registrations.
  • The InstanceRegistry would then access the deque to order the factory instances and close them in the correct order.

Copy link
Member

Choose a reason for hiding this comment

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

same thought here. Consider another proposal @shchuko


/**
* Retrieve an instance
Expand All @@ -47,17 +53,36 @@ abstract class InstanceFactory<T>(val beanDefinition: BeanDefinition<T>) : Locka
context.logger.debug("| (+) '$beanDefinition'")
try {
val parameters: ParametersHolder = context.parameters ?: emptyParametersHolder()
return beanDefinition.definition.invoke(
val instance = beanDefinition.definition.invoke(
context.scope,
parameters,
)
if (instanceCreationOrderSet.compareAndSet(false, true)) {
instanceCreationOrderPosition = INSTANCE_CREATION_ORDER_COUNTER.incrementAndGet()

// just to be sure that counter provides normal values
check(instanceCreationOrderPosition >= 0) {
"Unexpected negative instance creation order position"
}

// it's required to have `Long.MAX_VALUE` instantiations happened to make this check fail,
// which is not likely to happen
check(instanceCreationOrderPosition < Long.MAX_VALUE) {
Copy link
Contributor

@marcellogalhardo marcellogalhardo Oct 18, 2024

Choose a reason for hiding this comment

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

instanceCreationOrderPosition default value is MAX_VALUE and this check throw an exception if it hit MAX_VALUE.

I'm not sure if I understand your code comment above and it seems error prone.

"Instance creation order position reached Long.MAX_VALUE"
}
}
return instance
} catch (e: Exception) {
val stack = KoinPlatformTools.getStackTrace(e)
context.logger.error("* Instance creation error : could not create instance for '$beanDefinition': $stack")
throw InstanceCreationException("Could not create instance for '$beanDefinition'", e)
}
}

override fun compareTo(other: InstanceFactory<*>): Int {
return instanceCreationOrderPosition.compareTo(other.instanceCreationOrderPosition)
}

/**
* Is instance created
*/
Expand All @@ -72,5 +97,6 @@ abstract class InstanceFactory<T>(val beanDefinition: BeanDefinition<T>) : Locka

companion object {
const val ERROR_SEPARATOR = "\n\t"
private val INSTANCE_CREATION_ORDER_COUNTER = AtomicLong(-1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import org.koin.core.scope.Scope
import org.koin.core.scope.ScopeID
import org.koin.mp.KoinPlatformTools.safeHashMap
import kotlin.reflect.KClass
import kotlin.sequences.sortedDescending


@Suppress("UNCHECKED_CAST")
@OptIn(KoinInternalApi::class)
Expand Down Expand Up @@ -152,13 +154,18 @@ class InstanceRegistry(val _koin: Koin) {
}

internal fun dropScopeInstances(scope: Scope) {
_instances.values.filterIsInstance<ScopedInstanceFactory<*>>().forEach { factory -> factory.drop(scope) }
_instances.values
.asSequence()
.filterIsInstance<ScopedInstanceFactory<*>>()
.sortedDescending()
.forEach { factory -> factory.drop(scope) }
}

internal fun close() {
_instances.forEach { (_, factory) ->
factory.dropAll()
}
_instances.values
.asSequence()
.sortedDescending()
.forEach { factory -> factory.dropAll() }
_instances.clear()
}

Expand All @@ -181,12 +188,10 @@ class InstanceRegistry(val _koin: Koin) {
}

private fun unloadModule(module: Module) {
module.mappings.keys.forEach { mapping ->
if (_instances.containsKey(mapping)) {
_instances[mapping]?.dropAll()
_instances.remove(mapping)
}
}
module.mappings.keys
.mapNotNull { mapping -> _instances.remove(mapping) }
.sortedDescending()
.forEach { factory -> factory.dropAll() }
}

fun size(): Int {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.koin.dsl

import org.koin.Simple
import org.koin.core.annotation.KoinInternalApi
import org.koin.core.logger.Level
import org.koin.core.qualifier.named
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class CloseDefinitionTest {
Expand Down Expand Up @@ -50,4 +54,186 @@ class CloseDefinitionTest {
koin.unloadModules(listOf(module))
assertTrue(!closed)
}

@OptIn(KoinInternalApi::class)
@Test
fun test_onClose_order() {
val closeLog = mutableListOf<String>()

val module = module {
single { Simple.ComponentC(get()) } onClose {
if (it != null) {
closeLog += "c"
}
}
single { Simple.ComponentA() } onClose {
if (it != null) {
closeLog += "a"
}
}
single { Simple.ComponentB(get()) } onClose {
if (it != null) {
closeLog += "b"
}
}
}

val koin = koinApplication {
printLogger(Level.DEBUG)
modules(
module,
)
}.koin

assertNotNull(koin.getOrNull<Simple.ComponentC>())
assertEquals(
expected = emptyList(),
actual = closeLog,
)

// check instances sorting meets instantiation order
assertEquals(
expected = listOf(Simple.ComponentA::class, Simple.ComponentB::class, Simple.ComponentC::class),
actual = koin.instanceRegistry.instances.values.sorted().map { it.beanDefinition.primaryType },
)

koin.close()
assertEquals(
expected = listOf("c", "b", "a"),
actual = closeLog,
)
}

@Test
fun test_onClose_order_unload() {
val closeLog = mutableListOf<String>()

val moduleFirst = module {
single { Simple.MySingle(42) } onClose {
if (it != null) {
closeLog += "single"
}
}
}

val moduleSecond = module {
single { Simple.ComponentC(get()) } onClose {
if (it != null) {
closeLog += "c"
}
}
single { Simple.ComponentA() } onClose {
if (it != null) {
closeLog += "a"
}
}
single { Simple.ComponentB(get()) } onClose {
if (it != null) {
closeLog += "b"
}
}
}

val koin = koinApplication {
printLogger(Level.DEBUG)
modules(
moduleFirst,
moduleSecond,
)
}.koin

assertNotNull(koin.getOrNull<Simple.MySingle>())
assertNotNull(koin.getOrNull<Simple.ComponentC>())
assertEquals(emptyList(), closeLog)

koin.unloadModules(listOf(moduleSecond))
assertEquals(
expected = listOf("c", "b", "a"),
actual = closeLog,
)

koin.loadModules(listOf(moduleSecond))
assertNotNull(koin.getOrNull<Simple.ComponentC>())

koin.unloadModules(listOf(moduleSecond))
assertEquals(
expected = listOf("c", "b", "a", "c", "b", "a"),
actual = closeLog,
)

koin.close()
assertEquals(
expected = listOf("c", "b", "a", "c", "b", "a", "single"),
actual = closeLog,
)
}


@Test
fun test_onClose_order_scoped() {
val closeLog = mutableListOf<String>()

val scopeId = "_test_scope_id_"
val scopeKey = named("_test_scope_")

val module = module {
scope(scopeKey) {
scoped { Simple.ComponentC(get()) } onClose {
if (it != null) {
closeLog += "c"
}
}
scoped { Simple.ComponentA() } onClose {
if (it != null) {
closeLog += "a"
}
}
scoped { Simple.ComponentB(get()) } onClose {
if (it != null) {
closeLog += "b"
}
}
}
}

val koin = koinApplication {
printLogger(Level.DEBUG)
modules(
module,
)
}.koin

assertEquals(
expected = emptyList(),
actual = closeLog,
)

koin.createScope(scopeId, scopeKey)
assertEquals(
expected = emptyList(),
actual = closeLog,
)

koin.getScope(scopeId).let {
assertNotNull(it.getOrNull<Simple.ComponentC>())
it.close()
}
assertEquals(
expected = listOf("c", "b", "a"),
actual = closeLog,
)

koin.createScope(scopeId, scopeKey)
assertNotNull(koin.getScope(scopeId).getOrNull<Simple.ComponentC>())
assertEquals(
expected = listOf("c", "b", "a"),
actual = closeLog,
)

koin.close()
assertEquals(
expected = listOf("c", "b", "a", "c", "b", "a"),
actual = closeLog,
)
}
}