İçeriğe geç
Muhammet Şafak
en
Soran: Tolga Cevaplandı:

İç 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

  1. 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.Query yerine db.QueryContext, http.Get yerine req üzerine context konmuş client.Do. Hata neredeyse her zaman burada.

  2. İ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.Post gibi kısayollar context almaz. Blocking bir çağrı yoksa da iptal kendiliğinden fark edilmez.

  3. İ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çinse CancelRequestContextWatcherHandler’ı 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ı

  1. r.Context()’ten başlayın ve aşağı taşıyın. Context’i service → repository → driver boyunca 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)
  2. Çağrı yerlerini grep’leyin. context.Background(), context.TODO(), db.Query, http.Get geçen satırları tarayın; kopma noktası büyük ihtimalle bunlardan biridir.

  3. DB: QueryContext/ExecContext kullanı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.

  4. Dışarı giden HTTP: NewRequestWithContext. İsteği bu context’le kurun ki client, iptal ve deadline’a uysun.

  5. 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.

  6. İptalin yanına timeout ekleyin. Her dış çağrı için context.WithTimeout koyun; böylece bağlantı hiç kopmasa bile takılan bir bağımlılık sizi sonsuza kadar bekletmez.

  7. 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

Etiketler: #Go#Concurrency
Uzmanlık: Go Geliştirici
Paylaş:

Yorumlar

Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.

Diğer Sorular

Tüm sorular

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi