AI Asistent Učitelj Vasa: Praktična obuka za početnike (32 dana)

Rukovanje greškama i resilijentnost

Vreme potrebno: 90 – 180 minuta

Gde je Učitelj Vasa danas?

Učitelj Vasa može da komunicira sa AI servisima, automatski optimizuje parametre prema tipu pitanja, meri performanse i pokreće benchmark testove. Ali šta se dešava kada internet konekcija pukne, API servis privremeno ne radi, ili korisnik pošalje neočekivan unos? Trenutno, program može da krahira ili vrati grešku koju korisnik ne razume. Danas menjamo to – učićemo Vasu kako da elegantno rukuje greškama i nastavi da radi čak i kada stvari pođu po zlu!

Cilj današnje lekcije

Danas ćeš naučiti kako da implementiraš retry logiku za privremene greške, kreiraš fallback strategije kada glavni servis ne radi, zaštitиš sistem circuit breaker pattern-om i omogućiš graceful degradation. Nakon ove lekcije, Učitelj Vasa će biti stabilan i pouzdan asistent koji elegantno rukuje problemima umesto da krahira.

Predznanja

  • Funkcionalan multi-provider sistem sa tracker-om (Dan 4-5)
  • Razumevanje try/except blokova (korišćeni u prethodnim lekcijama)
  • BaseAIService interfejs i factory pattern (Dan 4)
  • Config modul i environment varijable (Dan 3)

Glavni sadržaj

Zašto je rukovanje greškama kritično važno?

Pre nego što krenemo sa implementacijom, razumimo zašto je ova tema možda najvažnija u celom kursu.

📚 NIVO 1 – Osnovno objašnjenje

Zamisli da voziš auto po autoputu i odjednom ti se ugasi motor. Šta bi bilo bolje:

  1. Auto se potpuno zakoči usred puta (kraš programa)
  2. Auto se polako kreće ka zaustavnoj traci i upali četiri migavca (graceful degradation)

Isto važi za programe! Kada nešto pođe po zlu (a uvek hoće), program treba da:

  • Pokuša da reši problem sam (retry)
  • Ima rezervni plan (fallback)
  • Obavesti korisnika šta se dešava
  • Nastavi da radi koliko god je moguće

🚀 NIVO 2 – Dublje razumevanje

Resilijentnost (otpornost) sistema se postiže kroz nekoliko ključnih principa:

  1. Fail Fast – Brzo prepoznaj problem
  2. Retry with Backoff – Pokušaj ponovo, ali pametno
  3. Circuit Breaker – Zaštiti sistem od kaskadnih padova
  4. Fallback – Uvek imaj Plan B
  5. Graceful Degradation – Bolje ograničena funkcionalnost nego nikakva

🔍 UVID: Ovi principi nisu samo “dobre prakse”, oni predstavljaju promenu načina razmišljanja. Prelazimo sa “pisanja koda koji radi” na “pisanje sistema koji preživljava”. U svetu gde se aplikacije oslanjaju na desetine spoljnih servisa (API-ji, baze, itd.), sposobnost sistema da izdrži i oporavi se od parcijalnih padova je direktno povezana sa pouzdanošću i poverenjem korisnika. Sistem je jak koliko i njegova najslabija karika, a ovi paterni ojačavaju svaku vezu u lancu.

📊 DIJAGRAM: Životni vek zahteva u fragilnom i resilientnom sistemu

Fragilan Sistem:

[Korisnik] → [Naša Aplikacija] ---x Neuspešan poziv x--> [Spoljni API]
    ↑                                                          ↓
    ╰----------------------- CRASH! (ili nerazumljiva greška) --╯

Resilientan Sistem (koji danas gradimo):

[Korisnik] → [Naša Aplikacija] ---------------------------> [Spoljni API]
                 |                                               |
                 | 1. Retry (pokušaj ponovo)                       x (privremena greška)
                 | 2. Circuit Breaker (ako često pada)           |
                 | 3. Fallback (prebaci na rezervni API)         |
                 | 4. Graceful Degradation (vrati bar nešto)      |
                 ↓                                               |
[Odgovor] <------(Uvek vraća smislen odgovor)--------------------╯

💡 PRO TIP: Nemoj da čekaš “savršenu” poruku za log – piši kratke ljudski razumljive log-linije odmah uz try/except. Kada se dogodi greška, tri sekunde uštede pri pisanju loga vrede mnogo više od sat vremena naknadnog kopanja po stack-trace-u. Poruka “Connection error: [Errno 11001] getaddrinfo failed” ne znači ništa prosečnom korisniku. Umesto toga: “Ne mogu da se povežem sa AI servisom. Proveri internet konekciju ili pokušaj ponovo za nekoliko sekundi.”

🎈 ZABAVNA ČINJENICA: Netflix-ov Chaos Monkey namerno “kvari” servere u produkciji da bi testirao resilijentnost sistema. Ova praksa, nazvana “chaos engineering”, pokazala je da sistemi koji se redovno suočavaju sa greškama postaju otporniji!

Kreiranje sistema za retry logiku

Počnimo sa implementacijom pametnog retry sistema koji će automatski pokušavati ponovo kada se dese privremene greške.

📚 NIVO 1 – Osnovno objašnjenje

Retry je kao kada pokušavaš da upališ auto zimi – ako ne upali iz prvog pokušaja, pokušaš ponovo. Ali ne pokušavaš 100 puta zaredom! Čekaš malo između pokušaja, možda malo duže svaki put. Ako ni posle nekoliko pokušaja ne radi, odustaneš i tražiš drugi način.

🚀 NIVO 2 – Dublje razumevanje

Exponential backoff je algoritam koji povećava vreme čekanja između pokušaja eksponencijalno (1s, 2s, 4s, 8s…). Dodavanje “jitter” (nasumičnosti) sprečava “thundering herd” problem kada više klijenata pokušava istovremeno.

💡 PRO TIP: Dok testiraš retry mehanizam, namerno smanji max_attempts na 2 i podigni initial_delay na 0.1 s. Tako ćeš brže videti kako kod reaguje, a kasnije samo vrati vrednosti u produkcijske.

Kreiraj novi fajl src/utils/retry_handler.py:

"""
Retry Handler za Učitelja Vasu
Implementira pametnu retry logiku sa exponential backoff
"""

import time
import random
import functools
from typing import Callable, Any, Optional, Tuple, Type
from datetime import datetime, timedelta


class RetryError(Exception):
    """Custom exception kada retry ne uspe nakon svih pokušaja."""
    def __init__(self, message: str, last_error: Optional[Exception] = None):
        super().__init__(message)
        self.last_error = last_error


class RetryConfig:
    """Konfiguracija za retry ponašanje."""
    def __init__(
        self,
        max_attempts: int = 3,
        initial_delay: float = 1.0,
        max_delay: float = 60.0,
        exponential_base: float = 2.0,
        jitter: bool = True
    ):
        self.max_attempts = max_attempts
        self.initial_delay = initial_delay
        self.max_delay = max_delay
        self.exponential_base = exponential_base
        self.jitter = jitter


# Globalne konfiguracije za različite scenarije
RETRY_CONFIGS = {
    "default": RetryConfig(max_attempts=3, initial_delay=1.0),
    "aggressive": RetryConfig(max_attempts=5, initial_delay=0.5),
    "conservative": RetryConfig(max_attempts=2, initial_delay=2.0),
    "api_rate_limit": RetryConfig(max_attempts=3, initial_delay=5.0, max_delay=30.0)
}


def calculate_delay(attempt: int, config: RetryConfig) -> float:
    """
    Računa koliko dugo da čeka pre sledećeg pokušaja.
    
    Args:
        attempt: Broj trenutnog pokušaja (počinje od 0)
        config: Retry konfiguracija
        
    Returns:
        Broj sekundi za čekanje
    """
    # Eksponencijalni backoff
    delay = min(
        config.initial_delay * (config.exponential_base ** attempt),
        config.max_delay
    )
    
    # Dodaj jitter ako je omogućen (random ±25%)
    if config.jitter:
        jitter_range = delay * 0.25
        delay = delay + random.uniform(-jitter_range, jitter_range)
    
    return max(0.1, delay)  # Minimum 0.1 sekunde


def should_retry(error: Exception) -> bool:
    """
    Određuje da li greška zaslužuje retry.
    
    Args:
        error: Exception koja se desila
        
    Returns:
        True ako treba pokušati ponovo, False inače
    """
    # Greške koje UVEK zaslužuju retry
    retry_errors = [
        "rate_limit", "rate limit",
        "timeout", "timed out",
        "connection", "network",
        "temporary", "unavailable",
        "429", "503", "502", "500"  # HTTP status kodovi
    ]
    
    error_str = str(error).lower()
    
    # Proveri da li poruka sadrži bilo koju od retry reči
    for retry_word in retry_errors:
        if retry_word in error_str:
            return True
    
    # Greške koje NIKAD ne zaslužuju retry
    no_retry_errors = [
        "invalid api key", "unauthorized",
        "insufficient_quota", "payment",
        "invalid request", "bad request"
    ]
    
    for no_retry_word in no_retry_errors:
        if no_retry_word in error_str:
            return False
    
    # Default: ne pokušavaj ponovo
    return False


