Blog'a dön
•
ByteGurus

Her 15 saniyede çalışıp hiçbir şey yapmayan arka plan işi

.NET arka plan işinde iki sessiz hata: await dönüşünde kaybolan bağlam ve aktörü olmayan yazma. 127 yeşil test ikisini de neden görmedi?

Mimari .NET Test MicroFrame

MicroFrame servislerimizden birindeki arka plan işi her 15 saniyede uyanıyor, veritabanına bakıyor, yapacak iş bulamıyor ve yeniden uyuyordu. Hiçbir şey loglamıyordu, çünkü raporlayacak bir şey yoktu. Oysa işlemesi gereken üç kayıt baştan beri tabloda beklemede duruyordu. Aynı servisteki ikinci bir yolun sorunu başkaydı: dağıtıldığı günden beri tek bir satır yazamamıştı. İki hata da sessizdi ve onları içeren koda karşı 127 birim testi geçiyordu.

Servis izin kayıtlarını tutuyor: bir kişinin bir kanaldan ticari ileti almayı kabul edip etmediğini ve bu kararın İleti Yönetim Sistemi’ne (İYS) bildirilip bildirilmediğini. Düzeltmeler küçüktü. Yazmaya değer olan, iki hatanın da neden hiç ses çıkarmadığı.

Birinci hata: dönüşte kaybolan bağlam

Her MicroFrame servisi sorgularını realm’e, yani kiracı sınırına göre süzer. Gateway’den gelen istek realm’ini taşır; ara katman onu ortam bağlamına (ambient context) koyar ve her sorgu filtresi oradan okur. Kod realm’i elden ele taşımaz.

Bağlam bir AsyncLocal içinde yaşar. İsteklerde bu yöntem işler, çünkü bağlamı akışın en başında ara katman kurar. Arka plan işinin ara katmanı yoktur. Bizimki realm’leri tek tek dolaşıyor ve her biri için şunu yapıyordu:

await ConsentRealmScope.EnterAsync(realmId);   // ortam realm'ini ayarlar
var due = await repository.ListDueAsync(ct);   // realm'e göre süzülen sorgu

EnterAsync realm’i bir async metodun içinde ayarlıyordu. .NET’te bir async metodun ExecutionContext üzerinde yaptığı değişiklik, metot dönünce çağırana geri akmaz. Bizim bağlam altyapımız her akış için tek ve değiştirilebilir bir kök tutar: kök varsa değer köke yazılır ve çağıran onu görür. Kök yoksa ilk ayar bir kök yaratır, ama o kök yardımcı metodun kendi yürütme bağlamına aittir ve EnterAsync döndüğü anda çöpe gider.

Sonuçta sorgu boş realm ile koştu, filtre hiçbir satırla eşleşmedi ve iş “yapacak bir şey yok” sonucuna vardı. Boş sonuç geçerli bir cevap olduğu için hiçbir şey şikâyet etmedi.

Düzeltme, herhangi bir değer ayarlanmadan önce kökü kurmak. Tek bir realm’i işleyen metodun başında tek bir çağrı; böylece bir realm’in bağlamı bir sonrakine de taşınmaz:

private async Task ProcessRealmAsync(Guid realmId, CancellationToken ct)
{
    AmbientContext.EnsureRoot();                     // akışın artık bir kökü var
    await ConsentRealmScope.EnterAsync(realmId);     // bu ayar çağırana ulaşır
    var due = await repository.ListDueAsync(ct);     // filtre realm'i görür
    // ...
}

İkinci hata: arkasında kimse olmayan yazma

İkinci yol Kafka’dan olay tüketiyordu. Handler realm’i doğru ayarlıyor, ardından bir izin kaydı oluşturuyordu. Platformdaki her aggregate created_by taşır ve bu kolon NOT NULL’dur. Bir isteğin kullanıcısı vardır; bir Kafka handler’ının ise biri ona vermedikçe yoktur.

Insert not-null ihlaliyle düştü, mesaj yolu üç kez yeniden denedi ve mesajı dead-letter kuyruğuna taşıdı. Dışarıdan servis sağlıklı görünüyordu: pod hazırdı, handler çalışıyordu, izin tablosu ise yalnızca boş kalıyordu.

Düzeltme zaten yazılmıştı. Bir sistem kimliği sağlayıcısı vardı, bağımlılık enjeksiyonuna kayıtlıydı ve hiçbir yerden çağrılmıyordu. Arka plan akışları artık yazmadan önce sistem kimliğini aktör olarak alıyor.

Testler neden ikisini de kaçırdı

Testler Entity Framework’ün bellek içi sağlayıcısını kullanıyordu. Bu sağlayıcı NOT NULL kuralını uygulamaz; aktörsüz yazma her testte başarılı oldu. İşin nasıl barındırıldığını da taklit etmediği için kaybolan bağlam da hiç görünmedi. Testler satırlara ve dönüş değerlerine bakıyordu; hatalar ise onları çevreleyen yerdeydi.

Eklediğimiz korumalar satırı değil bağlamı sabitliyor:

  • İki test, realm’i ayarlayan bir yardımcı metodu biri köksüz, biri köklü olarak çalıştırıyor. İlki realm’in kaybolduğunu, ikincisi çağırana ulaştığını doğruluyor. Hatanın dayandığı davranışı belgeliyorlar.
  • Bir test her arka plan işinin kaynak dosyasını okuyor ve AmbientContext.EnsureRoot() çağrısı yoksa düşüyor. Bu çağrıyı atlayan iş sessizlik üretir ve sessizlik kod incelemesinde görünmez.
  • Bir test de arka plan akışında realm’e girmenin yalnızca realm’i değil, bir aktörü de ayarladığını doğruluyor.

Bellek içi sağlayıcı mantığı hızlıca test etmek için hâlâ işe yarıyor. Ama bir yazmanın PostgreSQL’de başarılı olacağının kanıtı değil.

Bunu veri sorunundan nasıl ayırırsınız

Araştırmayı kısaltan ipucu şuydu: aynı pod’daki başka bir iş, İYS’den gelen bildirimleri işleyen iş, çalışıyordu. O iş IgnoreQueryFilters ile okuyor ve kimliğini kapsamlı (scoped) bir nesnede taşıyordu; ortam bağlamına hiç dayanmıyordu. Ortam durumunu atlayan bir yol çalışırken ona dayanan komşusu boş dönüyorsa, veriden önce bağlamdan şüphelenin.

Bundan çıkardıklarımız

  • Arka plan akışının isteği yoktur; bağlamını kimse onun yerine kurmaz. Her akışa ilk okuma ya da yazmadan önce bir kök ve bir aktör verin.
  • “Çalıştı, bir şey bulamadı” ile “çalıştı, bakamadı” aynı logu üretir. Bir iş meşru olarak boş sonuç bulabiliyorsa, yalnızca ne bulduğunu değil neyi aradığını da loglayın.
  • Kısıtları uygulamayan bir test sağlayıcısı, dağıttığınızdan farklı bir veritabanını test eder.
B

ByteGurus

Yazar

Tüm yazılara dön
Başlamaya hazır mısınız?

Projenize başlamaya hazır mısınız?

Fikirlerinizi hayata geçirmek için size nasıl yardımcı olabileceğimizi konuşalım.