Job и DAG
Создайте локальную CI-платформу для сборки архива исходного кода. Seed создаёт developer с фиксированным токеном в Authorization header; job доступны владельцу и локальному оператору. Сеть и Git remote не нужны. POST /api/v1/jobs принимает project name, архив до 20 MiB и manifest до 64 KiB. Архив содержит не более 5000 файлов, глубина каталога не более 20, символические ссылки запрещены, суммарный распакованный размер ограничен 100 MiB. Нормализация путей отвергает traversal, абсолютные имена, дубликаты и служебные каталоги. Ответ содержит job ID, trace_id, queued state и отдельный idempotency key; одинаковый key и одинаковый архив возвращает существующую job, другой архив конфликтует.
Manifest задаёт от 1 до 50 именованных задач, зависимости DAG, timeout и одну из allowlisted команд: gofmt-check, go-test, go-vet, go-build, file-count. Произвольные shell-строки, shell expansion, pipe, команды ОС и network access запрещены. Команды запускаются без shell в выделенном временном рабочем каталоге. Зависимость может стартовать только после успеха всех родителей. Ошибка одной ветки помечает зависящие от неё задачи skipped; независимые ветки продолжаются. Job итоговая state success, failed, cancelled или timed_out. POST /jobs/{id}/cancel отменяет ожидающие задачи и посылает cancellation работающим; GET /jobs/{id} и GET /jobs/{id}/tasks/{task_id} показывают состояние и bounded log. Журнал ограничен 1 MiB на задачу и содержит UTF-8 текст без environment secrets.
Координатор, исполнителей и восстановление
PostgreSQL хранит задания, digest архивов, DAG, попытки, права обработки, журналы и переходы; временные каталоги удаляются через 24 часа. Compose запускает PostgreSQL, координатор и двух исполнителей. Каждый исполнитель берёт одну задачу, право обработки действует 30 секунд с heartbeat каждые 10 секунд. По истечении права задача становится recoverable; проверку можно повторить дважды, пользовательскую задачу — всего три раза. Каждая попытка получает чистый каталог архива; поколение права отвергает устаревший результат. Отмена опрашивается каждую секунду, координатор ждёт до пяти секунд.
Ограничения ресурсов и API
На каждом исполнителе одновременно выполняется одна задача; общий предел параллельности равен числу доступных исполнителей и не превышает 8. Каждая задача имеет timeout до 10 минут, CPU quota 1 core и memory limit 512 MiB; суммарный временный диск job не больше 500 MiB. POST /jobs/{id}/retry доступен для failed, timed_out и recoverable job, но запрещён для success/cancelled; новый запуск создаёт attempt, не меняет историю старого. Структурированные журналы связывают trace_id с job_id, task_id, attempt_id и worker_id; метрики показывают очередь, возраст права обработки, heartbeat и память. SQL timeout 3 секунды, API request body limit действует до распаковки.
Структура программы
Координатор и исполнитель запускаются раздельно; ядро проверяет DAG и планирует задачи, хранилище ведёт задания, отдельный запускатель изолированно вызывает разрешённые команды. main.go только загружает конфигурацию, связывает зависимости и управляет запуском. Пакеты без циклов, общих utils и интерфейсов без потребителя. README показывает зависимости; тесты ядра без HTTP/БД, интеграционные отдельно.
Приёмка
Compose-приёмка на локальном архиве проверяет ветвящийся DAG, цикл, traversal, архивные лимиты и повреждение, запрет команд, timeout, cancel и гибель исполнителя. Завершение после команды до фиксации даёт повтор в чистом каталоге; при подаче 40 задач двум исполнителям одновременно running не более двух. Критерии: только allowlist, изоляция каталогов, восстановление менее 40 секунд, логи до 1 MiB и отмена до 6 секунд. Отчёт с командами, идентификаторами трассировки, временем и памятью воспроизводит нагрузку и сбой.