Bir müşteri tek sipariş için iki kez ücretlendirildi. Hiçbir şey bilerek iki kez çalışmadı: ödeme worker’ı kartı çekti, sonra mesajı ack’leyemeden çöktü — broker da sözünü tutup mesajı yeniden teslim etti.
Hata, mükerrer teslimat değildi. Mükerrer teslimat için inşa edilmemiş handler’dı.
At-least-once bir söz, bir arıza değil
Neredeyse her queue — ve yeniden denenen her HTTP çağrısı — at-least-once’tır. Bir handler aynı mesajı birden çok kez görebilir: bir çökme, bir release, bir redelivery ya da timeout’a düşüp yeniden deneyen bir istemci sonrası. Ağ üzerinde exactly-once, broker’ın ucuza veremeyeceği bir koordinasyon olmadan pratik değil; bu yüzden ciddi hiç kimse onu vaat etmez.
Bu, problemi yeniden çerçeveler. Redelivery, yok edilecek bir başarısızlık değil; etrafında tasarım yapılacak bir garanti. (Queue’nun başarısız bir job’u kaybetmek yerine yeniden denerken yaptığı pazarlığın aynısı.)
Idempotent demek “iki kez = bir kez” demek
Bir operasyon, defalarca uygulandığında etkisi bir kez uygulanmışla aynıysa idempotent’tir — f(f(x)) = f(x).
- “Sipariş durumunu
paidyap” idempotent’tir. - “Bakiyeyi 10 artır” değildir — iki kez +20 eder.
- “Karşılama e-postasını gönder” değildir — iki kez iki e-posta eder.
Bütün iş, ikinci tür operasyonu birinciye çevirmek.
Idempotency anahtarı göndericiden gelir
Bu operasyon için kararlı bir kimliğe ihtiyacın var ve bunu gönderici belirlemeli, alıcı değil:
- Bir queue mesajı için bu, üreticinin ürettiği mesaj-başına id’dir — mesaj başına bir id, trace/correlation id’den ayrı (o, birçok mesajı kapsar).
- Bir HTTP yazımı için bu, istemcinin ürettiği
Idempotency-Keybaşlığı’dır — Stripe modeli.
İki kural anahtarı dürüst tutar:
- Yalnızca payload’dan türetme. Meşru biçimde birbirinin aynı iki istek — aynı müşterinin aynı ürünü iki kez alması — çakışır ve ikincisi sessizce düşürülür.
- Anahtarı alıcıya ürettirme. Alıcı, bir retry’ı yepyeni bir çağrıdan ayıramaz; “bu, daha önce denediğim aynı operasyon” bilgisi yalnızca göndericide vardır.
Dedupe: zaten yaptığını hatırla
Handler, işten önce tek bir kontrol yapar: bu anahtar işlendi mi? Evetse atla ve ack’le. Hayırsa işi yap, sonra anahtarı kaydet. Tüm desen bu — ağırlık, kaydın nerede durduğunda.
İş ile kayıt arasındaki çökme
Zor kısım, işi yapmakla anahtarı kaydetmek arasındaki boşluk. İki şekil var ve doğrusu yan etkiye bağlı:
- Seen-set — başarıdan sonra kaydet. Handler döndükten sonra anahtarı Redis’e ya da bir tabloya yaz. Ucuz ve geniş uygulanabilir. Pencere: yan etkiden sonra ama kayıttan önce çökersen, bir redelivery yeniden işler. Yan etkinin kendisi idempotent’se sorun değil — bir
UPSERT, bir “set to paid”. - Transactional — işle birlikte kaydet. Idempotency kaydını, iş değişikliğiyle aynı veritabanı transaction’ında yaz. Pencere yok: anahtar ve etki birlikte commit olur ya da birlikte geri alınır. Bedeli: etki, senin kontrol ettiğin bir DB yazımı olmalı ve anahtar deposu aynı veritabanı olmalı — ayrı bir Redis değil. Çift-çekimi gerçekten bitiren budur.
Yani yan etki senin yerine seçer: sahip olduğun bir satır → transactional; transaction’ına dahil edemeyeceğin üçüncü-taraf bir çağrı (e-posta, bir çekim) → seen-set, ve kendi anahtarını onun Idempotency-Key’i olarak ileterek downstream’i de idempotent yap.
Eşzamanlı mükerrerlere kontrol değil, constraint lazım
Aynı anahtarın, ikisi de kaydetmeden yarışan iki teslimatı bir “önce kontrol et sonra yaz” dizisinden sıyrılır — o dizi atomik değil. Anahtar kolonundaki bir unique constraint atomiktir: ikinci insert, yakalayıp “zaten işlendi” diye ele aldığın bir conflict’e dönüşür. Bu, sıralı numara üretiminde bir race’in açtığı gap’lerin aynı şekli — benzersizlik gerçekte uygulamada değil, veritabanında zorlanır.
Buna ne zaman hiç ihtiyacın yok?
Handler zaten idempotent’se — kararlı bir id ile anahtarlanmış saf bir UPSERT, bir “alanı şu değere ayarla” — hiçbir şeye ihtiyacın olmayabilir. Anahtar + dedupe’a, operasyonun idempotent olmayan bir yan etkisi (para, e-posta, dış bir POST) ya da birikimi (increment, append) olduğunda uzan. Karmaşıklığı yalnızca ikinci çalıştırmanın gerçekten zarar verdiği yere harca.
At-least-once, düzelteceğin kısım değil; kabul ettiğin kısım. Handler’ı, ikinci teslimat bir no-op olacak şekilde kur; o zaman redelivery bir olay olmaktan çıkar ve olduğu şeye döner — broker’ın sözünü tutması.
İlgili: BabelQueue idempotency spec’i anahtarın nereden geldiğini ve dedupe’un nasıl davrandığını sabitler; idempotency-payments örneği ise tam da bu çift-çekim vakasının çalıştırılabilir hâlidir.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.