gRPC-GoMiddleCoding

Как работает propagation дедлайнов и таймаутов в gRPC-Go с помощью context?

В gRPC-Go дедлайны передаются через context.WithDeadline/WithTimeout; фреймворк автоматически транслирует оставшееся время в HTTP/2-заголовок grpc-timeout, а все downstream-вызовы наследуют тот же ctx.

Как context переносит дедлайн в gRPC-Go

В gRPC-Go каждый RPC-вызов принимает context.Context в качестве первого аргумента. Если в контексте есть дедлайн (установленный через context.WithDeadline или context.WithTimeout), транспортный уровень gRPC-Go автоматически вычисляет оставшееся время и передаёт его серверу в HTTP/2-псевдозаголовке grpc-timeout. Сервер восстанавливает дедлайн в собственном контексте запроса, и к моменту, когда handler начинает работу, ctx.Deadline() уже содержит корректное значение.

Разница между Deadline и Timeout

context.WithDeadline задаёт абсолютный момент времени (time.Time), context.WithTimeout — относительную длительность, которая внутри тоже превращается в time.Time. gRPC-Go всегда работает с абсолютным дедлайном: при передаче вниз по цепочке вызовов он вычитает уже потраченное время и отправляет оставшееся. Это гарантирует сквозной end-to-end budget.

Propagation через цепочку сервисов

Ключевое правило: каждый промежуточный сервис обязан передавать полученный ctx в downstream-вызовы — и в другие RPC, и в запросы к базе данных. Если вместо этого создать новый независимый таймаут, сквозной бюджет теряется: downstream продолжает работу после того, как исходный клиент уже получил DEADLINE_EXCEEDED и ушёл.

// client.go
func callChain(addr string) error {
    // Полный бюджет на всю цепочку — 500 мс
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()

    conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)
    resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "42"})
    if err != nil {
        // codes.DeadlineExceeded, если время вышло на любом hop
        return fmt.Errorf("GetUser: %w", err)
    }
    fmt.Println(resp.Name)
    return nil
}

// server.go — handler передаёт ctx дальше, не создаёт новый
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    // ctx уже содержит дедлайн, переданный клиентом
    row, err := s.db.QueryContext(ctx, "SELECT name FROM users WHERE id = $1", req.Id)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "db: %v", err)
    }
    defer row.Close()
    // ...
    return &pb.User{Name: name}, nil
}

Обработка ошибок дедлайна

Когда контекст истекает, gRPC-Go возвращает статус codes.DeadlineExceeded. Проверять его следует через пакет google.golang.org/grpc/status:

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

if st, ok := status.FromError(err); ok {
    if st.Code() == codes.DeadlineExceeded {
        log.Println("deadline exceeded, fast-fail")
    }
}

Отмена на стороне сервера

Если клиент закрывает соединение до завершения RPC, сервер получает сигнал через ctx.Done(). Handler должен периодически проверять канал и завершать работу, чтобы не тратить ресурсы на уже ненужный запрос:

select {
case <-ctx.Done():
    return nil, status.FromContextError(ctx.Err()).Err()
default:
}

Подводные камни

  • Создание нового context.WithTimeout на каждом hop вместо передачи исходного ctx — downstream может продолжать работу после того, как клиент уже получил ошибку.
  • Использование context.Background() для запросов к БД внутри handler — SQL-запросы не отменяются при дедлайне RPC, соединения зависают.
  • Игнорирование ctx.Done() в длинных циклах обработки — сервер продолжает работу после отмены клиентом, впустую тратя CPU и память.
  • Слишком короткий глобальный таймаут без учёта latency сети — частые DeadlineExceeded при нормальной нагрузке.
  • Отсутствие метрик по grpc_server_handling_seconds — невозможно понять, где именно бюджет тратится быстрее всего.
  • Путаница между codes.DeadlineExceeded и codes.Canceled: первый означает истёкший таймаут, второй — явную отмену клиентом; retry-стратегии для них разные.
  • Не установленный grpc.WithKeepaliveParams на клиенте — при долгих idle-соединениях HTTP/2-поток молча закрывается, и дедлайн срабатывает неожиданно.

Common mistakes

  • Давать ответ про deadlines и timeouts через context в gRPC-Go только на уровне определения, не показывая поведение в реальном приложении.
  • Игнорировать границы ответственности вокруг темы «deadlines и timeouts через context в gRPC-Go»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
  • Не связывать deadlines и timeouts через context в gRPC-Go с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.

What the interviewer is testing

  • Точно объясняет, что именно делает deadlines и timeouts через context в gRPC-Go и где это используется в Go-коде.
  • Связывает deadlines и timeouts через context в gRPC-Go с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
  • Не изобретает API и опирается на реальные контракты официальной документации.

Sources

Related topics