Entegrasyon

Dış Servise Bağlı Bildirimlerde Kesinti Bir Hata Değildir

Kayıt bizde, bildirim dışarıda. Servis yanıt vermediğinde kaydın kaybolmaması için bildirim durumunu ayrı bir varlık olarak ele almak.

Entegrasyon konulu teknik yazı kapağı

Bir kaydı hem kendi sisteminde tutup hem de dışarıdaki bir servise bildirmen gerekiyorsa — bir kamu web servisi, bir CRM, bir muhasebe sistemi — er ya da geç şu soruyla karşılaşıyorsun: servis yanıt vermediğinde ne oluyor?

Bu sorunun cevabı çoğu entegrasyonun kalitesini belirliyor. Çünkü dış servisin geçici olarak erişilemez olması bir istisna değil; yeterince uzun çalışan her sistemde beklenen bir durum.

"Gönder ve sonucu göster" neden yetmiyor

En doğal kurgu şu: kullanıcı kaydı oluşturur, aynı istek içinde dış servise gönderilir, sonuç ekranda gösterilir.

php
$record = Record::create($data);
$response = $client->notify($record);   // dış servis burada
return $response->successful() ? 'Bildirildi' : 'Hata';

Bu akış mutlu günde çalışıyor. Sorun mutsuz günde:

  • Servis yanıt vermiyor ve kullanıcı bir hata ekranı görüyor. Kayıt oluştu mu, olmadı mı belli değil.
  • Kullanıcı tekrar deniyor. Şimdi iki kayıt var.
  • Ya da kullanıcı vazgeçiyor. Kayıt sistemde duruyor ama hiç bildirilmemiş — ve bunu kimse bilmiyor.
  • İstek zaman aşımına uğruyor, ama dış servis aslında işlemi almış oluyor. En kötüsü bu.

Ortak kusur şu: kaydın kendisiyle bildiriminin durumu tek bir şeymiş gibi ele alınıyor. Halbuki bunlar iki ayrı olay ve ikisi aynı anda başarılı olmak zorunda değil.

Bildirim durumu ayrı bir varlıktır

Kayıtta bildirildi diye bir boolean alan tutmak yeterli görünüyor ama değil. Bir bildirimin kendi geçmişi var: ne zaman denendi, kaç kez denendi, en son ne cevap geldi, hâlâ denenmeli mi?

Bunu ayrı bir tabloya taşımak işi tümüyle değiştiriyor:

sql
CREATE TABLE outbox (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  record_id     BIGINT NOT NULL,
  event_type    VARCHAR(64) NOT NULL,
  payload       JSON NOT NULL,
  idempotency_key VARCHAR(80) NOT NULL,
  status        ENUM('pending','sent','failed') NOT NULL DEFAULT 'pending',
  attempts      INT NOT NULL DEFAULT 0,
  next_retry_at DATETIME NULL,
  last_error    TEXT NULL,
  created_at    DATETIME NOT NULL,
  UNIQUE KEY uniq_idem (idempotency_key),
  KEY idx_due (status, next_retry_at)
);

Artık "bu kayıt bildirildi mi" sorusunun cevabı bir tahmin değil. Ne zaman denendiği, neyle sonuçlandığı ve sırada olup olmadığı görülebiliyor.

Kaydı ve bildirim niyetini aynı anda yaz

Kritik nokta: kaydı oluşturan işlemle bildirim satırını oluşturan işlem aynı transaction içinde olmalı.

php
DB::transaction(function () use ($data) {
    $record = Record::create($data);
    Outbox::create([
        'record_id' => $record->id,
        'event_type' => 'record.created',
        'payload' => $record->toNotificationPayload(),
        'idempotency_key' => "record.created:{$record->id}",
    ]);
});

Bunun adı outbox kalıbı ve tek bir şeyi garanti ediyor: kayıt varsa bildirim niyeti de vardır. Kaydın oluşup bildiriminin hiç kuyruğa girmemesi mümkün olmuyor.

Gönderim ise ayrı bir işçi tarafından, isteğin dışında yapılıyor. Kullanıcı dış servisin hızını beklemiyor; kullanıcıya "işlem alındı" demek için dış servisin cevabına ihtiyaç kalmıyor.

Yeniden denemenin sınırları olmalı

Sonsuz yeniden deneme, çalışmayan bir entegrasyonu sonsuza kadar gizler. Üç şeyi belirlemek gerekiyor:

