Başarısız İşlemlerde Geri Alma İçin İş Akışı Motoru Gerekmeyebilir
Geri Alma: Saga Deseni iddiası ne anlatıyor? Dış dünyayla iletişim kuran hemen hemen her Laravel uygulamasında ortaya çıkan bir sıra şöyledir: Müşterinin kartını şarj edin. İş ortağı API'sıyla bir politika (veya sipariş veya rezervasyon) oluşturun. Bir…

Geri Alma: Saga Deseni iddiası ne anlatıyor?
Dış dünyayla iletişim kuran hemen hemen her Laravel uygulamasında ortaya çıkan bir sıra şöyledir: Müşterinin kartını şarj edin. İş ortağı API’sıyla bir politika (veya sipariş veya rezervasyon) oluşturun. Bir PDF onayı oluşturun. Adım 3 atar. Belki PDF kitaplığının belleği tükenmiştir, belki de depolama diski doludur.
Sebep ne olursa olsun, şu anda yüklü bir kartın ve kendi veritabanınızın var olduğuna dair hiçbir fikrinin olmadığı ortak taraflı bir kaydın üzerinde oturuyorsunuz. DB::transaction() sizi burada kurtaramaz; yalnızca kendi veritabanınızı bilir. Ödeme ağ geçidi ve iş ortağı API’si zaten kendilerine ait olanı taahhüt etmiştir ve kendi tablolarınıza yapacağınız hiçbir şey bunu geri alamaz.
Saga modelinin çözdüğü sorun budur: tek bir atomik işlem yerine, işlemi her biri bir çalıştır() ve telafi() içeren bir adımlar zinciri olarak modellersiniz. N adımı başarısız olursa, zaten başarılı olan her adımda, ters sırayla telafi() çağrısı yaparsınız (ödemeyi iade edin, iş ortağı politikasını iptal edin) ve başarısız adım hariç, başladığınız yere geri dönersiniz. Kavramsal olarak basit.
Tasarım ve teknik ayrıntılar
İlginç olan, Compens() işlevini doğru bir şekilde uygulamak için gerekenler ve bu yazının geri kalanı da bununla ilgili. Zaten orada olan şey Herhangi bir şey yazmadan önce Laravel için neyin var olduğuna baktım.
Burada gerçekten önceki teknik var ve bir araç seçmeden önce bunu anlamakta fayda var: Dayanıklı İş Akışı (eski adıyla Laravel İş Akışı) ve Saga Lara Flow Her ikisi de aynı temel fikir üzerine inşa edilmiştir: Geçici tarzda dayanıklı bir yürütme motoru.

