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 paid yap” 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-Key baş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.