İç 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. 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.
-
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. Önce bu çağrıları grep’leyin; hata neredeyse her zaman burada. -
r.Context()’ten başlayın. net/http, client bağlantıyı kestiğinde bu context’i iptal eder. Onuservice → repository → driverboyunca aşağı taşıyın. Context’i struct’a saklamayın; her fonksiyona ilk argüman olarak geçin (ctx context.Context). -
DB:
QueryContext/ExecContextkullanın.database/sql+ pgx ile context iptal edildiğinde driver Postgres’e bir cancel isteği gönderip çalışan sorguyu gerçekten durdurur. Driver’ınızın bunu desteklediğini doğrulayın — pgx destekler; iptal DB’ye kadar inmezse sorgu boşuna dönmeye devam eder. -
Dışarı giden HTTP:
NewRequestWithContext. İsteği bu context’le kurun ki client, iptal ve deadline’a uysun.http.Get/http.Postgibi kısayollar context almaz ve iptali hiç görmez. -
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. Blocking bir çağrı yoksa iptal kendiliğinden fark edilmez; 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.
İ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)
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.