8.5 Hata Yönetimi — Yeniden Deneme (Retry), Devre Kesici (Circuit Breaker), Yedek Plan (Fallback)¶
Yabancı kelime mi gördün?
Yeniden deneme (retry) = başarısız çağrıyı tekrar denemek. Üstel geri çekilme (exponential backoff) = denemeler arası süreyi kat kat artırma (1s, 2s, 4s, 8s). Sapma (jitter) = geri çekilme süresine rastgele eklenti; birçok istemci aynı anda tekrar denemesin diye. Devre kesici (circuit breaker) = sigorta; peş peşe başarısızlıkta servisi geçici kapatma. Yedek plan (fallback) = ana servis çalışmazsa yedek servise düşme. Ölü mektup kuyruğu (dead letter queue) = işlenemeyen mesajların saklandığı yer; sonradan incelenir.
Neden bu sayfa?¶
8.4'te log + metric kurdun. Log'da hatalar görünür olacak. Bu sayfada o hataları nasıl yakalıyıp çözeceğini öğreniyorsun:
- Anthropic API 429 (rate limit) — saniyede çok istek → bekle + tekrar dene (retry)
- Anthropic API 500 (server error) — Anthropic tarafı geçici → hemen tekrar dene
- Anthropic API timeout — ağ gecikmesi → backoff ile tekrar
- Peş peşe 5 hata → servisi geçici kapat (circuit breaker)
- Claude Sonnet down → Haiku'ya düş (fallback)
Bu davranışlar otomatik olmalı — her hatada elle müdahale imkansız. 9.5 agent saat başı çalışır, gece 03:00'da rate limit'e takılırsa ne olacak? Bu sayfanın cevabı.
İkincisi: Yanlış retry cehennem yaratır. Sonsuz retry + her retry $0.03 çağrı → 10.000 retry = $300 fatura, hiçbir cevap. 8.3'teki hard cap bunu sınırlar ama doğru retry disiplini ilk başta gerekli.
Üçüncüsü: Bu sayfa Bölüm 8'in 5. sayfası — sonraki sayfa 8.6 Production Checklist (imza sayfası). 8.6 bu sayfanın uygulama kontrol listesi; burada kavramı öğren, 8.6'da her canlı projene çalıştırırsın.
Hata taksonomisi — 4 tip¶
flowchart TB
ERR["❌ API Hatası"]
subgraph TRANSIENT["🔄 Geçici — retry uygula"]
T1["429 Rate Limit\nexponential backoff"]
T2["500/502/503\nhemen + backoff"]
T3["Timeout\nbackoff + timeout artır"]
end
subgraph PERM["🛑 Kalıcı — retry YOK"]
P1["400 Bad Request\nsenin hatan, düzelt"]
P2["401/403 Auth\nkey yanlış"]
P3["404 Not Found\nendpoint yok"]
end
subgraph DEGRAD["⚠️ Bozulma — fallback"]
D1["Sonnet down\n→ Haiku'ya düş"]
D2["Qdrant down\n→ cached response"]
end
ERR --> T1
ERR --> T2
ERR --> T3
ERR --> P1
ERR --> P2
ERR --> P3
ERR --> D1
ERR --> D2
classDef tr fill:#fef3c7,stroke:#ca8a04,color:#111
classDef pr fill:#fed7aa,stroke:#ea580c,color:#111
classDef dg fill:#ddd6fe,stroke:#7c3aed,color:#111
class T1,T2,T3 tr
class P1,P2,P3 pr
class D1,D2 dg
Kritik ayrım:
- Geçici (4xx=429, 5xx): retry uygula. Çoğu zaman 2. veya 3. denemede başarılı.
- Kalıcı (4xx ≠ 429): retry İŞE YARAMAZ. Request zaten yanlış; tekrarlamak aynı hata.
- Bozulma: ana servis ölü, yedeğe düş.
Yanlış retry (kalıcı hataları retry etmek) fatura patlatır + log'ı spam'ler.
Retry — Tenacity ile¶
Tenacity Python'da retry standartı.
Temel kullanım¶
# pip install tenacity==9.1.4
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import anthropic
client = anthropic.Anthropic()
@retry(
stop=stop_after_attempt(3), # En çok 3 deneme
wait=wait_exponential(multiplier=1, min=1, max=10), # 1s, 2s, 4s, ... (üstel geri çekilme)
retry=retry_if_exception_type((
anthropic.RateLimitError, # 429 — istek sınırı aşıldı
anthropic.APIConnectionError, # ağ kopukluğu
anthropic.APIStatusError, # 5xx (sunucu hatası)
anthropic.OverloadedError, # Anthropic geçici aşırı yüklenme
)),
reraise=True, # 3. denemede de başarısız → istisna fırlat
)
def claude_cagir(prompt: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
Ne oluyor:
- İlk çağrı 429 alırsa → 1 saniye bekle, tekrar dene
-
- deneme yine 429 → 2 saniye bekle
-
- deneme → 4 saniye bekle (exponential)
-
- deneme de başarısız → exception fırlat
Kalıcı hatalar (400, 401): retry_if_exception_type listede yok → hemen fırlatılır, retry yok.
Jitter — çakışma önle¶
Çok client aynı anda 429 aldıysa hepsi aynı ½/4 saniyede tekrar denemeye gelir → herd effect (sürü etkisi). Çözüm: jitter:
from tenacity import wait_exponential_jitter
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(initial=1, max=10), # 1s ± rand, 2s ± rand...
retry=retry_if_exception_type((anthropic.RateLimitError, anthropic.APIConnectionError)),
)
def claude_cagir(prompt: str) -> str:
...
Her retry farklı zamanda gelir, aynı saniyede 100 istek olmaz.
Async version¶
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(initial=1, max=10),
)
async def claude_cagir_async(prompt: str) -> str:
async with anthropic.AsyncAnthropic() as client:
response = await client.messages.create(...)
return response.content[0].text
Aynı decorator async fonksiyonda da çalışır.
Callback — her deneme log¶
from tenacity import retry, before_sleep_log
import logging
log = logging.getLogger("retry")
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(initial=1, max=10),
before_sleep=before_sleep_log(log, logging.WARNING),
)
def claude_cagir(prompt: str) -> str:
...
Her retry öncesi log'a WARNING yazar:
8.4'teki structured log'a trace_id ile entegre → hangi istek tekrar etti görünür.
Timeout disiplin¶
Retry ile beraber timeout olmalı. Timeout yoksa tek istek 60 saniye takılır, retry 3× → 3 dakika bekleme → kullanıcı çıkar.
Claude için sağlıklı timeout değerleri:
- Haiku: 10-15 saniye (hızlı model)
- Sonnet: 20-30 saniye (orta)
- Opus: 45-60 saniye (yavaş ama derin)
Streaming kullanıyorsan timeout ilk token için, toplam için infinite olabilir:
# Streaming
with client.messages.stream(
model="claude-sonnet-4-6",
messages=[...],
max_tokens=1024,
) as stream:
for text in stream.text_stream:
yield text
# İlk token için client timeout (30s) geçerli
# Toplam yanıt 90 saniye sürebilir — streaming'in avantajı
Circuit breaker — sigorta atışı¶
Peş peşe başarısızlıkta retry bile işlemiyor. Anthropic outage'ı ise 30 dakika sürebilir. Her istek 30 saniye bekleyip 429 alırsa çok vakit + token harcar. Circuit breaker: peş peşe X hata sonrası servisi kapatır, direkt exception döner:
# pip install pybreaker==1.4.1
import pybreaker
import anthropic
# Circuit breaker: 5 peş peşe başarısızlık → 60 saniye kapalı (OPEN)
claude_breaker = pybreaker.CircuitBreaker(
fail_max=5,
reset_timeout=60,
exclude=[ValueError, KeyError], # bu exception'lar fail saymaz
)
@claude_breaker
def claude_cagir(prompt: str) -> str:
response = client.messages.create(...)
return response.content[0].text
# Test — 5 hata sonrası
for i in range(10):
try:
sonuc = claude_cagir("test")
except pybreaker.CircuitBreakerError:
# Circuit açık, istek Anthropic'e bile gitmedi
print("Circuit açık, 60 saniye bekle")
break
except anthropic.RateLimitError:
print(f"{i}: RateLimit")
3 durum:
- CLOSED (normal): İstekler geçer, başarılı/başarısız sayılır.
- OPEN (sigorta atmış): 5 başarısızlık sonrası. 60 saniye boyunca hiçbir istek gitmez, anında
CircuitBreakerError. - HALF_OPEN (test): 60 saniye sonra 1 istek dener. Başarılı → CLOSED. Başarısız → tekrar OPEN.
Retry vs circuit breaker ilişkisi:
- Retry + circuit breaker birlikte kullanılır.
- Retry: geçici hataları tek çağrıda tekrarlar (3 deneme).
- Circuit breaker: birden çok çağrı boyunca hata say. 5 çağrıda 5 başarısızlık = servis gerçekten düştü → kapat.
Fallback — yedek model zinciri¶
Anthropic Sonnet down (nadir ama olur). Servisin çalışsın:
from anthropic import Anthropic, APIError
client = Anthropic()
MODEL_ZINCIRI = [
"claude-sonnet-4-6", # İlk tercih (kalite)
"claude-haiku-4-5", # Yedek (daha ucuz + hızlı)
]
def claude_cevapla_fallback(prompt: str) -> str:
"""Ana model down ise Haiku'ya düş."""
son_hata = None
for model in MODEL_ZINCIRI:
try:
response = client.messages.create(
model=model,
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
if model != MODEL_ZINCIRI[0]:
log.warning(f"Fallback: {MODEL_ZINCIRI[0]} yerine {model} kullanıldı")
return response.content[0].text
except APIError as e:
log.error(f"{model} başarısız: {e}")
son_hata = e
continue
# Tüm modeller başarısız
raise RuntimeError("Tüm modeller başarısız") from son_hata
Varyasyonlar:
- Tam fallback: Claude down → OpenAI GPT'ye düş. Proje OpenAI-uyumlu arayüz (LangChain veya manuel) kullanıyorsa.
- Cache fallback: Son bilinen iyi cevap Redis'te. LLM çağırmayı bile denemeden o cevap.
- Static fallback: "Sistem şu an yoğun, 5 dakika sonra dener misiniz?" — kullanıcıya dürüst cevap.
Graceful degradation — parça parça bozulma¶
RAG Chatbot'ta iki dış bağımlılık: Qdrant + Claude. Qdrant down ise:
async def rag_cevapla(soru: str) -> str:
"""RAG retrieval başarısız olursa LLM'e sadece soruyu gönder."""
try:
chunks = await qdrant_ara(soru)
context = "\n\n".join(chunks)
prompt = f"Kaynaklar:\n{context}\n\nSoru: {soru}"
except (qdrant_client.ApiException, ConnectionError) as e:
log.error(f"Qdrant başarısız, RAG'siz cevap: {e}")
# Graceful: Qdrant yok, sadece LLM ile devam
prompt = f"Soru: {soru}\n\n(Not: şu an kaynakları okuyamıyorum, genel bilgi.)"
return await claude_cevapla_fallback(prompt)
Kullanıcı "sistem çöktü" yerine daha az iyi ama çalışan cevap alır. "Working Okay" > "Perfect or Broken."
9.5 agent için özel: dead letter queue¶
9.5 cron agent saat başı 10-15 haber işler. Bir haber Anthropic'ten sürekli hata alırsa ne olur?
# pipeline.py
async def isle_haber(haber: Haber):
try:
ozet = await ozet_yaz(haber) # yazar
puan = await puanla(ozet) # evaluator
return haber, ozet, puan
except Exception as e:
# 3 retry sonrası hala başarısız
log.error(f"Haber işlenemedi: {haber.url}", extra={"error": str(e)})
# DLQ'ya yaz
conn.execute(
"INSERT INTO dead_letter (haber_url, hata, ts) VALUES (?, ?, ?)",
(haber.url, str(e), datetime.utcnow().isoformat()),
)
conn.commit()
return None # Bu haber atlanır, pipeline devam
Dead letter queue (DLQ) — ölmüş iş kaydı. Her sabah manuel bakarsın:
SELECT haber_url, hata, COUNT(*) as count
FROM dead_letter
WHERE ts > date('now', '-1 day')
GROUP BY hata
ORDER BY count DESC;
Aynı hata tekrarlıyorsa sistem problemi. Tek tük hata ise geçici. Yeniden işlemek istersen:
# reprocess_dlq.py
async def reprocess():
rows = conn.execute("SELECT haber_url FROM dead_letter WHERE ts > date('now', '-7 day')").fetchall()
for (url,) in rows:
try:
await isle_haber(Haber(url=url, ...))
conn.execute("DELETE FROM dead_letter WHERE haber_url = ?", (url,))
except:
pass # hala başarısız, DLQ'da kal
Graceful shutdown — SIGTERM yakalama¶
VPS reboot veya docker compose restart sırasında Uvicorn/cron agent inflight isteği tamamlamadan kapatılmamalı. FastAPI lifespan:
# app/main.py
from contextlib import asynccontextmanager
import signal
@asynccontextmanager
async def lifespan(app):
# Startup
app.state.qdrant = QdrantClient(...)
app.state.voyage = VoyageClient(...)
log.info("app_started")
yield
# Shutdown
log.info("app_stopping")
await app.state.qdrant.close()
log.info("app_stopped")
app = FastAPI(lifespan=lifespan)
Uvicorn SIGTERM aldığında: (1) yeni istek kabul etmez, (2) inflight istekleri tamamlar, (3) lifespan shutdown çalışır, (4) process çıkar. Systemd:
TimeoutStopSec=30 — 30 saniye boyunca graceful shutdown. Sonrası SIGKILL (force).
Custom exception hiyerarşisi¶
Her hatayı Exception olarak yakalamak kabadır. Kendi exception yapısı:
# app/errors.py
class AppError(Exception):
"""Uygulama hata taban sınıfı."""
class RetryableError(AppError):
"""Geçici hata, retry denenebilir."""
class PermanentError(AppError):
"""Kalıcı hata, retry anlamsız."""
class ExternalServiceError(RetryableError):
"""Dış servis hatası (Claude, Qdrant, Voyage)."""
class ValidationError(PermanentError):
"""Kullanıcı girdisi yanlış."""
# Kullanım
try:
sonuc = await risky_function()
except RetryableError as e:
# Retry decorator yakalayacak
raise
except PermanentError as e:
# Kullanıcıya 400 dön
raise HTTPException(400, str(e))
except Exception as e:
# Bilinmeyen hata, 500 + Sentry'e rapor et
log.exception("unknown_error")
raise HTTPException(500, "Internal server error")
Retry decorator da bu sınıflara göre davranır:
CTO tuzakları — 10 hata yönetimi hatası¶
| # | Tuzak | Sonuç | Doğru |
|---|---|---|---|
| 1 | Sonsuz yeniden deneme | $1000 fatura + kilitlenme | stop_after_attempt(3) zorunlu |
| 2 | Jitter (sapma) yok | Sürü etkisi, servis tekrar düşer | wait_exponential_jitter |
| 3 | Zaman aşımı yok | 1 istek 60 sn takılır | Anthropic(timeout=30) |
| 4 | Her istisnada yeniden dene | 400 bad request de tekrarlanır | retry_if_exception_type ile spesifik |
| 5 | Devre kesici yok | 10 dk kesintide $100 fatura | pybreaker 5 hata → 60 sn açık |
| 6 | Yedek model zinciri yok | Sonnet düşerse tam kesinti | Haiku 4.5 yedek |
| 7 | Ölü mektup kuyruğu (DLQ) yok | Başarısız işler sessiz kayıp | SQLite dead_letter tablosu |
| 8 | Graceful shutdown yok | Yayına alma sırasında istek düşer | lifespan + SIGTERM |
| 9 | Genel except Exception |
Her hata aynı | Özel hiyerarşi (RetryableError / PermanentError) |
| 10 | Yeniden deneme logu yok | Neden yavaş bilemezsin | before_sleep_log + trace_id |
Tipik hata yönetimi hataları — şu durum şu çözüm
| Durum | Sebep | Çözüm |
|---|---|---|
| "InvalidRequestError" Anthropic SDK'da yok | Eski OpenAI sınıfı sanılıyor | Anthropic'te BadRequestError (400) kullan |
| Retry sonsuz döngüye girdi | stop kuralı eksik |
stop_after_attempt(3) veya stop_after_delay(60) |
| 401 hatasında bile retry | Authentication retry edilmemeli | retry_if_exception_type listesine AuthenticationError ekleme |
| Deploy sonrası istekler 502 | Graceful shutdown yok | FastAPI lifespan + systemd KillSignal=SIGTERM |
| Circuit breaker hep açık | reset_timeout çok uzun | reset_timeout=60 (saniye) makul; 600+ canlıda fazla |
| 5xx hatası ama Anthropic SDK retry yapmadı | max_retries parametresi 0'a çekilmiş |
Anthropic(max_retries=2) veya tenacity ile elle |
Anthropic SDK hata tipleri¶
🤖 Anthropic-öz: SDK exception hiyerarşisi
anthropic-sdk-python exception sınıfları:
anthropic.APIError (base)
├── anthropic.APIConnectionError # Network
│ └── anthropic.APITimeoutError # Timeout
├── anthropic.APIStatusError # HTTP status
│ ├── anthropic.BadRequestError # 400
│ ├── anthropic.AuthenticationError # 401
│ ├── anthropic.PermissionDeniedError # 403
│ ├── anthropic.NotFoundError # 404
│ ├── anthropic.UnprocessableEntityError # 422
│ ├── anthropic.RateLimitError # 429 ⭐ en yaygın
│ └── anthropic.InternalServerError # 5xx
└── anthropic.APIResponseValidationError
Hangisi retry edilir:
| Exception | Retry? | Neden |
|---|---|---|
APIConnectionError |
✅ | Network geçici |
APITimeoutError |
✅ | Timeout geçici |
RateLimitError (429) |
✅ | Anthropic "biraz bekle" diyor |
InternalServerError (5xx) |
✅ | Anthropic tarafı geçici |
BadRequestError (400) |
❌ | Senin request yanlış |
AuthenticationError (401) |
❌ | Key yanlış |
PermissionDeniedError (403) |
❌ | Yetki yok |
Retry helper:
RETRY_ERRORS = (
anthropic.APIConnectionError,
anthropic.APITimeoutError,
anthropic.RateLimitError,
anthropic.InternalServerError,
)
@retry(retry=retry_if_exception_type(RETRY_ERRORS), ...)
def claude_cagir(prompt):
...
Retry-After header:
Anthropic 429 response'unda retry-after header döner (saniye). Ideal wait ona göre ayarlanmalı:
from tenacity import wait_incrementing
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(initial=1, max=30),
# Basit versiyon — retry-after kullanmıyor
# İleri seviye: wait_chain(retry-after'dan oku, sonra backoff)
retry=retry_if_exception_type(RETRY_ERRORS),
)
def claude_cagir(prompt):
...
Tavsiye: Anthropic resmi anthropic-python SDK'sında built-in retry var. Anthropic(max_retries=3) parametresi basit retry yapar. Ama özel davranış için (jitter, log, circuit breaker) tenacity + pybreaker ekstra.
Çıktı kanıtları — 3 kanıt¶
1. Retry + timeout:
app/claude_client.py içinde tenacity decorator + Anthropic(timeout=30). Test: 429 mock → 3 retry + jitter + log. Kod + test.
2. Circuit breaker:
pybreaker.CircuitBreaker(fail_max=5, reset_timeout=60) Claude call'a wrap. Test: 5 peş peşe 500 → 6. çağrı CircuitBreakerError (hiç Anthropic'e gitmez).
3. Fallback zinciri + DLQ:
Model zinciri (Sonnet → Haiku) kodu var. 9.5 agent DLQ tablosu oluşturuldu, son 24 saatte başarısız işler kayıtlı.
Kanıt klasörü: muhendisal-notlarim/bolum-8/05-hata-yonetimi/
Görev — 60 dk retry + circuit + fallback¶
pip install tenacity==9.1.4 pybreaker==1.4.1ekle.app/claude_client.pyoluştur:client = Anthropic(timeout=30)@retrydecorator (exponential jitter + 3 attempt + trace log)@claude_breaker(pybreaker 5 fail → 60s open)- Fallback zinciri
MODEL_ZINCIRI = ["claude-sonnet-4-6", "claude-haiku-4-5"]. app/errors.pycustom exception hiyerarşisi (RetryableError / PermanentError).- 9.5 için
dead_letterSQLite tablo ekle + DLQ insert helper. - Test: 3 çağrıyı mock rate limit ile dene, retry + log kanıt.
- lifespan graceful shutdown test: Ctrl+C sonra "app_stopped" log.
Başarı kriteri: 1 saat sonra canlı projen 429/500/timeout'ları otomatik yönetir, 10 dakika Anthropic outage'ında fatura şoku yok, 9.5 agent DLQ ile sessiz başarısızlıktan korunur.
- **A → B:** 4 hata tipi (geçici/kalıcı/bozulma) farklı savunma gerekir; retry her yere uygulanmaz. Bu yüzden **hata tipi önce belirlenmeli.**
- **B → C:** Tenacity + `retry_if_exception_type` + jitter + `before_sleep_log` — 4 satır decorator. Bu yüzden **kütüphane boilerplate kaldırır.**
- **C → D:** Timeout retry olmadan anlamsız (30 sn hedef, Haiku 10-15, Opus 45-60). Bu yüzden **timeout + retry birlikte kurulur.**
- **D → E:** Circuit breaker (pybreaker) peş peşe 5 fail → 60s kapalı; retry ile birlikte tamamlayıcı. Bu yüzden **circuit breaker kaskad başarısızlık önler.**
- **E → F:** Fallback model zinciri (Sonnet → Haiku); graceful degradation (Qdrant ölü → LLM tek başına). Bu yüzden **hizmet tamamen durmaz.**
- **F → G:** 9.5 agent DLQ SQLite tablo; sessiz başarısızlık engeli. Bu yüzden **başarısız işler kaybolmaz.**
- **G → H:** Graceful shutdown lifespan + systemd SIGTERM + TimeoutStopSec=30. Bu yüzden **kapatma da hata değil.**
- **H → I:** Custom exception hiyerarşisi (RetryableError / PermanentError) retry decorator'ı kalibre eder. Bu yüzden **exception sınıfı retry kararını verir.**
Sonuç: 3 seviye hata savunması aktif. 429/500/timeout otomatik retry, outage'da circuit açar, ana model down ise yedeğe düşer. Canlı projen dayanıklı. Sonraki (8.6): Bölüm 8 imza sayfası — 15 maddeli production checklist, senin projene birebir çalıştırma.
8.6 Production Checklist → — Bölüm 8 İMZA SAYFASI. 15 maddeli pre-launch checklist + her maddenin kanıtı.
← 8.4 Loglama ve İzleme | Bölüm 8 girişi | Ana sayfa
Pekiştirme: Tenacity docs + pybreaker README + Anthropic SDK errors. Üçünü bir hafta sonu oku, production refleksin tamam olur.