rspec/rules/S6306/kotlin/rule.adoc

96 lines
3.4 KiB
Plaintext

== Why is this an issue?
Kotlin coroutines follow the principle of structured concurrency. This helps in preventing resource leaks and ensures that scopes are only exited once all child coroutines have exited. Hence, structured concurrency enables developers to build concurrent applications while having to worry less about cleaning up concurrent tasks manually.
It is possible to break this concept of structured concurrency in various ways. Generally, this is not advised, as it can open the door to coroutines being leaked or lost. Ask yourself if breaking structured concurrency here is really necessary for the application's business logic, or if it could be avoided by refactoring parts of the code.
This rule raises an issue when it detects that the structured concurrency principles are violated. It avoids reporting on valid use cases and in situations where developers have consciously opted into using delicate APIs (e.g. by using the `@OptIn` annotation) and hence should be aware of the possible pitfalls.
=== Noncompliant code example
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html[GlobalScope]:
[source,kotlin]
----
fun main() {
GlobalScope.launch { // Noncompliant: no explicit opt-in to DelicateCoroutinesApi
// Do some work
}.join()
}
----
Manual job instantiation:
[source,kotlin]
----
fun startLongRunningBackgroundJob(job: Job) {
val coroutineScope = CoroutineScope(job)
coroutineScope.launch(Job()) { // Noncompliant: new job instance passed to launch()
// Do some work
}
}
----
Manual supervisor instantiation:
[source,kotlin]
----
coroutineScope {
launch(SupervisorJob()) { // Noncompliant: new supervisor instance passed to launch()
// Do some work
}
}
----
=== Compliant solution
In many situations, a good pattern is to use `coroutineScope` as provided in suspending functions:
[source,kotlin]
----
suspend fun main() {
worker()
}
suspend fun worker() {
coroutineScope {
launch { // Compliant: no manually created job/supervisor instance passed to launch()
// Do some work
}
}
}
----
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html[GlobalScope]:
[source,kotlin]
----
@OptIn(DelicateCoroutinesApi::class)
fun main() {
GlobalScope.launch { // Compliant: explicit opt-in to DelicateCoroutinesApi via method annotation
// Do some work
}.join()
}
----
No manual job instantiation:
[source,kotlin]
----
fun startLongRunningBackgroundJob(job: Job) {
val coroutineScope = CoroutineScope(job)
coroutineScope.launch { // Compliant: no manually created job/supervisor instance passed to launch()
// Do some work
}
}
----
Using a supervisor scope instead of manually instantiating a supervisor:
[source,kotlin]
----
supervisorScope {
launch {
// Do some work
}
}
----
== Resources
* https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency[Structured concurrency] in the Kotlin docs
* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html[GlobalScope documentation]
* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html[coroutineScope documentation]
* https://developer.android.com/kotlin/coroutines/coroutines-best-practices[Android coroutines best practices]