Context bilan ishlash
Foydalanuvchi sahifani ochdi. Server bazaga og‘ir so‘rov yubordi va u 30 soniya davom etadi. Foydalanuvchi 3 soniya kutdi-da, sahifani yopdi. Endi hech kim natijani kutmayapti, lekin baza 27 soniya davomida bekorga ishlashda davom etadi. Shu vaqt ichida ulanish band, CPU ham band.
Yuzlab foydalanuvchi shunday qilsa, ulanishlar puli tugaydi va server yangi so‘rovlarga javob bermay qo‘yadi.
context.Context bu muammoni hal qiladi. U HTTP so‘rovdan boshlab database so‘roviga qadar "ish bekor qilindi" signalini yetkazadi. Context haqidagi asosiy ma’lumotlar context asoslari darsida berilgan. Bu darsda uni API va database bilan ishlashda qo‘llaymiz.
So‘rovning contexti
Har bir HTTP so‘rovda tayyor context bor:
Bu context uch holatda bekor bo‘ladi:
- klient ulanishni uzsa: sahifani yopsa, internet uzilsa yoki
curlniCtrl+Cbilan to‘xtatsa; - HTTP/2 da klient shu so‘rovni bekor qilsa;
- handler ishini tugatsa.
Bu contextni bazagacha uzatsak, klient ketgan zahoti database so‘rovi ham to‘xtaydi.
Contextni uzatish zanjiri
HTTP so‘rov
│ r.Context()
↓
Handler ─── ctx ───→ Service ─── ctx ───→ Repository
│
↓
pool.Query(ctx, ...) ← shu yerda ishlatiladi
Qoida oddiy: context qabul qiladigan har bir funksiya uni birinchi parametr sifatida oladi va chaqirgan funksiyalarga uzatadi.
// Handler
func (h *Handler) getBook(w http.ResponseWriter, r *http.Request) {
b, err := h.books.Get(r.Context(), id)
// ...
}
// Service
func (s *Service) Get(ctx context.Context, id int) (Book, error) {
return s.repo.Get(ctx, id)
}
// Repository
func (r *PostgresBookRepo) Get(ctx context.Context, id int) (book.Book, error) {
row := r.pool.QueryRow(ctx, `SELECT ... WHERE id = $1`, id)
// ...
}
Warning
Handler ichida context.Background() yoki context.TODO() yaratib, bazaga uzatmang. Bunday context hech qachon bekor bo‘lmaydi. Natijada klient allaqachon ketgan bo‘lsa ham, database ishlashda davom etadi.
Klient uzilganda nima bo‘ladi?
Quyidagi dastur bazada 10 soniya davom etadigan so‘rovni ishga tushiradi. pg_sleep — PostgreSQL’da kutish funksiyasi. U og‘ir so‘rovni taqlid qilish uchun ishlatiladi.
Server logida:
Klient uzilgan zahoti context bekor qilindi. pgx bu signalni olib, PostgreSQL’ga so‘rovni bekor qilish buyrug‘ini yubordi. Baza 8 soniyani bekorga sarflamadi.
Klient o‘zi ketgani uchun unga javob yozishning ma’nosi yo‘q. Shuning uchun handler shunchaki return qiladi. Bu holatni 500 xatosi sifatida log qilmang, u server xatosi emas.
Timeout qo‘yish
Klient ulanishni uzmasa ham, so‘rov cheksiz davom etmasligi kerak. Masalan, bazada qulf (lock) tufayli so‘rov osilib qolishi mumkin. Buning uchun context.WithTimeout ishlatiladi:
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
rows, err := pool.Query(ctx, `SELECT ...`)
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "so‘rov juda uzoq davom etdi", http.StatusGatewayTimeout)
return
}
Muhim jihatlar:
- Yangi context
r.Context()dan yaratildi,context.Background()dan emas. Shunda ikkala shart ham ishlaydi: 3 soniya o‘tsa ham, klient ketsa ham so‘rov to‘xtaydi. defer cancel()majburiy. U timeout uchun ajratilgan resurslarni vaqt tugashini kutmasdan bo‘shatadi.go vetcancelchaqirilmaganini aniqlab, ogohlantirish beradi.- Timeout qayerda qo‘yilishi haqida kelishib oling. Ko‘pincha u repository’da har bir so‘rov uchun alohida yoki middleware’da butun so‘rov uchun qo‘yiladi.
Repository ichida timeout
const queryTimeout = 3 * time.Second
func (r *PostgresBookRepo) List(ctx context.Context) ([]book.Book, error) {
ctx, cancel := context.WithTimeout(ctx, queryTimeout)
defer cancel()
rows, err := r.pool.Query(ctx, `SELECT ... FROM books ORDER BY id`)
// ...
}
Tashqi context’da muddat allaqachon qisqaroq bo‘lsa, masalan 1 soniya qolgan bo‘lsa, yangi context ham 1 soniyada tugaydi. Ichki context tashqisidan uzoq yashay olmaydi.
Butun so‘rov uchun timeout middleware
func Timeout(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
r.WithContext(ctx) yangi context bilan so‘rov nusxasini yaratadi. Keyingi barcha handlerlar va ular chaqirgan funksiyalar shu muddatli contextni oladi.
Xatolarni to‘g‘ri tasniflash
Context bilan bog‘liq xatolar ikki xil:
| Xato | Sababi | Javob |
|---|---|---|
context.Canceled |
Klient ketdi yoki ish bekor qilindi | Javob yozmaslik mumkin, 500 sifatida log qilinmaydi |
context.DeadlineExceeded |
Belgilangan vaqt tugadi | 503 Service Unavailable yoki 504 Gateway Timeout |
Drayverlar bu xatolarni o‘z xatolariga o‘rab qaytaradi. Shuning uchun == bilan emas, errors.Is bilan tekshiriladi.
switch {
case errors.Is(err, context.Canceled):
return // klient ketdi
case errors.Is(err, context.DeadlineExceeded):
writeError(w, http.StatusServiceUnavailable, "server band, keyinroq urinib ko‘ring")
case err != nil:
writeError(w, http.StatusInternalServerError, "ichki server xatosi")
}
Tranzaksiyalar va context
Tranzaksiya — bir nechta so‘rovni "yo hammasi bajariladi, yo hech biri" tamoyili bilan birlashtirish. Klassik misol — pul o‘tkazish:
- Alining hisobidan 100 000 so‘m yechiladi.
- Valining hisobiga 100 000 so‘m qo‘shiladi.
Birinchi qadamdan keyin server o‘chib qolsa, pul yo‘qoladi. Tranzaksiya ichida esa ikkinchi qadam bajarilmasa, birinchisi ham bekor qilinadi.
Kutubxonadan kitob olishni tranzaksiya bilan yozamiz. Kitobni berish ikki amaldan iborat: nusxalar sonini kamaytirish va berish yozuvini qo‘shish.
Kod qanday ishlaydi:
pool.Begin(ctx)tranzaksiyani boshlaydi. Tranzaksiya ichidagi barcha so‘rovlartxorqali yuboriladi,poolorqali emas.defer tx.Rollback(ctx)— xavfsizlik to‘ri. Funksiya xato bilan chiqsa, tranzaksiyadagi barcha o‘zgarishlar bekor qilinadi.Commitmuvaffaqiyatli bajarilgan bo‘lsa,Rollbackhech narsa qilmaydi.available > 0shartiUPDATEichida tekshirildi. AlohidaSELECTbilan tekshirilganda, ikkita parallel so‘rov ikkalasi ham "bitta nusxa bor" deb o‘ylashi mumkin edi.UPDATEesa qatorni qulflaydi, shuning uchun ikkinchi so‘rov o‘zgargan qiymatni ko‘radi.- Context butun tranzaksiya uchun umumiy. Vaqt tugasa yoki klient ketsa, tranzaksiya avtomatik bekor qilinadi.
database/sqlda ham xuddi shu tartib: db.BeginTx(ctx, nil), tx.ExecContext(ctx, ...), tx.Commit() va defer tx.Rollback().
Warning
Tranzaksiya ichida tashqi API chaqirmang, email yubormang va uzoq hisob-kitob qilmang. Tranzaksiya ochiq turgan vaqt davomida u ulanishni band qiladi va qatorlarni qulflab turadi. Tranzaksiya imkon qadar qisqa bo‘lishi kerak.
Context’ga nimalarni joylash mumkin?
context.WithValue bilan contextga qiymat joylash mumkin. Lekin u faqat so‘rov doirasidagi metadata uchun mo‘ljallangan: request ID, autentifikatsiyadan o‘tgan foydalanuvchi, trace ID.
Funksiya ishlashi uchun zarur bo‘lgan parametrlarni, masalan bookID yoki database ulanishini, contextga joylamang. Ular funksiya imzosida ochiq ko‘rinib turishi kerak.
Xulosa
r.Context()klient uzilganda bekor bo‘ladi. Uni handlerdan repository’gacha birinchi parametr sifatida uzating.- Handler ichida
context.Background()ishlatmang. context.WithTimeoutso‘rov davomiyligini cheklaydi.defer cancel()ni unutmang.context.Canceledvacontext.DeadlineExceededxatolarinierrors.Isbilan tekshirib, alohida qayta ishlang.- Tranzaksiya
Begin(ctx)bilan boshlanadi,defer Rollbackbilan himoyalanadi vaCommitbilan yakunlanadi.
Keyingi darsda eng xavfli zaifliklardan biri — SQL injection va undan himoyalanishni ko‘ramiz.