def retry_with_config(config: RetryConfig):
    """
    Dekorator koji dodaje retry logiku funkciji.
    
    Args:
        config: RetryConfig objekat sa postavkama
        
    Returns:
        Dekorator funkcija
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            last_error = None
            
            for attempt in range(config.max_attempts):
                try:
                    # Pokušaj da pozoveš funkciju
                    result = func(*args, **kwargs)
                    
                    # Ako je uspelo, vrati rezultat
                    if attempt > 0:
                        print(f"✅ Uspelo iz {attempt + 1}. pokušaja!")
                    
                    return result
                    
                except Exception as e:
                    last_error = e
                    
                    # Proveri da li treba retry
                    if not should_retry(e):
                        print(f"❌ Greška ne zaslužuje retry: {str(e)[:100]}")
                        raise
                    
                    # Ako je poslednji pokušaj, ne čekaj
                    if attempt == config.max_attempts - 1:
                        break
                    
                    # Izračunaj delay
                    delay = calculate_delay(attempt, config)
                    
                    print(f"⚠️ Pokušaj {attempt + 1}/{config.max_attempts} neuspešan: {str(e)[:50]}...")
                    print(f"⏳ Čekam {delay:.1f} sekundi pre sledećeg pokušaja...")
                    
                    time.sleep(delay)
            
            # Svi pokušaji neuspešni
            raise RetryError(
                f"Neuspešno nakon {config.max_attempts} pokušaja",
                last_error
            )
        
        return wrapper
    return decorator


def retry(config_name: str = "default"):
    """
    Jednostavan dekorator koji koristi predefinisanu konfiguraciju.
    
    Args:
        config_name: Ime konfiguracije iz RETRY_CONFIGS
        
    Returns:
        Dekorator sa odgovarajućom konfiguracijom
    """
    config = RETRY_CONFIGS.get(config_name, RETRY_CONFIGS["default"])
    return retry_with_config(config)


class SmartRetry:
    """Napredniji retry sistem sa pamćenjem i statistikom."""
    
    def __init__(self):
        self.failure_history = {}  # Pamti greške po funkciji
        self.success_after_retry = {}  # Broji uspešne retry pokušaje
    
    def execute_with_retry(
        self, 
        func: Callable,
        args: tuple = (),
        kwargs: dict = None,
        config: Optional[RetryConfig] = None
    ) -> Tuple[bool, Any]:
        """
        Izvršava funkciju sa retry logikom i vraća (success, result).
        
        Args:
            func: Funkcija za izvršavanje
            args: Pozicioni argumenti
            kwargs: Imenovani argumenti
            config: Retry konfiguracija
            
        Returns:
            Tuple (da li je uspelo, rezultat ili greška)
        """
        if kwargs is None:
            kwargs = {}
        
        if config is None:
            config = RETRY_CONFIGS["default"]
        
        func_name = func.__name__
        last_error = None
        
        for attempt in range(config.max_attempts):
            try:
                result = func(*args, **kwargs)
                
                # Uspeh!
                if attempt > 0:
                    # Zapamti da je retry pomogao
                    self.success_after_retry[func_name] = \
                        self.success_after_retry.get(func_name, 0) + 1
                    print(f"✅ {func_name} uspeo iz {attempt + 1}. pokušaja!")
                
                # Očisti istoriju grešaka za ovu funkciju
                if func_name in self.failure_history:
                    del self.failure_history[func_name]
                
                return True, result
                
            except Exception as e:
                last_error = e
                
                # Zapamti grešku
                if func_name not in self.failure_history:
                    self.failure_history[func_name] = []
                
                self.failure_history[func_name].append({
                    "time": datetime.now(),
                    "error": str(e),
                    "attempt": attempt + 1
                })
                
                # Proveri da li treba retry
                if not should_retry(e) or attempt == config.max_attempts - 1:
                    return False, e
                
                # Delay sa backoff
                delay = calculate_delay(attempt, config)
                print(f"⏳ Retry {func_name} za {delay:.1f}s...")
                time.sleep(delay)
        
        return False, last_error
    
    def get_reliability_score(self, func_name: str) -> float:
        """
        Vraća score pouzdanosti funkcije (0-100).
        
        Args:
            func_name: Ime funkcije
            
        Returns:
            Score od 0 do 100
        """
        if func_name not in self.failure_history:
            return 100.0  # Nema grešaka
        
        # Broj skorašnjih grešaka (poslednji sat)
        recent_errors = [
            err for err in self.failure_history[func_name]
            if err["time"] > datetime.now() - timedelta(hours=1)
        ]
        
        # Formula: 100 - (10 * broj_grešaka), minimum 0
        score = max(0, 100 - (10 * len(recent_errors)))
        
        # Bonus poeni za uspešne retry pokušaje
        retry_success = self.success_after_retry.get(func_name, 0)
        score = min(100, score + (retry_success * 5))
        
        return score


# Globalna instanca
smart_retry = SmartRetry()


# Test funkcionalnost
if __name__ == "__main__":
    print("🧪 Test Retry Handler-a")
    print("=" * 50)
    
    # Simulacija funkcije koja ponekad ne radi
    call_count = 0
    
    @retry("default")
    def flaky_function():
        global call_count
        call_count += 1
        
        if call_count < 3:
            raise ConnectionError(f"Network timeout (pokušaj {call_count})")
        
        return f"Uspeh nakon {call_count} pokušaja!"
    
    # Test
    try:
        result = flaky_function()
        print(f"\n✅ Rezultat: {result}")
    except RetryError as e:
        print(f"\n❌ Retry neuspešan: {e}")
        print(f"   Poslednja greška: {e.last_error}")
    
    # Test smart retry
    print("\n" + "=" * 50)
    print("Test Smart Retry sistema:")
    
    def another_flaky_function(threshold: int):
        import random
        if random.random() < 0.6:  # 60% šanse za grešku
            raise TimeoutError("API timeout")
        return "Success!"
    
    # Pokreni nekoliko puta
    for i in range(5):
        success, result = smart_retry.execute_with_retry(
            another_flaky_function,
            args=(i,)
        )
        print(f"Pokušaj {i+1}: {'Uspeh' if success else 'Neuspeh'}")
        time.sleep(0.5)
    
    # Prikaži statistiku
    print(f"\nPouzdanost funkcije: {smart_retry.get_reliability_score('another_flaky_function'):.1f}%")

SAVET ZA OPTIMIZACIJU: Naivna retry logika (pokušaj ponovo odmah) može biti opasna. Ako API servis uspori i svi klijenti počnu da ga bombarduju ponovnim pokušajima, to može dovesti do potpunog pada servisa. “Exponential backoff” nije samo fin, on je ključan za performanse celog ekosistema. Time što “usporavate”, vi zapravo pomažete servisu da se oporavi, što dugoročno dovodi do bržeg i stabilnijeg sistema za sve.

🎯 ALTERNATIVNO REŠENJE

from tenacity import retry, stop_after_attempt, wait_exponential_jitter

@retry(
    stop=stop_after_attempt(4),              # maksimalno 4 pokušaja
    wait=wait_exponential_jitter(1, 8),      # 1 s, 2 s, 4 s, 8 s ± jitter
    reraise=True
)
def call_ai_safe(prompt: str) -> str:
    """Brza alternativa koristeći biblioteku Tenacity."""
    return ai_client.chat(prompt)

# Upotreba:
try:
    answer = call_ai_safe("Šta je resilijentnost?")
except Exception as err:
    print(f"AI nije dostupan: {err}")

Tenacity ti skraćuje 100+ linija sopstvenog backoff koda na svega par dekoratora. Idealno kada želiš da se fokusiraš na logiku, a ne na infrastrukturu.

🤔 MINI-KVIZ

  1. Koja je razlika između fixed i exponential backoff-a?
  2. Šta znači jitter i čemu služi?
  3. Koliko pokušaja će Tenacity primer iznad maksimalno napraviti?

💡 PRO TIP: Exponential backoff sa jitter-om je industriski standard koji koriste AWS, Google Cloud i Azure. Formula delay * 2^attempt + random() osigurava da se serveru da vremena da se oporavi, a random komponenta sprečava da svi klijenti pokušaju u isto vreme.

Implementacija Circuit Breaker pattern-a

Circuit Breaker je kao osigurač u tvojoj kući – kada detektuje problem, prekida vezu da zaštiti sistem.

📚 NIVO 1 – Osnovno objašnjenje

Circuit Breaker ima tri stanja:

  1. CLOSED (zatvoreno) – sve radi normalno, pozivi prolaze
  2. OPEN (otvoreno) – previše grešaka, blokira sve pozive
  3. HALF-OPEN (polu-otvoreno) – testira da li servis ponovo radi

To je kao kada ti roditelji kažu: “Ako još jednom zakasniš, nećeš izlaziti mesec dana!” Nakon nekoliko kašnjenja, zabrana stupa na snagu. Posle nekog vremena, daju ti šansu da dokažeš da si se popravio.

🚀 NIVO 2 – Dublje razumevanje

Circuit Breaker sprečava kaskadne padove sistema i daje servisu vremena da se oporavi. Ključni parametri su failure threshold (kada da otvori), recovery timeout (koliko da čeka) i success threshold (kada da zatvori).

💡 PRO TIP: Umesto da gledaš samo broj uzastopnih grešaka, razmisli o procentu neuspešnih poziva u poslednjih N sekundi. Taj pristup smanjuje lažno otvaranje „osigurača” tokom kratkotrajnog burst-a grešaka.

📊 DIJAGRAM: Stanja Circuit Breaker-a

        +-------------------------------------------------+
        |                                                 |
        |      Uspešan poziv                              |
        |   (resetuje brojač grešaka)                     |
        |                                                 |
        V   /---------------------\                       |
    +----------+  Dostignut prag  +----------+            |
    |  ✅      |---- grešaka ---->|  🔴      |            |
    |  CLOSED  |                  |   OPEN   |------------+
    |          |<--- Uspešan ----|          |
    +----------+  test poziv    +----------+
        ^       \---------------/     |
        |         Neuspešan test      | Istekao recovery
        |                             | timeout
        |                             ↓
        |                       +-----------+
        +-----------------------|  🟡       |
                                | HALF-OPEN |
                                +-----------+

Kreiraj src/utils/circuit_breaker.py:

"""
Circuit Breaker za Učitelja Vasu
Štiti sistem od kaskadnih padova
"""

import time
from enum import Enum
from typing import Callable, Any, Optional
from datetime import datetime, timedelta
from dataclasses import dataclass, field
import functools


class CircuitState(Enum):
    """Stanja Circuit Breaker-a."""
    CLOSED = "closed"      # Normalno stanje - pozivi prolaze
    OPEN = "open"         # Otvoreno - blokira pozive
    HALF_OPEN = "half_open"  # Testira oporavak


@dataclass
class CircuitStats:
    """Statistika za Circuit Breaker."""
    success_count: int = 0
    failure_count: int = 0
    consecutive_failures: int = 0
    last_failure_time: Optional[datetime] = None
    state_changes: list = field(default_factory=list)
    
    def record_success(self):
        """Beleži uspešan poziv."""
        self.success_count += 1
        self.consecutive_failures = 0
    
    def record_failure(self):
        """Beleži neuspešan poziv."""
        self.failure_count += 1
        self.consecutive_failures += 1
        self.last_failure_time = datetime.now()
    
    def get_failure_rate(self) -> float:
        """Računa stopu neuspeha."""
        total = self.success_count + self.failure_count
        if total == 0:
            return 0.0
        return (self.failure_count / total) * 100


class CircuitBreaker:
    """
    Circuit Breaker implementacija.
    
    Štiti sistem tako što prekida pozive kada servis nije dostupan.
    """
    
    def __init__(
        self,
        name: str,
        failure_threshold: int = 5,
        recovery_timeout: float = 60.0,
        expected_exception: type = Exception,
        success_threshold: int = 2
    ):
        """
        Inicijalizuje Circuit Breaker.
        
        Args:
            name: Ime circuit-a (za logovanje)
            failure_threshold: Broj grešaka pre otvaranja
            recovery_timeout: Vreme čekanja pre testiranja (sekunde)
            expected_exception: Tip greške koji se očekuje
            success_threshold: Broj uspešnih poziva za zatvaranje
        """
        self.name = name
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.expected_exception = expected_exception
        self.success_threshold = success_threshold
        
        self.state = CircuitState.CLOSED
        self.stats = CircuitStats()
        self.half_open_success_count = 0
        self.state_changed_at = datetime.now()
    
    def _should_attempt_reset(self) -> bool:
        """Proverava da li je vreme za pokušaj reseta."""
        return (
            self.state == CircuitState.OPEN and
            datetime.now() > self.state_changed_at + timedelta(seconds=self.recovery_timeout)
        )
    
    def _record_state_change(self, new_state: CircuitState, reason: str):
        """Beleži promenu stanja."""
        old_state = self.state
        self.state = new_state
        self.state_changed_at = datetime.now()
        
        self.stats.state_changes.append({
            "time": self.state_changed_at,
            "from": old_state.value,
            "to": new_state.value,
            "reason": reason
        })
        
        # Prikaži promenu
        emoji = {"closed": "✅", "open": "🔴", "half_open": "🟡"}
        print(f"\n🔌 Circuit '{self.name}' promenio stanje:")
        print(f"   {emoji[old_state.value]} {old_state.value.upper()} → "
              f"{emoji[new_state.value]} {new_state.value.upper()}")
        print(f"   Razlog: {reason}\n")
    
    def call(self, func: Callable, *args, **kwargs) -> Any:
        """
        Poziva funkciju kroz circuit breaker.
        
        Args:
            func: Funkcija za pozivanje
            *args: Pozicioni argumenti
            **kwargs: Imenovani argumenti
            
        Returns:
            Rezultat funkcije
            
        Raises:
            CircuitOpenError: Ako je circuit otvoren
            Exception: Originalna greška funkcije
        """
        # Proveri da li treba pokušati reset
        if self._should_attempt_reset():
            self._record_state_change(
                CircuitState.HALF_OPEN,
                f"Pokušaj oporavka nakon {self.recovery_timeout}s"
            )
            self.half_open_success_count = 0
        
        # Ako je OPEN, odbij poziv
        if self.state == CircuitState.OPEN:
            self.stats.record_failure()
            raise CircuitOpenError(
                f"Circuit '{self.name}' je otvoren zbog previše grešaka. "
                f"Pokušaj ponovo za {self._time_until_retry():.0f} sekundi."
            )
        
        # Pokušaj poziv
        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
            
        except self.expected_exception as e:
            self._on_failure()
            raise
    
    def _on_success(self):
        """Obrađuje uspešan poziv."""
        self.stats.record_success()
        
        if self.state == CircuitState.HALF_OPEN:
            self.half_open_success_count += 1
            
            if self.half_open_success_count >= self.success_threshold:
                self._record_state_change(
                    CircuitState.CLOSED,
                    f"Servis oporavljen nakon {self.success_threshold} uspešnih poziva"
                )
    
    def _on_failure(self):
        """Obrađuje neuspešan poziv."""
        self.stats.record_failure()
        
        if self.state == CircuitState.HALF_OPEN:
            self._record_state_change(
                CircuitState.OPEN,
                "Test oporavka neuspešan"
            )
        
        elif (self.state == CircuitState.CLOSED and 
              self.stats.consecutive_failures >= self.failure_threshold):
            self._record_state_change(
                CircuitState.OPEN,
                f"Prekoračen prag od {self.failure_threshold} uzastopnih grešaka"
            )
    
    def _time_until_retry(self) -> float:
        """Vraća sekunde do sledećeg pokušaja."""
        if self.state != CircuitState.OPEN:
            return 0.0
        
        elapsed = (datetime.now() - self.state_changed_at).total_seconds()
        return max(0, self.recovery_timeout - elapsed)
    
    def get_status(self) -> dict:
        """Vraća trenutni status circuit breaker-a."""
        return {
            "name": self.name,
            "state": self.state.value,
            "stats": {
                "success_count": self.stats.success_count,
                "failure_count": self.stats.failure_count,
                "failure_rate": f"{self.stats.get_failure_rate():.1f}%",
                "consecutive_failures": self.stats.consecutive_failures
            },
            "time_until_retry": self._time_until_retry() if self.state == CircuitState.OPEN else None
        }
    
    def reset(self):
        """Ručno resetuje circuit breaker."""
        self._record_state_change(
            CircuitState.CLOSED,
            "Ručni reset"
        )
        self.stats = CircuitStats()
        self.half_open_success_count = 0


class CircuitOpenError(Exception):
    """Greška kada je circuit otvoren."""
    pass


def circuit_breaker(
    failure_threshold: int = 5,
    recovery_timeout: float = 60.0,
    expected_exception: type = Exception,
    success_threshold: int = 2
):
    """
    Dekorator za dodavanje circuit breaker zaštite.
    
    Args:
        failure_threshold: Broj grešaka pre otvaranja
        recovery_timeout: Vreme oporavka u sekundama
        expected_exception: Tip greške koji se prati
        success_threshold: Broj uspešnih poziva za zatvaranje
    """
    def decorator(func: Callable) -> Callable:
        # Kreiraj circuit breaker za ovu funkciju
        cb = CircuitBreaker(
            name=func.__name__,
            failure_threshold=failure_threshold,
            recovery_timeout=recovery_timeout,
            expected_exception=expected_exception,
            success_threshold=success_threshold
        )
        
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return cb.call(func, *args, **kwargs)
        
        # Dodaj metodu za pristup circuit breaker-u
        wrapper.circuit_breaker = cb
        
        return wrapper
    
    return decorator


# Globalni registar svih circuit breaker-a
circuit_registry = {}


def register_circuit(name: str, circuit: CircuitBreaker):
    """Registruje circuit breaker u globalni registar."""
    circuit_registry[name] = circuit


def get_all_circuits_status() -> str:
    """Vraća status svih registrovanih circuit breaker-a."""
    if not circuit_registry:
        return "Nema registrovanih circuit breaker-a."
    
    status = "🔌 STATUS SVIH CIRCUIT BREAKER-A\n"
    status += "=" * 50 + "\n\n"
    
    for name, circuit in circuit_registry.items():
        info = circuit.get_status()
        state_emoji = {
            "closed": "✅",
            "open": "🔴", 
            "half_open": "🟡"
        }
        
        status += f"{state_emoji[info['state']]} {name}: {info['state'].upper()}\n"
        status += f"   Uspešnih: {info['stats']['success_count']}\n"
        status += f"   Neuspešnih: {info['stats']['failure_count']}\n"
        status += f"   Stopa greške: {info['stats']['failure_rate']}\n"
        
        if info['time_until_retry']:
            status += f"   ⏰ Sledeći pokušaj za: {info['time_until_retry']:.0f}s\n"
        
        status += "\n"
    
    return status


# Test funkcionalnost
if __name__ == "__main__":
    print("🧪 Test Circuit Breaker-a")
    print("=" * 50)
    
    # Simulacija servisa koji pada
    fail_count = 0
    
    @circuit_breaker(failure_threshold=3, recovery_timeout=5.0)
    def unreliable_service():
        global fail_count
        fail_count += 1
        
        # Prva 4 poziva će pasti
        if fail_count <= 4:
            raise ConnectionError(f"Service unavailable (pokušaj {fail_count})")
        
        return f"Success nakon {fail_count} pokušaja!"
    
    # Registruj u globalni registar
    register_circuit("unreliable_service", unreliable_service.circuit_breaker)
    
    # Test scenario
    print("Simulacija servisa koji pada...\n")
    
    for i in range(10):
        try:
            result = unreliable_service()
            print(f"✅ Poziv {i+1}: {result}")
        except CircuitOpenError as e:
            print(f"🔴 Poziv {i+1}: {e}")
        except ConnectionError as e:
            print(f"❌ Poziv {i+1}: {e}")
        
        # Pauza između poziva
        if i == 5:
            print("\n⏰ Čekam 6 sekundi da prođe recovery timeout...\n")
            time.sleep(6)
        else:
            time.sleep(0.5)
    
    # Prikaži finalni status
    print("\n" + get_all_circuits_status())

🌐 REAL-WORLD PRIMENA: Ovaj patern je srž pouzdanosti Google-ovih sistema. Google Cloud Load Balancer, na primer, koristi “health checks” da bi utvrdio da li su serveri iza njega zdravi. Ako server ne odgovori na nekoliko provera (slično našem failure_threshold), Load Balancer ga privremeno izbacuje iz rotacije (otvara circuit). Nakon nekog vremena, počinje da mu šalje mali procenat saobraćaja (half-open) da proveri da li se oporavio. Implementacijom ovog patterna, vi zapravo primenjujete principe na kojima rade najveći svetski cloud sistemi.

🎯 ALTERNATIVNO REŠENJE

from pybreaker import CircuitBreaker

cb = CircuitBreaker(fail_max=4, reset_timeout=20)

@cb
def unstable_call():
    # … poziv eksternog servisa …
    return response

Biblioteka pybreaker prati sve state-ove i emituje signal događaje (on_open, on_close) koje možeš da povežeš sa Prometheus alert-om.

🔍 UVID: Circuit Breaker pattern je inspirisan električnim osiguračima. U softverskom svetu, omogućava “fail fast” pristup – bolje je odmah reći korisniku da servis nije dostupan nego ga terati da čeka 30 sekundi na timeout. Ovo takođe daje servisu koji ne radi vremena da se oporavi bez dodatnog opterećenja.

Kreiranje Fallback sistema

Sada ćemo implementirati fallback strategije – Plan B kada glavni servis ne radi.

📚 NIVO 1 – Osnovno objašnjenje

Fallback je kao rezervni plan. Ako ne možeš da odeš autobusom (glavni plan), ideš biciklom (fallback). U našem slučaju:

  • Ako OpenAI ne radi → prebaci na Gemini
  • Ako nijedan AI ne radi → koristi lokalnu simulaciju
  • Ako ništa ne radi → vrati predefinisan odgovor

🚀 NIVO 2 – Dublje razumevanje

Fallback strategije mogu biti hijerarhijske (primary → secondary → tertiary) ili bazirane na kontekstu (različit fallback za različite tipove zahteva). Ključ je u tome da korisnik dobije najbolji mogući odgovor u datim okolnostima.

💡 PRO TIP: Caching je često “najjeftiniji” fallback. Pre nego što pređeš na rezervni AI, proveri da li već imaš hash-ovan odgovor u Redis-u i vrati ga trenutno – korisnik će dobiti ekspresan rezultat, a sistem će uštedeti stotine tokena.

📊 DIJAGRAM: Hijerarhija Fallback Lanca

[ZAHTEV]
   │
   ↓
+-----------------------+
| Pokušaj PRIMARY       | (npr. OpenAI)
|   (Glavni servis)     |
+-----------------------+
   |         |
(Uspeh)      (Neuspeh)
   |           ↓
   |   +-----------------------+
   |   | Pokušaj SECONDARY     | (npr. Gemini)
   |   |   (Rezervni servis)   |
   |   +-----------------------+
   |         |         |
   |   (Uspeh)      (Neuspeh)
   |         |           ↓
   |         |   +-----------------------+
   |         |   | Pokušaj TERTIARY      | (npr. Lokalna simulacija)
   |         |   | (Lokalni fallback)    |
   |         |   +-----------------------+
   |         |         |         |
   |         |   (Uspeh)      (Neuspeh)
   |         |         |           ↓
   |         |         |   +-----------------------+
   |         |         |   | Koristi EMERGENCY     | (npr. Statički odgovor)
   |         |         |   | (Poslednja opcija)    |
   |         |         |   +-----------------------+
   |         |         |         |
   ↓         ↓         ↓         ↓
+-----------------------------------+
|          FINALNI ODGOVOR          |
+-----------------------------------+

Kreiraj src/utils/fallback_manager.py:

"""
Fallback Manager za Učitelja Vasu
Upravlja rezervnim strategijama kada glavni servisi ne rade
"""

from typing import List, Callable, Any, Optional, Dict
from dataclasses import dataclass
from datetime import datetime
from enum import Enum

from utils.retry_handler import SmartRetry, RetryConfig
from utils.circuit_breaker import CircuitOpenError


class FallbackLevel(Enum):
    """Nivoi fallback strategije."""
    PRIMARY = "primary"          # Glavni servis
    SECONDARY = "secondary"      # Rezervni servis
    TERTIARY = "tertiary"       # Lokalna alternativa
    EMERGENCY = "emergency"      # Poslednja linija odbrane


@dataclass
class FallbackOption:
    """Definiše jednu fallback opciju."""
    name: str
    level: FallbackLevel
    handler: Callable
    description: str
    degradation_message: Optional[str] = None


class FallbackChain:
    """
    Lanac fallback opcija koji se izvršavaju redom.
    """
    
    def __init__(self, name: str):
        """
        Inicijalizuje fallback lanac.
        
        Args:
            name: Ime lanca (za logovanje)
        """
        self.name = name
        self.options: List[FallbackOption] = []
        self.execution_history = []
    
    def add_option(self, option: FallbackOption):
        """Dodaje opciju u lanac."""
        self.options.append(option)
        # Sortiraj po nivou (PRIMARY=0, SECONDARY=1, itd)
        self.options.sort(key=lambda x: list(FallbackLevel).index(x.level))
    
    def execute(self, *args, **kwargs) -> Any:
        """
        Izvršava lanac - pokušava opcije redom dok jedna ne uspe.
        
        Args:
            *args: Argumenti za handler funkcije
            **kwargs: Imenovani argumenti
            
        Returns:
            Rezultat prve uspešne opcije
            
        Raises:
            Exception: Ako nijedna opcija ne uspe
        """
        errors = []
        start_time = datetime.now()
        
        for i, option in enumerate(self.options):
            try:
                print(f"\n🔄 Pokušavam {option.level.value}: {option.name}")
                
                # Pozovi handler
                result = option.handler(*args, **kwargs)
                
                # Uspeh! Zapamti u istoriji
                self.execution_history.append({
                    "time": datetime.now(),
                    "option": option.name,
                    "level": option.level.value,
                    "success": True,
                    "attempt_number": i + 1,
                    "total_time": (datetime.now() - start_time).total_seconds()
                })
                
                # Ako nije primary, obavesti korisnika o degradaciji
                if option.level != FallbackLevel.PRIMARY and option.degradation_message:
                    print(f"ℹ️ {option.degradation_message}")
                
                return result
                
            except Exception as e:
                errors.append((option.name, str(e)))
                print(f"   ❌ {option.name} neuspešan: {str(e)[:50]}...")
                
                # Zapamti neuspeh
                self.execution_history.append({
                    "time": datetime.now(),
                    "option": option.name,
                    "level": option.level.value,
                    "success": False,
                    "error": str(e),
                    "attempt_number": i + 1
                })
        
        # Sve opcije neuspešne
        error_summary = "\n".join([f"  - {name}: {err}" for name, err in errors])
        raise Exception(
            f"Sve fallback opcije za '{self.name}' su neuspešne:\n{error_summary}"
        )
    
    def get_statistics(self) -> Dict[str, Any]:
        """Vraća statistiku korišćenja fallback opcija."""
        stats = {
            "total_executions": len(self.execution_history),
            "by_level": {},
            "success_rate": 0
        }
        
        if not self.execution_history:
            return stats
        
        # Računaj statistiku po nivou
        for level in FallbackLevel:
            level_calls = [h for h in self.execution_history if h["level"] == level.value]
            if level_calls:
                successful = sum(1 for h in level_calls if h["success"])
                stats["by_level"][level.value] = {
                    "total": len(level_calls),
                    "successful": successful,
                    "rate": (successful / len(level_calls)) * 100
                }
        
        # Ukupna stopa uspeha
        total_success = sum(1 for h in self.execution_history if h["success"])
        stats["success_rate"] = (total_success / len(self.execution_history)) * 100
        
        return stats


class FallbackManager:
    """
    Centralizovani manager za sve fallback strategije.
    """
    
    def __init__(self):
        self.chains: Dict[str, FallbackChain] = {}
        self.smart_retry = SmartRetry()
    
    def create_chain(self, name: str) -> FallbackChain:
        """Kreira novi fallback lanac."""
        chain = FallbackChain(name)
        self.chains[name] = chain
        return chain
    
    def get_chain(self, name: str) -> Optional[FallbackChain]:
        """Vraća postojeći lanac."""
        return self.chains.get(name)
    
    def execute_with_fallback(
        self, 
        chain_name: str, 
        *args, 
        retry_config: Optional[RetryConfig] = None,
        **kwargs
    ) -> Any:
        """
        Izvršava lanac sa retry logikom.
        
        Args:
            chain_name: Ime lanca za izvršavanje
            *args: Argumenti za handler funkcije
            retry_config: Konfiguracija za retry
            **kwargs: Imenovani argumenti
            
        Returns:
            Rezultat izvršavanja
        """
        chain = self.chains.get(chain_name)
        if not chain:
            raise ValueError(f"Nepoznat fallback lanac: {chain_name}")
        
        # Ako nema retry config, samo izvrši lanac
        if not retry_config:
            return chain.execute(*args, **kwargs)
        
        # Inače, koristi smart retry
        success, result = self.smart_retry.execute_with_retry(
            chain.execute,
            args=args,
            kwargs=kwargs,
            config=retry_config
        )
        
        if success:
            return result
        else:
            raise result  # result je Exception u slučaju neuspeha
    
    def get_health_report(self) -> str:
        """Generiše izveštaj o zdravlju sistema."""
        report = "🏥 FALLBACK SISTEM - ZDRAVSTVENI IZVEŠTAJ\n"
        report += "=" * 60 + "\n\n"
        
        if not self.chains:
            report += "Nema konfigurisanih fallback lanaca.\n"
            return report
        
        for name, chain in self.chains.items():
            stats = chain.get_statistics()
            
            report += f"📊 Lanac: {name}\n"
            report += f"   Ukupno izvršavanja: {stats['total_executions']}\n"
            report += f"   Stopa uspeha: {stats['success_rate']:.1f}%\n"
            
            if stats['by_level']:
                report += "   Po nivoima:\n"
                for level, level_stats in stats['by_level'].items():
                    report += f"     - {level}: {level_stats['successful']}/{level_stats['total']} "
                    report += f"({level_stats['rate']:.1f}%)\n"
            
            report += "\n"
        
        return report


# Globalni fallback manager
fallback_manager = FallbackManager()


# Pomocne funkcije za brže kreiranje opcija
def create_ai_fallback_chain(
    openai_handler: Callable,
    gemini_handler: Callable,
    simulation_handler: Callable,
    static_response: str = "Izvini, trenutno ne mogu da odgovorim. Pokušaj ponovo kasnije."
) -> FallbackChain:
    """
    Kreira standardni AI fallback lanac.
    
    Args:
        openai_handler: Funkcija za OpenAI poziv
        gemini_handler: Funkcija za Gemini poziv
        simulation_handler: Funkcija za lokalnu simulaciju
        static_response: Poslednji fallback odgovor
        
    Returns:
        Konfigurisani FallbackChain
    """
    chain = fallback_manager.create_chain("ai_response")
    
    # Primary - OpenAI
    chain.add_option(FallbackOption(
        name="OpenAI API",
        level=FallbackLevel.PRIMARY,
        handler=openai_handler,
        description="Glavni AI servis"
    ))
    
    # Secondary - Gemini
    chain.add_option(FallbackOption(
        name="Google Gemini",
        level=FallbackLevel.SECONDARY,
        handler=gemini_handler,
        description="Rezervni AI servis",
        degradation_message="Koristim rezervni AI servis (Gemini)"
    ))
    
    # Tertiary - Lokalna simulacija
    chain.add_option(FallbackOption(
        name="Lokalna simulacija",
        level=FallbackLevel.TERTIARY,
        handler=simulation_handler,
        description="Offline simulacija",
        degradation_message="AI servisi nisu dostupni - koristim lokalnu simulaciju"
    ))
    
    # Emergency - Statički odgovor
    chain.add_option(FallbackOption(
        name="Statički odgovor",
        level=FallbackLevel.EMERGENCY,
        handler=lambda *args, **kwargs: static_response,
        description="Predefinisan odgovor",
        degradation_message="Svi servisi su trenutno nedostupni"
    ))
    
    return chain


# Test funkcionalnost
if __name__ == "__main__":
    print("🧪 Test Fallback Manager-a")
    print("=" * 50)
    
    # Simuliraj različite servise
    def primary_service(msg):
        """Uvek pada."""
        raise ConnectionError("Primary service down")
    
    def secondary_service(msg):
        """Pada prva 2 puta."""
        if not hasattr(secondary_service, 'count'):
            secondary_service.count = 0
        secondary_service.count += 1
        
        if secondary_service.count <= 2:
            raise TimeoutError("Secondary service timeout")
        return f"Secondary odgovor na: {msg}"
    
    def backup_service(msg):
        """Uvek radi."""
        return f"Backup odgovor na: {msg}"
    
    # Kreiraj lanac
    chain = fallback_manager.create_chain("test_chain")
    
    chain.add_option(FallbackOption(
        "Primary", FallbackLevel.PRIMARY, primary_service, "Glavni servis"
    ))
    
    chain.add_option(FallbackOption(
        "Secondary", FallbackLevel.SECONDARY, secondary_service, "Rezervni servis",
        "Koristim rezervni servis"
    ))
    
    chain.add_option(FallbackOption(
        "Backup", FallbackLevel.TERTIARY, backup_service, "Backup servis",
        "Koristim backup - ograničene mogućnosti"
    ))
    
    # Test izvršavanja
    print("Test 1: Prvi pokušaj")
    try:
        result = chain.execute("Test poruka 1")
        print(f"✅ Rezultat: {result}")
    except Exception as e:
        print(f"❌ Greška: {e}")
    
    print("\nTest 2: Drugi pokušaj")
    try:
        result = chain.execute("Test poruka 2")
        print(f"✅ Rezultat: {result}")
    except Exception as e:
        print(f"❌ Greška: {e}")
    
    print("\nTest 3: Treći pokušaj")
    try:
        result = chain.execute("Test poruka 3")
        print(f"✅ Rezultat: {result}")
    except Exception as e:
        print(f"❌ Greška: {e}")
    
    # Prikaži statistiku
    print("\n" + fallback_manager.get_health_report())

💡 PRO TIP: Fallback strategije ne moraju biti samo “drugi servis”. Mogu uključivati keširan odgovor, pojednostavljen algoritam, ili čak pitanje korisniku da sačeka. Ključ je u tome da korisnik uvek dobije NEŠTO umesto error poruke.

Implementacija Graceful Degradation

Graceful degradation omogućava sistemu da postupno smanjuje funkcionalnost umesto potpunog pada.

📚 NIVO 1 – Osnovno objašnjenje

Graceful degradation je kao kada ti telefon ima slab signal – ne možeš gledati video, ali možeš slati poruke. Umesto da telefon kaže “nema signala, ništa ne radi”, on ti omogućava da koristiš ono što može. U našem slučaju, ako AI ne može da generiše detaljan odgovor, može bar da kaže “Zdravo” ili prikaže osnovne informacije.

🚀 NIVO 2 – Dublje razumevanje

Graceful degradation zahteva dizajniranje sistema u slojevima, gde svaki sloj može da funkcioniše nezavisno. Ovo omogućava parcijalni pad funkcionalnosti bez totalnog kolapsa sistema.

🔍 UVID: ResilientAIServiceWrapper klasa je odličan primer “Decorator” patterna. Umesto da menjamo OpenAIService i GeminiService da sadrže retry i circuit breaker logiku, mi ih “umotavamo” u wrapper koji dodaje tu funkcionalnost. Ovo je moćan koncept jer održava naš osnovni kod čistim i fokusiranim na jednu stvar (komunikaciju sa API-jem), dok se “poprečni preseci” (cross-cutting concerns) poput otpornosti na greške dodaju kao slojevi. To čini sistem lakšim za održavanje i testiranje.

Ažuriraj src/ai_services/ai_factory.py dodavanjem resilience funkcionalnosti:


"""
AI Service Factory
Automatski kreira pravi AI servis na osnovu konfiguracije
"""

import sys
import os
import logging
from typing import Optional, List, Dict, Any

# Dodaj parent folder u path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from utils.config import Config
from .base_service import BaseAIService
from .openai_service import OpenAIService
from .gemini_service import GeminiService

# Resilience importi - proveri da li postoje pre importovanja
try:
    from utils.retry_handler import retry, RetryConfig, RetryError
    from utils.circuit_breaker import circuit_breaker, CircuitOpenError, register_circuit
    from utils.fallback_manager import fallback_manager, FallbackLevel, FallbackOption
    RESILIENCE_AVAILABLE = True
except ImportError:
    print("⚠️ Resilience moduli nisu dostupni. Nastavljam bez napredne zaštite.")
    RESILIENCE_AVAILABLE = False


class AIServiceFactory:
    """Factory klasa za kreiranje AI servisa."""
    
    _instance: Optional[BaseAIService] = None
    
    @classmethod
    def get_service(cls, force_new: bool = False) -> BaseAIService:
        """
        Vraća instancu AI servisa na osnovu konfiguracije.
        Koristi Singleton pattern za efikasnost.
        
        Args:
            force_new: Ako je True, kreira novu instancu
            
        Returns:
            Instanca AI servisa (OpenAI ili Gemini)
            
        Raises:
            ValueError: Ako je AI_PROVIDER nepoznat
        """
        # Ako već imamo instancu i ne tražimo novu, vrati postojeću
        if cls._instance is not None and not force_new:
            return cls._instance
        
        # Kreiraj novu instancu na osnovu providera
        provider = Config.AI_PROVIDER.lower()
        
        print(f"\n🏭 AI Factory: Kreiram {provider.upper()} servis...")
        
        if provider == 'openai':
            cls._instance = OpenAIService()
        elif provider == 'gemini':
            cls._instance = GeminiService()
        else:
            raise ValueError(
                f"Nepoznat AI provider: {provider}. "
                f"Dozvoljeni: 'openai', 'gemini'"
            )
        
        print(f"✅ {provider.upper()} servis uspešno kreiran!\n")
        return cls._instance
    
    @classmethod
    def reset(cls):
        """Resetuje factory (korisno za testiranje)."""
        cls._instance = None
        print("🔄 AI Factory resetovan")
    
    @classmethod
    def switch_provider(cls, new_provider: str) -> BaseAIService:
        """
        Prebacuje na drugi provider i vraća novi servis.
        
        Args:
            new_provider: 'openai' ili 'gemini'
            
        Returns:
            Nova instanca AI servisa
        """
        # Promeni provider u konfiguraciji
        Config.AI_PROVIDER = new_provider
        
        # Resetuj postojeću instancu
        cls.reset()
        
        # Kreiraj i vrati novu
        return cls.get_service()


# Jednostavna simulacija za fallback
def simuliraj_ai_odgovor(poruka: str) -> str:
    """Lokalna simulacija AI odgovora kada servisi nisu dostupni."""
    poruka_lower = poruka.lower()
    
    if any(word in poruka_lower for word in ["zdravo", "pozdrav", "ćao", "hej"]):
        return "Zdravo! Trenutno radim u offline režimu, ali mogu da pomognem sa osnovnim stvarima."
    elif "python" in poruka_lower:
        return "Python je odličan programski jezik za početnike! Ima jednostavnu sintaksu i moćne biblioteke."
    else:
        return "Izvini, trenutno radim u ograničenom režimu. Pokušaj ponovo kasnije za potpun odgovor."


# Resilience klase - definiši samo ako su moduli dostupni
if RESILIENCE_AVAILABLE:
    
    class ResilientAIServiceFactory(AIServiceFactory):
        """
        Proširena factory klasa sa resilience funkcionalnostima.
        """
        
        @classmethod
        def create_resilient_service(cls) -> BaseAIService:
            """
            Kreira AI servis sa ugrađenim resilience mehanizmima.
            
            Returns:
                AI servis sa retry, circuit breaker i fallback logikom
            """
            # Prvo pokušaj da kreiraš osnovni servis
            try:
                base_service = cls.get_service()
                
                # Omotaj ga u resilience wrapper
                return ResilientAIServiceWrapper(base_service)
                
            except Exception as e:
                print(f"⚠️ Ne mogu da kreiram {Config.AI_PROVIDER} servis: {e}")
                print("📌 Kreiram degradirani servis sa ograničenim mogućnostima...")
                
                # Vrati degradirani servis
                return DegradedAIService()
    
    
    class ResilientAIServiceWrapper(BaseAIService):
        """
        Wrapper koji dodaje resilience funkcionalnosti postojećem servisu.
        """
        
        def __init__(self, base_service: BaseAIService):
            self.base_service = base_service
            self.provider_name = Config.AI_PROVIDER
            
            # Kreiraj fallback lanac
            self._setup_fallback_chain()
            
            # Registruj circuit breaker
            register_circuit(f"ai_{self.provider_name}", self._circuit_breaker_call.circuit_breaker)
        
        def _setup_fallback_chain(self):
            """Postavlja fallback lanac za ovaj servis."""
            chain_name = f"ai_response_{self.provider_name}"
            chain = fallback_manager.create_chain(chain_name)
            
            # Primary - glavni servis sa circuit breaker-om
            chain.add_option(FallbackOption(
                name=f"{self.provider_name.upper()} (glavni)",
                level=FallbackLevel.PRIMARY,
                handler=self._circuit_breaker_call,
                description=f"Glavni {self.provider_name} servis sa zaštitom"
            ))
            
            # Secondary - alternativni AI (ako postoji)
            if Config.OPENAI_API_KEY and Config.GEMINI_API_KEY:
                alt_provider = "gemini" if self.provider_name == "openai" else "openai"
                chain.add_option(FallbackOption(
                    name=f"{alt_provider.upper()} (rezerva)",
                    level=FallbackLevel.SECONDARY,
                    handler=self._try_alternative_provider,
                    description=f"Rezervni {alt_provider} servis",
                    degradation_message=f"Prebacujem na {alt_provider} servis..."
                ))
            
            # Tertiary - lokalna simulacija
            chain.add_option(FallbackOption(
                name="Simulacija",
                level=FallbackLevel.TERTIARY,
                handler=lambda msg, **kwargs: simuliraj_ai_odgovor(msg),
                description="Offline simulacija",
                degradation_message="AI servisi nedostupni - koristim simulaciju"
            ))
            
            self.fallback_chain_name = chain_name
        
        @circuit_breaker(
            failure_threshold=3,
            recovery_timeout=30.0,
            expected_exception=Exception
        )
        def _circuit_breaker_call(self, message: str, **kwargs):
            """Poziva osnovni servis kroz circuit breaker."""
            return self._retry_call(message, **kwargs)
        
        @retry("default")
        def _retry_call(self, message: str, **kwargs):
            """Poziva osnovni servis sa retry logikom."""
            return self.base_service.pozovi_ai(message, **kwargs)
        
        def _try_alternative_provider(self, message: str, **kwargs):
            """Pokušava da koristi alternativni provider."""
            # Privremeno promeni provider
            original_provider = Config.AI_PROVIDER
            alt_provider = "gemini" if original_provider == "openai" else "openai"
            
            try:
                Config.AI_PROVIDER = alt_provider
                AIServiceFactory.reset()
                alt_service = AIServiceFactory.get_service()
                
                return alt_service.pozovi_ai(message, **kwargs)
                
            finally:
                # Vrati originalni provider
                Config.AI_PROVIDER = original_provider
                AIServiceFactory.reset()
        
        def pozovi_ai(self, poruka: str, system_prompt: Optional[str] = None) -> str:
            """
            Resilient poziv AI servisa.
            
            Args:
                poruka: Korisnikova poruka
                system_prompt: System prompt
                
            Returns:
                AI odgovor ili fallback
            """
            try:
                # Koristi fallback lanac
                return fallback_manager.execute_with_fallback(
                    self.fallback_chain_name,
                    poruka,
                    system_prompt=system_prompt
                )
                
            except Exception as e:
                # Poslednja linija odbrane
                logging.error(f"Totalni pad sistema: {e}")
                return self._emergency_response(poruka)
        
        def _emergency_response(self, message: str) -> str:
            """Generiše emergency odgovor kada sve ostalo ne radi."""
            responses = {
                "pozdrav": "Zdravo! Trenutno imam tehničkih problema, ali tu sam!",
                "python": "Python je odličan programski jezik! Izvini što ne mogu detaljnije.",
                "pomoć": "Pokušaj ponovo za nekoliko minuta. Radim na rešavanju problema!",
                "default": "Izvini, trenutno ne mogu da odgovorim kako treba. Molim te pokušaj ponovo kasnije."
            }
            
            # Jednostavna logika za izbor odgovora
            message_lower = message.lower()
            for key in responses:
                if key in message_lower:
                    return responses[key]
            
            return responses["default"]
        
        def pozovi_sa_istorijom(self, messages: List[Dict[str, str]]) -> str:
            """Poziva sa istorijom - sa fallback logikom."""
            try:
                return self.base_service.pozovi_sa_istorijom(messages)
            except Exception as e:
                # Fallback na poslednju poruku
                if messages:
                    last_user_msg = next(
                        (m["content"] for m in reversed(messages) if m["role"] == "user"),
                        "Nastavi razgovor"
                    )
                    return self.pozovi_ai(last_user_msg)
                return self._emergency_response("Nastavi razgovor")
        
        def test_konekcija(self) -> bool:
            """Testira konekciju sa graceful degradation."""
            try:
                return self.base_service.test_konekcija()
            except:
                # Čak i ako test ne radi, sistem može da funkcioniše
                return True  # Optimistično
        
        def get_current_settings(self) -> Dict[str, Any]:
            """Vraća postavke sa informacijom o degradaciji."""
            try:
                settings = self.base_service.get_current_settings()
            except:
                settings = {"model": "unknown", "temperature": 0.7, "max_tokens": 150}
            
            # Dodaj informaciju o stanju
            if hasattr(self._circuit_breaker_call, 'circuit_breaker'):
                cb = self._circuit_breaker_call.circuit_breaker
                settings["circuit_state"] = cb.state.value
                settings["reliability_score"] = 100 - (cb.stats.get_failure_rate())
            
            return settings
        
        def apply_settings(self, settings: Dict[str, Any]):
            """Primenjuje postavke ako je moguće."""
            try:
                self.base_service.apply_settings(settings)
            except Exception as e:
                print(f"⚠️ Ne mogu da primenim postavke: {e}")
                # Nastavi rad sa postojećim postavkama
    
    
    class DegradedAIService(BaseAIService):
        """
        Minimalni AI servis koji radi kada ništa drugo ne radi.
        """
        
        def __init__(self):
            print("🔧 Kreiram degradirani servis...")
            self.responses = {
                "greeting": [
                    "Zdravo! Radim u ograničenom režimu, ali tu sam da pomognem!",
                    "Pozdrav! Imam tehničkih problema, ali pokušaću da pomognem.",
                    "Hej! Sistemi nisu u punoj snazi, ali hajde da probamo!"
                ],
                "error": [
                    "Izvini, trenutno ne mogu da pristupim AI servisima.",
                    "Ups, izgleda da imam problema sa konekcijom.",
                    "Molim te pokušaj ponovo za par minuta."
                ],
                "encouragement": [
                    "Ne odustaj! Programiranje je putovanje, ne destinacija.",
                    "Svaki ekspert je bio početnik. Nastavi da učiš!",
                    "Greške su deo procesa učenja. To je potpuno normalno!"
                ]
            }
        
        def pozovi_ai(self, poruka: str, system_prompt: Optional[str] = None) -> str:
            """Vraća predefinisan odgovor."""
            import random
            
            poruka_lower = poruka.lower()
            
            # Pokušaj da prepoznaš tip poruke
            if any(word in poruka_lower for word in ["zdravo", "pozdrav", "hej", "ćao"]):
                return random.choice(self.responses["greeting"])
            elif any(word in poruka_lower for word in ["greška", "error", "problem", "ne radi"]):
                return random.choice(self.responses["encouragement"])
            else:
                return random.choice(self.responses["error"])
        
        def pozovi_sa_istorijom(self, messages: List[Dict[str, str]]) -> str:
            """Ignorise istoriju, vraća osnovni odgovor."""
            if messages:
                last_msg = messages[-1].get("content", "")
                return self.pozovi_ai(last_msg)
            return "Sistem trenutno radi u ograničenom režimu."
        
        def test_konekcija(self) -> bool:
            """Uvek vraća True jer je lokalni."""
            return True
        
        def get_current_settings(self) -> Dict[str, Any]:
            """Vraća minimalne postavke."""
            return {
                "model": "degraded_mode",
                "temperature": 0.5,
                "max_tokens": 100,
                "status": "limited_functionality"
            }
        
        def apply_settings(self, settings: Dict[str, Any]):
            """Ne može da menja postavke."""
            pass
# Dodaj create_resilient_service metodu na AIServiceFactory
AIServiceFactory.create_resilient_service = staticmethod(
lambda: ResilientAIServiceFactory.create_resilient_service()
)
# Test funkcionalnosti
if __name__ == "__main__":
    print("🧪 Test AI Factory")
    print("=" * 50)

    try:
        # Test 1: Kreiraj servis na osnovu trenutne konfiguracije
        print(f"Trenutni provider: {Config.AI_PROVIDER}")
        service1 = AIServiceFactory.get_service()

        # Test 2: Proveri Singleton
        service2 = AIServiceFactory.get_service()
        print(f"\n🔍 Singleton test: service1 == service2? {service1 is service2}")

        # Test 3: Test poziva
        print("\n📤 Test poziv...")
        response = service1.pozovi_ai("Reci 'Zdravo' na srpskom")
        print(f"📥 Odgovor: {response}")

        # Test 4: Prebacivanje providera (samo ako imaš oba ključa)
        if Config.OPENAI_API_KEY and Config.GEMINI_API_KEY:
            drugi_provider = 'gemini' if Config.AI_PROVIDER == 'openai' else 'openai'
            print(f"\n🔄 Prebacujem na {drugi_provider}...")

            service3 = AIServiceFactory.switch_provider(drugi_provider)
            response2 = service3.pozovi_ai("Reci 'Cao' na srpskom")
            print(f"📥 Odgovor od {drugi_provider}: {response2}")

            # Vrati na originalni
            AIServiceFactory.switch_provider(Config.AI_PROVIDER)

        # Test 5: Resilient servis (ako su moduli dostupni)
        if RESILIENCE_AVAILABLE:
            print("\n🛡️ Test resilient servisa...")
            resilient_service = AIServiceFactory.create_resilient_service()
            response3 = resilient_service.pozovi_ai("Šta je Python?")
            print(f"📥 Resilient odgovor: {response3}")

    except Exception as e:
        print(f"❌ Factory test neuspešan: {e}")

Integracija resilience sistema u glavni program

Sada ćemo integrisati sve resilience komponente u main.py.

Ažuriraj početak src/main.py:

# Dodaj ove importe nakon postojećih
from utils.circuit_breaker import get_all_circuits_status, CircuitOpenError
from utils.fallback_manager import fallback_manager
from utils.retry_handler import smart_retry

# Zameni postojeću inicijalizuj_ai_servis funkciju sa:
def inicijalizuj_ai_servis():
    """Pokušava da kreira resilient AI servis."""
    global ai_service
    
    print("\n🔧 Inicijalizujem AI servis sa naprednom zaštitom...")
    
    try:
        # Koristi resilient factory
        ai_service = AIServiceFactory.create_resilient_service()
        
        print(f"✅ {Config.AI_PROVIDER.upper()} servis pokrenut sa:")
        print("   ✓ Retry logikom (automatski pokušaji)")
        print("   ✓ Circuit breaker zaštitom")
        print("   ✓ Fallback strategijama")
        print("   ✓ Graceful degradation podrškom")
        
        # Test da li radi
        if ai_service.test_konekcija():
            print("   ✓ Konekcija stabilna!")
        else:
            print("   ⚠️ Konekcija nestabilna, ali sistem će pokušati da radi")
        
        return True
        
    except Exception as e:
        print(f"⚠️ Problem pri inicijalizaciji: {e}")
        print("📌 Sistem će raditi u degradiranom režimu")
        
        # Čak i ako inicijalizacija ne uspe, imamo degraded servis
        from ai_services.ai_factory import DegradedAIService
        ai_service = DegradedAIService()
        
        return False

# Dodaj novu opciju u glavni_meni_profilisanje funkciju:
def prikazi_sistem_zdravlje():
    """Prikazuje zdravlje i status svih resilience komponenti."""
    print("\n🏥 ZDRAVLJE SISTEMA")
    print("=" * 60)
    
    # Circuit breakers status
    print("\n" + get_all_circuits_status())
    
    # Fallback statistike
    print(fallback_manager.get_health_report())
    
    # Retry statistike
    if hasattr(ai_service, '_circuit_breaker_call'):
        cb = ai_service._circuit_breaker_call.circuit_breaker
        print(f"📊 Pouzdanost glavnog servisa: {100 - cb.stats.get_failure_rate():.1f}%")
    
    # Degradacija status
    if hasattr(ai_service, 'get_current_settings'):
        settings = ai_service.get_current_settings()
        if settings.get('status') == 'limited_functionality':
            print("\n⚠️ UPOZORENJE: Sistem radi u DEGRADIRANOM režimu!")
            print("   Funkcionalnosti su ograničene.")

Praktična implementacija

Test scenario za resilience

Kreiraj src/test_resilience.py za testiranje svih komponenti:

"""
Test scenariji za resilience funkcionalnosti
Simulira različite failure scenario
"""

import time
import random
from ai_services.ai_factory import AIServiceFactory
from utils.config import Config


def simulate_network_issues():
    """Simulira probleme sa mrežom."""
    print("\n🧪 TEST 1: Simulacija mrežnih problema")
    print("=" * 50)
    
    # Kreiraj resilient servis  
    service = AIServiceFactory.create_resilient_service()
    
    # Simuliraj više poziva sa povremenim greškama
    for i in range(5):
        print(f"\nPokušaj {i+1}:")
        
        # Random da li će raditi
        if random.random() < 0.6:  # 60% šanse za grešku
            # Privremeno "pokvari" servis
            original_key = Config.get_api_key()
            Config.OPENAI_API_KEY = "invalid_key"
            Config.GEMINI_API_KEY = "invalid_key"
        
        try:
            response = service.pozovi_ai("Šta je Python?")
            print(f"✅ Odgovor: {response[:100]}...")
        except Exception as e:
            print(f"❌ Greška: {e}")
        finally:
            # Vrati pravi ključ
            if 'original_key' in locals():
                if Config.AI_PROVIDER == "openai":
                    Config.OPENAI_API_KEY = original_key
                else:
                    Config.GEMINI_API_KEY = original_key
        
        time.sleep(1)


def test_circuit_breaker():
    """Testira circuit breaker funkcionalnost."""
    print("\n🧪 TEST 2: Circuit Breaker test")
    print("=" * 50)
    
    service = AIServiceFactory.create_resilient_service()
    
    # Forsiraj greške
    original_key = Config.get_api_key()
    if Config.AI_PROVIDER == "openai":
        Config.OPENAI_API_KEY = "invalid"
    else:
        Config.GEMINI_API_KEY = "invalid"
    
    print("Forsiram greške da aktiviram circuit breaker...")
    
    for i in range(6):
        print(f"\nPoziv {i+1}:")
        try:
            response = service.pozovi_ai("Test")
            print(f"Odgovor: {response}")
        except Exception as e:
            print(f"Status: {type(e).__name__}")
    
    # Vrati ključ i čekaj recovery
    if Config.AI_PROVIDER == "openai":
        Config.OPENAI_API_KEY = original_key
    else:
        Config.GEMINI_API_KEY = original_key
    
    print("\n⏰ Čekam 35 sekundi za recovery timeout...")
    time.sleep(35)
    
    print("\nPokušavam ponovo nakon recovery perioda:")
    try:
        response = service.pozovi_ai("Test nakon recovery")
        print(f"✅ Uspeh: {response}")
    except Exception as e:
        print(f"❌ Još uvek ne radi: {e}")


def test_graceful_degradation():
    """Testira graceful degradation."""
    print("\n🧪 TEST 3: Graceful Degradation")
    print("=" * 50)
    
    # Sačuvaj originalne ključeve
    orig_openai = Config.OPENAI_API_KEY
    orig_gemini = Config.GEMINI_API_KEY
    
    # Ukloni sve API ključeve
    Config.OPENAI_API_KEY = None
    Config.GEMINI_API_KEY = None
    
    print("Svi API ključevi uklonjeni - testiram degraded mode...")
    
    try:
        service = AIServiceFactory.create_resilient_service()
        
        test_messages = [
            "Zdravo!",
            "Imam problem sa kodom",
            "Kako da naučim Python?",
            "Random poruka"
        ]
        
        for msg in test_messages:
            print(f"\nPoruka: '{msg}'")
            response = service.pozovi_ai(msg)
            print(f"Odgovor: {response}")
    
    finally:
        # Vrati ključeve
        Config.OPENAI_API_KEY = orig_openai
        Config.GEMINI_API_KEY = orig_gemini


if __name__ == "__main__":
    print("🚀 RESILIENCE TEST SUITE")
    print("=" * 60)
    
    # Proveri da li je bar jedan servis konfigurisan
    if not (Config.OPENAI_API_KEY or Config.GEMINI_API_KEY):
        print("❌ Potreban je bar jedan API ključ za testiranje!")
        exit(1)
    
    # Pokreni testove
    simulate_network_issues()
    test_circuit_breaker()
    test_graceful_degradation()
    
    print("\n✅ Svi testovi završeni!")
Rukovanje greškama i resilijentnost

Rukovanje greškama i resilijentnost

🔄 VEŽBA

  1. U RetryConfig dodaj opciju max_total_time (npr. 30 s) koja prekida sve pokušaje čim pređe ukupno vreme.
  2. Proširi SmartRetry da vodi statistiku „prosečno trajanje uspešnog poziva”.
  3. Izmeri kako promena exponential_base utiče na stres servera – koristi time.perf_counter() oko funkcije.

Česte greške i rešenja

GREŠKA: Circuit breaker se otvara prebrzo
💡 REŠENJE:

  • Povećaj failure_threshold (npr. sa 3 na 5)
  • Proveri da li razlikuješ privremene od trajnih grešaka
  • Možda imaš sporu internet konekciju – povećaj timeout

🔬 DETALJNIJE: Ova greška se dešava kada je sistem previše “osetljiv”. Postoje tri glavna sistemska uzroka:

  1. Mrežna latencija: Vaša konekcija ka API-ju je spora ili nestabilna. Kratki timeout-i će se aktivirati, brojati kao greške i otvoriti circuit. Rešenje je povećati timeout u samom pozivu ka API-ju, a ne nužno menjati prag circuit breaker-a.
  2. Pogrešna klasifikacija greške: should_retry funkcija možda tretira grešku koja nije privremena (npr. “Bad Request” – 400) kao grešku koja zaslužuje retry. To dovodi do nepotrebnih pokušaja koji uvek ne uspeju i brzo aktiviraju circuit breaker. Rešenje je preciznije definisati koje HTTP kodove i poruke smatrate privremenim.
  3. Preagresivna podešavanja: failure_threshold od 3 je dobar za testiranje, ali u produkciji može biti previše nizak. Ako servis ima prolazne “štucavice”, prag od 5-10 uz recovery_timeout od 60 sekundi daje sistemu više prostora da “diše” pre nego što proglasi servis nedostupnim.

GREŠKA: Retry pokušava zauvek
💡 REŠENJE:

  • Proveri should_retry funkciju – možda vraća True za grešku koja nije privremena
  • Smanji max_attempts u RetryConfig
  • Dodaj maksimalno ukupno vreme za sve pokušaje

GREŠKA: Fallback ne radi kako očekujem
💡 REŠENJE:

  • Proveri redosled opcija u lancu (PRIMARY → SECONDARY → …)
  • Svaka opcija mora ili vratiti rezultat ili baciti Exception
  • Koristi get_health_report() da vidiš šta se dešava

GREŠKA: Degraded servis vraća generičke odgovore
💡 REŠENJE:

  • To je by design – degraded servis je poslednja linija odbrane
  • Možeš proširiti responses dictionary sa više opcija
  • Razmisli o dodavanju keširanih odgovora iz prethodnih sesija

GREŠKA: Memory leak sa circuit breaker istorijom
💡 REŠENJE:

  • Dodaj čišćenje stare istorije (npr. starije od 24h)
  • Ograniči maksimalan broj zapisa u state_changes
  • Implementiraj rotaciju log fajlova

Proveri svoje razumevanje

[NIVO 1]:

  1. Šta je retry logika i zašto je korisna?
  2. Objasni tri stanja circuit breaker-a
  3. Šta znači fallback strategija?
  4. Zašto je graceful degradation bolje od potpunog pada?

[NIVO 2]:

  1. Kako exponential backoff sprečava preopterećenje servera?
  2. Kada bi koristio circuit breaker umesto samo retry logike?
  3. Kako bi dizajnirao fallback lanac za e-commerce sajt?
  4. Koje metrike bi pratio za health monitoring sistema?
  5. Kako bi implementirao keširanje kao deo fallback strategije?

🤔 MINI-KVIZ (dodatna pitanja)

  1. U kom bi slučaju fallback direktno na statički odgovor bio bolji od pokušaja lokalne simulacije?
  2. Navedite najmanje dva metrika koja bi alarmirala DevOps tim da je circuit breaker prečesto OPEN.
  3. Kako biste implementirali graceful degradation u scenario gde se AI koristi za generisanje alt-teksta na e-commerce sajtu?

Ažuriranje dokumentacije

Ažuriraj README.md:

## 🚀 Trenutni Status

- ✅ Dan -3: Python 3.13+ instaliran
- ✅ Dan -2: PyCharm unified edition podešen  
- ✅ Dan -1: GitHub repository kreiran
- ✅ Dan 0: Profesionalna struktura projekta
- ✅ Dan 1: Prvi Python moduli - Vasa može da pozdravi!
- ✅ Dan 2: Razumevanje AI API-ja - simulacija komunikacije
- ✅ Dan 3: Multi-provider podrška - OpenAI i Gemini
- ✅ Dan 4: Prvi AI poziv - univerzalni sistem sa SSL fix-om
- ✅ Dan 5: Profilisanje i optimizacija - automatski izbor najboljih postavki
- ✅ Dan 6: Resilience sistem - retry, circuit breaker, fallback i graceful degradation! 🛡️
- ⏳ Dan 7: Napredna personalizacija Vase (sutra)

## 🛡️ Resilience funkcionalnosti

Učitelj Vasa sada ima naprednu zaštitu:
- **Retry logika**: Automatski pokušava ponovo pri privremenim greškama
- **Circuit Breaker**: Štiti sistem od kaskadnih padova
- **Fallback strategije**: Primary → Secondary → Simulation → Static
- **Graceful Degradation**: Radi sa ograničenim mogućnostima umesto pada
- **Health Monitoring**: Praćenje zdravlja svih komponenti

## 🏥 Sistem stabilnosti

[AI Poziv] → [Retry Wrapper] → [Circuit Breaker] → [Fallback Chain]
↓ ↓ ↓
(3 pokušaja) (Zaštita od pada) (Plan B, C, D)

Dodaj u docs/development_log.md:

## Dan 6: Rukovanje greškama i resilijentnost (18.06.2025)

### Šta je urađeno:
- ✅ Kreiran RetryHandler sa exponential backoff
- ✅ Implementiran SmartRetry sa statistikama
- ✅ Kreiran CircuitBreaker sa tri stanja
- ✅ Implementiran FallbackManager sa lancima
- ✅ ResilientAIServiceWrapper integrisan u factory
- ✅ DegradedAIService za emergency situacije
- ✅ Health monitoring sistem
- ✅ Test scenariji za sve failure modove

### Naučene lekcije:
- Greške nisu izuzetak već pravilo u distribuiranim sistemima
- Exponential backoff sprečava preopterećenje
- Circuit breaker daje servisu vreme da se oporavi
- Fallback strategije omogućavaju kontinuitet rada
- Graceful degradation je bolje od potpunog pada
- Korisnik uvek treba da dobije NEKI odgovor

### Problemi i rešenja:
- **Problem**: Kako elegantno integrisati sve resilience komponente?
- **Rešenje**: Wrapper pattern omogućava dodavanje bez menjanja postojećeg koda
- **Problem**: Kada retry a kada circuit breaker?
- **Rešenje**: Retry za pojedinačne pozive, CB za zaštitu celog servisa

### Testiranje:
- Network issues: Sistem se oporavlja nakon 2-3 pokušaja
- Circuit breaker: Otvara se nakon 3 greške, recovery nakon 30s
- Graceful degradation: Osnovni odgovori kada ništa ne radi
- Fallback chain: Glatko prebacivanje između servisa

### Za sutra (Dan 7):
- Napredna personalizacija Vase
- User profili i preference
- Kontekstualna prilagođavanja

Git commit za danas

git add .
git commit -m "Dan 6: Implementiran kompletan resilience sistem!"
git push

ČESTITAM! 🎉 Učitelj Vasa je sada izuzetno otporan sistem koji elegantno rukuje greškama! Implementirao si:

  • Retry logiku koja pametno pokušava ponovo
  • Circuit breaker koji štiti od kaskadnih padova
  • Fallback strategije za kontinuitet rada
  • Graceful degradation za najgore scenarije

Ovo su napredne tehnike koje koriste najveće tech kompanije za svoje kritične sisteme!

Sutra Učitelj Vasa uči

Sutra se vraćamo Vasinoj ličnosti i ponašanju. Naučićeš kako da kreiraš sistem za user profile koji pamti preference korisnika, kako da prilagodiš Vasino ponašanje različitim tipovima korisnika (početnik vs napredni), i kako da implementiraš kontekstualno svesno ponašanje. Vasa će postati personalizovan asistent koji se prilagođava svakom korisniku!

📚 REČNIK DANAŠNJE LEKCIJE:

  • Resilience: Sposobnost sistema da nastavi rad uprkos greškama
  • Retry Logic: Automatsko ponavljanje neuspešnih operacija
  • Exponential Backoff: Algoritam koji progresivno povećava vreme između pokušaja
  • Jitter: Nasumična komponenta koja sprečava sinhronizovane pokušaje
  • Circuit Breaker: Pattern koji prekida pozive kada servis ne radi
  • Fallback: Rezervni plan kada primarna opcija ne radi
  • Graceful Degradation: Postupno smanjenje funkcionalnosti umesto potpunog pada
  • Thundering Herd: Problem kada više klijenata pokušava istovremeno
  • Health Monitoring: Praćenje zdravlja i performansi sistema
  • Cascading Failure: Kada pad jednog servisa izaziva pad drugih

Dodatni primeri i zabavne vežbe

  1. Vežba „Greška na zahtev” – u unreliable_service slučajno oborite svaki drugi zahtev (if i % 2 == 0) da biste posmatrali smenjivanje CLOSED → OPEN → HALF-OPEN stanja.
  2. „Game over, again!” – napravite CLI igricu koja šalje upite AI-ju; kada AI padne, retry + fallback automatski vraćaju simplified engine kako bi igra bila pokretna i offline.
  3. „Latency bingo” – u petlji od 100 poziva merite RTT; nacrtajte histogram i označite koji percentil najviše doprinosi otvaranju circuit-a.

🎈 ZABAVNA ČINJENICA: Google je jednom analizirao više od 200 miliona request-ova i otkrio da je median HTTP odgovor 52 ms – ali da 1 % najsporijih zahteva generiše čak 70 % ukupnih grešaka! Drugim rečima, lov na retke „spore” slučajeve dramatično poboljšava stabilnost. Netflix-ov tim je otkrio da se polovina njihovih production bug-ova pojavi tek na nivou fallback-a, nikad u primarnom kodu – zato testiraj i Plan B jednako ozbiljno kao Plan A!