# distbuild В этом задании вам нужно будет реализовать систему распределённой сборки. Система сборки получает на вход граф сборки и файлы с исходным кодом. Результатом сборки являются исполняемые файлы и stderr/stdout запущенных процессов. ## Граф сборки Граф сборки состоит из джобов. Каждый джоб описывает команды, которые нужно запустить на одной машине, вместе со всеми входными файлами, которые нужны этим командам для работы. Джобы в графе сборки запускают произвольные команды. Например, вызывать компилятор, линкер или запускать тесты. Команды внутри джоба могут читать файлы с файловой системы. Мы будем различать два вида файлов: - Файлы с исходным кодом с машины пользователя. - Файлы, которые породили другие джобы. Команды внутри джоба могут писать результаты своей работы в файлы на диске. Выходные файлы обязаны находиться внутри `OUTPUT_DIR` директории. ```go package graph import "crypto/sha1" // ID задаёт уникальный идентификатор джоба. // // Мы будем использовать sha1 хеш, поэтому ID будет занимать 20 байт. type ID [sha1.Size]byte // Job описывает одну вершину графа сборки. type Job struct { // ID задаёт уникальный идентификатор джоба. // // ID вычисляется как хеш от всех входных файлов, команд запуска и хешей зависимых джобов. // // Выход джоба целиком опеределяется его ID. Это важное свойство позволяет кешировать // результаты сборки. ID ID // Name задаёт человекочитаемое имя джоба. // // Например: // build gitlab.com/slon/disbuild/pkg/b // vet gitlab.com/slon/disbuild/pkg/a // test gitlab.com/slon/disbuild/pkg/test Name string // Inputs задаёт список файлов из директории с исходным кодом, // которые нужны для работы этого джоба. // // В типичном случае, тут будут перечислены все .go файлы одного пакета. Inputs []string // Deps задаёт список джобов, выходы которых нужны для работы этого джоба. Deps []ID // Cmds описывает список команд, которые нужно выполнить в рамках этого джоба. Cmds []Cmd } // Cmd описывает одну команду сборки. // // Есть несколько видов команд. Все виды команд описываются одной структурой. // Реальный тип определяется тем, какие поля структуры заполнены. // // exec - выполняет произвольную команду // cat - записывает строку в файл // // Все строки в описании команды могут содержать в себе на переменные. Перед выполнением // реальной команды, переменные заменяются на их реальные значения. // // {{OUTPUT_DIR}} - абсолютный путь до выходной директории джоба. // {{SOURCE_DIR}} - абсолютный путь до директории с исходными файлами. // {{DEP:f374b81d81f641c8c3d5d5468081ef83b2c7dae9}} - абсолютный путь до директории, // содержащей выход джоба с id f374b81d81f641c8c3d5d5468081ef83b2c7dae9. type Cmd struct { // Exec описывает команду, которую нужно выполнить. Exec []string // Environ описывает переменные окружения, которые необходимы для работы команды из Exec. Environ []string // WorkingDirectory задаёт рабочую директорию для команды из Exec. WorkingDirectory string // CatTemplate задаёт шаблон строки, которую нужно записать в файл. CatTemplate string // CatOutput задаёт выходной файл для команды типа cat. CatOutput string } ``` ## Архитектура системы Наша система будет состоять из трех компонент. * Клиент - процесс запускающий сборку. * Воркер - процесс запускающий команды компиляции и тестирования. * Координатор - центральный процесс в системе, общается с клиентами и воркерами. Раздаёт задачи воркерам. Типичная сборка выглядит так: 1. Клиент подключается к координатору, посылает ему граф сборки и входные файлы для графа сборки. 2. Кооринатор сохраняет граф сборки в памяти и начинает его исполнение. 3. Воркеры начинают выполнять вершины графа, пересылая друг другу выходные директории джобов. 4. Результаты работы джобов скачиваются на клиента. ## Протоколы Общение между компонентами будет происходить поверх HTTP и json. В реальной системе мы бы взяли более продвинутый протокол и более эффективный формат сериализации, но в этой учебной задаче нам важнее уменьшить сложность системы. ### Протокол: Клиент <-> Координатор При общении клиента и кординатора, клиент всегда выступает инициатором запроса. * `POST /build` - стартует новый билд. Клиент посылает в Body запроса json c описанием сборки. Сервер стримит в body ответа json сообщения описывающие прогресс сборки (тут правильнее было бы использовать websocket, но нас устраивает более простое решение). * `POST /source/{sha1}` - загружает файл с исходным кодом на координатор. Клиент посылает содержимое файла в body запроса. `{sha1}` - равен `{sha1}` хешу от содержимого файла. ### Протокол: Координатор <-> Воркер При общении воркера и координатора, воркер всегда выступает инициатором запроса. * `GET /source/{sha1}` - скачивает файл с исходным кодом с координатора. * `POST /heartbeat` - синхронизирует состояние воркера и координатора. Воркер посылает в теле запроса json описывающий изменение в своём состоянии. Коордитора отвечает json-ом со списком задач, которые должен выполнить воркер. ```go package proto // CompleteJob описывает результат работы джоба. type CompletedJob struct { ID graph.ID Stdout, Stderr []byte // Error описывает сообщение об ошибке, из-за которого джоб не удалось выполнить. // // Если Error == nil, значит джоб завершился успешно. Error *string } type HeartbeatRequest struct { // WorkerID задаёт персистентный идентификатор данного воркера. // // WorkerID так же выступает в качестве endpoint-а, к которому можно подключиться по HTTP. // // В наших тестов, идентификатор будет иметь вид "localhost:%d". WorkerID string // ProcessID задаёт эфемерный идентификатор текущего процесса воркера. // // Координатор запоминает ProcessID для каждого воркера. // // Измение ProcessID значит, что воркер перезапустился. ProcessID string // RunningJobs перечисляет список джобов, которые выполняются на этом воркере // в данный момент. RunningJobs []graph.ID DownloadingSources []graph.ID DownloadingArtifacts []graph.ID // FreeSlots сообщаяет, сколько еще процессов можно запустить на этом воркере. FreeSlots int // CompletedJobs сообщает координатору, какие джобы завершили исполнение на этом воркере // на этой итерации цикла. CompletedJobs []CompletedJob // AddedArtifacts говорит, какие артефакты появились в кеше на этой итерации цикла. AddedArtifacts []graph.ID // AddedSourceFiles говорит, какие файлы появились в кеше на этой итерации цикла. AddedSourceFiles []graph.ID } // JobSpec описывает джоб, который нужно запустить. type JobSpec struct { } // ArtifactSpec описывает артефакт, который нужно скачать с другого воркера. type ArtifactSpec struct { } // SourceFileSpec описывает файл с исходным кодом, который нужно скачать с координатора. type SourceFileSpec struct { } type HeartbeatResponse struct { JobsToRun map[graph.ID]JobSpec ArtifactsToDownload map[graph.ID]ArtifactSpec ArtifactsToRemove []graph.ID SourceFilesToDownload map[graph.ID]SourceFileSpec SourceFilesToRemove []graph.ID } ``` ### Протокол: Воркер <-> Воркер Общение между воркерам происходит тогда, когда системе нужно передать артефакты сборки с одного воркера на другой. * `GET /artifact/{sha1}` - возвращает директорию с выходными данными джоба в формате `tar`. ## Кеширование ### Кеш исходного кода ### Кеш артефактов ## Тестирование