Update distbuild/README.md

This commit is contained in:
Fedor Korotkiy 2020-04-05 15:47:58 +03:00
parent a28ac30f2e
commit 3476778243
4 changed files with 72 additions and 173 deletions

View file

@ -18,10 +18,11 @@
- Файлы, которые породили другие джобы.
Команды внутри джоба могут писать результаты своей работы в файлы на диске. Выходные файлы
обязаны находиться внутри `OUTPUT_DIR` директории.
обязаны находиться внутри его выходной директории. Директория с результатом работы джоба называется
артефактом.
```go
package graph
package build
import "crypto/sha1"
@ -32,65 +33,33 @@ type ID [sha1.Size]byte
// Job описывает одну вершину графа сборки.
type Job struct {
// ID задаёт уникальный идентификатор джоба.
//
// ID вычисляется как хеш от всех входных файлов, команд запуска и хешей зависимых джобов.
//
// Выход джоба целиком опеределяется его ID. Это важное свойство позволяет кешировать
// результаты сборки.
ID ID
// 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
// 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
// Inputs задаёт список файлов из директории с исходным кодом,
// которые нужны для работы этого джоба.
//
// В типичном случае, тут будут перечислены все .go файлы одного пакета.
Inputs []string
// Deps задаёт список джобов, выходы которых нужны для работы этого джоба.
Deps []ID
// 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
// Cmds описывает список команд, которые нужно выполнить в рамках этого джоба.
Cmds []Cmd
}
```
@ -108,125 +77,32 @@ type Cmd struct {
3. Воркеры начинают выполнять вершины графа, пересылая друг другу выходные директории джобов.
4. Результаты работы джобов скачиваются на клиента.
## Протоколы
# Как решать эту задачу
Общение между компонентами будет происходить поверх HTTP и json. В реальной системе мы бы
взяли более продвинутый протокол и более эффективный формат сериализации, но в этой учебной
задаче нам важнее уменьшить сложность системы.
Задача разбита на шаги. В начале, вам нужно будет реализовать небольшой набор независимых пакетов,
которые реализует нужные примитивы. Код в этих пакетах покрыт юниттестами. В каждом пакете находится
файл README.md, объясняющий подзадачу.
### Протокол: Клиент <-> Координатор
Рекомендуемый порядок выполнения:
При общении клиента и кординатора, клиент всегда выступает инициатором запроса.
- [`distbuild/pkg/build`](./distbuild/pkg/build) - определение графа сборки. В этом пакете ничего писать не нужно,
нужно ознакомиться с существующим кодом.
- [`distbuild/pkg/tarstream`](./distbuild/pkg/tarstream) - передача директории через сокет.
- [`distbuild/pkg/api`](./distbuild/pkg/api) - протокол общения между компонентами.
- [`distbuild/pkg/artifact`](./distbuild/pkg/artifact) - кеш артефактов и протокол передачи артефактов между воркерами.
- [`distbuild/pkg/filecache`](./distbuild/pkg/filecache) - кеш файлов и протокол передачи файлов между компонентами.
- [`distbuild/pkg/scheduler`](./distbuild/pkg/filecache) - планировщик с экристикой локальности.
* `POST /build` - стартует новый билд. Клиент посылает в Body запроса json c описанием сборки. Сервер
стримит в body ответа json сообщения описывающие прогресс сборки (тут правильнее было бы использовать
websocket, но нас устраивает более простое решение).
После того, как все кубики будут готовы, нужно будет соединить их вместе, реализовав `distbuild/pkg/worker`,
`distbuild/pkg/client` и `distbuild/pkg/coordinator`. Код в этих пакетах нужно отлаживать на
интеграционных тестах в [`distbuild/disttest`](./distbuild/disttest).
* `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`.
## Кеширование
### Кеш исходного кода
### Кеш артефактов
## Тестирование
# Критерии оценки
Решение должно проходить все тесты, так же как в обычной задаче. После успешной попытки, в таблице gdoc
будет стоять 0. После этого, проверяющие должны будут просмотреть решение и заменить оценку в таблице на 1.
Это будет значить, что задача засчитана. Code Review не будет, проверка нужна только чтобы удостовериться что
посылка честно проходит все тесты.

View file

@ -0,0 +1,19 @@
# disttest
Пакет `disttest` содержит интеграционные тесты.
Тесты запускают все компоненты внутри одного процесса. Это сделано для удобства отладки. В случае
паники в любом месте, весь тест упадёт целиком. Все логи пишутся в один файл, так что всегда сразу понятен
порядок событий. А к зависшему тесту можно подключиться в отладчике прямо из goland.
- `fixture.go` содержит код инициализации и остановки. Вам не нужно его менять. В `testdata/{{ .TestName }}`
хранится директория с исходным кодом, которую использует клиент в соответствующем тесте. `workdir/{{ .TestName }}`
сохраняет файлы после работы теста.
- `single_worker_test.go` содержит тесты с одним воркером. Каждый тест проверяет отдельную функциональность.
Отлаживайте тесты по одному, в порядке усложнения.
- `three_workers_test.go` содержит тесты с тремя воркерами. Приступайте к их отладке, после того как тесты с одним
воркером полностью пройдут.
Все тесты останавливают окружение отменяя корневой контекст. Если ваш код где-то неправильно обрабатывает
отмену контекста, то тест может зависать на остановке. Вы можете отладить такое зависание, подключившись
к зависшему тесту в дебагере, или послав SIGQUIT зависшему процессу.

View file

@ -11,6 +11,8 @@ import (
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"gitlab.com/slon/shad-go/distbuild/pkg/api"
"gitlab.com/slon/shad-go/distbuild/pkg/artifact"
"gitlab.com/slon/shad-go/distbuild/pkg/client"
@ -152,5 +154,7 @@ func newEnv(t *testing.T, config *Config) (e *env, cancel func()) {
cancelRootContext()
_ = env.HTTP.Shutdown(context.Background())
_ = env.Logger.Sync()
goleak.VerifyNone(t)
}
}

View file

@ -60,5 +60,5 @@ func TestArtifactTransferBetweenWorkers(t *testing.T) {
wg.Wait()
testDuration := time.Since(startTime)
assert.True(t, testDuration < time.Second*2, "test duration should be less than 2 seconds")
assert.True(t, testDuration < time.Second*5/2, "test duration should be less than 2.5 seconds")
}