Длинные операции через 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-юнит:
{ "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 команды, иначе клиент оборвёт соединение до её завершения.
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-события инкрементально, по мере поступления, а не буферизуйте ответ до конца — иначе теряется весь смысл потокового прогресса.