Her adımın durumu veritabanınızda kalıcı olarak saklanır, iş akışı kuyruğunuz boyunca yürütülür ve bir çalışanın yürütmenin ortasında ölmesi durumunda iş akışı, orta tazminat da dahil olmak üzere kaldığı yerden devam eder. Tekrar oynatma, günlere yayılan uzun süreli iş akışları, harici etkinliklerde bekleme sinyalleri ve paralel dallara sahip olursunuz.
Bugünden bakınca ne kadar güvenilir?
Ayrıca farkı bölmeye çalışan en az bir paket de var – isteğe bağlı kalıcılıkla varsayılan olarak bellek içi yürütme, sonunda istediğim şeye daha yakın, ancak yeterli yüzey alanına sahip (DAG yürütme, onay kapıları, web kancası giden kutuları, kontrol panelleri) pratikte hafif hissetmeyi bırakıyor.
Tüm bu dayanıklılık gerçekten değerlidir; iş akışınız yasal olarak saatler sürebilirse veya uçuş sırasında bir dağıtımdan sağ çıkması gerekiyorsa, tam olarak bunu istersiniz. Ancak bunun bir maliyeti var: Bir yerde çalışan bir kuyruk çalışanı, iş akışı durumu tabloları için geçişler ve onu tetikleyen istekte artık satır içi yürütme yapmayan bir adımlar zinciri.
Ödemem → politika → PDF dizim bunların hiçbirine ihtiyaç duymuyor. Tamamen tek bir HTTP isteği içerisinde, baştan sona birkaç yüz milisaniyede çalışır. Bunun için kuyruk destekli dayanıklı bir yürütme motorunu kullanmak, tek bir iş parçacığı bırakmayan bir değişkeni korumak için dağıtılmış bir kilide ulaşmak gibidir. Aslında istediğim şey Senkronize bir orkestratör: kuyruk yok, veritabanı yok, geçiş yok.
Okuyucu için pratik anlamı
Adımlar, onları tetikleyen aynı istekte yürütülür ve eğer biri başarısız olursa, tamamlananlar, istek geri dönmeden önce telafi edilir; arayan kişi, başka herhangi bir şey için kullanacağı aynı deneme/yakalama işleminde bunu hemen öğrenir.
son sınıf ChargePayment, CompensatorStep’i uyguluyor { genel işlev yürütme(CompensatorContext $bağlam): karışık { $ödeme = PaymentGateway::charge($context->get(‘amount’)); $bağlam->set(‘ödeme_kimliği’, $ödeme->id); $ödemeyi iade edin; } genel işlev telafisi(CompensatorContext $bağlam): void { PaymentGateway::refund($context->get(‘payment_id’)); } } son sınıf CreatePartnerPolicy CompensatorStep’i uygular { genel işlev yürütme(CompensatorContext $bağlam): karışık { $policy = PartnerApi::createPolicy($context->all()); $bağlam->set(‘policy_id’, $policy->id); $politikasını döndür; } genel işlev telafisi(CompensatorContext $bağlam): void { PartnerApi::cancelPolicy($context->get(‘policy_id’)); } } $sonuç = (yeni Dengeleyici()) ->addStep(yeni ÜcretÖdeme()) ->addStep(new CreatePartnerPolicy()) ->step(execute: fn ($ctx) => Pdf::generate($ctx->get(‘policy_id’)) name: ‘generate_pdf’) ->run(new CompensatorContext([‘tutar’ => 4999])); if ($result->needsManualCleanup()) { // zincir başarısız oldu VE geri alma başarısız oldu — bir şey gerçekten çıkmaza girdi } Bütün şekli budur.
besteci gerektirir, yapılandırma dosyası yok, satıcı yok: yayınlama, hiçbir şey yok göç etmek üzere. Paketi, tam olarak bu tür bir geri alma adımının teriminden sonra – “telafi edici işlem” olarak adlandırdım. API yüzeyinin küçük olması işin kolay kısmıydı. Arıza semantiğini doğru yapmak (compens() çağrısı etrafında gerçekleşen her şey) asıl tasarım çalışmasının çoğunun burada yapıldığı ortaya çıktı.
Aslında önemli olan kısım: geri alma işlemi başarısız olduğunda ne olur? Bu modelin saf versiyonu, telafi() fonksiyonunun her zaman başarılı olduğunu varsayar. Uygulamada, bir politika oluşturmayı yeni kabul eden aynı iş ortağı API’si, otuz saniye sonra onu iptal etmeye çalıştığınızda kesintiye uğrayan API olabilir.
Bir geri alma çağrısının yapıldığı anı telafi etmeyi bırakırsanız, o adımdan önceki her şeyi de geri alınmamış halde bırakırsınız; bu genellikle bir arızayı rapor edip devam etmekten daha kötüdür.
Dolayısıyla, varsayılan davranış, bir Compensation() atımından sonra bile zincirin geri kalanını telafi etmeye devam etmek ve sonuçtaki her başarısızlığı yutmak yerine rapor etmektir: if ($result->needsManualCleanup()) { foreach ($result->compensationFailures as $failure) { logger()->kritik(‘sıkışık yan etki’, [ ‘adım’ => $başarısızlık->adımAdı, ‘denemeler’ => $başarısızlık->denemeler, ‘hata’ => $failure->istisna->getMessage(), ]); } } needManualCleanup() özellikle dikkat edilmesi gereken işarettir: zincir başarısız oldu ve geri alma başarısız oldu.
Bu kombinasyon gerçek bir yan etki anlamına gelir; ücret, bir ortak kaydı – orada çözülmeden duruyor ve istek içinde hiçbir yeniden deneme bunu düzeltmeyecek. Birinin bilmesi gerekiyor. Bu da bir sonraki soruyu gündeme getiriyor: Bir tazminat geçici olarak başarısız olabileceğinden (geri ödeme API’si bir saniyeliğine kesintiye uğrar), Compensator bunu yeniden denemeli mi?
Şunları tercih edebilir: (yeni Dengeleyici()) ->retryCompensation(times: 2, uykuMs: 200) Başlangıçta kaçırdığım bir inceliğin kaçınılmaz hale geldiği yer burasıdır: eğer compens() birden fazla kez çalıştırılabilirse – yeniden deneme nedeniyle veya daha sonraki bir süreç kesintiye uğramış bir geri dönüşü sürdürdüğü için – önemsiz olmalıdır.
“Bu ödemeyi geri ödeyin” harekete geçmeden önce ödemenin hala iade edilebilir olup olmadığını kontrol etmelidir, bunun birinin bunu ilk kez söylediğini varsaymayın: public function tazminat(CompensatorContext $context): void { $ödeme = PaymentGateway::find($context->get(‘payment_id’)); if ($ödeme?->isRefundable()) { $ödeme->iade(); } } Bu tek gereklilik – tasarım gereği önemsizdir, kazara değil – muhtemelen tüm paketin belgelerindeki en önemli cümledir ve mutlu yolun taslağını çizerken atlanması kolay türden bir şeydir.
Dürüst sınırlama İşte, gömmek istemediğim ödünleşim: veritabanından vazgeçmek, dayanıklılıktan vazgeçmek anlamına gelir. PHP adımlar arasında ölürse (bellek yetersizliği nedeniyle ölümcül bir durum, konuşlandırmanın ortasında bir çalışanın öldürülmesi), bunu hiçbir şey yakalayamaz, geri alma asla çalışmaz ve elinizde bir şarjlı kart ve hiçbir yerde sıfır kaydı.
Kuyruk destekli dayanıklı bir motor tam olarak bu senaryoda hayatta kalır; Kalıcı durumun bütün amacı budur. Compensator bunu yumuşatabilir, çözmez; ölümcül bir hatadan sonra bile geri dönmeyi deneyen bir kapatma işleyicisi: (new Compensator()) ->protectAgainstFatals() Bu en iyi çabadır. Hiçbir şey SIGKILL’den veya makinenin güç kaybından kurtulamaz.
Eğer zor durumda kalan bir yan etki, inşa ettiğiniz şey için gerçekten kabul edilemezse – gerçek para, yasal olarak bağlayıcı herhangi bir şey – bu, kapatma işleyicisine daha fazla yaslanmak yerine dayanıklı bir motora ulaşmanız gerektiğinin sinyalidir. Aksini iddia etmek, kaçınmaya çalıştığım şeyin daha kötü bir versiyonunu sunmaktan başka bir işe yaramaz.
Hattın gerçekte nerede olduğu Bunu geçtikten sonra, karar “Kompansatöre karşı dayanıklı motorlar” değil, iş akışınızın şekliyle ilgili bir sorudur: Tek bir istek içinde çalışır, milisaniyeler ila saniyeler içinde tamamlanır, uçuş ortasında bir kazadan sağ çıkmaya gerek yoktur → senkronize bir orkestratör yeterlidir ve bir kuyruk tamamen ek yüktür.
Yasal olarak dakikalarca, saatlerce veya günlerce çalışabilir; çalışanın yeniden başlatılmasından ve konuşlandırılmasından sağ çıkması gerekiyor; harici sinyalleri veya insan onayını beklemeniz gerekiyor → bir kuyruk + kalıcı durumun aslında sizi satın aldığı dayanıklılığı istiyorsunuz. Yeniden denemeler ve kapatma işleyicileriyle bunun etrafında yönlendirme yapmaya çalışmayın.
Sients/compensator olarak oluşturduğum şeyi (MIT lisanslı) Packagist’te yayınlamaya son verdim. PHP 8.2+ / Laravel 12+. Aynı sorunla karşılaşırsanız (bir istekte temiz, sıralı bir geri alma gerektiren bir avuç harici çağrı) bu, aksi takdirde yapacağım array_reverse döngüsünün aynısını yazmaktan sizi kurtarabilir
İlgili yazılar
- npm 12 Güncellemesi: Güvenlik İçin Kurulum Scriptleri Varsayılan Kapalı
- Godot 4’te Karakter Hareketi İçin Görsel İzler Oluşturma
Ek okuma: MDN Web Docs
Editörün Notu
Başarısız işlemlerde veri tutarlılığını sağlamak için karmaşık iş akışı motorlarına gerek kalmadan, Saga modeli gibi dağıtık işlem yönetimi yaklaşımlarını tercih etmek uygulama güvenilirliğini artırır. Böylece farklı sistemler arasındaki geri alma süreçleri daha kontrollü ve esnek hale gelir.
