İç içe geçmiş servis çağrılarında context iptalini doğru şekilde nasıl yayarım?
Soru
Go ile yazdığım HTTP handler'larında, client bağlantıyı kestiğinde downstream Postgres sorgusunun ve dışarı giden HTTP çağrısının da iptal olmasını istiyorum. Ama pratikte istek çoktan gitmiş olsa bile handler'larım çalışmaya devam ediyor; sorgu tamamlanıyor, dış API çağrılıyor. Zinciri `handler → service → repository → driver` şeklinde katmanladım. context iptalini bu katmanlar boyunca doğru şekilde nasıl yayarım, nerede yanlış yapıyor olabilirim?
Cevap
Kısa cevap: İptal ancak context’i gerçekten aşağı geçirirseniz yayılır.
Kısa cevap
r.Context()’ten başlayıp her downstream çağrıya (db.QueryContext, http.NewRequestWithContext) o context’i vermeniz gerekir; zincirin herhangi bir yerinde context.Background() kullanır veya context’i düşürürseniz iş çalışmaya devam eder. Aynı disiplinin bir başka yüzünü — ctx’i gözetmeyen işlerin arkada birikmesini — goroutine sızıntıları kaydında ele almıştım.
Neden
-
En sık sebep: context’in koparılması. Bir yerde
context.Background()/context.TODO()kullanılmış ya da Context’siz metot çağrılmıştır:db.Queryyerinedb.QueryContext,http.Getyerinereqüzerine context konmuşclient.Do. Hata neredeyse her zaman burada. -
İptal işbirlikçidir; kısayollar onu hiç görmez. net/http, client bağlantıyı kestiğinde
r.Context()’i iptal eder — ama bu sinyalin bir etkisi olması için çağrının onu gözlemesi gerekir.http.Get/http.Postgibi kısayollar context almaz. Blocking bir çağrı yoksa da iptal kendiliğinden fark edilmez. -
İptal DB’ye kadar inmeyebilir.
database/sql+ pgx ile context iptal edildiğinde driver varsayılan olarak bağlantıya deadline koyup kapatır; sorgunun Postgres tarafında da gerçekten iptal edilmesi içinseCancelRequestContextWatcherHandler’ı ayrıca yapılandırmanız gerekir, bu varsayılan davranış değildir. İptal DB’ye kadar inmezse sorgu boşuna dönmeye devam eder.
Ne yapmalı
-
r.Context()’ten başlayın ve aşağı taşıyın. Context’iservice → repository → driverboyunca geçirin. Struct’a saklamayın; her fonksiyona ilk argüman olarak verin (ctx context.Context). İskelet şöyle:func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // client kopunca iptal olur ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := h.svc.Do(ctx, id); err != nil { if errors.Is(err, context.Canceled) { return // client gitti, sessizce çık } http.Error(w, "internal", http.StatusInternalServerError) } } // repository: row := db.QueryRowContext(ctx, "SELECT ...", id) // dış çağrı: req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) -
Çağrı yerlerini grep’leyin.
context.Background(),context.TODO(),db.Query,http.Getgeçen satırları tarayın; kopma noktası büyük ihtimalle bunlardan biridir. -
DB:
QueryContext/ExecContextkullanın. Driver’ınızın context iptalini nasıl ele aldığını doğrulayın; gerekiyorsa Postgres tarafında iptali de yapılandırın. -
Dışarı giden HTTP:
NewRequestWithContext. İsteği bu context’le kurun ki client, iptal ve deadline’a uysun. -
Fazla iptal etmeyin — yaşaması gerekeni ayırın. Bir side-effect isteğin ömründen bağımsız tamamlanmalıysa (outbox’a yazma, event yayınlama), yeni bir context türetin: Go 1.21+ ile
context.WithoutCancel(ctx), ya da kendi timeout’una sahip taze bir context. Aksi halde client’ın kopması side-effect’lerinizi de öldürür. -
İptalin yanına timeout ekleyin. Her dış çağrı için
context.WithTimeoutkoyun; böylece bağlantı hiç kopmasa bile takılan bir bağımlılık sizi sonsuza kadar bekletmez. -
Kendi goroutine’leriniz ve döngüleriniz de ctx’i gözlemlesin. Uzun süren döngülerde
select { case <-ctx.Done(): return ctx.Err() ... }ile çıkış kontrolü koyun. Handler içinde başlattığınız goroutine’lere de aynıctx’i verin, yoksa istek gitse bile arka planda çalışmaya devam ederler.
Sonuç: Ben olsam tüm çağrı yerlerini denetlerdim: içeri r.Context(), her katmanda *Context’li metotlar ve yalnızca kopmadan sağ çıkması gereken birkaç side-effect için WithoutCancel. Bir kez bu disiplini oturttuğunuzda client bağlantıyı kestiği an Postgres sorgusu ve dış çağrı da kendiliğinden düşer; boşa yanan CPU ve bağlantı da onlarla birlikte kaybolur.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.