Birbirine bağımlı birden fazla işi, ilk hata hepsini iptal edecek şekilde errgroup ile mi yürütmeliyim?
Soru
Go ile yazdığımız bir API'de her isteğin başında beş ayrı upstream servise (kullanıcı, abonelik, kampanya, envanter ve fiyatlandırma) paralel HTTP çağrısı yapıyoruz. Şu an bunları düz bir `sync.WaitGroup` ile fan out edip tüm sonuçları topluyorum. Sorun şu: bu beş çağrıdan biri hata verdiği anda geri kalan dördünün cevabını beklemenin anlamı kalmıyor, çünkü isteği zaten hatayla bitireceğim. İlk hata anında tüm batch'i iptal etmek için `errgroup`'a mı geçmeliyim, yoksa bu overkill mi olur?
Cevap
Kısa cevap: Evet, geçin.
Kısa cevap
Bu tam olarak errgroup.WithContext’in çözmek için var olduğu senaryo; burada overkill değil, doğru araç. sync.WaitGroup size “hepsini bekle” verir, sizin istediğiniz ise “biri düşerse hepsini bırak”. errgroup, golang.org/x/sync/errgroup altında yaşayan küçük ve olgun bir bağımlılıktır; bu tür fan-out’lar için taşımaya kesinlikle değer.
Neden
-
errgroup.WithContext deyimi budur.
WithContext, gruba bir türetilmiş context verir ve goroutine’lerden birinilolmayan bir hata döndürdüğü anda bu context’i iptal eder. Yani ilk hata, kalan çağrılara “durun” sinyalini otomatik gönderir; sizin elle sinyalleşme kurmanıza gerek kalmaz. -
İptal işbirlikçidir — çağrılarınız ctx’e uymak zorunda. Burası en sık atlanan nokta.
ctxiptal olsa bile, HTTP çağrınızhttp.NewRequestWithContextile bu context’i taşımıyorsa iptalin hiçbir etkisi olmaz; çağrı bitene kadar çalışmaya devam eder. Aynısı DB sorguları için de geçerli. Context’i tabana kadar geçirmezseniz errgroup’un yarısı boşa gider.g, ctx := errgroup.WithContext(ctx) results := make([]Result, len(ids)) for i, id := range ids { i, id := i, id // Go 1.22 öncesi döngü değişkeni yakalama g.Go(func() error { r, err := lookup(ctx, id) // ctx içeride http.NewRequestWithContext'e gider if err != nil { return fmt.Errorf("lookup %s: %w", id, err) } results[i] = r return nil }) } if err := g.Wait(); err != nil { return nil, err // yalnızca İLK hata } -
g.Wait() yalnızca ilk hatayı döner. Diğer goroutine’lerin hataları düşürülür. Beş çağrının hepsinin hatasını toplamak istiyorsanız errgroup yanlış araçtır; o zaman
WaitGroup+ kanal ya daerrors.Joingerekir. Ama sizin ihtiyacınız “ilki yeter, gerisini iptal et” olduğu için birebir uyuyor. -
Wait() yine de tüm goroutine’ler dönene kadar bloklar. İptal “durun” der ama
Wait, her goroutine gerçekten geri dönene kadar beklemeyi sürdürür. Ctx’i yok sayan bir çağrınız varsa yine en yavaşı kadar beklersiniz — bu yüzden 2. madde kritik.
Ne yapmalı
-
Paylaşılan sonuçlara senkronizasyonsuz yazmayın. Her goroutine kendi
results[i]indeksine yazıyorsa güvenlidir (ayrı bellek hücreleri). Ama ortak birmap’e yazarsanız data race olur —-raceile yakalanır. Slice indeksi kullanın ya da mutex koyun. -
Fan out büyürse SetLimit. Beş çağrı için sınırsız concurrency sorun değil, ama bu sayı büyürse
g.SetLimit(n)ile eşzamanlı goroutine sayısını kapatın; upstream’i ezmeyin. -
Üst context’e bir deadline koyun. İptal, yalnızca çağrılardan biri hata dönerse tetiklenir. Hiçbiri hata dönmüyor ama biri asılı kalıyorsa (yavaş upstream, yanıt vermeyen soket),
errgroupsonsuza kadar bekler. Bunun içinWithContext’e verdiğiniz context’icontext.WithTimeoutile sarın; deadline dolduğunda ctx iptal olur, ctx’e uyan tüm çağrılar geri döner ve isteğiniz belirli bir üst sınırda biter.
Sonuç: Ben olsam errgroup.WithContext’e geçer, üstüne bir request-level timeout koyar, ctx’i her HTTP/DB çağrısına kadar geçirir ve sonuçları per-index slice’a yazardım. Go 1.22+ kullanıyorsanız döngü değişkeni yakalama satırına da gerek yok. Kısacası: hem “biri düşerse bırak” hem de “kimse dönmezse zamanaşımına uğra” garantisini birlikte kurun.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.