Aralık. Sabit aralıkla denemek, servis toparlandığı anda ona toplu bir yük bindiriyor. Üstel artan bir bekleme ve üzerine rastgele bir sapma (jitter) daha iyi çalışıyor — bütün işçiler aynı saniyede uyanmıyor.

php
// 1., 2., 3. deneme: ~1dk, ~4dk, ~9dk (üzerine rastgele sapma)
$delay = ($attempt ** 2) * 60 + random_int(0, 30);

Üst sınır. Belirli bir denemeden sonra kayıt failed olmalı. Bu bir kayıp değil, bir görünürlük kazancı: artık birinin bakması gereken bir liste var.

Kalıcı hata ayrımı. Her hata yeniden denemeyi hak etmiyor:

  • Yeniden dene: zaman aşımı, bağlantı hatası, 5xx, 429.
  • Deneme, hemen işaretle: 400 (geçersiz veri), 401/403 (yetki), 422 (doğrulama).

Geçersiz veriyi kırk kez göndermek onu geçerli yapmıyor. Bu ayrımı yapmayan bir kuyruk, çözülemeyecek işlerle doluyor ve gerçekten geçici olan hatalar arada kayboluyor.

Mükerrer gönderimi engellemek

Zaman aşımı, en zor durumu üretiyor: isteği gönderdin, cevap gelmedi, ama karşı taraf işlemi almış olabilir. Yeniden denersen aynı işlem iki kez oluşabilir.

Çözüm iki taraflı. Kendi tarafında her bildirime kararlı bir anahtar veriyorsun — record.created:1042 gibi, rastgele değil, aynı olay için hep aynı üretilen bir değer. Karşı taraf bu anahtarı destekliyorsa (çoğu modern API Idempotency-Key başlığıyla destekliyor) tekrar gönderim güvenli hâle geliyor.

Desteklemiyorsa, göndermeden önce sorgulamak gerekiyor: "bu referansla bir kayıt var mı?" Bu ekstra bir istek maliyeti getiriyor ama mükerrer kayıt üretmekten ucuz.

Bunun neden önemli olduğunu ödeme tarafında çalışırken daha net görüyorsun: aynı ödemenin iki kez işlenmesi sadece bir fazla satır değil, dönüşüm oranını da bozan bir hata. Rapor yanlış çıkıyor ve yanlış olduğunu fark etmek günler alıyor.

Takılan kayıt görünür olmalı

Bir kuyruk, kimse bakmadığında sessizce dolan bir yere dönüşürse hiçbir şey çözmemiş olur. En az iki şey gerekiyor:

  • Operasyon ekranı. failed durumundaki kayıtlar, hatasıyla birlikte listeleniyor ve elle yeniden denenebiliyor.
  • Eşik uyarısı. Bekleyen kayıt sayısı ya da en eski bekleyen kaydın yaşı bir eşiği aştığında haber veren bir kontrol.

İkincisi olmadan, dış servisin üç gündür veri almadığını ancak karşı taraf arayınca öğreniyorsun.

İş sağlığı ve güvenliği süreçlerini kamu web servisleriyle entegre eden bir sistemde çalışırken şunu öğrendim: resmi bildirim yapan bir sistemde "bildirildi" bilgisi, kaydın bir alanı değil, kendi geçmişi olan ayrı bir durumdur. Ne zaman denendiği ve neyle sonuçlandığı görülemiyorsa, bildirimin yapıldığını söylemek de mümkün olmuyor.

Özet

Dayanıklı bir entegrasyon şu dört şeyden oluşuyor:

  1. Kayıt ve bildirim niyeti aynı transaction'da yazılıyor.
  2. Gönderim isteğin dışında, ayrı bir işçide yapılıyor.
  3. Yeniden deneme sınırlı, artan aralıklı ve kalıcı hatayı ayırt ediyor.
  4. Başarısız kayıtlar sessizce birikmiyor, görünür oluyor.

Hiçbiri karmaşık değil. Zor olan, kesintiyi baştan beklenen bir durum olarak kabul etmek — sonradan eklenen bir yeniden deneme mantığı, en başından bu varsayımla kurulmuş bir akış kadar iyi çalışmıyor.

Yazıyı paylaş𝕏
Önceki yazıÇok Kiracılı Sistemlerde Veri İzolasyonuSonraki yazı Aynı Metrik İki Ekranda Neden Farklı Çıkıyor?

Okumaya devam et

Tüm yazılar