Çift Faturalandırma Kabusu
Smart Tech Devs’te Stripe veya kurumsal CRM’ler gibi ödeme işleme sistemleri ile entegre olurken, webhook’lar kullanmak zorunludur. Ancak, dağıtık sistemler doğası gereği kaotiktir. Stripe, API’nize bir charge.succeeded webhook’u gönderdiğinde, 3 saniye içinde 200 OK HTTP yanıtı bekler.
Eğer Laravel sunucunuz kullanıcının çalışma alanını sağlamak için 4 saniye alıyorsa, Stripe teslimatın başarısız olduğunu varsayar ve bir retry tetikler. Şimdi sunucunuz, tam olarak aynı $5,000 kurumsal ücreti bir kez daha işliyor. Aniden iki çalışma alanı oluşturmuş, iki makbuz göndermiş ve muhasebe defterinizi bozmuş oluyorsunuz. Bu kaçınılmaz retry fırtınalarında hayatta kalmak için, webhook uç noktalarınızı Idempotency için mimarlamak zorundasınız.
Idempotency Nedir?
Matematik ve bilgisayar bilimlerinde, idempotent bir işlem, tek seferde veya on binlerce defa çalıştırıldığında aynı sonucu üreten bir işlemdir.
Bunu Laravel’de başarmak için, satıcı tarafından sağlanan benzersiz Olay ID’sini (örneğin, Stripe’ın evt_12345) bir Idempotency Key olarak kullanırız. Bir webhook geldiğinde, o Olay ID’sini anında veritabanımızda veya Redis önbelleğinde kilitleriz. Eğer 2 saniye sonra bir retry gelirse ve ilk görev hala işleniyorsa, sistemimiz kilidi görerek tekrar eden payload’u yok sayar ve satıcıyı memnun etmek için nazik bir 200 OK yanıtı döner.
Adım 1: Idempotent Görevi Mimarisi
Webhook işleme işini arka planda çalışan bir Queue Worker’a devrediyoruz, böylece satıcıya hemen yanıt verebiliriz ve gerçek iş mantığını katı bir atomik Redis kilidi içine sarıyoruz.
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Cache;
class ProcessStripePayment implements ShouldQueue
{
use Dispatchable, Queueable;
public array $webhookPayload;
public function __construct(array $payload)
{
$this->webhookPayload = $payload;
}
public function handle(): void
{
// 1. Satıcıdan gelen benzersiz Olay ID'sini çıkarıyoruz
$eventId = $this->webhookPayload['id'];
$lockKey = "webhook_processing_{$eventId}";
// 2. ATOMİK KİLİT: 10 dakika boyunca bir kilit almaya çalışıyoruz.
// get() metodu, başka bir işçi bu kilidi zaten tutuyorsa hemen false döner!
$lock = Cache::lock($lockKey, 600);
if (! $lock->get()) {
// Bir retry fırtınası gerçekleşiyor! Diğer bir iş parçacığı bu tam olayı zaten işliyor.
\Log::info("Idempotency Tetiklendi: Tekrar eden webhook {$eventId} yavaşça atlanıyor.");
return;
}
try {
// 3. Kilidi elde ettik. Kritik iş mantığını BİR KEZ yürütüyoruz.
\Log::info("Olay {$eventId} için ödeme işleniyor...");
// Çalışma alanını sağla, makbuz gönder, defteri güncelle...
// 4. Gelecek benzer webhook'ların (günler sonra) da yok sayılması için kalıcı başarı kaydını tut
Cache::put("webhook_completed_{$eventId}", true, now()->addDays(30));
} catch (\Exception $e) {
// 5. Eğer iş mantığımız HATA VERİRSE, diğer Stripe retry'larının güvenle denemesi için kilidi serbest bırakıyoruz.
$lock->release();
throw $e;
}
}
}
Adım 2: Kontrolcü Devretesi
API kontrolcünüz artık yalnızca bir trafik polisi. İmza doğrulamasını onaylar, görevi gönderir ve hemen bağlantıyı keser ve böylece satıcının zaman aşımını önler.
public function handleWebhook(Request $request)
{
// (HMAC imza doğrulama aracıların geçtiğini varsayıyoruz)
// Önceden bu olayı kalıcı olarak tamamlayıp tamamlamadığımızı kontrol ediyoruz
$eventId = $request->input('id');
if (Cache::has("webhook_completed_{$eventId}")) {
return response()->json(['status' => 'already_processed']);
}
// Idempotent görevi gönder ve yanıt döndür
return response()->json(['status' => 'queued']);
}
Mühendislik ROI’si
Idempotency, dağıtık sistemler için son güvenlik ağıdır. Atomik Redis kilitlerine ve satıcının sağladığı Olay ID’lerine dayanarak, uygulamanızın veri bütünlüğünü ağ gecikmesinin belirsizliğinden tamamen ayırırsınız. Çift faturalandırma riskini ortadan kaldırır, veritabanı bozulmasını önler ve API’nizi büyük retry fırtınalarını sorunsuz bir şekilde absorbe edecek şekilde inşa edersiniz.
Kaynak: Orijinal Makale


