Асинхронный вызов облачных функций в Яндекс Облаке
В прошлом посте на эту тему я рассказал о том, как можно использовать очереди сообщений для асинхронного вызова облачных функций в Яндекс Облаке. В этом посте я расскажу о том, как можно сделать это еще проще и удобнее.
Асинхронный вызов облачных функций в Яндекс Облаке находится на стадии Preview.
Давайте сравним два подхода к асинхронному вызову облачных функций. В первом случае мы создаем очередь сообщений и
отправляем в нее сообщения тем или иным способом. Во втором случае мы просто вызыва ем облачную функцию с
параметром ?integration=async
.
Плюсы асинхронного вызова
- Не нужно создавать очередь сообщений и настраивать триггеры на ней.
- Проще авторизация — можно использовать просто IAM-токен (не нужно создавать ключи сервисного аккаунта или API Gateway для работы с очередью сообщений)
Минусы асинхронного вызова
- Нет возможности упорядочить асинхронные вызовы (если вам важен порядок обработки сообщений). Для этого нужно использовать Yandex DataStream и триггеры на нем.
- Нет возможности управлять кодом ответа. Если вы хотите, чтобы облачная функция возвращала код ответа 200, а не 202, асинхронный вызов вам не подойдет.
- Нет интеграции с API Gateway.
- Параметры можно передать только в теле запроса. Нет возможности передать параметры через query-параметры или заголовки HTTP-запроса. То есть, вам нужно будет использовать POST-запросы. Вкупе с отсутствием интеграции с API Gateway это делает асинхронный вызов неудобным для интеграции с frontend-приложениями, так как вы наверняка столкнетесь с проблемой CORS.
Как это работает
Вызвать произвольную облачную функцию с параметром ?integration=async
не получится. Для этого нужно создать
версию функции с параметром
async_invocation_config
. В этом параметре можно указать количество ретраев и будет ли
использоваться очередь сообщений для хранения результатов выполнения функции и ошибок, или результат будет отбрасываться.
Если не указать параметр async_invocation_config
, то при асинхронном вызове функции вы получите ошибку:
{
"errorType": "ProxyIntegrationError",
"errorCode": 412,
"errorMessage": "async invocation is disabled on this function version"
}
Нормально сконфигурированная для асинхронного вызова функция будет возвращать код ответа 202 и пустое тело ответа вида:
HTTP/1.1 202 Accepted
Date: Fri, 04 Jan 2024 13:46:22 GMT
Content-Length: 0
Connection: keep-alive
Server: Yandex-Cloud-Functions/1.0
X-Content-Type-Options: nosniff
X-Function-Id: d4e...
X-Function-Version-Id: d4e...
X-Request-Id: 03a6dce5-d51e-4b84-b052-079677625d66
Access-Control-Allow-Origin: *
Можно ли функцию, настроенную для асинхронного вызова, вызвать синхронно? Да, можно. Для этого она должна возвращать не произвольный ответ, а ответ вида:
{
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "Some string"
}
Но в таком случае при асинхронном вызове функции в очередь сообщений будет отправлено сообщение точно такой же структуры.
Сигнатура функции
Давайте обратимся к документации:
При использовании такой формы вызова функция не может анализировать и задавать HTTP-заголовки:
- Содержимое тела HTTPS-запроса передается первым аргументом (без преобразования в JSON-структуру).
- Содержимое тела HTTPS-ответа совпадает с ответом функции (без преобразования и проверки структуры), HTTP-статус ответа: 202.
Что это значит?
- Ваша функция не сможет получить доступ к заголовкам HTTP-запроса и ответа. Т.е. у вас не будет возможности проверить авторизацию, получить информацию о пользователе, который вызвал функцию, и т.д.
- Вы не сможете предать в параметры через query-параметры или заголовки HTTP-запроса. Все параметры должны быть переданы в теле запроса.
То что, например, для Go функции вы не сможете использовать HTTP-хендлер вида
func(w http.ResponseWriter, r *http.Request)
Вам придется использовать хендлеры вида:
func () error
func (request) error
func () (response, error)
func (request) (response, error)
func (context.Context) error
func (context.Context, request) error
func (context.Context) (response, error)
func (context.Context, request) (response, error)
Где request
— []byte|string|struct|*struct
, а response
— []byte|string|struct|*struct|interface{}
Для TypeScript все немного проще. Вам нужно просто экспортировать функцию с сигнатурой:
function (request: any, context): Promise<any>
Если вы указали в async_invocation_config
в параметре success_target
очередь сообщений, то в нее будет отправлено
сообщение с результатом выполнения функции. Результат выполнения функции будет сериализован в JSON и помещен в тело
сообщения. То есть, если вы возвращаете из функции строку, то в очередь будет отправлено сообщение с телом вида:
"Some string"
Обратите внимание на кавычки. Если вы возвращаете из функции объект, то в очередь будет отправлено сообщение с телом вида:
{
"field1": "value1",
"field2": "value2"
}
Отдельно сериализовывать результат выполнения функции в JSON не нужно иначе вы получите двойную сериализацию.
Если вы указали в async_invocation_config
в параметре error_target
очередь сообщений, то в нее будет
отправлено сообщение с ошибкой. Ошибка будет сериализована в JSON и помещена в тело сообщения.
Области применения
На мой взгляд, асинхронный вызов облачных функций удобно использовать в следующих случаях:
- Когда вам не важен результат выполнения функции.
- Когда вы хотите отложить выполнение задачи в облачной функции. Например, вы хотите отправить письмо, но не хотите ждать пока оно отправится, а хотите сразу вернуть ответ клиенту.
При этом учитывая ограничения асинхронного вызова, я бы не рекомендовал использовать его напрямую в случаях, когда вы интегрируетесь с какой-либо внешней системой. Из-за отсутствия доступа к заголовкам HTTP-запроса и ответа вы будете ограничены в возможностях интеграции. В таком случае, мне видится более предпочтительным использовать вызов обычной функции, которая в свою очередь может вызвать асинхронную функцию, если есть жесткие требования к времени ответа. Или же можно использова ть API Gateway и его интеграцию с очередью сообщений.
К сожалению, в данный момент асинхронный вызов облачных функций не поддерживается в триггерах. То есть, не получится легко собрать цепочку из нескольких функций, которые будут вызываться друг за другом, передавая друг другу результат выполнения через очередь сообщений.
Пример функции на Go и TypeScript.