Idempotency: API'lerde Güvenli Retry için Başlangıç Rehberi
API, ödeme akışı ve mesaj tüketicisi geliştiren yazılımcılar için idempotency'ye pratik bir giriş. HTTP metot semantiği, idempotency key'leri, veritabanı upsert ve yaygın tuzakları çalışan Node.js örnekleriyle anlatır.
Ağ bağlantıları kopar. İstekler zaman aşımına uğrar. Client'lar retry yapar. API'niz gerçek yeni bir istek ile önceki bir isteğin retry'ı arasındaki farkı anlayamıyorsa kullanıcılarınız iki kez ücretlendirilir, siparişleri tekrarlanır ve destek kutunuz dolar.
Idempotency, retry'ları güvenli hale getiren özelliktir. Bu rehber ne anlama geldiğini, neden önemli olduğunu ve gerçek kodda nasıl uygulanacağını anlatıyor.
Idempotency Ne Demek
Bir işlem idempotent ise, onu birden fazla kez çalıştırmak bir kez çalıştırmakla aynı sonucu üretir. Matematiksel tanımı f(f(x)) = f(x). Pratikte: aynı endpoint'i aynı girdiyle iki kez çağırırsanız sistem, tek çağırmışsınız gibi aynı duruma varır.
İncelik burada. Idempotency, duplicate isteklerin sisteme ulaşmasını engellemek değildir. Duplicate isteklerin duplicate etki oluşturmamasını sağlamaktır. Ağ, siz istesiniz ya da istemeyin retry yapacak. Sizin göreviniz buna tolerans göstermek.
Üç İlgili Terim
- Safe: hiç yan etki yok (GET isteği)
- Idempotent: yan etki oluşur ama tekrarlar yeni bir şey eklemez (PUT, DELETE)
- Pure: deterministik, yan etkisiz, dış state'e bağımsız (matematik fonksiyonları)
Her safe işlem idempotent'tır. Her idempotent işlem safe değildir.
Neden Önemli: Çift Ücretlendirme Problemi
Bir ödeme akışını düşünün:
İlk ödeme sunucuda başarıyla gerçekleşti ama yanıt client'a hiç ulaşmadı. Client retry yaptı ve müşteri iki kez ücretlendirildi. Sunucunun, ikinci isteğin birincinin retry'ı olduğunu anlamasının bir yolu yoktu.
Çözüm bir idempotency key: client'ın bir kez üretip aynı mantıksal işlemin tüm retry'larında yeniden kullandığı benzersiz bir ID.
HTTP Metotları ve Idempotency
RFC 9110, standart HTTP metotları için sözleşmeyi tanımlar:
Aynı gövdeyle üç kez çağrılan bir PUT /users/123, kullanıcı kaydını aynı durumda bırakır. İki kez çağrılan DELETE /orders/456, siparişin silinmiş olmasıyla sonuçlanır. Ama POST /orders her seferinde yeni bir sipariş oluşturur, dolayısıyla varsayılan olarak idempotent değildir.
Bu önemli çünkü tarayıcılar, proxy'ler ve HTTP client'ları GET, PUT ve DELETE isteklerini otomatik retry edebilir, güvenli olduklarını varsayarlar. PUT uygun olan yerde POST kullanırsanız bu garantiyi kaybedersiniz.
Idempotency Key Deseni
Bu, POST işlemlerini idempotent yapmanın standart yolu ve Stripe tarafından yaygınlaştırıldı.
Nasıl Çalışır
- Client, isteği göndermeden önce bir UUID üretir.
- Client onu bir
Idempotency-Keyheader'ında gönderir. - Sunucu, hızlı bir store'da (Redis, DynamoDB) bu key'i arar.
- Key yeniyse sunucu isteği işler ve tam yanıtı TTL ile saklar.
- Key zaten varsa sunucu, iş mantığını yeniden çalıştırmadan saklanan yanıtı döndürür.
Minimal Bir Express Uygulaması
Bu middleware temelleri ele alıyor: kullanıcıya göre scope, eşzamanlılık için kilit, tam yanıtı saklama ve retry'da tekrar oynatma. Production sistemleri genelde daha fazlasını ekler: işleniyor/tamamlandı ayrımı, request body'nin saklanan key ile eşleştiğini doğrulama ve daha zengin hata yönetimi.
Client Tarafı
Önemli nokta: client key'i bir kez, ilk denemeden önce üretir ve her retry'da aynısını kullanır. Her denemede yeni bir UUID üretmek, tüm deseni geçersiz kılar.
Veritabanı Seviyesinde Idempotency
Bazen bu işi veritabanı sizin için yapabilir. Unique constraint'ler ve conditional write'lar, neredeyse hiç uygulama kodu olmadan idempotency sağlar.
PostgreSQL Upsert
Çağıran taraf sipariş ID'sini sağlıyorsa bunu iki kez çalıştırmak tabloda aynı satırı bırakır. İkinci çağrı DO NOTHING yüzünden hiçbir şey döndürmez ve handler'ınız bunu başarılı bir no-op olarak değerlendirebilir.
DynamoDB Conditional Write
attribute_not_exists koşulu write'ın sadece ilk seferde başarılı olmasını sağlar. Retry'lar catch bloğuna düşer ve no-op olur.
Mesaj Kuyruklarında Idempotency
Kuyrukların çoğu exactly-once değil, at-least-once teslimat garantisi verir. SQS, Kafka, RabbitMQ ve Pub/Sub; hepsi aynı mesajı birden fazla kez teslim edebilir. Tüketiciler ack atmadan önce çöker, visibility timeout'lar dolar, producer'lar retry yapar. Tüketiciniz replay'leri tolere etmek zorunda.
Mesaj ID'si ile anahtarlanan, retention penceresinden biraz uzun TTL'li basit bir dedup tablosu genelde yeterli. Daha güçlü garantiler için yan etkiyi ve "işlendi" işaretini aynı veritabanı transaction'ında birleştirin (outbox deseni).
Exactly-Once Bir Mit
Dağıtık sistemlerde exactly-once teslimat imkansızdır. Bu teorik bir sonuç (İki Generaller Problemi, daha fazlası için okuyabilirsiniz). Başarabileceğiniz şey, at-least-once teslimat ile idempotent handler'ları birleştirerek exactly-once işleme:
Kafka'nın "exactly-once semantics"i Kafka ekosistemi içinde çalışır; ama bir e-posta gönderdiğiniz veya dış bir API çağırdığınız anda yine idempotent handler'lara ihtiyacınız var.
Yaygın Tuzaklar
Pratikte idempotency'yi bozan hatalar bunlar.
1. Handler İçinde Wall-Clock Time Kullanmak
Handler'ınız her çağrıda created_at = NOW() hesaplıyorsa, saklanan satırlar ilk çağrı ile retry arasında farklılaşır. İşlem artık katı anlamda idempotent değildir.
Çözüm: timestamp'leri bir kez yakalayın ve idempotency key ile birlikte saklayın ya da parametre olarak geçirin.
2. Idempotency Sınırı Dışındaki Yan Etkiler
Yaygın bir desen: önce veritabanına commit, sonra e-posta gönder. E-posta gönderimi başarısız olup client retry yaparsa veritabanı write'ı iki kez gerçekleşir (korunmuyorsa) ya da e-posta iki kez gider (korunuyorsa).
Çözüm: e-posta niyetini aynı transaction içinde veritabanına yazın ve ayrı bir worker'ın idempotent şekilde teslim etmesini sağlayın.
3. Idempotency Key Olarak Timestamp
user-123-1696000000 gibi key'ler yük altında çakışır ve saat kayması altında bozulur. UUID v4 veya v7 kullanın. Yalnızca wall-clock time'a asla güvenmeyin.
4. Yanıt Gövdesini Saklamayı Unutmak
Bir key'i "işlendi" olarak işaretleyip yanıtı saklamamak, replay yapamamanıza yol açar. Retry ya hata verir ya da mantığı yeniden çalıştırır.
Çözüm: tam yanıtı (status, header, body) işlendi işareti ile atomik olarak saklayın.
5. Eşzamanlılığı Yok Saymak
Aynı key ile iki eşzamanlı istek, ikisi de cache'i ıskalar, ikisi de handler'ı çalıştırır, ikisi de sonuç saklar. Biri kazanır ama her iki yan etki de gerçekleşmiştir.
Çözüm: "işleniyor olarak işaretle" adımında key üzerinde kilit ya da unique constraint kullanın.
6. Tenant'lar Arasında Key Sızıntısı
Scope'suz global bir key store, bir müşterinin key'inin başkasının işlemiyle eşleşmesine izin verir.
Çözüm: key'leri tenant_id:user_id:endpoint:key olarak scope'layın.
7. Çok Kısa TTL
Key'ler, client pes etmeden önce sona ererse geç bir retry ikinci bir execution'a yol açar.
Çözüm: herhangi makul bir retry penceresinden daha uzun bir TTL seçin. 24 saat makul bir varsayılan.
Ne Zaman Neyi Kullanmalı
- Salt okunur endpoint: GET kullanın. Başka bir şey gerekmiyor.
- Tam değiştirme: PUT kullanın. Sözleşme gereği idempotent.
- Kaynak silme: DELETE kullanın. Sözleşme gereği idempotent.
- Client'ın bildiği ID ile oluşturma: PUT kullanın ya da ID üzerinde unique constraint olan POST.
- Sunucunun ürettiği ID ile oluşturma:
Idempotency-Keyheader'lı POST kullanın. - Mesaj kuyruğu tüketicisi: dedup tablosu ya da outbox deseni, her zaman.
- Ödeme, sipariş, e-posta: idempotency key'leri pazarlık dışıdır.
Sonuç
Idempotency, ölçeklenmeye başladığınızda öğreneceğiniz ileri bir konu değil. Retry mantığı olan her API için temel bir gereksinimdir ki bu da her API demek. HTTP metotlarını doğru seçin, gerçek dünya yan etkileri olan POST endpoint'lerine idempotency key ekleyin, mümkün olan yerde veritabanı constraint'lerini kullanın ve kuyruk tüketicilerinizi replay'leri tolere edecek şekilde tasarlayın.
Desenler karmaşık değil. Önemli olan, ilk çift ücretlendirmenizden sonra değil, öncesinde bunları bilinçli uygulamak.
Kaynaklar
- RFC 9110: HTTP Semantics - Idempotent Methods - Hangi metotların idempotent olduğunu tanımlayan resmi HTTP spesifikasyonu
- MDN Web Docs: Idempotent - HTTP metot idempotency'sinin anlaşılır açıklaması
- Stripe API Documentation: Idempotent Requests - Ödeme API'sinde idempotency key'lerinin kanonik örneği
- Stripe Engineering: Designing Robust and Predictable APIs - Stripe'ın iç implementasyonuna derinlemesine bakış
- AWS Lambda Powertools: Idempotency Utility - Lambda için DynamoDB tabanlı production kütüphanesi
- AWS SQS FIFO: Exactly-Once Processing - Deduplication üzerine AWS dokümantasyonu
- IETF Draft: The Idempotency-Key HTTP Header Field - Header için gelişen standart
- PostgreSQL Documentation: INSERT ON CONFLICT - Idempotent insert'ler için upsert söz dizimi
- DynamoDB Conditional Writes - Idempotent write'lar için condition expression'lar
- Apache Kafka: Semantics and Idempotent Producer - Exactly-once semantics'in sınırları
- The Two Generals Problem - Exactly-once teslimatın neden imkansız olduğu
- Square Developer Docs: Idempotency - Başka bir ödeme sağlayıcısından alternatif bakış