shad-go/distbuild
2020-03-29 19:03:07 +03:00
..
disttest Client protocol 2020-03-29 19:03:07 +03:00
pkg Client protocol 2020-03-29 19:03:07 +03:00
README.md wip 2020-03-27 22:56:55 +03:00

distbuild

В этом задании вам нужно будет реализовать систему распределённой сборки.

Система сборки получает на вход граф сборки и файлы с исходным кодом. Результатом сборки являются исполняемые файлы и stderr/stdout запущенных процессов.

Граф сборки

Граф сборки состоит из джобов. Каждый джоб описывает команды, которые нужно запустить на одной машине, вместе со всеми входными файлами, которые нужны этим командам для работы.

Джобы в графе сборки запускают произвольные команды. Например, вызывать компилятор, линкер или запускать тесты.

Команды внутри джоба могут читать файлы с файловой системы. Мы будем различать два вида файлов:

  • Файлы с исходным кодом с машины пользователя.
  • Файлы, которые породили другие джобы.

Команды внутри джоба могут писать результаты своей работы в файлы на диске. Выходные файлы обязаны находиться внутри OUTPUT_DIR директории.

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-ом со списком задач, которые должен выполнить воркер.

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.

Кеширование

Кеш исходного кода

Кеш артефактов

Тестирование