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:
- Auto se potpuno zakoči usred puta (kraš programa)
- 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:
- Fail Fast – Brzo prepoznaj problem
- Retry with Backoff – Pokušaj ponovo, ali pametno
- Circuit Breaker – Zaštiti sistem od kaskadnih padova
- Fallback – Uvek imaj Plan B
- 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
- Koja je razlika između fixed i exponential backoff-a?
- Šta znači jitter i čemu služi?
- 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:
- CLOSED (zatvoreno) – sve radi normalno, pozivi prolaze
- OPEN (otvoreno) – previše grešaka, blokira sve pozive
- 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
🔄 VEŽBA
- U
RetryConfigdodaj opcijumax_total_time(npr. 30 s) koja prekida sve pokušaje čim pređe ukupno vreme. - Proširi
SmartRetryda vodi statistiku „prosečno trajanje uspešnog poziva”. - Izmeri kako promena
exponential_baseutiče na stres servera – koristitime.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:
- 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.
- Pogrešna klasifikacija greške:
should_retryfunkcija 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. - Preagresivna podešavanja:
failure_thresholdod 3 je dobar za testiranje, ali u produkciji može biti previše nizak. Ako servis ima prolazne “štucavice”, prag od 5-10 uzrecovery_timeoutod 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_retryfunkciju – možda vraća True za grešku koja nije privremena - Smanji
max_attemptsu 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
responsesdictionary 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]:
- Šta je retry logika i zašto je korisna?
- Objasni tri stanja circuit breaker-a
- Šta znači fallback strategija?
- Zašto je graceful degradation bolje od potpunog pada?
[NIVO 2]:
- Kako exponential backoff sprečava preopterećenje servera?
- Kada bi koristio circuit breaker umesto samo retry logike?
- Kako bi dizajnirao fallback lanac za e-commerce sajt?
- Koje metrike bi pratio za health monitoring sistema?
- Kako bi implementirao keširanje kao deo fallback strategije?
🤔 MINI-KVIZ (dodatna pitanja)
- U kom bi slučaju fallback direktno na statički odgovor bio bolji od pokušaja lokalne simulacije?
- Navedite najmanje dva metrika koja bi alarmirala DevOps tim da je circuit breaker prečesto OPEN.
- 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
- Vežba „Greška na zahtev” – u
unreliable_serviceslučajno oborite svaki drugi zahtev (if i % 2 == 0) da biste posmatrali smenjivanje CLOSED → OPEN → HALF-OPEN stanja. - „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.
- „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!