Irbisa · Шпаргалка
Context и отмена
Дерево контекстов, таймауты, дедлайны, значения, распространение отмены
Что такое
context.Contextв Go, для чего он нужен и какие конструкторы есть в пакетеcontext?func handle(w http.ResponseWriter, r *http.Request) { // A child of the request context with a 2 s budget. ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM orders WHERE user_id = $1", userID(r))…Чем
context.Background()отличается отcontext.TODO()?Что выведет эта программа и что она показывает о том, как отмена проходит между родительским и дочерним контекстом?
func main() { parent, cancelParent := context.WithCancel(context.Background()) child, cancelChild := context.WithCancel(parent) cancelChild() fmt.Println(parent.Err(), child.Err()) child2, cancelChild2 := context.WithTimeout(parent, time.Hour) defer cancelChild2() cancelParent() fmt.Println(parent.Err(), child2.Err())}Чем
ctx.Done()отличается отctx.Err()и какие ошибки может вернутьErr()?func worker(ctx context.Context, jobs <-chan Job) error { for { select { case <-ctx.Done(): return ctx.Err() // context.Canceled or context.DeadlineExceeded case j := <-jobs:…Почему функцию
cancel, которую вернулиcontext.WithTimeoutилиcontext.WithCancel, нужно вызывать всегда?// wrong: vet (lostcancel) reports the discarded cancelfunc fetchBad(parent context.Context, id string) (*User, error) { ctx, _ := context.WithTimeout(parent, time.Second) return get(ctx, id)}…Почему
context.Contextпередают первым параметром функции, а не хранят в структуре, и почему нельзя передаватьnil?Останавливает ли отмена контекста горутину, которая его использует? Что должна делать сама горутина?
// wrong: time.Sleep ignores ctx, so the loop runs on after cancelfunc pollBad(ctx context.Context) { for { check() time.Sleep(10 * time.Second) }…В каких случаях использовать
context.WithCancel, когда выбиратьcontext.WithTimeout, а когдаcontext.WithDeadline?Слой репозитория оборачивает каждый запрос к базе в
context.WithTimeout(ctx, 5*time.Second), но в продакшене запросы обрываются через 300 мс. Откуда берутся 300 мс?func (r *Repo) Load(ctx context.Context, id int64) (*Row, error) { // A ceiling on what is left, not a grant of five seconds. ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if dl, ok := ctx.Deadline(); ok && time.Until(dl) < 20*time.Millisecond {…Поиск в кэше в коде ниже должен получать 100 мс, а база должна получать до 2 с, но
h.db.Loadпадает сcontext deadline exceededуже через 100 мс. Почему?func (h *Handler) Get(ctx context.Context, id string) (*Page, error) { ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() if p, err := h.cache.Get(ctx, id); err == nil { return p, nil } return h.db.Load(ctx, id) // the query needs about 300 ms}Что выведет эта программа и как объявлять ключи контекста, чтобы два пакета не могли столкнуться?
type key string func main() { ctx := context.WithValue(context.Background(), key("user"), "alice") ctx = context.WithValue(ctx, "user", "bob") fmt.Println(ctx.Value("user")) fmt.Println(ctx.Value(key("user"))) fmt.Println(ctx.Value("tenant"))}Как ретраи и обработка ошибок должны по-разному относиться к
context.Canceledиcontext.DeadlineExceeded?func retry(ctx context.Context, attempts int, f func(context.Context) error) error { var err error for i := range attempts { actx, cancel := context.WithTimeout(ctx, time.Second) // per-attempt budget err = f(actx) cancel()…Найдите баг: куча этого воркера очереди растёт всё время, пока он работает, хотя каждая задача выполняется за миллисекунды.
func (w *Worker) Run(ctx context.Context) error { for { select { case <-ctx.Done(): return ctx.Err() case job := <-w.jobs: jobCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() if err := w.handle(jobCtx, job); err != nil { slog.ErrorContext(ctx, "job failed", "id", job.ID, "err", err) } } }}Что происходит с
r.Context(), когда HTTP-клиент отключается, и как сделать так, чтобы исходящие вызовы хендлера остановились вместе с ним?var client = &http.Client{} // shared; no Client.Timeout, which would cut long streams func proxy(w http.ResponseWriter, r *http.Request) { // http.NewRequest would use context.Background() and ignore the disconnect. req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstream+r.URL.Path, nil) if err != nil {…Найдите баг: под нагрузкой число горутин в этом сервисе растёт каждый раз, когда апстрим начинает тормозить.
func fetch(ctx context.Context, url string) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() ch := make(chan result) go func() { body, err := download(url) // legacy client: takes no context ch <- result{body, err} }() select { case r := <-ch: return r.body, r.err case <-ctx.Done(): return nil, ctx.Err() }}Что делает
context.WithoutCancelи когда он нужен?func handler(w http.ResponseWriter, r *http.Request) { // wrong: go audit.Write(r.Context(), ev) is cancelled when the handler returns // right: keep the values, drop the cancellation, add our own bound ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 5*time.Second) ev := eventFrom(r) // read r now, not after the handler has returned…Как пакет
contextустроен внутри: как отмена доходит до потомков и почему поиск вValueлинейный?Каждая ошибка в fan-out сообщает только
context canceled. Как сделать, чтобы отмена несла свою причину: какой шард упал, какой бюджет закончился?var ErrSearchBudget = errors.New("search budget exhausted") func search(parent context.Context, q string) ([]Hit, error) { ctx, cancel := context.WithTimeoutCause(parent, 200*time.Millisecond, ErrSearchBudget) defer cancel()…У стороннего клиента есть
Close(), но он не принимаетcontext.Context. Как прервать его по отмене, не паркуя по горутине на каждый вызов?// right: a registered callback instead of a waiting goroutinefunc call(ctx context.Context, c *vendor.Client) error { stop := context.AfterFunc(ctx, func() { c.Close() // runs in its own goroutine when ctx ends }) err := c.Run()…Отменяет ли
srv.Shutdown(ctx)контексты запросов, которые ещё выполняются? Как заставить долгие хендлеры остановиться при завершении?func main() { sigCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() base, cancelBase := context.WithCancel(context.Background()) defer cancelBase()…Сервис A даёт B 500 мс, но B вызывает C с фиксированным таймаутом 2 с, и C тратит CPU на запросы, которых уже никто не ждёт. Как передавать дедлайны между сервисами?
const hopHeadroom = 50 * time.Millisecond // Caller: put the remaining time on the wire, never an absolute timestamp.func forward(ctx context.Context, req *http.Request) (*http.Response, error) { if dl, ok := ctx.Deadline(); ok { budget := time.Until(dl) - hopHeadroom…