Skip to content

Commit 6290bcb

Browse files
1.0.0-GA1 Fix Thread Collector CTA dispatch
1 parent c2033bb commit 6290bcb

4 files changed

Lines changed: 85 additions & 19 deletions

File tree

koin-compiler-plugin/src/org/koin/compiler/plugin/KoinPluginComponentRegistrar.kt

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,33 @@ import java.util.concurrent.atomic.AtomicInteger
2828
* FIR extensions don't receive configuration directly, so we store it globally.
2929
*/
3030
object KoinPluginLogger {
31+
/**
32+
* Per-compilation message collector storage.
33+
*
34+
* Was `@Volatile var messageCollector: MessageCollector`. Under K2's parallel-daemon mode
35+
* multiple compilations can share a daemon, and each one's `init()` writes through this
36+
* singleton. With a single volatile field, compilation B's `init()` could overwrite A's
37+
* collector while A was still mid-IR-generation — A's subsequent diagnostics would land in
38+
* B's output stream (wrong build's log).
39+
*
40+
* `InheritableThreadLocal` scopes the collector to the compilation's entry thread and any
41+
* threads it spawns (IR generation occasionally fans out). The volatile `fallbackCollector`
42+
* preserves the original behavior for callers reached without a per-thread context — bare
43+
* CLI, unit tests, and the legacy alias in [KoinPluginMessageCollector].
44+
*
45+
* Read order in [effectiveCollector]: thread-local first, fallback second.
46+
*/
47+
@PublishedApi
48+
internal val threadCollector: InheritableThreadLocal<MessageCollector?> =
49+
InheritableThreadLocal()
50+
3151
@Volatile
3252
@PublishedApi
33-
internal var messageCollector: MessageCollector = MessageCollector.NONE
53+
internal var fallbackCollector: MessageCollector = MessageCollector.NONE
54+
55+
@PublishedApi
56+
internal val effectiveCollector: MessageCollector
57+
get() = threadCollector.get() ?: fallbackCollector
3458

3559
@Volatile
3660
var userLogsEnabled: Boolean = false
@@ -86,15 +110,41 @@ object KoinPluginLogger {
86110
* trailing AI-assist CTA so it shares a per-file output bucket with the error,
87111
* preventing renderers (Gradle's K2 collector) from sorting the location-less CTA
88112
* ahead of file-anchored messages.
113+
*
114+
* Per-thread (InheritableThreadLocal) for the same daemon-parallel reason as
115+
* [threadCollector] — two compilations would otherwise stomp each other's anchor.
89116
*/
90-
@Volatile
91-
private var lastDiagnosticLocation: CompilerMessageLocation? = null
117+
private val lastDiagnosticLocation: InheritableThreadLocal<CompilerMessageLocation?> =
118+
InheritableThreadLocal()
119+
120+
/**
121+
* Bind [collector] to the current thread's slot for the duration of one compilation phase
122+
* (typically `IrGenerationExtension.generate`). Use a try/finally with [unbindThreadCollector]
123+
* to ensure it's cleared even on exceptions.
124+
*
125+
* Needed because Gradle daemon worker pools can dispatch a compilation's IR phase on a
126+
* different thread than the one that called [init]; without a re-bind, the per-thread
127+
* collector slot would be empty and reads would fall through to [fallbackCollector]
128+
* (which a parallel compilation may have overwritten).
129+
*/
130+
fun bindThreadCollector(collector: MessageCollector) {
131+
threadCollector.set(collector)
132+
}
133+
134+
fun unbindThreadCollector() {
135+
threadCollector.remove()
136+
}
92137

93138
/**
94139
* Initialize the logger with configuration from the compiler.
140+
*
141+
* Writes the collector to both the per-thread slot (so this compilation's diagnostics
142+
* stay scoped) and to the volatile fallback (for callers reached outside the compilation
143+
* thread group — primarily the legacy [KoinPluginMessageCollector] alias).
95144
*/
96145
fun init(collector: MessageCollector, userLogs: Boolean, debugLogs: Boolean, unsafeDslChecks: Boolean = true, skipDefaultValues: Boolean = true, compileSafety: Boolean = true, aiAssist: Boolean = true, moduleId: String? = null, lookupTracker: LookupTracker? = null) {
97-
messageCollector = collector
146+
threadCollector.set(collector)
147+
fallbackCollector = collector
98148
userLogsEnabled = userLogs
99149
debugLogsEnabled = debugLogs
100150
unsafeDslChecksEnabled = unsafeDslChecks
@@ -104,7 +154,7 @@ object KoinPluginLogger {
104154
this.moduleId = moduleId?.takeIf { it.isNotBlank() }
105155
this.lookupTracker = lookupTracker
106156
highestDiagnosticSeverity.set(0)
107-
lastDiagnosticLocation = null
157+
lastDiagnosticLocation.remove()
108158
}
109159

110160
/**
@@ -118,7 +168,7 @@ object KoinPluginLogger {
118168
*/
119169
inline fun user(message: () -> String) {
120170
if (userLogsEnabled) {
121-
messageCollector.report(CompilerMessageSeverity.WARNING, "[Koin] ${message()}")
171+
effectiveCollector.report(CompilerMessageSeverity.WARNING, "[Koin] ${message()}")
122172
}
123173
}
124174

@@ -133,7 +183,7 @@ object KoinPluginLogger {
133183
*/
134184
inline fun debug(message: () -> String) {
135185
if (debugLogsEnabled) {
136-
messageCollector.report(CompilerMessageSeverity.WARNING, "[Koin-Debug] ${message()}")
186+
effectiveCollector.report(CompilerMessageSeverity.WARNING, "[Koin-Debug] ${message()}")
137187
}
138188
}
139189

@@ -142,7 +192,7 @@ object KoinPluginLogger {
142192
* Use for critical messages that should never be silenced (e.g., @Monitor without SDK).
143193
*/
144194
fun warn(message: String) {
145-
messageCollector.report(CompilerMessageSeverity.WARNING, "[Koin] $message")
195+
effectiveCollector.report(CompilerMessageSeverity.WARNING, "[Koin] $message")
146196
}
147197

148198
/**
@@ -153,7 +203,7 @@ object KoinPluginLogger {
153203
*/
154204
inline fun userFir(message: () -> String) {
155205
if (userLogsEnabled) {
156-
messageCollector.report(CompilerMessageSeverity.WARNING, "[Koin-FIR] ${message()}")
206+
effectiveCollector.report(CompilerMessageSeverity.WARNING, "[Koin-FIR] ${message()}")
157207
}
158208
}
159209

@@ -165,7 +215,7 @@ object KoinPluginLogger {
165215
*/
166216
inline fun debugFir(message: () -> String) {
167217
if (debugLogsEnabled) {
168-
messageCollector.report(CompilerMessageSeverity.WARNING, "[Koin-Debug-FIR] ${message()}")
218+
effectiveCollector.report(CompilerMessageSeverity.WARNING, "[Koin-Debug-FIR] ${message()}")
169219
}
170220
}
171221

@@ -174,7 +224,7 @@ object KoinPluginLogger {
174224
* This will cause compilation to fail.
175225
*/
176226
fun error(message: String) {
177-
messageCollector.report(CompilerMessageSeverity.ERROR, "[Koin] $message")
227+
effectiveCollector.report(CompilerMessageSeverity.ERROR, "[Koin] $message")
178228
}
179229

180230
/**
@@ -185,7 +235,7 @@ object KoinPluginLogger {
185235
val location = if (filePath != null) {
186236
CompilerMessageLocation.create(filePath, line, column, null)
187237
} else null
188-
messageCollector.report(CompilerMessageSeverity.ERROR, "[Koin] $message", location)
238+
effectiveCollector.report(CompilerMessageSeverity.ERROR, "[Koin] $message", location)
189239
}
190240

191241
/**
@@ -205,8 +255,8 @@ object KoinPluginLogger {
205255
val location = if (filePath != null && line >= 0) {
206256
CompilerMessageLocation.create(filePath, line, column.coerceAtLeast(0), null)
207257
} else null
208-
if (location != null) lastDiagnosticLocation = location
209-
messageCollector.report(severity, body, location)
258+
if (location != null) lastDiagnosticLocation.set(location)
259+
effectiveCollector.report(severity, body, location)
210260
}
211261

212262
/**
@@ -224,17 +274,17 @@ object KoinPluginLogger {
224274
* Called once per compilation by [org.koin.compiler.plugin.ir.KoinIrExtension] at
225275
* the tail of IR processing.
226276
*/
227-
fun flushAiAssistCta(collector: MessageCollector = messageCollector) {
277+
fun flushAiAssistCta(collector: MessageCollector = effectiveCollector) {
228278
val rank = highestDiagnosticSeverity.get()
229279
if (!aiAssistEnabled || rank == 0) return
230280
val severity = if (rank >= 2) CompilerMessageSeverity.ERROR else CompilerMessageSeverity.WARNING
231281
collector.report(
232282
severity,
233283
"[Koin] → Fix with AI: set up Kotzilla MCP at ${KoinPluginConstants.AI_ASSIST_CTA_URL}",
234-
lastDiagnosticLocation,
284+
lastDiagnosticLocation.get(),
235285
)
236286
highestDiagnosticSeverity.set(0)
237-
lastDiagnosticLocation = null
287+
lastDiagnosticLocation.remove()
238288
}
239289
}
240290

koin-compiler-plugin/src/org/koin/compiler/plugin/ir/KoinIrExtension.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ class KoinIrExtension(
1717
private val messageCollector: MessageCollector,
1818
) : IrGenerationExtension {
1919
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
20+
// Re-anchor the per-thread collector on this compilation's captured MessageCollector.
21+
// Necessary because `KoinPluginLogger.init()` ran on the thread that called
22+
// `registerExtensions`, which may differ from the thread running IR generation in
23+
// parallel-daemon mode. `InheritableThreadLocal` inherits only at thread creation —
24+
// pool workers picked up later won't see init()'s value. Setting here ensures every
25+
// diagnostic emitted during this `generate()` lands on the right compilation's
26+
// collector even if another compilation's `init()` mutated the shared singleton.
27+
KoinPluginLogger.bindThreadCollector(messageCollector)
28+
try {
29+
generateInternal(moduleFragment, pluginContext)
30+
} finally {
31+
KoinPluginLogger.unbindThreadCollector()
32+
}
33+
}
34+
35+
private fun generateInternal(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
2036
KoinPluginLogger.debug { "IR Phase starting for module: ${moduleFragment.name}" }
2137

2238
// Phase 0: Generate bodies for FIR-generated hint functions and registry function

playground-apps/app-annotations/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
agp = "8.13.2"
33
kotlin = "2.3.20"
44
koin = "4.2.1"
5-
koin-plugin = "1.0.0"
5+
koin-plugin = "1.0.0-GA1"
66
composeBom = "2026.03.00"
77
navigation = "2.9.7"
88
room = "2.8.4"

playground-apps/app-dsl/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
agp = "8.13.2"
33
kotlin = "2.3.20"
44
koin = "4.2.1"
5-
koin-plugin = "1.0.0"
5+
koin-plugin = "1.0.0-GA1"
66
composeBom = "2026.03.00"
77
navigation = "2.9.7"
88
room = "2.8.4"

0 commit comments

Comments
 (0)