Как разработчик, вы неизбежно столкнетесь с ситуацией, когда вам нужно поставить работу в очередь асинхронно. Хотя это типичный инструмент, есть несколько ловушек, в которые невероятно легко попасть — вот 3 ловушки, в которые вы, надеюсь, никогда не попадете после прочтения этого!

Отказ от ответственности: я разработчик Django, поэтому мои примеры могут быть больше ориентированы на варианты использования Python/Django.

1) Постановка в очередь внутри атомарного блока

Это лишь один из многих способов выстрелить себе в ногу атомным блоком 🙃

Допустим, вы являетесь сайтом электронной коммерции, и клиент размещает заказ. Вы хотите отправить электронное письмо «продавцу», в котором сообщаете ему, что кто-то купил товар, который он продает, поэтому для этого вы ставите в очередь код.

Однако, если вы раньше работали на сайте электронной коммерции, вы знаете, что шаг «Разместить заказ» не так прост, как кажется. Обычно есть несколько шагов, которые мы хотим выполнить атомарно, поэтому, если какая-либо из операций завершится неудачей, все остальные тоже должны быть отброшены. Примером этого является сбой платежа кредитной картой, мы хотим отменить создание заказа.

В этом заключается опасность постановки в очередь в атомарном блоке — если мы ставим задание в очередь внутри атомарного блока, но более поздняя часть атомарного блока дает сбой, наше поставленное в очередь задание остается в очереди.

Здесь ставки не так высоки, и в худшем случае просто отправить ошибочное электронное письмо продавцу. Однако вы можете увидеть, как это становится очень проблематичным в других ситуациях, особенно когда речь идет о деньгах или конфиденциальных данных.

Решение

Просто переместите очередь за пределы атомарного блока после ее успешного завершения!

2) Постановка в очередь конфиденциальных данных

Это не сразу очевидная ошибка, но это серьезная проблема безопасности.

Если вы раньше работали со сторонними библиотеками, то что-то вроде этого должно быть вам знакомо:

import third_party_lib
api_client = third_party_lib.create_client()
api_client.api_key = get_env_variable("STRIPE_API_SECRET_KEY")

Сторонние библиотеки часто создают некую форму «клиента», объекта, к которому вы прикрепляете учетные данные и используете для вызовов их API. Допустим, мы хотим сделать что-то асинхронно с этим клиентом.

Используя синтаксис django-rq в качестве примера, вот пример того, что может показаться разумным, но определенно не рекомендуется.

api_client = third_party_lib.create_client()
api_client.api_key = get_env_variable("STRIPE_API_SECRET_KEY")
queue = django_rq.get_queue(queue_name)
queue.enqueue(api_client.do_something, args, kwargs)

Чтобы ответить, почему это плохо, мы должны знать, что происходит, когда вы ставите задание в очередь. Глядя на django-rq исходный код, можно получить некоторое представление.

Это еще не все, но нас больше всего интересуют первые два условия. Когда мы передаем «метод» (то есть принадлежит экземпляру класса), мы назначаем job._instance для сохранения объекта.

Теперь вспомните, что мы только что присвоили этому «объекту» (то есть нашему api_client )? Наш очень секретный ключ API… упс 😬. Это задание (с объектом api_client и секретным ключом) будет записано в какое-нибудь хранилище данных, такое как Redis, обычно в незашифрованном виде!

Решение

Сделайте привычкой ставить в очередь только методы класса, которые будут ставиться в очередь как просто строка пути к имени функции (например, «app.payments.utils.payment_utils.PaymentUtils.some_class_method»).

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

3) Без учета условий гонки

Допустим, мы — Netflix, и у нас есть повторяющееся задание, которое проверяет всех пользователей с предстоящим продлением подписки, а это означает, что нам нужно взимать с них плату и продлевать их подписку еще на месяц.

В большой системе вполне вероятно, что мы будем ставить это задание в очередь довольно часто и даже между несколькими рабочими процессами.

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

Решение

Есть несколько способов обойти это, один из них — использовать «блокировку» общего кеша. Если есть общий кеш, из которого все рабочие могут читать/записывать, мы можем поместить PK базы данных обновляемых объектов в общий кеш, чтобы другие рабочие могли знать, что выполняется, а что еще нет.

Надеюсь, теперь вы знаете об этих различных ловушках при постановке в очередь асинхронных заданий. Я попытался привести несколько реалистичных примеров, но это, конечно, просто статья высокого уровня, и ваша конкретная ситуация может отличаться!

Буду рада вашим отзывам в комментариях :) Спасибо за прочтение!