Показаны сообщения с ярлыком контексты в Go. Показать все сообщения
Показаны сообщения с ярлыком контексты в Go. Показать все сообщения

вторник, 7 сентября 2021 г.

Отмена незавершенных операций с базой данных в Golang

Вы можете управлять незавершенными операциями с помощью Go context.Context. Контекст - это стандартное значение данных Go, которое может сообщать, была ли общая операция, которую он представляет, отменена и больше не нужна. Передавая context.Context между вызовами функций и сервисами в вашем приложении, они могут перестать работать раньше и возвращать ошибку, когда их обработка больше не требуется.

Например, вы можете захотеть:

  • Завершить длительные операции, включая операции с базой данных, выполнение которых занимает слишком много времени.
  • Распространять запросы на отмену из других источников, например, когда клиент закрывает соединение.

Многие API-интерфейсы для разработчиков Go включают методы, которые принимают аргумент Context, что упрощает использование контекста во всем приложении.

Отмена операций с базой данных после таймаута

Вы можете использовать контекст, чтобы установить таймаут или крайний срок, по истечении которого операция будет отменена. Чтобы получить контекст с таймаутом или крайним сроком, вызовите context.WithTimeout или context.WithDeadline.

Код в следующем примере таймаута извлекает контекст и передает его в метод sql.DB QueryContext.

func QueryWithTimeout(ctx context.Context) {
    // Создаем Context с таймаутом.
    queryCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // Передаем Context с таймаутом с запросом.
    rows, err := db.QueryContext(queryCtx, "SELECT * FROM album")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    // Обрабатываем возвращенные строки.
}

Когда один контекст является производным от внешнего контекста, поскольку queryCtx является производным от ctx в этом примере, если внешний контекст отменяется, то производный контекст также автоматически отменяется. Например, в HTTP-серверах метод http.Request.Context возвращает контекст, связанный с запросом. Этот контекст отменяется, если HTTP-клиент отключает или отменяет HTTP-запрос (возможно в HTTP/2). Передача контекста HTTP-запроса в QueryWithTimeout выше приведет к преждевременной остановке запроса к базе данных либо в том случае, если общий HTTP-запрос был отменен, либо если запрос занял более пяти секунд.

Примечание. Всегда откладывайте (defer) вызов функции отмены, которая возвращается при создании нового контекста с таймаутом или крайним сроком. Это освобождает ресурсы, удерживаемые новым контекстом, при выходе из содержащей его функции. Он также отменяет queryCtx, но к тому времени, когда функция вернется, уже ничто не должно использовать queryCtx.


Читайте также:


Купить gopher

четверг, 25 марта 2021 г.

Контексты и структуры в Golang

Во многих API Go, особенно современных, первым аргументом функций и методов часто является context.Context. Контекст предоставляет средства передачи крайних сроков, отмены вызывающего абонента и других значений области запроса через границы API и между процессами. Он часто используется, когда библиотека напрямую или транзитивно взаимодействует с удаленными серверами, такими как базы данных, API-интерфейсы и т. д.

В документации для контекста указано:

Контексты не должны храниться внутри типа структуры, а вместо этого передаваться каждой функции, которая в этом нуждается.

Этот пост дополняет этот совет причинами и примерами, описывающими, почему важно передавать Context, а не сохранять его в другом типе. Он также подчеркивает редкий случай, когда хранение Context в типе структуры может иметь смысл, и как это сделать безопасно.

Предпочитать контексты, передаваемые в качестве аргументов

Чтобы понять совет не хранить контекст в структурах, давайте рассмотрим предпочтительный подход контекста как аргумента:

type Worker struct { /* … */ }

type Work struct { /* … */ }

func New() *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx 
  // ctx для каждого вызова используется для отмены, 
  // крайних сроков и метаданных.
}

func (w *Worker) Process(ctx context.Context, work *Work) error {
  _ = ctx
  // ctx для каждого вызова используется для отмены, 
  // крайних сроков и метаданных.
}

Здесь методы (*Worker).Fetch и (*Worker).Process принимают контекст напрямую. Благодаря этой конструкции передачи в качестве аргумента пользователи могут устанавливать для каждого вызова крайние сроки, отмену и метаданные. И ясно, как будет использоваться context.Context, переданный каждому методу: нет никаких ожиданий, что context.Context, переданный одному методу, будет использоваться любым другим методом. Это связано с тем, что контекст ограничен настолько малой операцией, насколько это необходимо, что значительно увеличивает полезность и ясность контекста в этом пакете.

Хранение контекста в структурах приводит к путанице

Давайте еще раз проверим приведенный выше пример Worker с нежелательным подходом контекст-в-структуре. Проблема в том, что когда вы сохраняете контекст в структуре, вы скрываете время жизни для вызывающих или, что еще хуже, смешиваете две области вместе непредсказуемым образом:

type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch() (*Work, error) {
  _ = w.ctx
  // Общий w.ctx используется для отмены, 
  // крайних сроков и метаданных.
}

func (w *Worker) Process(work *Work) error {
  _ = w.ctx 
  // Общий w.ctx используется для отмены, 
  // крайних сроков и метаданных.
}

Оба метода (*Worker).Fetch и (*Worker).Process используют контекст, хранящийся в Worker. Это не позволяет вызывающим объектам Fetch и Process (которые сами могут иметь разные контексты) указывать крайний срок, запрашивать отмену и прикреплять метаданные для каждого вызова. Например: пользователь не может указать крайний срок только для (*Worker).Fetch или отменить только вызов (*Worker).Process. Время жизни вызывающей стороны смешано с общим контекстом, а контекст ограничен временем жизни, в котором создается Worker.

