Короткий ответ
Нужно использовать таймауты на каждый внешний вызов (а не ждать бесконечно), паттерн circuit breaker для временного отключения вызовов к стабильно отказывающему сервису, и явно решить для каждой интеграции — является ли она критичной для основного пути запроса (тогда отказ должен приводить к явной ошибке клиенту) или вспомогательной (тогда можно деградировать функциональность, но вернуть основной ответ).
Как это работает подробнее
Без явного таймаута один медленный внешний вызов может удерживать поток/корутину обработчика существенно дольше ожидаемого, что при достаточном количестве параллельных запросов приводит к исчерпанию пула соединений/потоков и деградации всего сервиса, а не только той функциональности, что зависит от медленного внешнего сервиса. Circuit breaker отслеживает долю неудачных вызовов к конкретному внешнему сервису и при превышении порога временно перестаёт пытаться его вызывать (сразу возвращая отказ или деградированный ответ), давая внешнему сервису время восстановиться и не трату ресурсов на вызовы, которые скорее всего снова провалятся.
Жёсткие таймауты и circuit breaker повышают устойчивость основного сервиса к деградации внешних зависимостей, но могут приводить к отказу обслуживания операции, которая на самом деле могла бы успешно завершиться чуть позже без этих защит — то есть снижают доступность в обмен на предсказуемость и защиту ресурсов. Деградация вспомогательной функциональности (пропуск необязательного шага) сохраняет основной путь доступным, но требует отдельного надёжного механизма донести пропущенный шаг позже (очередь с повтором), что добавляет архитектурную сложность. Критерий выбора — критичность конкретной интеграции для основного бизнес-процесса и приемлемость временной потери неосновной функциональности.
Пример кода
import httpx
async def charge_payment(order_id: int, amount: float):
# Критичная интеграция: явный таймаут, отказ должен быть виден клиенту
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.post("https://payments.example.com/charge",
json={"order_id": order_id, "amount": amount},
headers={"Idempotency-Key": f"order-{order_id}"})
resp.raise_for_status()
return resp.json()
except httpx.HTTPError:
raise PaymentProviderUnavailable()
async def notify_user_async(order_id: int):
# Вспомогательная интеграция: отказ не должен ломать основной ответ,
# задача просто уходит в очередь на повтор.
try:
async with httpx.AsyncClient(timeout=2.0) as client:
await client.post("https://notify.example.com/send", json={"order_id": order_id})
except httpx.HTTPError:
enqueue_retry("send_notification", order_id=order_id)Что использовать на практике
- Практика. Критичные интеграции (например, сам платёжный провайдер для операции оплаты) обычно не могут быть просто «пропущены» — отказ должен быть явным для клиента с понятным сообщением и, возможно, retry с идемпотентным ключом. Вспомогательные интеграции (например, отправка уведомления после успешной оплаты) логичнее деградировать отдельно — основная операция (оплата) считается успешной, а уведомление ставится в очередь на повтор отдельно, не блокируя основной ответ.
Подводные камни
- Риск. Ставят одинаковый подход (например, просто ретраи без таймаута и circuit breaker) для критичных и вспомогательных интеграций одинаково, из-за чего отказ вспомогательного сервиса уведомлений может замедлить или даже сломать основной путь оплаты; не учитывают, что повторные попытки (retry) без идемпотентности на стороне внешнего сервиса могут вызвать дублирование операций (например, двойное списание), если исходный запрос на самом деле был обработан, но ответ не дошёл из-за таймаута.