2020-03-08 19:13:14 +00:00
|
|
|
|
# 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. Результаты работы джобов скачиваются на клиента.
|
|
|
|
|
|
2020-03-10 12:08:59 +00:00
|
|
|
|
## Протоколы
|
2020-03-08 19:13:14 +00:00
|
|
|
|
|
2020-03-10 12:08:59 +00:00
|
|
|
|
Общение между компонентами будет происходить поверх 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`.
|
2020-03-08 19:13:14 +00:00
|
|
|
|
|
|
|
|
|
## Кеширование
|
|
|
|
|
|
|
|
|
|
### Кеш исходного кода
|
|
|
|
|
|
|
|
|
|
### Кеш артефактов
|
|
|
|
|
|
|
|
|
|
## Тестирование
|
|
|
|
|
|