API также намного больше сбивает пользователей с толку по сравнению с подходом с передачей в качестве аргумента. Пользователи могут спросить себя:

  • Поскольку New принимает context.Context, выполняет ли конструктор работу, требующую отмены или крайних сроков?
  • Применяется ли context.Context, переданный в New, для работы в (*Worker).Fetch и (*Worker).Process? Ни один? Один, а не другой?

API потребуется много документации, чтобы точно сообщить пользователю, для чего используется context.Context. Пользователю, возможно, также придется читать код вместо того, чтобы полагаться на структуру передаваемого API.

И, наконец, может быть довольно опасно проектировать сервер производственного уровня, запросы которого не имеют контекста и, следовательно, не могут должным образом учитывать отмену. Без возможности устанавливать крайние сроки для каждого вызова ваш процесс может исчерпать свои ресурсы (например, память)!

Исключение из правила: сохранение обратной совместимости

Когда был выпущен Go 1.7, который представил context.Context, большому количеству API пришлось добавить поддержку контекста обратно совместимыми способами. Например, методы Client net/http, такие как Get и Do, были отличными кандидатами в контекст. Каждый внешний запрос, отправленный с помощью этих методов, выиграет от наличия крайнего срока, отмены и поддержки метаданных, которые поставляются с context.Context.

Существует два подхода к добавлению поддержки context.Context обратно совместимыми способами: включение контекста в структуру, как мы вскоре увидим, и дублирование функций с дублированием, принимающим context.Context и имеющим Context в качестве суффикса имени функции. Подход с дублированием должен быть предпочтительнее контекста-в-структуре и более подробно обсуждается в посте Обеспечение совместимости ваших модулей. Однако в некоторых случаях это непрактично: например, если ваш API предоставляет большое количество функций, то дублирование их всех может оказаться невозможным.

Пакет net/http выбрал подход контекст-в-структуре, который представляет собой полезный пример. Давайте посмотрим на Do. До введения context.Context Do определялся следующим образом:

func (c *Client) Do(req *Request) (*Response, error)

После Go 1.7 Do мог бы выглядеть следующим образом, если бы не тот факт, что это нарушило бы обратную совместимость:

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

Но сохранение обратной совместимости и соблюдение обещания совместимости Go 1 имеет решающее значение для стандартной библиотеки. Поэтому вместо этого разработчики решили добавить context.Context в структуру http.Request, чтобы разрешить поддержку context.Context без нарушения обратной совместимости:

type Request struct {
  ctx context.Context

  // ...
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // Упрощено для краткости.
  return &Request{
    ctx: ctx,
    // ...
  }
}

func (c *Client) Do(req *Request) (*Response, error)

При модернизации вашего API для поддержки контекста может иметь смысл добавить context.Context в структуру, как указано выше. Однако не забудьте сначала подумать о дублировании ваших функций, что позволяет модифицировать context.Context для обеспечения обратной совместимости без ущерба для полезности и понимания. Например:

func (c *Client) Call() error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}

Вывод

Контекст упрощает передачу важной информации между библиотеками и API вниз по стеку вызовов. Но он должен использоваться последовательно и четко, чтобы оставаться понятным, легко поддающимся отладке и эффективным.

При передаче в качестве первого аргумента в методе, а не хранении в типе структуры, пользователи могут в полной мере воспользоваться его расширяемостью, чтобы построить мощное дерево информации об отмене, сроках и метаданных через стек вызовов. И, что лучше всего, его область действия ясно понимается, когда он передается в качестве аргумента, что приводит к четкому пониманию и возможности отладки вверх и вниз по стеку.

При разработке API с контекстом помните совет: передайте context.Context в качестве аргумента; не храните его в структурах.


Читайте также:


суббота, 23 февраля 2019 г.

Go Code Review Comments: контексты

Значения типа context.Context содержат учетные данные безопасности, информацию о трассировке, крайние сроки и сигналы отмены через API и границы процесса. Программы Go передают Контексты явно по всей цепочке вызовов функций от входящих RPC и HTTP-запросов до исходящих запросов.

Большинство функций, которые используют контекст, должны принять его как свой первый параметр:

func F (ctx context.Context, / * другие аргументы * /) {}

Функция, которая никогда не зависит от запроса, может использовать context.Background(), кроме ошибки при передаче Context, даже если вы считаете, что вам это не нужно. По умолчанию - передавайте Context; используйте context.Background() только в том случае, если у вас есть веская причина, по которой альтернатива является ошибкой.

Не добавляйте член Context в тип структуры; вместо этого добавьте параметр ctx к каждому методу того типа, который должен передать его. Единственное исключение - для методов, чья подпись должна соответствовать интерфейсу в стандартной библиотеке или в сторонней библиотеке.

Не создавайте пользовательские типы контекста и не используйте интерфейсы, отличные от контекста, в сигнатурах функций.

Если у вас есть данные приложения для передачи, поместите их в параметр, в получатель, в глобальные переменные или, если они действительно принадлежат, в значение Context.

Контексты являются неизменяемыми, поэтому можно передавать один и тот же ctx нескольким вызовам с одинаковым крайним сроком, сигналом отмены, учетными данными, родительской трассировкой и т.д.


Читайте также: