Kendi siteme içerik üreten AI pipeline’ını ilk kurduğumda, her şeyin lineer bir akışta kusursuz çalışacağını varsaymıştım. Bir gün sabah uyandığımda, backend loglarında aynı başlık için üretilmiş üç farklı taslak ve her biri için ayrı ayrı harcanmış Gemini Flash token’ları gördüm. Sorun basitti ama çözümü sistem mimarisinin temel taşlarından birine dokunuyordu: Idempotency.
Yazılım dünyasında “bir işlemin birden fazla kez uygulanmasının, ilk uygulamadan sonra sonucu değiştirmemesi” olarak tanımlanan bu kavram, özellikle maliyetli ve uzun süren AI işlemlerinde hayati önem taşıyor. Eğer bir API isteği network hatası yüzünden yarıda kalırsa ve sisteminiz bunu “hiç yapılmadı” sayıp tekrar denerse, hem cüzdanınızdan hem de veri bütünlüğünüzden oluyorsunuz.
Sorun: Neden İki Kere Ödüyoruz?
AI içerik üretim süreci, tipik bir CRUD işleminden çok daha farklı bir doğaya sahip. Bir LLM’e (Large Language Model) prompt gönderdiğinizde, yanıtın gelmesi 10 saniye ile 2 dakika arasında sürebiliyor. Bu süre zarfında HTTP bağlantısı kopabilir, gunicorn worker’ı timeout yiyebilir veya kullandığınız provider (OpenRouter, Groq vs.) yanıtı gönderse bile sizin tarafınızda bir ConnectionResetError oluşabilir.
Geçen ay başıma gelen tam olarak buydu. Bir içerik üretimi başlattım, sistem Gemini Flash’a isteği gönderdi, model içeriği üretti ama yanıt bana dönerken network katmanında bir paket kaybı yaşandı. Benim retry (tekrar deneme) mekanizmam devreye girdi ve aynı içerik için tekrar istek attı. Sonuç? Veritabanında mükerrer kayıtlar ve boşuna giden işlemci gücü.
Idempotency Key Stratejisi
Bu sorunu çözmek için ilk yaptığım iş, her bir içerik üretim talebine benzersiz bir idempotency_key atamak oldu. Bu key’i genellikle içeriğin temel parametrelerinden (başlık, hedef dil ve tarih gibi) bir hash oluşturarak elde ediyorum. Eğer sistem aynı hash ile yeni bir istek alırsa, yeni bir işlem başlatmak yerine mevcut işlemin durumunu kontrol ediyor.
Bu stratejiyi uygularken veritabanı seviyesinde UNIQUE constraint kullanmak en sağlam yol. PostgreSQL üzerinde çalışırken, sadece bir sütunu değil, belirli bir logic’i korumak için EXCLUSION constraint veya PARTIAL INDEX bile gerekebiliyor. Ama en basiti, işleme özel üretilen anahtarı veritabanına “önce ben geldim” diyerek yazmak.
Veritabanı Seviyesinde Koruma
PostgreSQL’de ON CONFLICT ifadesi hayat kurtarır. Bir içeriği üretmeye başlamadan önce bir “işlem kaydı” oluşturuyorum. Eğer bu kayıt zaten varsa, veritabanı bana hata döndürüyor ve ben de “bu zaten yapılıyor veya yapıldı” diyerek süreci sonlandırıyorum.
INSERT INTO content_tasks (idempotency_key, status, title)
VALUES ('hsh_827364812', 'processing', 'Sistem Mimarisi Üzerine')
ON CONFLICT (idempotency_key)
DO NOTHING;
Bu basit SQL sorgusu, aynı anda tetiklenen iki worker’ın aynı işi yapmasını engelliyor. Eğer ilk satır eklendiyse, ikinci worker DO NOTHING sayesinde sessizce dağılıyor. Ancak burada dikkat edilmesi gereken bir nokta var: İşlem yarıda kalırsa ne olacak?
Durum Yönetimi ve “Zombi” Tasklar
İşlem başladı (processing), ancak sunucu restart attı veya kernel panic oldu. Bu durumda o idempotency_key veritabanında sonsuza kadar “işleniyor” olarak kalır. Ben buna “zombi task” diyorum. Bu sorunu aşmak için bir updated_at sütunu ve bir timeout mekanizması kullanıyorum.
Eğer bir task 10 dakikadan uzun süredir “processing” durumundaysa, sistem bunu “fail” kabul edip tekrar denenebilir hale getiriyor. Bunu yaparken Redis üzerinde dağıtık bir kilit (distributed lock) kullanmak, birden fazla sunucunun (node) olduğu senaryolarda çakışmaları önlemek için şart.
Multi-Provider Fallback ve State Karmaşası
Kendi pipeline’ımda sadece tek bir AI provider’a bağlı kalmıyorum. Gemini Flash yanıt vermezse OpenRouter üzerinden başka bir modele, o da olmazsa Groq’a fallback yapıyorum. Bu durum idempotency yönetimini daha da karmaşıklaştırıyor. Çünkü her provider’ın farklı timeout süreleri ve hata kodları var.
Bir üretim ERP’sinde çalışırken öğrendiğim en önemli ders şuydu: “Yazılım mimarisi çoğu zaman kod değil, organizasyonel akıştır.” AI tarafında da bu böyle. Hangi modelin hangi aşamada başarısız olduğunu takip etmezseniz, sisteminiz “idempotent” olsa bile tutarsız veri üretebilir. Bu yüzden her provider denemesini ayrı bir attempt olarak logluyorum.
| Aşama | Provider | Durum | Süre (sn) |
|---|---|---|---|
| Attempt 1 | Gemini Flash | Timeout | 30 |
| Attempt 2 | Groq (Llama 3) | Success | 4.2 |
| Attempt 3 | - | Skipped | - |
Pratik Bir Uygulama: FastAPI ve SQLAlchemy Örneği
Python (FastAPI) tarafında bu işi nasıl yönettiğime dair bir kod parçası paylaşayım. Burada önemli olan, veritabanı işlemini bir transaction içinde yönetmek ve dış servise (AI API) gitmeden önce durumu “başladı” olarak işaretlemektir.
async def generate_ai_content(db: Session, request: ContentRequest):
# Idempotency key oluştur
id_key = generate_hash(request.title, request.context)
# Mevcut taskı kontrol et
task = db.query(ContentTask).filter_by(idempotency_key=id_key).first()
if task:
if task.status == "completed":
return task.result
if task.status == "processing" and not is_timed_out(task):
raise HTTPException(status_code=409, detail="İşlem devam ediyor")
# Yeni task oluştur veya eskini güncelle
task = upsert_task(db, id_key, status="processing")
try:
# Asıl AI çağrısı burada yapılıyor
content = await ai_provider.call(request.prompt)
task.status = "completed"
task.result = content
db.commit()
return content
except Exception as e:
task.status = "failed"
task.error_log = str(e)
db.commit()
raise e
Buradaki upsert_task fonksiyonu, eğer task varsa ve timeout olmuşsa onu tekrar “processing”e çekmeli. Bu, “atleast-once” delivery garantisi sağlar. Yani içerik en az bir kere üretilecektir ama idempotency sayesinde asla mükerrer olmayacaktır.
Monitoring ve Observability
Sistemi kurup bırakmak yetmiyor. PostgreSQL WAL (Write-Ahead Logging) bloat’u gibi sorunlar bazen bu hızlı insert-update trafiğinden kaynaklanabiliyor. Özellikle yüksek hacimli bir içerik üretim hattınız varsa, vacuum operasyonlarını yakından izlemeniz gerekir. Ben kendi sistemimde auditd ile dosya bütünlüğünü izlerken, uygulama seviyesinde de her başarısız idempotency kontrolünü bir metrik olarak Prometheus’a basıyorum.
Eğer “Conflict” hataları (HTTP 409) bir anda artıyorsa, bu genellikle bir yerlerde retry mekanizmasının çok agresif çalıştığını veya bir worker’ın “deadlock” durumuna düştüğünü gösterir. 20 yıllık saha tecrübemde gördüğüm en büyük hata, bu tip metrikleri “zaten çalışıyor” diyerek görmezden gelmektir.
Sonuç
AI içerik pipeline’ı kurmak sadece bir prompt gönderip yanıtı kaydetmek değildir. Gerçek dünya şartlarında; ağ kopar, API çöker, disk dolar. Idempotency, bu kaosun içinde verilerinizin temiz kalmasını sağlayan emniyet kemeridir. Kendi yan ürünlerimde ve kurumsal projelerimde bu prensibi ne zaman es geçsem, mutlaka bir gece yarısı operasyonuyla veritabanı temizlemek zorunda kaldım.
Bir sonraki yazıda, bu pipeline’ın üzerine inşa ettiğim RAG mimarisinde vektör veritabanı seçimlerimi ve neden bazı popüler çözümlerden kaçındığımı anlatacağım.