Длинные операции через exec

🕐 Сложность: beginner | Скоупы: vibe:infra | Стек: cURL / Python

Установка пакетов, восстановление дампа базы данных, тяжёлая сборка — операции, которые идут дольше одного быстрого вызова. Для них у `POST /v1/infra/servers/:id/exec` есть три инструмента, и выбор зависит от длительности и от того, нужен ли живой прогресс.

Три инструмента и когда каждый уместен:

  • Синхронный JSON с параметром timeout — команда укладывается в 600 секунд, прогресс по ходу не нужен. Клиент блокируется до конца и получает stdout, stderr и exitCode одним JSON-ответом.
  • Потоковый режим ?stream=true — команда укладывается в timeout, но нужен построчный прогресс. Ответ идёт SSE-событиями stdout / stderr / exit по мере вывода.
  • Фоновая задача через systemd-run — команда идёт дольше 600 секунд, либо канал /exec нужно освободить сразу. Вызов возвращается мгновенно, а процесс продолжает работать независимо от HTTP-соединения.
Когда Как Мониторинг
Команда укладывается в 600 секунд, прогресс не нужен { "command": "...", "timeout": 600 } без ?stream=true — блокирующий JSON-ответ exitCode, stdout, stderr в теле ответа
Команда укладывается в timeout, нужен живой прогресс тот же запрос с ?stream=true — SSE-события stdout / stderr / exit читать события по мере поступления
Команда дольше 600 секунд или надо освободить канал фоновая задача systemd-run --unit=<name>/exec возвращается сразу `GET /v1/infra/servers/:id/logs?service=`

timeout ограничен 600 секундами и выше не поднимается. По его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без паузы на мягкое завершение). Если операция не укладывается в этот предел — она уходит в фоновую задачу.

Фоновые задачи

Фоновая задача получает собственный cgroup и живёт независимо от exec-сессии: /exec вернётся мгновенно, канал освободится (следующий вызов не упрётся в EXEC_BUSY), а процесс переживёт истечение timeout. Рекомендуемый способ — транзиентный systemd-юнит:

JSON
{ "command": "systemctl reset-failed restore-db 2>/dev/null; systemd-run --unit=restore-db /bin/bash /opt/data/task.sh", "timeout": 30 }

systemctl reset-failed <name> перед запуском обязателен — иначе systemd откажется создать юнит с именем ранее упавшей задачи. Управление и мониторинг — стандартными вызовами:

  • статус: { "command": "systemctl is-active restore-db" }active (идёт), inactive (завершилась успешно), failed (упала).
  • логи: вывод юнита journald подхватывает автоматически — читайте через `GET /v1/infra/servers/:id/logs?service=restore-db`.

Секреты — пароли базы данных и подобное — передавайте через поле env, а не внутри command: команды журналируются агентом.

Анти-паттерн — голый nohup без перенаправления вывода. nohup cmd & без > файл 2>&1 не работает: команда выполняется в канале, а не в терминале, поэтому фоновый процесс наследует канал exec-сессии. Агент ждёт закрытия этого канала до самого timeout, а затем принудительно завершает всю процесс-группу, включая ваш «фоновый» процесс. Всегда добавляйте перенаправление: cmd > /tmp/job.log 2>&1.

Шаги install и preStart у `POST /v1/infra/servers/:id/deploy` имеют собственный таймаут 300 секунд — отдельный от 600 секунд у /exec. Поднять timeout у /exec шагу установки не поможет: тяжёлые установки выносите в разовый /exec или в фоновую задачу, а не в install-скрипт.

Настройка HTTP-клиента

Если команда выполняется дольше 15 секунд, сервер в JSON-режиме периодически шлёт пробелы в тело ответа — это удерживает соединение открытым, чтобы nginx и промежуточные прокси не разорвали простаивающее соединение. JSON-парсер игнорирует начальные и конечные пробелы, поэтому клиент прочитает корректный JSON по окончании.

Отсюда правило настройки клиента: read-таймаут HTTP-клиента отсчитывается между байтами, а пробелы-заполнители каждые ~15 секунд сбрасывают этот отсчёт. Общий дедлайн запроса не ставьте меньше значения timeout команды, иначе клиент оборвёт соединение до её завершения.

Python
import requests
r = requests.post(
    f"{VIBE_URL}/v1/infra/servers/{SERVER_ID}/exec",
    headers={"X-Api-Key": VIBE_API_KEY},
    json={"command": "npm ci --production", "workdir": "/opt/app", "timeout": 600},
    timeout=(10, 60),   # read-таймаут считается МЕЖДУ байтами; keepalive-пробелы каждые ~15 с держат соединение
)

В режиме ?stream=true читайте SSE-события инкрементально, по мере поступления, а не буферизуйте ответ до конца — иначе теряется весь смысл потокового прогресса.

Смотрите также