Gestiunea Avansată a Excepțiilor și a Anulării în Kotlin Coroutines: SupervisorJob și CoroutineExceptionHandler în Kotlin

Ghid complet de producție pentru tratarea erorilor și anularea concurențială în Kotlin Coroutines: diferențele critice între Job și SupervisorJob, coroutineScope vs supervisorScope, mecanica anulării cooperative și propagarea excepțiilor.

Sintaxă
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + exceptionHandler)

Î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.

Mecanismul de Propagare a Erorilor în Concurența Structurată
# 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-ul Prinderii Oarbe de Excepții vs Tratarea Corectă
// ❌ 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):

ResilientWorkerPool.kt – Serviciu Concurent de Înaltă Rezistență
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()
}