From 2f338463cce831cdbb4ce7e9c5d65ed2a2491e4a Mon Sep 17 00:00:00 2001 From: Ilya Nemtsev Date: Sun, 29 Mar 2026 21:11:51 +0700 Subject: [PATCH] fix and test for closed concurrency --- .../kotlin/org/koin/core/scope/Scope.kt | 1 + .../kotlin/org/koin/core/ConcurrencyTest.kt | 60 ++++++++++++++++++- .../org/koin/core/ScopeClosedVolatileTest.kt | 22 +++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ScopeClosedVolatileTest.kt diff --git a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/scope/Scope.kt b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/scope/Scope.kt index fb65e876b..38dd5141f 100644 --- a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/scope/Scope.kt +++ b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/scope/Scope.kt @@ -77,6 +77,7 @@ class Scope( _parameterStack ?: ThreadLocal>().also { _parameterStack = it } } + @Volatile private var _closed: Boolean = false val logger: Logger get() = _koin.logger diff --git a/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ConcurrencyTest.kt b/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ConcurrencyTest.kt index d38984f61..bba77e13d 100644 --- a/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ConcurrencyTest.kt +++ b/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ConcurrencyTest.kt @@ -101,4 +101,62 @@ class ConcurrencyTest { assertTrue(result.values.all { it == 1 }) } -} \ No newline at end of file + + @OptIn(KoinInternalApi::class) + @Test + fun closed_flag_should_be_visible_across_coroutines_after_close() = runTest { + // Verify that scope.closed is visible across threads after close() returns. + // Without @Volatile on _closed, a write in close() on one thread may not be + // visible to a read on another thread due to CPU cache staleness. + // + // Strategy: many coroutines on Dispatchers.Default (real threads) each create + // a scope, close it, and then verify the closed flag is visible. + + startKoin { + modules(module { + scope { + scoped { CounterService() } + } + }) + } + + val iterations = 10_000 + val staleReads = KoinPlatformTools.safeHashMap() + + try { + coroutineScope { + val jobs = List(iterations) { i -> + launch(kotlinx.coroutines.Dispatchers.Default) { + val koin = KoinPlatform.getKoin() + val counter = Counter() + val scope = koin.createScope(counter.getScopeId()) + scope.get() + + // Close on this thread + scope.close() + + // Immediately check visibility - on a different CPU core, + // without @Volatile this read may see stale false + if (!scope.closed) { + // Double-check after yield to give caches time + kotlinx.coroutines.yield() + if (!scope.closed) { + staleReads[scope.id] = true + } + } + } + } + jobs.joinAll() + } + } finally { + stopKoin() + } + + assertTrue( + staleReads.isEmpty(), + "Detected ${staleReads.size} stale reads of _closed flag out of $iterations iterations. " + + "This indicates _closed is not safely published across threads - it should be @Volatile." + ) + } + +} diff --git a/projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ScopeClosedVolatileTest.kt b/projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ScopeClosedVolatileTest.kt new file mode 100644 index 000000000..49e324bdf --- /dev/null +++ b/projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ScopeClosedVolatileTest.kt @@ -0,0 +1,22 @@ +package org.koin.core + +import org.koin.core.scope.Scope +import java.lang.reflect.Modifier +import kotlin.test.Test +import kotlin.test.assertTrue + +class ScopeClosedVolatileTest { + + @Test + fun `_closed field should be marked volatile for cross-thread visibility`() { + // Deterministic check: verify the _closed field has the volatile modifier. + // Without @Volatile, writes in close() under synchronized may not be visible + // to unsynchronized reads in checkScopeIsOpen() on other threads. + val field = Scope::class.java.getDeclaredField("_closed") + assertTrue( + Modifier.isVolatile(field.modifiers), + "Scope._closed field must be @Volatile to guarantee visibility across threads. " + + "Without it, a scope closed on Thread A may appear open on Thread B.", + ) + } +}