[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-recipes\u002Flong-operations":3,"docs-tabs-recipes\u002Flong-operations":6},{"content":4,"lastmod":5},"# Длинные операции через exec\n\n🕐 **Сложность:** beginner | **Скоупы:** vibe:infra | **Стек:** cURL \u002F Python\n\nУстановка пакетов, восстановление дампа базы данных, тяжёлая сборка — операции, которые идут дольше одного быстрого вызова. Для них у [`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fexec`](\u002Fdocs\u002Finfra\u002Fdeploy\u002Fexec) есть три инструмента, и выбор зависит от длительности и от того, нужен ли живой прогресс.\n\nТри инструмента и когда каждый уместен:\n\n- **Синхронный JSON с параметром `timeout`** — команда укладывается в 600 секунд, прогресс по ходу не нужен. Клиент блокируется до конца и получает `stdout`, `stderr` и `exitCode` одним JSON-ответом.\n- **Потоковый режим `?stream=true`** — команда укладывается в `timeout`, но нужен построчный прогресс. Ответ идёт SSE-событиями `stdout` \u002F `stderr` \u002F `exit` по мере вывода.\n- **Фоновая задача через `systemd-run`** — команда идёт дольше 600 секунд, либо канал `\u002Fexec` нужно освободить сразу. Вызов возвращается мгновенно, а процесс продолжает работать независимо от HTTP-соединения.\n\n| Когда | Как | Мониторинг |\n|-------|-----|------------|\n| Команда укладывается в 600 секунд, прогресс не нужен | `{ \"command\": \"...\", \"timeout\": 600 }` без `?stream=true` — блокирующий JSON-ответ | `exitCode`, `stdout`, `stderr` в теле ответа |\n| Команда укладывается в `timeout`, нужен живой прогресс | тот же запрос с `?stream=true` — SSE-события `stdout` \u002F `stderr` \u002F `exit` | читать события по мере поступления |\n| Команда дольше 600 секунд или надо освободить канал | фоновая задача `systemd-run --unit=\u003Cname>` — `\u002Fexec` возвращается сразу | [`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs?service=\u003Cname>`](\u002Fdocs\u002Finfra\u002Fdeploy\u002Flogs) |\n\n`timeout` ограничен 600 секундами и выше не поднимается. По его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без паузы на мягкое завершение). Если операция не укладывается в этот предел — она уходит в фоновую задачу.\n\n### Фоновые задачи\n\nФоновая задача получает собственный cgroup и живёт независимо от exec-сессии: `\u002Fexec` вернётся мгновенно, канал освободится (следующий вызов не упрётся в `EXEC_BUSY`), а процесс переживёт истечение `timeout`. Рекомендуемый способ — транзиентный systemd-юнит:\n\n```json\n{ \"command\": \"systemctl reset-failed restore-db 2>\u002Fdev\u002Fnull; systemd-run --unit=restore-db \u002Fbin\u002Fbash \u002Fopt\u002Fdata\u002Ftask.sh\", \"timeout\": 30 }\n```\n\n`systemctl reset-failed \u003Cname>` перед запуском обязателен — иначе systemd откажется создать юнит с именем ранее упавшей задачи. Управление и мониторинг — стандартными вызовами:\n\n- статус: `{ \"command\": \"systemctl is-active restore-db\" }` — `active` (идёт), `inactive` (завершилась успешно), `failed` (упала).\n- логи: вывод юнита journald подхватывает автоматически — читайте через [`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs?service=restore-db`](\u002Fdocs\u002Finfra\u002Fdeploy\u002Flogs).\n\nСекреты — пароли базы данных и подобное — передавайте через поле `env`, а не внутри `command`: команды журналируются агентом.\n\n**Анти-паттерн — голый `nohup` без перенаправления вывода.** `nohup cmd &` без `> файл 2>&1` не работает: команда выполняется в канале, а не в терминале, поэтому фоновый процесс наследует канал exec-сессии. Агент ждёт закрытия этого канала до самого `timeout`, а затем принудительно завершает всю процесс-группу, включая ваш «фоновый» процесс. Всегда добавляйте перенаправление: `cmd > \u002Ftmp\u002Fjob.log 2>&1`.\n\nШаги `install` и `preStart` у [`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy`](\u002Fdocs\u002Finfra\u002Fdeploy\u002Fdeploy) имеют собственный таймаут 300 секунд — отдельный от 600 секунд у `\u002Fexec`. Поднять `timeout` у `\u002Fexec` шагу установки не поможет: тяжёлые установки выносите в разовый `\u002Fexec` или в фоновую задачу, а не в install-скрипт.\n\n### Настройка HTTP-клиента\n\nЕсли команда выполняется дольше 15 секунд, сервер в JSON-режиме периодически шлёт пробелы в тело ответа — это удерживает соединение открытым, чтобы nginx и промежуточные прокси не разорвали простаивающее соединение. JSON-парсер игнорирует начальные и конечные пробелы, поэтому клиент прочитает корректный JSON по окончании.\n\nОтсюда правило настройки клиента: read-таймаут HTTP-клиента отсчитывается **между байтами**, а пробелы-заполнители каждые ~15 секунд сбрасывают этот отсчёт. Общий дедлайн запроса не ставьте меньше значения `timeout` команды, иначе клиент оборвёт соединение до её завершения.\n\n```python\nimport requests\nr = requests.post(\n    f\"{VIBE_URL}\u002Fv1\u002Finfra\u002Fservers\u002F{SERVER_ID}\u002Fexec\",\n    headers={\"X-Api-Key\": VIBE_API_KEY},\n    json={\"command\": \"npm ci --production\", \"workdir\": \"\u002Fopt\u002Fapp\", \"timeout\": 600},\n    timeout=(10, 60),   # read-таймаут считается МЕЖДУ байтами; keepalive-пробелы каждые ~15 с держат соединение\n)\n```\n\nВ режиме `?stream=true` читайте SSE-события инкрементально, по мере поступления, а не буферизуйте ответ до конца — иначе теряется весь смысл потокового прогресса.\n\n## Смотрите также\n\n- [Выполнить команду](\u002Fdocs\u002Finfra\u002Fdeploy\u002Fexec)\n- [Логи сервиса](\u002Fdocs\u002Finfra\u002Fdeploy\u002Flogs)\n- [Загрузка дампа БД на сервер](\u002Fdocs\u002Frecipes\u002Fdb-dump-restore)\n","2026-07-07",{}]