În dezvoltarea aplicațiilor asincrone la scară enterprise, cel mai critic aspect nu este pornirea corutinelor, ci controlul eșecurilor și al anulărilor. Spre deosebire de modelele clasice de threading unde fiecare fir de execuție eșuează izolat, Kotlin Coroutines funcționează pe principiul fundamental al Concurenței Structurate (Structured Concurrency): corutinele formează o ierarhie strictă părinte-copil, în care ciclul de viață și erorile sunt legate indisolubil.
Fără o înțelegere profundă a modului în care funcționează Job, SupervisorJob, CancellationException și CoroutineExceptionHandler, o singură interogare eșuată către o bază de date sau un timeout HTTP poate prăbuși instantaneu întregul context asincron al aplicației.
# 1. Ierarhie cu Job Obișnuit (Propagare Bidirecțională):
# Root CoroutineScope (Job)
# ├── Child 1 (Job) ---> Aruncă RuntimeException!
# │ │
# │ ▼ [Semnal de Anulare către Părinte]
# └── Child 2 (Job) <--- Părintele anulează toți copiii frați și se anulează pe sine!
# 2. Ierarhie cu SupervisorJob (Propagare Unidirecțională):
# Root CoroutineScope (SupervisorJob)
# ├── Child 1 (Job) ---> Aruncă RuntimeException! (Eșuează izolat)
# │ │ [Nu anulează părintele]
# └── Child 2 (Job) ---> Continuă execuția neperturbat!
Comparație Tehnică: Job vs SupervisorJob și coroutineScope vs supervisorScope
| Construct Concurent | Comportament la Eșecul Copilului | Propagare Anulare Părinte->Copii | Scenariu de Utilizare Recomandat |
|---|---|---|---|
Job() |
Eșecul oricărui copil anulează părintele și toți frații. | Da (Părintele anulat anulează toți copiii). | Operațiuni tranzacționale de tip „totul sau nimic” (ex: procesare plată + debitare cont). |
SupervisorJob() |
Eșecul unui copil nu afectează părintele sau copiii frați. | Da (Dacă supervizorul este anulat explicit, toți copiii se anulează). | Domenii cu ciclu de viață lung (ex: UI ViewModel, HTTP Server Request Handler, Background Workers). |
coroutineScope { } |
Eșecul oricărei corutine din bloc anulează întregul bloc și aruncă excepția mai departe. | Da. | Descompunerea unei sarcini paralele unde toate sub-sarcinile sunt obligatorii (ex: fan-out / fan-in). |
supervisorScope { } |
Eșecul unei corutine interne nu anulează celelalte corutine lansate în același bloc. | Da. | Procesare independentă de loturi (batch jobs) unde erorile elementelor individuale sunt izolate și logate. |
Regulile de Aur ale Anulării Cooperative (Cooperative Cancellation)
În Kotlin, anularea unei corutine nu este o întrerupere forțată la nivel de thread (cum era periculosul Thread.stop() din vechiul Java). Anularea este cooperativă: corutina semnalează anularea aruncând o excepție internă de tip kotlinx.coroutines.CancellationException la primul punct de suspendare.
// ❌ ANTIPATTERN CRITIC: Prinderea oarbă suprimă CancellationException!
// Corutina refuză să se oprească chiar dacă Job-ul a fost anulat!
suspend fun doWorkBroken() {
try {
delay(1000)
} catch (e: Exception) { // Prinde și CancellationException!
println("Eroare ignorată: ${e.message}")
// Corutina continuă să ruleze pe ascuns!
}
}
// CORECT: Re-aruncarea explicită a CancellationException
suspend fun doWorkCorrect() {
try {
delay(1000)
} catch (e: CancellationException) {
throw e // Obligatoriu: permite mecanismului de anulare să funcționeze!
} catch (e: Exception) {
println("Eroare de business: ${e.message}")
}
}
// SAU: Verificarea stării active în bucle intensive CPU
fun CoroutineScope.computeHeavy(data: List) {
for (item in data) {
ensureActive() // Aruncă imediat CancellationException dacă job-ul a fost anulat
processItem(item)
}
}
Implementare de Producție: Resilient Worker Pool cu SupervisorJob
Iată un model complet de procesare concurentă rezilientă în producție, combinând SupervisorJob, CoroutineExceptionHandler, limitarea concurenței prin Semaphore și curățarea resurselor cu withContext(NonCancellable):
package ro.dexblog.resilience
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import java.util.concurrent.atomic.AtomicInteger
class ResilientWorkerPool(
private val maxConcurrency: Int = 4,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private val processedCounter = AtomicInteger(0)
private val failedCounter = AtomicInteger(0)
// Handler global pentru excepții neprinse în corutine rădăcină (launch)
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
System.err.println("[GLOBAL HANDLER] Excepție neprinsă interceptată: ${exception.message}")
}
// Contextul de bază al serviciului folosește SupervisorJob
private val serviceScope = CoroutineScope(SupervisorJob() + dispatcher + exceptionHandler)
private val semaphore = Semaphore(maxConcurrency)
fun submitTasks(tasks: List String>) = serviceScope.launch {
// Utilizăm supervisorScope pentru a izola eșecul fiecărui task individual
supervisorScope {
tasks.forEachIndexed { index, task ->
launch {
semaphore.withPermit {
try {
val result = task()
processedCounter.incrementAndGet()
println("[Task #$index] Succes: $result")
} catch (e: CancellationException) {
// Re-aruncăm mereu anularea pentru a respecta ciclul de viață
throw e
} catch (e: Exception) {
failedCounter.incrementAndGet()
System.err.println("[Task #$index] Eșuat izolat: ${e.message}")
} finally {
// Curățare garantată chiar dacă procesul a fost anulat
withContext(NonCancellable) {
cleanupTaskResources(index)
}
}
}
}
}
}
}
private suspend fun cleanupTaskResources(taskId: Int) {
delay(10) // Simulare curățare resurse (conexiuni, fișiere deschise)
println("[Task #$taskId] Resurse eliberate curat.")
}
fun shutdown() {
println("Oprire ResilientWorkerPool: Anulare toate corutinele active...")
serviceScope.cancel(CancellationException("Serviciul a primit semnal de oprire (SIGTERM)."))
}
}
suspend fun main() {
val pool = ResilientWorkerPool(maxConcurrency = 2)
val jobList: List String> = listOf(
{ delay(100); "Procesare fișier date 1" },
{ delay(150); throw IllegalStateException("Eroare de rețea la nodul secundar!") },
{ delay(120); "Procesare fișier date 3" },
{ delay(80); "Procesare fișier date 4" }
)
pool.submitTasks(jobList).join()
delay(500)
pool.shutdown()
}