"""Общий хелпер безопасной постановки Celery-задачи по имени. Вынесен из `_send_summarize_task` (`workers.tasks.pipeline`) как общая защита от потери шага при недоступности Redis: временная недоступность брокера в момент `app.send_task` не должна ронять вызывающую задачу — предыдущий шаг (фразы/саммари) уже закоммичен, и `failed` из-за одного лишь сбоя постановки СЛЕДУЮЩЕЙ задачи стёр бы уже проделанную работу. Используется дважды: `workers.tasks.pipeline.run_pipeline` (постановка `summarize_session`) и `workers.tasks.summarize. summarize_session` (постановка `notify_session`) — оба вызывающих места сами решают, что делать при `False` (не роняются, статус пайплайна не откатывается и не проваливается; восстановление — соответствующая beat-задача `workers.tasks.maintenance`, уровень 2 защиты). """ import asyncio import logging from kombu.exceptions import ConnectionError as KombuConnectionError from kombu.exceptions import OperationalError as KombuOperationalError logger = logging.getLogger(__name__) SEND_TASK_MAX_ATTEMPTS = 3 """Число попыток поставить задачу в очередь при недоступности брокера.""" SEND_TASK_BACKOFF_S = 2.0 """Базовая пауза (сек) между попытками; растёт линейно с номером попытки.""" async def send_task_with_retry(task_name: str, args: list[str]) -> bool: """Поставить задачу `task_name` в очередь с несколькими попытками при сбое брокера. `args` — позиционные аргументы задачи (как ожидает `Celery.send_task`). Возвращает `True`, если постановка удалась хотя бы с одной попытки; `False` — все попытки исчерпаны. Вызывающая сторона логирует контекст (какой сеанс/задача) и оставляет восстановление уровню 2 защиты — не откатывает и не проваливает уже проделанную работу из-за одного лишь сбоя постановки следующей задачи. """ # Отложенный импорт `app`: на верхнем уровне модуля это создало бы цикл # (`workers.celery_app` импортирует `workers.tasks.pipeline`/`summarize`, # а те — `send_task_with_retry` из этого модуля) — импорт откладывается # до первого вызова, когда `workers.celery_app` уже полностью загружен. from workers.celery_app import app for attempt in range(1, SEND_TASK_MAX_ATTEMPTS + 1): try: app.send_task(task_name, args=args) except (KombuOperationalError, KombuConnectionError) as exc: if attempt < SEND_TASK_MAX_ATTEMPTS: logger.warning( "send_task_with_retry: сбой постановки %s (попытка %d/%d): %s " "— повтор через %.1fс", task_name, attempt, SEND_TASK_MAX_ATTEMPTS, exc, SEND_TASK_BACKOFF_S * attempt, ) await asyncio.sleep(SEND_TASK_BACKOFF_S * attempt) continue logger.error( "send_task_with_retry: не удалось поставить %s за %d попыток (%s)", task_name, SEND_TASK_MAX_ATTEMPTS, exc, ) return False else: return True return False # недостижимо: цикл либо возвращает, либо продолжает до предела