ГлавнаяБлогEventBridge Scheduler и долгие задачи: скрытый таймаут и решение
Алгоритмы

EventBridge Scheduler и долгие задачи: скрытый таймаут и решение

Разбираем, почему EventBridge Scheduler помечает успешные вызовы AgentCore в DLQ из-за 30-секундного таймаута, и как исправить это с помощью асинхронных задач.

Al
Редакция Algolitalgolit.ru
7 мин чтения2 августа 2026 г.

Почему ваш агент попадает в DLQ, хотя всё работает

Если вы используете EventBridge Scheduler для запуска агентов AgentCore и замечаете, что вызовы уходят в очередь недоставленных сообщений (DLQ), хотя задача выполняется успешно, — вы не одиноки. В этой статье мы разберём скрытую проблему, связанную с синхронными вызовами и таймаутом, и покажем, как её решить с помощью асинхронных задач.

Суть проблемы: синхронные вызовы и таймаут 30 секунд

EventBridge Scheduler universal targets (функция, позволяющая вызывать API-действия AWS напрямую, без Lambda) работают синхронно. Scheduler делает вызов и ждёт ответа, прежде чем решить, успешен ли вызов. В документации это описано как at-least-once delivery, но важный нюанс — ответ должен быть получен в течение определённого времени.

Когда вы вызываете действие invokeAgentRuntime из Bedrock AgentCore, оно блокируется до завершения агента. Поиск может занять от 30 до 75 секунд. Scheduler сдаётся примерно через 30 секунд и помечает вызов как неудачный. Агент, не подозревая об этом, продолжает работу, завершает поиск и отправляет уведомление через SNS. Вы получаете письмо, а неудачный вызов уходит в DLQ.

Вот как выглядит сообщение в DLQ:

// Пример сообщения DLQ (упрощённо)
{
  "version": "0",
  "source": "aws.scheduler",
  "detail": {
    "error": "TargetExecutionFailed",
    "reason": "Invocation timed out"
  }
}

Вся последующая цепочка работает, но Scheduler считает вызов неудачным. Проблема не в вашем коде, а в том, как Scheduler обрабатывает долгие вызовы.

Где искать документацию по таймауту?

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

Единственный настраиваемый таймаут — MaximumEventAgeInSeconds в политике повторов. Это максимальный возраст события при повторах, а не лимит на один вызов. Это разные вещи.

Таким образом, ~30 секунд — это наблюдаемое значение, а не контракт. Я рекомендую проектировать систему так, чтобы ответ приходил быстро, а не рассчитывать на конкретное значение.

Решение: возвращаем подтверждение сразу, а работу выполняем в фоне

Вместо того чтобы ускорять поиск, я ускорил ответ. Точка входа теперь запускает поиск как фоновую задачу и немедленно возвращает результат. Так Scheduler получает ответ в пределах окна ожидания, а агент работает столько, сколько нужно (до лимита сессии AgentCore в 8 часов).

AgentCore поддерживает такой подход из коробки: вы регистрируете фоновую работу с помощью add_async_task, рантайм сообщает HealthyBusy на /ping во время выполнения, и сессия остаётся активной, а не завершается. Это важно, потому что AgentCore завершает сессии после 15 минут простоя, и без отслеживания задач ваша фоновая работа выглядит как простой.

Вот ключевая часть кода точки входа:

# Сильные ссылки: asyncio хранит слабые ссылки на задачи
_background_tasks: set[asyncio.Task[Any]] = set()

async def _tracked_job_search(company: str, title: str, location: str) -> None:
    """Запускает поиск работы как отслеживаемую асинхронную задачу, чтобы ping сообщал HealthyBusy."""
    task_id = app.add_async_task("job_search")
    try:
        await run_job_search(company, title, location)
    finally:
        app.complete_async_task(task_id)

@app.entrypoint
async def invoke(payload: dict[str, Any] | None = None) -> dict[str, Any]:
    """Основная точка входа для вызова агента."""
    # ...валидация payload...
    if payload.get("sync"):
        return await run_job_search(company, title, location)

    # Отвечаем до того, как таймаут Scheduler (~30 сек) отправит вызов в DLQ
    task = asyncio.create_task(_tracked_job_search(company, title, location))
    _background_tasks.add(task)
    task.add_done_callback(_background_tasks.discard)

    return {
        "status": "accepted",
        "search_criteria": {
            "company": company,
            "title": title,
            "location": location
        }
    }

Scheduler получает {"status": "accepted"} менее чем за секунду. Поиск выполняется в фоне. DLQ больше не пополняется.

Что учесть при использовании фоновых задач

Вызов Strands должен быть асинхронным

Если вы вызываете agent(prompt) внутри async def, это блокирует цикл событий. Когда рантайму нужно отвечать на health check во время поиска, блокирующий вызов останавливает всё. Переключение на await agent.invoke_async(prompt) — необходимость, а не стилистическое предпочтение.

Держите ссылку на задачу

asyncio хранит только слабые ссылки на задачи из create_task, поэтому задача без ссылки может быть удалена сборщиком мусора во время выполнения. Именно для этого существует множество _background_tasks — это паттерн из документации Python. Обычно всё работает, но без ссылки может внезапно сломаться.

Оставьте синхронный путь

Fire-and-forget отлично подходит для планировщика, но ужасен для локальной разработки. Флаг "sync": true в payload запускает поиск инлайн и возвращает полный результат. Я использую его для curl и в песочнице AgentCore.

Ещё раз о повторах

В оригинальной статье я отключил повторы, потому что повторный вызов "неудачной" инвокации приводил к дублированию агентов и писем. По умолчанию в CDK цель имеет 185 повторов с экспоненциальной задержкой до 24 часов, поэтому каждый запланированный поиск мог повторяться (и списывать деньги), пока я это не заметил.

С асинхронным путём я вернул повторы (с retryAttempts: 0 на 2). Теперь Scheduler получает подтверждение сразу, поэтому не будет повторять вызов, пока поиск ещё идёт. Остаётся небольшой шанс дублирования, если подтверждение потеряется, но для меня это приемлемый компромисс.

Важно понимать: теперь Scheduler подтверждает только запуск агента, но не факт успешного завершения. Если поиск упадёт через 40 секунд, Scheduler будет доволен, а DLQ останется пустым. Нужны логи, метрики или само уведомление, чтобы знать, что работа завершилась. Для меня отсутствие письма — явный сигнал. Если ваша система не такая явная, продумайте мониторинг.

Если вы столкнулись с этой проблемой

Если вы вызываете AgentCore Runtime из расписания EventBridge и видите сообщения в DLQ для явно успешных запусков:

  1. Проверьте, сколько времени реально занимает ваш агент. Всё, что больше ~30 секунд, подозрительно.
  2. Возвращайте подтверждение из точки входа вместо результата.
  3. Оберните реальную работу в add_async_task / complete_async_task, чтобы рантайм знал, что сессия активна.
  4. Убедитесь, что ничего в фоновом пути не блокирует цикл событий.

Код доступен в репозитории job-search-agent, а изменения — в PR #51, если хотите увидеть полный дифф.

Эта проблема не специфична для AgentCore. Любая длительная задача, вызываемая из EventBridge Scheduler как universal target, столкнётся с тем же. Агенты просто особенно хороши в медленной работе. 😅

Если вы сталкивались с подобным для других длительных целей или нашли документированный таймаут, который я пропустил, — напишите мне, буду рад обсудить.

#EventBridge Scheduler#AgentCore#асинхронные задачи#DLQ#AWS
Al
Редакция Algolit

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

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →