Merge branch 'distbuild'
This commit is contained in:
commit
7784d2bc9d
55 changed files with 2492 additions and 0 deletions
|
@ -1,3 +1,17 @@
|
||||||
|
- group: "[HW] Distbuild"
|
||||||
|
start: 09-04-2020 18:00
|
||||||
|
deadline: 30-04-2020 23:59
|
||||||
|
tasks:
|
||||||
|
- task: disttest
|
||||||
|
score: 0
|
||||||
|
|
||||||
|
- group: Distbuild blocks
|
||||||
|
start: 09-04-2020 18:00
|
||||||
|
deadline: 23-04-2020 23:59
|
||||||
|
tasks:
|
||||||
|
- task: distbuild
|
||||||
|
score: 300
|
||||||
|
|
||||||
- group: HTTP and Context
|
- group: HTTP and Context
|
||||||
start: 02-04-2020 18:00
|
start: 02-04-2020 18:00
|
||||||
deadline: 12-04-2020 23:59
|
deadline: 12-04-2020 23:59
|
||||||
|
|
145
distbuild/README.md
Normal file
145
distbuild/README.md
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
# distbuild
|
||||||
|
|
||||||
|
В этом задании вам нужно будет реализовать систему распределённой сборки.
|
||||||
|
|
||||||
|
Система сборки получает на вход граф сборки и файлы с исходным кодом. Результатом сборки
|
||||||
|
являются исполняемые файлы и stderr/stdout запущенных процессов.
|
||||||
|
|
||||||
|
## Граф сборки
|
||||||
|
|
||||||
|
Граф сборки состоит из джобов. Каждый джоб описывает команды, которые нужно запустить на одной машине,
|
||||||
|
вместе со всеми входными файлами, которые нужны этим командам для работы.
|
||||||
|
|
||||||
|
Джобы в графе сборки запускают произвольные команды. Например, вызывать компилятор, линкер или
|
||||||
|
запускать тесты.
|
||||||
|
|
||||||
|
Команды внутри джоба могут читать файлы с файловой системы. Мы будем различать два вида файлов:
|
||||||
|
- Файлы с исходным кодом с машины пользователя.
|
||||||
|
- Файлы, которые породили другие джобы.
|
||||||
|
|
||||||
|
Команды внутри джоба могут писать результаты своей работы в файлы на диске. Выходные файлы
|
||||||
|
обязаны находиться внутри его выходной директории. Директория с результатом работы джоба называется
|
||||||
|
артефактом.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package build
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектура системы
|
||||||
|
|
||||||
|
Наша система будет состоять из трех компонент.
|
||||||
|
* Клиент - процесс запускающий сборку.
|
||||||
|
* Воркер - процесс запускающий команды компиляции и тестирования.
|
||||||
|
* Координатор - центральный процесс в системе, общается с клиентами и воркерами. Раздаёт задачи
|
||||||
|
воркерам.
|
||||||
|
|
||||||
|
Типичная сборка выглядит так:
|
||||||
|
1. Клиент подключается к координатору, посылает ему граф сборки и входные файлы для графа сборки.
|
||||||
|
2. Кооринатор сохраняет граф сборки в памяти и начинает его исполнение.
|
||||||
|
3. Воркеры начинают выполнять вершины графа, пересылая друг другу выходные директории джобов.
|
||||||
|
4. Результаты работы джобов скачиваются на клиента.
|
||||||
|
|
||||||
|
# Как решать эту задачу
|
||||||
|
|
||||||
|
Задача разбита на шаги. В начале, вам нужно будет реализовать небольшой набор независимых пакетов,
|
||||||
|
которые реализует нужные примитивы. Код в этих пакетах покрыт юниттестами. В каждом пакете находится
|
||||||
|
файл README.md, объясняющий подзадачу.
|
||||||
|
|
||||||
|
Рекомендуемый порядок выполнения:
|
||||||
|
|
||||||
|
- [`distbuild/pkg/build`](./pkg/build) - определение графа сборки. В этом пакете ничего писать не нужно,
|
||||||
|
нужно ознакомиться с существующим кодом.
|
||||||
|
- [`distbuild/pkg/tarstream`](./pkg/tarstream) - передача директории через сокет.
|
||||||
|
- [`distbuild/pkg/api`](./pkg/api) - протокол общения между компонентами.
|
||||||
|
- [`distbuild/pkg/artifact`](./pkg/artifact) - кеш артефактов и протокол передачи артефактов между воркерами.
|
||||||
|
- [`distbuild/pkg/filecache`](./pkg/filecache) - кеш файлов и протокол передачи файлов между компонентами.
|
||||||
|
- [`distbuild/pkg/scheduler`](./pkg/scheduler) - планировщик с эвристикой локальности.
|
||||||
|
|
||||||
|
После того, как все кубики будут готовы, нужно будет соединить их вместе, реализовав `distbuild/pkg/worker`,
|
||||||
|
`distbuild/pkg/client` и `distbuild/pkg/dist`. Код в этих пакетах нужно отлаживать на
|
||||||
|
интеграционных тестах в [`distbuild/disttest`](../disttest).
|
||||||
|
|
||||||
|
Код тестов в этом задании менять нельзя. Это значит, что вы не можете менять интерфейсы в тех местах, где
|
||||||
|
код покрыт тестами.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary markdown="span">Сколько кода нужно написать?</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
prime@bee ~/C/shad-go> find distbuild -iname '*.go' | grep -v test | grep -v mock | grep -v pkg/build | xargs wc -l
|
||||||
|
23 distbuild/pkg/worker/state.go
|
||||||
|
111 distbuild/pkg/worker/worker.go
|
||||||
|
45 distbuild/pkg/worker/download.go
|
||||||
|
281 distbuild/pkg/worker/job.go
|
||||||
|
69 distbuild/pkg/api/heartbeat.go
|
||||||
|
121 distbuild/pkg/api/build_client.go
|
||||||
|
53 distbuild/pkg/api/build.go
|
||||||
|
60 distbuild/pkg/api/heartbeat_handler.go
|
||||||
|
142 distbuild/pkg/api/build_handler.go
|
||||||
|
56 distbuild/pkg/api/heartbeat_client.go
|
||||||
|
288 distbuild/pkg/scheduler/scheduler.go
|
||||||
|
119 distbuild/pkg/dist/build.go
|
||||||
|
120 distbuild/pkg/dist/coordinator.go
|
||||||
|
98 distbuild/pkg/tarstream/stream.go
|
||||||
|
42 distbuild/pkg/artifact/client.go
|
||||||
|
191 distbuild/pkg/artifact/cache.go
|
||||||
|
54 distbuild/pkg/artifact/handler.go
|
||||||
|
124 distbuild/pkg/client/build.go
|
||||||
|
83 distbuild/pkg/filecache/client.go
|
||||||
|
99 distbuild/pkg/filecache/handler.go
|
||||||
|
111 distbuild/pkg/filecache/filecache.go
|
||||||
|
2290 total
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# Критерии оценки
|
||||||
|
|
||||||
|
Решение должно проходить все тесты, так же как в обычной задаче.
|
||||||
|
|
||||||
|
Задача разбита на две части:
|
||||||
|
- `distbuild` проверяет решение всех "кубиков". Эта задача расчитывается как обычная семинарская.
|
||||||
|
- `disttest` проверяет интеграционные тесты. Эта задача оценивается как домашка. После успешной попытки, в таблице gdoc
|
||||||
|
будет стоять 0. После этого, проверяющие должны будут просмотреть решение и заменить оценку в таблице на 1.
|
||||||
|
Это будет значить, что домашнее задание засчитано. Code Review не будет, проверка нужна только чтобы удостовериться что
|
||||||
|
посылка честно проходит все тесты. Отдельный Merge Request создавать не нужно.
|
||||||
|
|
||||||
|
Чтобы запустить проверку внутри `disttest`, сделайте коммит добавляющий незначащий перенос строки в какой-нибудь файл
|
||||||
|
из этой директории.
|
32
distbuild/pkg/api/README.md
Normal file
32
distbuild/pkg/api/README.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# api
|
||||||
|
|
||||||
|
Пакет api реализует протокол, по которому общаются компоненты системы.
|
||||||
|
|
||||||
|
Этот пакет не занимается передачей файлов и артефактов, соответствующие функции находятся в
|
||||||
|
пакетах `filecache` и `artifact`.
|
||||||
|
|
||||||
|
## Worker <-> Coordinator
|
||||||
|
|
||||||
|
- Worker и Coordinator общаются через один запрос `POST /heartbeat`.
|
||||||
|
- Worker посылает `HeartbeatRequest` и получает в ответ `HeartbeatResponse`.
|
||||||
|
- Запрос и ответ передаются в формате json.
|
||||||
|
- Ошибка обработки heartbeat передаётся как текстовая строка.
|
||||||
|
|
||||||
|
## Client <-> Coordinator
|
||||||
|
|
||||||
|
Client и Coordinator общаются через два вызова.
|
||||||
|
|
||||||
|
- `POST /build` - стартует новый билд.
|
||||||
|
* Client посылает в Body запроса json c описанием сборки.
|
||||||
|
* Coordinator стримит в body ответа json сообщения описывающие прогресс сборки.
|
||||||
|
* Первым сообщением в ответе Coordinator присылает `buildID`.
|
||||||
|
* _Тут можно было бы использовать websocket, но нас устраивает более простое решение._
|
||||||
|
|
||||||
|
- `POST /signal?build_id=12345` - посылает сигнал бегущему билду.
|
||||||
|
* Запрос и ответ передаются в формате json.
|
||||||
|
|
||||||
|
# Замечания
|
||||||
|
|
||||||
|
- Конструкторы клиентов и хендлеров принимают первым параметром `*zap.Logger`. Запишите в лог события
|
||||||
|
получения/отправки запроса и все ошибки. Это поможет вам отлаживать интеграционные тесты
|
||||||
|
в следующей части задания.
|
53
distbuild/pkg/api/build.go
Normal file
53
distbuild/pkg/api/build.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuildRequest struct {
|
||||||
|
Graph build.Graph
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildStarted struct {
|
||||||
|
ID build.ID
|
||||||
|
MissingFiles []build.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusUpdate struct {
|
||||||
|
JobFinished *JobResult
|
||||||
|
BuildFailed *BuildFailed
|
||||||
|
BuildFinished *BuildFinished
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildFailed struct {
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildFinished struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadDone struct{}
|
||||||
|
|
||||||
|
type SignalRequest struct {
|
||||||
|
UploadDone *UploadDone
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignalResponse struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusWriter interface {
|
||||||
|
Started(rsp *BuildStarted) error
|
||||||
|
Updated(update *StatusUpdate) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
StartBuild(ctx context.Context, request *BuildRequest, w StatusWriter) error
|
||||||
|
SignalBuild(ctx context.Context, buildID build.ID, signal *SignalRequest) (*SignalResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusReader interface {
|
||||||
|
Close() error
|
||||||
|
Next() (*StatusUpdate, error)
|
||||||
|
}
|
26
distbuild/pkg/api/build_client.go
Normal file
26
distbuild/pkg/api/build_client.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuildClient struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuildClient(l *zap.Logger, endpoint string) *BuildClient {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BuildClient) StartBuild(ctx context.Context, request *BuildRequest) (*BuildStarted, StatusReader, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BuildClient) SignalBuild(ctx context.Context, buildID build.ID, signal *SignalRequest) (*SignalResponse, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
20
distbuild/pkg/api/build_handler.go
Normal file
20
distbuild/pkg/api/build_handler.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewBuildService(l *zap.Logger, s Service) *BuildHandler {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BuildHandler) Register(mux *http.ServeMux) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
159
distbuild/pkg/api/build_test.go
Normal file
159
distbuild/pkg/api/build_test.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
mock "gitlab.com/slon/shad-go/distbuild/pkg/api/mock"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockgen -package mock -destination mock/mock.go . Service
|
||||||
|
|
||||||
|
type env struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
mock *mock.MockService
|
||||||
|
server *httptest.Server
|
||||||
|
client *api.BuildClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *env) stop() {
|
||||||
|
e.server.Close()
|
||||||
|
e.ctrl.Finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnv(t *testing.T) (*env, func()) {
|
||||||
|
env := &env{}
|
||||||
|
env.ctrl = gomock.NewController(t)
|
||||||
|
env.mock = mock.NewMockService(env.ctrl)
|
||||||
|
|
||||||
|
log := zaptest.NewLogger(t)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
handler := api.NewBuildService(log, env.mock)
|
||||||
|
handler.Register(mux)
|
||||||
|
|
||||||
|
env.server = httptest.NewServer(mux)
|
||||||
|
|
||||||
|
env.client = api.NewBuildClient(log, env.server.URL)
|
||||||
|
|
||||||
|
return env, env.stop
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSignal(t *testing.T) {
|
||||||
|
env, stop := newEnv(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
buildIDa := build.ID{01}
|
||||||
|
buildIDb := build.ID{02}
|
||||||
|
req := &api.SignalRequest{}
|
||||||
|
rsp := &api.SignalResponse{}
|
||||||
|
|
||||||
|
env.mock.EXPECT().SignalBuild(gomock.Any(), buildIDa, req).Return(rsp, nil)
|
||||||
|
env.mock.EXPECT().SignalBuild(gomock.Any(), buildIDb, req).Return(nil, fmt.Errorf("foo bar error"))
|
||||||
|
|
||||||
|
_, err := env.client.SignalBuild(ctx, buildIDa, req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = env.client.SignalBuild(ctx, buildIDb, req)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "foo bar error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildStartError(t *testing.T) {
|
||||||
|
env, stop := newEnv(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
env.mock.EXPECT().StartBuild(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("foo bar error"))
|
||||||
|
|
||||||
|
_, _, err := env.client.StartBuild(ctx, &api.BuildRequest{})
|
||||||
|
require.Contains(t, err.Error(), "foo bar error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRunning(t *testing.T) {
|
||||||
|
env, stop := newEnv(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
buildID := build.ID{02}
|
||||||
|
|
||||||
|
req := &api.BuildRequest{
|
||||||
|
Graph: build.Graph{SourceFiles: map[build.ID]string{{01}: "a.txt"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
started := &api.BuildStarted{ID: buildID}
|
||||||
|
finished := &api.StatusUpdate{BuildFinished: &api.BuildFinished{}}
|
||||||
|
|
||||||
|
env.mock.EXPECT().StartBuild(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
DoAndReturn(func(_ context.Context, req *api.BuildRequest, w api.StatusWriter) error {
|
||||||
|
if err := w.Started(started); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Updated(finished); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("foo bar error")
|
||||||
|
})
|
||||||
|
|
||||||
|
rsp, r, err := env.client.StartBuild(ctx, req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, started, rsp)
|
||||||
|
|
||||||
|
u, err := r.Next()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, finished, u)
|
||||||
|
|
||||||
|
u, err = r.Next()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, u.BuildFailed.Error, "foo bar error")
|
||||||
|
|
||||||
|
_, err = r.Next()
|
||||||
|
require.Equal(t, err, io.EOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildResultsStreaming(t *testing.T) {
|
||||||
|
// Test is hanging?
|
||||||
|
// See https://golang.org/pkg/net/http/#Flusher
|
||||||
|
|
||||||
|
env, stop := newEnv(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
buildID := build.ID{02}
|
||||||
|
req := &api.BuildRequest{}
|
||||||
|
started := &api.BuildStarted{ID: buildID}
|
||||||
|
|
||||||
|
env.mock.EXPECT().StartBuild(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
DoAndReturn(func(ctx context.Context, req *api.BuildRequest, w api.StatusWriter) error {
|
||||||
|
if err := w.Started(started); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
})
|
||||||
|
|
||||||
|
rsp, _, err := env.client.StartBuild(ctx, req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, started, rsp)
|
||||||
|
}
|
69
distbuild/pkg/api/heartbeat.go
Normal file
69
distbuild/pkg/api/heartbeat.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobResult описывает результат работы джоба.
|
||||||
|
type JobResult struct {
|
||||||
|
ID build.ID
|
||||||
|
|
||||||
|
Stdout, Stderr []byte
|
||||||
|
|
||||||
|
ExitCode int
|
||||||
|
|
||||||
|
// Error описывает сообщение об ошибке, из-за которого джоб не удалось выполнить.
|
||||||
|
//
|
||||||
|
// Если Error == nil, значит джоб завершился успешно.
|
||||||
|
Error *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerID string
|
||||||
|
|
||||||
|
func (w WorkerID) String() string {
|
||||||
|
return string(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeartbeatRequest struct {
|
||||||
|
// WorkerID задаёт персистентный идентификатор данного воркера.
|
||||||
|
//
|
||||||
|
// WorkerID так же выступает в качестве endpoint-а, к которому можно подключиться по HTTP.
|
||||||
|
//
|
||||||
|
// В наших тестов, идентификатор будет иметь вид "localhost:%d".
|
||||||
|
WorkerID WorkerID
|
||||||
|
|
||||||
|
// RunningJobs перечисляет список джобов, которые выполняются на этом воркере
|
||||||
|
// в данный момент.
|
||||||
|
RunningJobs []build.ID
|
||||||
|
|
||||||
|
// FreeSlots сообщает, сколько еще процессов можно запустить на этом воркере.
|
||||||
|
FreeSlots int
|
||||||
|
|
||||||
|
// JobResult сообщает координатору, какие джобы завершили исполнение на этом воркере
|
||||||
|
// на этой итерации цикла.
|
||||||
|
FinishedJob []JobResult
|
||||||
|
|
||||||
|
// AddedArtifacts говорит, какие артефакты появились в кеше на этой итерации цикла.
|
||||||
|
AddedArtifacts []build.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobSpec описывает джоб, который нужно запустить.
|
||||||
|
type JobSpec struct {
|
||||||
|
// SourceFiles задаёт список файлов, который должны присутствовать в директории с исходным кодом при запуске этого джоба.
|
||||||
|
SourceFiles map[build.ID]string
|
||||||
|
|
||||||
|
// Artifacts задаёт воркеров, с которых можно скачать артефакты необходимые этом джобу.
|
||||||
|
Artifacts map[build.ID]WorkerID
|
||||||
|
|
||||||
|
build.Job
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeartbeatResponse struct {
|
||||||
|
JobsToRun map[build.ID]JobSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeartbeatService interface {
|
||||||
|
Heartbeat(ctx context.Context, req *HeartbeatRequest) (*HeartbeatResponse, error)
|
||||||
|
}
|
20
distbuild/pkg/api/heartbeat_client.go
Normal file
20
distbuild/pkg/api/heartbeat_client.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HeartbeatClient struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHeartbeatClient(l *zap.Logger, endpoint string) *HeartbeatClient {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HeartbeatClient) Heartbeat(ctx context.Context, req *HeartbeatRequest) (*HeartbeatResponse, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
20
distbuild/pkg/api/heartbeat_handler.go
Normal file
20
distbuild/pkg/api/heartbeat_handler.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HeartbeatHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHeartbeatHandler(l *zap.Logger, s HeartbeatService) *HeartbeatHandler {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HeartbeatHandler) Register(mux *http.ServeMux) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
56
distbuild/pkg/api/heartbeat_test.go
Normal file
56
distbuild/pkg/api/heartbeat_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api/mock"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockgen -package mock -destination mock/heartbeat.go . HeartbeatService
|
||||||
|
|
||||||
|
func TestHeartbeat(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
l := zaptest.NewLogger(t)
|
||||||
|
m := mock.NewMockHeartbeatService(ctrl)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
api.NewHeartbeatHandler(l, m).Register(mux)
|
||||||
|
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := api.NewHeartbeatClient(l, server.URL)
|
||||||
|
|
||||||
|
req := &api.HeartbeatRequest{
|
||||||
|
WorkerID: "worker0",
|
||||||
|
}
|
||||||
|
rsp := &api.HeartbeatResponse{
|
||||||
|
JobsToRun: map[build.ID]api.JobSpec{
|
||||||
|
{0x01}: {Job: build.Job{Name: "cc a.c"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
gomock.InOrder(
|
||||||
|
m.EXPECT().Heartbeat(gomock.Any(), gomock.Eq(req)).Times(1).Return(rsp, nil),
|
||||||
|
m.EXPECT().Heartbeat(gomock.Any(), gomock.Eq(req)).Times(1).Return(nil, fmt.Errorf("build error: foo bar")),
|
||||||
|
)
|
||||||
|
|
||||||
|
clientRsp, err := client.Heartbeat(context.Background(), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, rsp, clientRsp)
|
||||||
|
|
||||||
|
_, err = client.Heartbeat(context.Background(), req)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "build error: foo bar")
|
||||||
|
}
|
50
distbuild/pkg/api/mock/heartbeat.go
Normal file
50
distbuild/pkg/api/mock/heartbeat.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: gitlab.com/slon/shad-go/distbuild/pkg/api (interfaces: HeartbeatService)
|
||||||
|
|
||||||
|
// Package mock is a generated GoMock package.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
api "gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockHeartbeatService is a mock of HeartbeatService interface
|
||||||
|
type MockHeartbeatService struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockHeartbeatServiceMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockHeartbeatServiceMockRecorder is the mock recorder for MockHeartbeatService
|
||||||
|
type MockHeartbeatServiceMockRecorder struct {
|
||||||
|
mock *MockHeartbeatService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockHeartbeatService creates a new mock instance
|
||||||
|
func NewMockHeartbeatService(ctrl *gomock.Controller) *MockHeartbeatService {
|
||||||
|
mock := &MockHeartbeatService{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockHeartbeatServiceMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockHeartbeatService) EXPECT() *MockHeartbeatServiceMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat mocks base method
|
||||||
|
func (m *MockHeartbeatService) Heartbeat(arg0 context.Context, arg1 *api.HeartbeatRequest) (*api.HeartbeatResponse, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Heartbeat", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(*api.HeartbeatResponse)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat indicates an expected call of Heartbeat
|
||||||
|
func (mr *MockHeartbeatServiceMockRecorder) Heartbeat(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Heartbeat", reflect.TypeOf((*MockHeartbeatService)(nil).Heartbeat), arg0, arg1)
|
||||||
|
}
|
65
distbuild/pkg/api/mock/mock.go
Normal file
65
distbuild/pkg/api/mock/mock.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: gitlab.com/slon/shad-go/distbuild/pkg/api (interfaces: Service)
|
||||||
|
|
||||||
|
// Package mock is a generated GoMock package.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
api "gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
build "gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockService is a mock of Service interface
|
||||||
|
type MockService struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockServiceMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockServiceMockRecorder is the mock recorder for MockService
|
||||||
|
type MockServiceMockRecorder struct {
|
||||||
|
mock *MockService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockService creates a new mock instance
|
||||||
|
func NewMockService(ctrl *gomock.Controller) *MockService {
|
||||||
|
mock := &MockService{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockServiceMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockService) EXPECT() *MockServiceMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalBuild mocks base method
|
||||||
|
func (m *MockService) SignalBuild(arg0 context.Context, arg1 build.ID, arg2 *api.SignalRequest) (*api.SignalResponse, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SignalBuild", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(*api.SignalResponse)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalBuild indicates an expected call of SignalBuild
|
||||||
|
func (mr *MockServiceMockRecorder) SignalBuild(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignalBuild", reflect.TypeOf((*MockService)(nil).SignalBuild), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBuild mocks base method
|
||||||
|
func (m *MockService) StartBuild(arg0 context.Context, arg1 *api.BuildRequest, arg2 api.StatusWriter) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StartBuild", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBuild indicates an expected call of StartBuild
|
||||||
|
func (mr *MockServiceMockRecorder) StartBuild(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartBuild", reflect.TypeOf((*MockService)(nil).StartBuild), arg0, arg1, arg2)
|
||||||
|
}
|
27
distbuild/pkg/artifact/README.md
Normal file
27
distbuild/pkg/artifact/README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# artifact
|
||||||
|
|
||||||
|
Пакет `artifact` реализует кеш хранения артефактов и протокол для передачи артефактов между воркерами.
|
||||||
|
|
||||||
|
Артефакт - это директория, содержащая в себе результат работы джоба. Артефакт может состоять из произвольного
|
||||||
|
набора файлов и директорий.
|
||||||
|
|
||||||
|
Основной тип `artifact.Cache` занимается хранением артефактов на диске и контролем одновременного доступа.
|
||||||
|
Все методы `artifact.Cache` должны быть *concurrency safe*.
|
||||||
|
|
||||||
|
Одна горутина может начать писать артефакт. Начало записи берёт лок на запись. Никто другой не может работать с артефактом,
|
||||||
|
на который взят лок на запись. Горутина должна позвать `commit` или `abort` после того, как она закончила работать с артефактом.
|
||||||
|
|
||||||
|
`commit` помечает артефакт в кеш. `abort` отменяет запись артефакта, удаляя все данные.
|
||||||
|
|
||||||
|
Горутина может начать читать артефакт, позвав метод `Get`. Много горутин могут читать артефакт одновременно.
|
||||||
|
Горутина должна позвать `unlock`, после того как она закончила работать с артефактом.
|
||||||
|
|
||||||
|
## Скачивание артефакта
|
||||||
|
|
||||||
|
`*artifact.Handler` должен реализовывать один метод `GET /artifact?id=1234`. Хендлер отвечает на
|
||||||
|
запрос содержимым артефакта в формате `tarstream`.
|
||||||
|
|
||||||
|
Функция `Download` должна скачивать артефакт из удалённого кеша в локальный.
|
||||||
|
|
||||||
|
Обратите внимание, что конструктор хендлера принимает `*zap.Logger`. Запишите в этот логгер интересные события,
|
||||||
|
это поможет при отладке в следующих частях задачи.
|
39
distbuild/pkg/artifact/cache.go
Normal file
39
distbuild/pkg/artifact/cache.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package artifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("artifact not found")
|
||||||
|
ErrExists = errors.New("artifact exists")
|
||||||
|
ErrWriteLocked = errors.New("artifact is locked for write")
|
||||||
|
ErrReadLocked = errors.New("artifact is locked for read")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache(root string) (*Cache, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Range(artifactFn func(artifact build.ID) error) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Remove(artifact build.ID) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Create(artifact build.ID) (path string, commit, abort func() error, err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Get(artifact build.ID) (path string, unlock func(), err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
100
distbuild/pkg/artifact/cache_test.go
Normal file
100
distbuild/pkg/artifact/cache_test.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package artifact_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/artifact"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCache struct {
|
||||||
|
*artifact.Cache
|
||||||
|
tmpDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testCache) cleanup() {
|
||||||
|
_ = os.RemoveAll(c.tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestCache(t *testing.T) *testCache {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cache, err := artifact.NewCache(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.RemoveAll(tmpDir)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &testCache{Cache: cache, tmpDir: tmpDir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
defer c.cleanup()
|
||||||
|
|
||||||
|
idA := build.ID{'a'}
|
||||||
|
|
||||||
|
path, commit, _, err := c.Create(idA)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, _, err = c.Create(idA)
|
||||||
|
require.Truef(t, errors.Is(err, artifact.ErrWriteLocked), "%v", err)
|
||||||
|
|
||||||
|
_, err = os.Create(filepath.Join(path, "a.txt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, commit())
|
||||||
|
|
||||||
|
path, unlock, err := c.Get(idA)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(path, "a.txt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Truef(t, errors.Is(c.Remove(idA), artifact.ErrReadLocked), "%v", err)
|
||||||
|
|
||||||
|
idB := build.ID{'b'}
|
||||||
|
_, _, err = c.Get(idB)
|
||||||
|
require.Truef(t, errors.Is(err, artifact.ErrNotFound), "%v", err)
|
||||||
|
|
||||||
|
require.NoError(t, c.Range(func(artifact build.ID) error {
|
||||||
|
require.Equal(t, idA, artifact)
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbortWrite(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
defer c.cleanup()
|
||||||
|
|
||||||
|
idA := build.ID{'a'}
|
||||||
|
|
||||||
|
_, _, abort, err := c.Create(idA)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, abort())
|
||||||
|
|
||||||
|
_, _, err = c.Get(idA)
|
||||||
|
require.Truef(t, errors.Is(err, artifact.ErrNotFound), "%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifactExists(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
defer c.cleanup()
|
||||||
|
|
||||||
|
idA := build.ID{'a'}
|
||||||
|
|
||||||
|
_, commit, _, err := c.Create(idA)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, commit())
|
||||||
|
|
||||||
|
_, _, _, err = c.Create(idA)
|
||||||
|
require.Truef(t, errors.Is(err, artifact.ErrExists), "%v", err)
|
||||||
|
}
|
14
distbuild/pkg/artifact/client.go
Normal file
14
distbuild/pkg/artifact/client.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package artifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Download artifact from remote cache into local cache.
|
||||||
|
func Download(ctx context.Context, endpoint string, c *Cache, artifactID build.ID) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
53
distbuild/pkg/artifact/client_test.go
Normal file
53
distbuild/pkg/artifact/client_test.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package artifact_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/artifact"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArtifactTransfer(t *testing.T) {
|
||||||
|
remoteCache := newTestCache(t)
|
||||||
|
defer remoteCache.cleanup()
|
||||||
|
localCache := newTestCache(t)
|
||||||
|
defer localCache.cleanup()
|
||||||
|
|
||||||
|
id := build.ID{0x01}
|
||||||
|
|
||||||
|
dir, commit, _, err := remoteCache.Create(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "a.txt"), []byte("foobar"), 0777))
|
||||||
|
require.NoError(t, commit())
|
||||||
|
|
||||||
|
l := zaptest.NewLogger(t)
|
||||||
|
|
||||||
|
h := artifact.NewHandler(l, remoteCache.Cache)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.Register(mux)
|
||||||
|
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
require.NoError(t, artifact.Download(ctx, server.URL, localCache.Cache, id))
|
||||||
|
|
||||||
|
dir, unlock, err := localCache.Get(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(filepath.Join(dir, "a.txt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []byte("foobar"), content)
|
||||||
|
|
||||||
|
err = artifact.Download(ctx, server.URL, localCache.Cache, build.ID{0x02})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
20
distbuild/pkg/artifact/handler.go
Normal file
20
distbuild/pkg/artifact/handler.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package artifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(l *zap.Logger, c *Cache) *Handler {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Register(mux *http.ServeMux) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
4
distbuild/pkg/build/README.md
Normal file
4
distbuild/pkg/build/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# build
|
||||||
|
|
||||||
|
Пакет `build` содержит описание графа сборки и набор хелпер-функций для работы с графом. Вам не нужно
|
||||||
|
писать новый код в этом пакете, но нужно научиться пользоваться тем кодом который вам дан.
|
69
distbuild/pkg/build/cmd.go
Normal file
69
distbuild/pkg/build/cmd.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobContext struct {
|
||||||
|
SourceDir string
|
||||||
|
OutputDir string
|
||||||
|
Deps map[ID]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render replaces variable references with their real value.
|
||||||
|
func (c *Cmd) Render(ctx JobContext) (*Cmd, error) {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
var fixedCtx struct {
|
||||||
|
SourceDir string
|
||||||
|
OutputDir string
|
||||||
|
Deps map[string]string
|
||||||
|
}
|
||||||
|
fixedCtx.SourceDir = ctx.SourceDir
|
||||||
|
fixedCtx.OutputDir = ctx.OutputDir
|
||||||
|
fixedCtx.Deps = map[string]string{}
|
||||||
|
|
||||||
|
for k, v := range ctx.Deps {
|
||||||
|
fixedCtx.Deps[k.String()] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
render := func(str string) string {
|
||||||
|
t, err := template.New("").Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
if err := t.Execute(&b, fixedCtx); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList := func(l []string) []string {
|
||||||
|
var result []string
|
||||||
|
for _, in := range l {
|
||||||
|
result = append(result, render(in))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var rendered Cmd
|
||||||
|
|
||||||
|
rendered.CatOutput = render(c.CatOutput)
|
||||||
|
rendered.CatTemplate = render(c.CatTemplate)
|
||||||
|
rendered.WorkingDirectory = render(c.WorkingDirectory)
|
||||||
|
rendered.Exec = renderList(c.Exec)
|
||||||
|
rendered.Environ = renderList(c.Environ)
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return nil, fmt.Errorf("error rendering cmd: %w", errs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rendered, nil
|
||||||
|
}
|
31
distbuild/pkg/build/cmd_test.go
Normal file
31
distbuild/pkg/build/cmd_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmdRender(t *testing.T) {
|
||||||
|
tmpl := Cmd{
|
||||||
|
CatOutput: "{{.OutputDir}}/import.map",
|
||||||
|
CatTemplate: `bytes={{index .Deps "6100000000000000000000000000000000000000"}}/lib.a`,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := JobContext{
|
||||||
|
OutputDir: "/distbuild/jobs/b",
|
||||||
|
Deps: map[ID]string{
|
||||||
|
{'a'}: "/distbuild/jobs/a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tmpl.Render(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &Cmd{
|
||||||
|
CatOutput: "/distbuild/jobs/b/import.map",
|
||||||
|
CatTemplate: "bytes=/distbuild/jobs/a/lib.a",
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, expected, result)
|
||||||
|
}
|
70
distbuild/pkg/build/graph.go
Normal file
70
distbuild/pkg/build/graph.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
// 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 - записывает строку в файл
|
||||||
|
//
|
||||||
|
// Все строки в описании команды могут содержать в себе на переменные. Перед выполнением
|
||||||
|
// реальной команды, переменные заменяются на их реальные значения.
|
||||||
|
//
|
||||||
|
// {{.OutputDir}} - абсолютный путь до выходной директории джоба.
|
||||||
|
// {{.SourceDir}} - абсолютный путь до директории с исходными файлами.
|
||||||
|
// {{index .Deps "f374b81d81f641c8c3d5d5468081ef83b2c7dae9"}} - абсолютный путь до директории,
|
||||||
|
// содержащей выход джоба с id f374b81d81f641c8c3d5d5468081ef83b2c7dae9.
|
||||||
|
type Cmd struct {
|
||||||
|
// Exec описывает команду, которую нужно выполнить.
|
||||||
|
Exec []string
|
||||||
|
|
||||||
|
// Environ описывает переменные окружения, которые необходимы для работы команды из Exec.
|
||||||
|
Environ []string
|
||||||
|
|
||||||
|
// WorkingDirectory задаёт рабочую директорию для команды из Exec.
|
||||||
|
WorkingDirectory string
|
||||||
|
|
||||||
|
// CatTemplate задаёт шаблон строки, которую нужно записать в файл.
|
||||||
|
CatTemplate string
|
||||||
|
|
||||||
|
// CatOutput задаёт выходной файл для команды типа cat.
|
||||||
|
CatOutput string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Graph struct {
|
||||||
|
SourceFiles map[ID]string
|
||||||
|
|
||||||
|
Jobs []Job
|
||||||
|
}
|
52
distbuild/pkg/build/id.go
Normal file
52
distbuild/pkg/build/id.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ID [sha1.Size]byte
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = encoding.TextMarshaler(ID{})
|
||||||
|
_ = encoding.TextUnmarshaler(&ID{})
|
||||||
|
)
|
||||||
|
|
||||||
|
func (id ID) String() string {
|
||||||
|
return hex.EncodeToString(id[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) Path() string {
|
||||||
|
return filepath.Join(hex.EncodeToString(id[:1]), hex.EncodeToString(id[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(hex.EncodeToString(id[:])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *ID) UnmarshalText(b []byte) error {
|
||||||
|
raw, err := hex.DecodeString(string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(raw) != len(id) {
|
||||||
|
return fmt.Errorf("invalid id size: %q", b)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(id[:], raw)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewID() ID {
|
||||||
|
var id ID
|
||||||
|
_, err := rand.Read(id[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("crypto/rand is unavailable: %v", err))
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
31
distbuild/pkg/build/top_sort.go
Normal file
31
distbuild/pkg/build/top_sort.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
// TopSort sorts jobs in topological order assuming dependency graph contains no cycles.
|
||||||
|
func TopSort(jobs []Job) []Job {
|
||||||
|
var sorted []Job
|
||||||
|
visited := make([]bool, len(jobs))
|
||||||
|
|
||||||
|
jobIDIndex := map[ID]int{}
|
||||||
|
for i, j := range jobs {
|
||||||
|
jobIDIndex[j.ID] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
var visit func(jobIndex int)
|
||||||
|
visit = func(jobIndex int) {
|
||||||
|
if visited[jobIndex] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
visited[jobIndex] = true
|
||||||
|
for _, dep := range jobs[jobIndex].Deps {
|
||||||
|
visit(jobIDIndex[dep])
|
||||||
|
}
|
||||||
|
sorted = append(sorted, jobs[jobIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range jobs {
|
||||||
|
visit(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
}
|
29
distbuild/pkg/build/top_sort_test.go
Normal file
29
distbuild/pkg/build/top_sort_test.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTopSort(t *testing.T) {
|
||||||
|
jobs := []Job{
|
||||||
|
{
|
||||||
|
ID: ID{'a'},
|
||||||
|
Deps: []ID{{'b'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: ID{'b'},
|
||||||
|
Deps: []ID{{'c'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: ID{'c'},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted := TopSort(jobs)
|
||||||
|
require.Equal(t, 3, len(sorted))
|
||||||
|
require.Equal(t, ID{'c'}, sorted[0].ID)
|
||||||
|
require.Equal(t, ID{'b'}, sorted[1].ID)
|
||||||
|
require.Equal(t, ID{'a'}, sorted[2].ID)
|
||||||
|
}
|
12
distbuild/pkg/client/README.md
Normal file
12
distbuild/pkg/client/README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# client
|
||||||
|
|
||||||
|
Пакет `client` реализует клиента системы распределённой сборки. Клиент запускается локально, и имеет доступ к
|
||||||
|
директории с исходным кодом.
|
||||||
|
|
||||||
|
Клиент получает на вход `build.Graph` и запускает сборку на координаторе.
|
||||||
|
|
||||||
|
После того, как координатор создал новую сборку, клиент заливает недостающие файлы и посылает сигнал о завершении стадии заливки.
|
||||||
|
|
||||||
|
После этого, клиент следит за прогрессом сборки, дожидается завершения и выходит.
|
||||||
|
|
||||||
|
Клиент тестируется интеграционными тестами из пакета `disttest`.
|
34
distbuild/pkg/client/build.go
Normal file
34
distbuild/pkg/client/build.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(
|
||||||
|
l *zap.Logger,
|
||||||
|
apiEndpoint string,
|
||||||
|
sourceDir string,
|
||||||
|
) *Client {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildListener interface {
|
||||||
|
OnJobStdout(jobID build.ID, stdout []byte) error
|
||||||
|
OnJobStderr(jobID build.ID, stderr []byte) error
|
||||||
|
|
||||||
|
OnJobFinished(jobID build.ID) error
|
||||||
|
OnJobFailed(jobID build.ID, code int, error string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Build(ctx context.Context, graph build.Graph, lsn BuildListener) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
5
distbuild/pkg/dist/README.md
vendored
Normal file
5
distbuild/pkg/dist/README.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# dist
|
||||||
|
|
||||||
|
Пакет `dist` реализует координатора системы распределённой сборки.
|
||||||
|
|
||||||
|
Основная функциональность воркера тестируется интеграционными тестами из пакета `disttest`.
|
32
distbuild/pkg/dist/coordinator.go
vendored
Normal file
32
distbuild/pkg/dist/coordinator.go
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package dist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/scheduler"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Coordinator struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultConfig = scheduler.Config{
|
||||||
|
CacheTimeout: time.Millisecond * 10,
|
||||||
|
DepsTimeout: time.Millisecond * 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCoordinator(
|
||||||
|
log *zap.Logger,
|
||||||
|
fileCache *filecache.Cache,
|
||||||
|
) *Coordinator {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Coordinator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
18
distbuild/pkg/filecache/README.md
Normal file
18
distbuild/pkg/filecache/README.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# filecache
|
||||||
|
|
||||||
|
Пакет `filecache` занимается хранением кеша файлов и определяет протокол передачи файлов между частями системы.
|
||||||
|
|
||||||
|
`filecache.Cache` управляет файлами и занимается контролем одновременного доступа. Вы можете реализовать этот
|
||||||
|
тип поверх `*artifact.Cache`, поведение требуется точно такое же.
|
||||||
|
|
||||||
|
## Передача файлов
|
||||||
|
|
||||||
|
Тип `filecache.Handler` реализует handler, позволяющий заливать и скачивать файлы из кеша.
|
||||||
|
|
||||||
|
- Вызов `GET /file?id=123` должен возвращать содержимое файла с `id=123`.
|
||||||
|
- Вызов `PUT /file?id=123` должен заливать содержимое файла с `id=123`.
|
||||||
|
|
||||||
|
**Обратите внимание:** Несколько клиентов могут начать заливать в кеш один и тот же набор файлов. В наивной реализации,
|
||||||
|
первый клиент залочит файл на запись, а следующие упадут с ошибкой. Ваш код должен обрабатывать эту ситуацию корректно,
|
||||||
|
то есть последующие запросы должны дожидаться, пока первый запрос завершится. Для реализации этой логики
|
||||||
|
поведения вам поможет пакет [singleflight](https://godoc.org/golang.org/x/sync/singleflight).
|
26
distbuild/pkg/filecache/client.go
Normal file
26
distbuild/pkg/filecache/client.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package filecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(l *zap.Logger, endpoint string) *Client {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Upload(ctx context.Context, id build.ID, localPath string) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Download(ctx context.Context, localCache *Cache, id build.ID) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
143
distbuild/pkg/filecache/client_test.go
Normal file
143
distbuild/pkg/filecache/client_test.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package filecache_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type env struct {
|
||||||
|
cache *testCache
|
||||||
|
server *httptest.Server
|
||||||
|
client *filecache.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnv(t *testing.T) *env {
|
||||||
|
l := zaptest.NewLogger(t)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
cache := newCache(t)
|
||||||
|
defer func() {
|
||||||
|
if cache != nil {
|
||||||
|
cache.cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler := filecache.NewHandler(l, cache.Cache)
|
||||||
|
handler.Register(mux)
|
||||||
|
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
|
||||||
|
client := filecache.NewClient(l, server.URL)
|
||||||
|
|
||||||
|
env := &env{
|
||||||
|
cache: cache,
|
||||||
|
server: server,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = nil
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *env) stop() {
|
||||||
|
e.server.Close()
|
||||||
|
e.cache.cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileUpload(t *testing.T) {
|
||||||
|
env := newEnv(t)
|
||||||
|
defer env.stop()
|
||||||
|
|
||||||
|
content := bytes.Repeat([]byte("foobar"), 1024*1024)
|
||||||
|
|
||||||
|
tmpFilePath := filepath.Join(env.cache.tmpDir, "foo.txt")
|
||||||
|
require.NoError(t, ioutil.WriteFile(tmpFilePath, content, 0666))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("UploadSingleFile", func(t *testing.T) {
|
||||||
|
id := build.ID{0x01}
|
||||||
|
|
||||||
|
require.NoError(t, env.client.Upload(ctx, id, tmpFilePath))
|
||||||
|
|
||||||
|
path, unlock, err := env.cache.Get(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, content, content)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RepeatedUpload", func(t *testing.T) {
|
||||||
|
id := build.ID{0x02}
|
||||||
|
|
||||||
|
require.NoError(t, env.client.Upload(ctx, id, tmpFilePath))
|
||||||
|
require.NoError(t, env.client.Upload(ctx, id, tmpFilePath))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ConcurrentUpload", func(t *testing.T) {
|
||||||
|
const (
|
||||||
|
N = 10
|
||||||
|
G = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(G)
|
||||||
|
|
||||||
|
id := build.ID{0x03, byte(i)}
|
||||||
|
for j := 0; j < G; j++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
assert.NoError(t, env.client.Upload(ctx, id, tmpFilePath))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileDownload(t *testing.T) {
|
||||||
|
env := newEnv(t)
|
||||||
|
defer env.stop()
|
||||||
|
|
||||||
|
localCache := newCache(t)
|
||||||
|
defer localCache.cleanup()
|
||||||
|
|
||||||
|
id := build.ID{0x01}
|
||||||
|
|
||||||
|
w, abort, err := env.cache.Write(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = abort() }()
|
||||||
|
|
||||||
|
_, err = w.Write([]byte("foobar"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, w.Close())
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
require.NoError(t, env.client.Download(ctx, localCache.Cache, id))
|
||||||
|
|
||||||
|
path, unlock, err := localCache.Get(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []byte("foobar"), content)
|
||||||
|
}
|
40
distbuild/pkg/filecache/filecache.go
Normal file
40
distbuild/pkg/filecache/filecache.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package filecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("file not found")
|
||||||
|
ErrExists = errors.New("file exists")
|
||||||
|
ErrWriteLocked = errors.New("file is locked for write")
|
||||||
|
ErrReadLocked = errors.New("file is locked for read")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(rootDir string) (*Cache, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Range(fileFn func(file build.ID) error) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Remove(file build.ID) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Write(file build.ID) (w io.WriteCloser, abort func() error, err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Get(file build.ID) (path string, unlock func(), err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
58
distbuild/pkg/filecache/filecache_test.go
Normal file
58
distbuild/pkg/filecache/filecache_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package filecache_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCache struct {
|
||||||
|
*filecache.Cache
|
||||||
|
tmpDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCache(t *testing.T) *testCache {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "filecache")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c, err := filecache.New(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &testCache{Cache: c, tmpDir: tmpDir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testCache) cleanup() {
|
||||||
|
_ = os.Remove(c.tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileCache(t *testing.T) {
|
||||||
|
cache := newCache(t)
|
||||||
|
|
||||||
|
_, abort, err := cache.Write(build.ID{01})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, abort())
|
||||||
|
|
||||||
|
_, _, err = cache.Get(build.ID{01})
|
||||||
|
require.Truef(t, errors.Is(err, filecache.ErrNotFound), "%v", err)
|
||||||
|
|
||||||
|
f, _, err := cache.Write(build.ID{02})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = f.Write([]byte("foo bar"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, f.Close())
|
||||||
|
|
||||||
|
path, unlock, err := cache.Get(build.ID{02})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []byte("foo bar"), content)
|
||||||
|
}
|
20
distbuild/pkg/filecache/handler.go
Normal file
20
distbuild/pkg/filecache/handler.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package filecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(l *zap.Logger, cache *Cache) *Handler {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Register(mux *http.ServeMux) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
41
distbuild/pkg/scheduler/README.md
Normal file
41
distbuild/pkg/scheduler/README.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# scheduler
|
||||||
|
|
||||||
|
Пакет `scheduler` реализует планировщик системы. `scheduler.Scheduler` хранит полное состояние кластера
|
||||||
|
и принимает решение на каком воркере и какой джоб нужно запустить.
|
||||||
|
|
||||||
|
Шедулер является точкой координации между воркерами и билдами. Бегущие билды обращаются к шедулеру,
|
||||||
|
передавая джобы в функцию `ScheduleJob`. Воркеры забирают джобы из шедулера вызывая функцию `PickJob`.
|
||||||
|
|
||||||
|
Вы можете отложить реализацию полной версии шедулера на последний шаг, и реализовать упрощённую версию
|
||||||
|
на одном глобальном канале. Такой реализации будет достаточно, чтобы работали все интеграционные тесты с одним
|
||||||
|
воркером.
|
||||||
|
|
||||||
|
## Алгоритм планирования
|
||||||
|
|
||||||
|
Планировщик поддерживает множество очередей:
|
||||||
|
1. Одна глобальная очередь
|
||||||
|
2. По две локальные очереди на воркер.
|
||||||
|
|
||||||
|
При запросе нового джоба воркер выбирает случайную джобу из трех очередей - глобальной, и двух локальных относящихся
|
||||||
|
к этому воркеру. Случайная очередь выбирается одним вызовом `select {}`.
|
||||||
|
|
||||||
|
Ожидающий исполнения джоб всегда находится в первой локальной очереди воркеров, на которых есть
|
||||||
|
результаты работы этого джоба.
|
||||||
|
|
||||||
|
Если джоб ждёт выполнения дольше `CacheTimeout` или если в момент `SchedulerJob` джоба не было в кеше ни на одном
|
||||||
|
из воркеров, то он попадает во все вторые локальные очереди воркеров, на которых есть хотя бы один артефакт
|
||||||
|
из множества зависимостей этого джоба.
|
||||||
|
|
||||||
|
Определения первой и второй локальной очереди не зависят от того, в каком порядке в шедулер пришли джобы
|
||||||
|
и информация о кеше артефактов. То есть, если джоб уже находится в глобальной очереди, и в этот момент приходит
|
||||||
|
информация, что этот джоб находится в кеше на воркере `W0`, то джоб должен быть добавлен
|
||||||
|
в первую локальную очередь `W0`.
|
||||||
|
|
||||||
|
Если джоб ждёт выполнения дольше `DepTimeout`, то он помещается в глобальную очередь.
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Вместо реального времени, юниттесты шедулера используют библиотеку `clockwork`. Это накладывает ограничения
|
||||||
|
на детали вашей реализации. Ожидание `CacheTimeout` и `DepTimeout` должно быть реализовано как `select {}` на
|
||||||
|
канале, который вернула функция `timeAfter`. Мы считаем что `CacheTimeout > DepTimeout`, и ожидание этих
|
||||||
|
таймаутов происходит последовательно в одной горутине.
|
3
distbuild/pkg/scheduler/export_test.go
Normal file
3
distbuild/pkg/scheduler/export_test.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package scheduler
|
||||||
|
|
||||||
|
var TimeAfter = &timeAfter
|
53
distbuild/pkg/scheduler/scheduler.go
Normal file
53
distbuild/pkg/scheduler/scheduler.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
var timeAfter = time.After
|
||||||
|
|
||||||
|
type PendingJob struct {
|
||||||
|
Job *api.JobSpec
|
||||||
|
Finished chan struct{}
|
||||||
|
Result *api.JobResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
CacheTimeout time.Duration
|
||||||
|
DepsTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scheduler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScheduler(l *zap.Logger, config Config) *Scheduler {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Scheduler) LocateArtifact(id build.ID) (api.WorkerID, bool) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Scheduler) RegisterWorker(workerID api.WorkerID) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Scheduler) OnJobComplete(workerID api.WorkerID, jobID build.ID, res *api.JobResult) bool {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Scheduler) ScheduleJob(job *api.JobSpec) *PendingJob {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Scheduler) PickJob(ctx context.Context, workerID api.WorkerID) *PendingJob {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
137
distbuild/pkg/scheduler/scheduler_test.go
Normal file
137
distbuild/pkg/scheduler/scheduler_test.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package scheduler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/goleak"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/scheduler"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
workerID0 api.WorkerID = "w0"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
config = scheduler.Config{
|
||||||
|
CacheTimeout: time.Second,
|
||||||
|
DepsTimeout: time.Minute,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type testScheduler struct {
|
||||||
|
*scheduler.Scheduler
|
||||||
|
clockwork.FakeClock
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestScheduler(t *testing.T) *testScheduler {
|
||||||
|
log := zaptest.NewLogger(t)
|
||||||
|
|
||||||
|
s := &testScheduler{
|
||||||
|
FakeClock: clockwork.NewFakeClock(),
|
||||||
|
Scheduler: scheduler.NewScheduler(log, config),
|
||||||
|
}
|
||||||
|
|
||||||
|
*scheduler.TimeAfter = s.FakeClock.After
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testScheduler) stop(t *testing.T) {
|
||||||
|
*scheduler.TimeAfter = time.After
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_SingleJob(t *testing.T) {
|
||||||
|
s := newTestScheduler(t)
|
||||||
|
defer s.stop(t)
|
||||||
|
|
||||||
|
job0 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
|
pendingJob0 := s.ScheduleJob(job0)
|
||||||
|
|
||||||
|
s.BlockUntil(1)
|
||||||
|
s.Advance(config.DepsTimeout) // At this point job must be in global queue.
|
||||||
|
|
||||||
|
s.RegisterWorker(workerID0)
|
||||||
|
pickerJob := s.PickJob(context.Background(), workerID0)
|
||||||
|
|
||||||
|
require.Equal(t, pendingJob0, pickerJob)
|
||||||
|
|
||||||
|
result := &api.JobResult{ID: job0.ID, ExitCode: 0}
|
||||||
|
s.OnJobComplete(workerID0, job0.ID, result)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-pendingJob0.Finished:
|
||||||
|
require.Equal(t, pendingJob0.Result, result)
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Fatalf("job0 is not finished")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_PickJobCancelation(t *testing.T) {
|
||||||
|
s := newTestScheduler(t)
|
||||||
|
defer s.stop(t)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
s.RegisterWorker(workerID0)
|
||||||
|
require.Nil(t, s.PickJob(ctx, workerID0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_CacheLocalScheduling(t *testing.T) {
|
||||||
|
s := newTestScheduler(t)
|
||||||
|
defer s.stop(t)
|
||||||
|
|
||||||
|
cachedJob := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
|
uncachedJob := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
|
|
||||||
|
s.RegisterWorker(workerID0)
|
||||||
|
s.OnJobComplete(workerID0, cachedJob.ID, &api.JobResult{})
|
||||||
|
|
||||||
|
pendingUncachedJob := s.ScheduleJob(uncachedJob)
|
||||||
|
pendingCachedJob := s.ScheduleJob(cachedJob)
|
||||||
|
|
||||||
|
s.BlockUntil(2) // both jobs should be blocked
|
||||||
|
|
||||||
|
firstPickedJob := s.PickJob(context.Background(), workerID0)
|
||||||
|
assert.Equal(t, pendingCachedJob, firstPickedJob)
|
||||||
|
|
||||||
|
s.Advance(config.DepsTimeout) // At this point uncachedJob is put into global queue.
|
||||||
|
|
||||||
|
secondPickedJob := s.PickJob(context.Background(), workerID0)
|
||||||
|
assert.Equal(t, pendingUncachedJob, secondPickedJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_DependencyLocalScheduling(t *testing.T) {
|
||||||
|
s := newTestScheduler(t)
|
||||||
|
defer s.stop(t)
|
||||||
|
|
||||||
|
job0 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
|
s.RegisterWorker(workerID0)
|
||||||
|
s.OnJobComplete(workerID0, job0.ID, &api.JobResult{})
|
||||||
|
|
||||||
|
job1 := &api.JobSpec{Job: build.Job{ID: build.NewID(), Deps: []build.ID{job0.ID}}}
|
||||||
|
job2 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
|
|
||||||
|
pendingJob2 := s.ScheduleJob(job2)
|
||||||
|
pendingJob1 := s.ScheduleJob(job1)
|
||||||
|
|
||||||
|
s.BlockUntil(2) // both jobs should be blocked on DepsTimeout
|
||||||
|
|
||||||
|
firstPickedJob := s.PickJob(context.Background(), workerID0)
|
||||||
|
require.Equal(t, pendingJob1, firstPickedJob)
|
||||||
|
|
||||||
|
s.Advance(config.DepsTimeout) // At this point job2 is put into global queue.
|
||||||
|
|
||||||
|
secondPickedJob := s.PickJob(context.Background(), workerID0)
|
||||||
|
require.Equal(t, pendingJob2, secondPickedJob)
|
||||||
|
}
|
21
distbuild/pkg/tarstream/README.md
Normal file
21
distbuild/pkg/tarstream/README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# tarstream
|
||||||
|
|
||||||
|
Вам нужно уметь передавать директорию с артефактами между воркерами. Для этого, вам нужно
|
||||||
|
реализовать две операции:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package tarstream
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Send рекурсивно обходит директорию и сериализует её содержимое в поток w.
|
||||||
|
func Send(dir string, w io.Writer) error
|
||||||
|
|
||||||
|
// Receive читает поток r и материализует содержимое потока внутри dir.
|
||||||
|
func Receive(dir string, r io.Reader) error
|
||||||
|
```
|
||||||
|
|
||||||
|
- Функции должны корректно обрабатывать директории и обычные файлы.
|
||||||
|
- executable бит на файлах должен сохраняться.
|
||||||
|
- Используйте формат [tar](https://golang.org/pkg/archive/tar/)
|
||||||
|
- Используйте [filepath.Walk](https://golang.org/pkg/path/filepath/) для рекурсивного обхода.
|
15
distbuild/pkg/tarstream/stream.go
Normal file
15
distbuild/pkg/tarstream/stream.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package tarstream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Send(dir string, w io.Writer) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Receive(dir string, r io.Reader) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
60
distbuild/pkg/tarstream/stream_test.go
Normal file
60
distbuild/pkg/tarstream/stream_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package tarstream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTarStream(t *testing.T) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "tarstream")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("running inside %s", tmpDir)
|
||||||
|
|
||||||
|
from := filepath.Join(tmpDir, "from")
|
||||||
|
to := filepath.Join(tmpDir, "to")
|
||||||
|
|
||||||
|
require.NoError(t, os.Mkdir(from, 0777))
|
||||||
|
require.NoError(t, os.Mkdir(to, 0777))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
require.NoError(t, os.Mkdir(filepath.Join(from, "a"), 0777))
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Join(from, "b", "c", "d"), 0777))
|
||||||
|
require.NoError(t, ioutil.WriteFile(filepath.Join(from, "a", "x.bin"), []byte("xxx"), 0777))
|
||||||
|
require.NoError(t, ioutil.WriteFile(filepath.Join(from, "b", "c", "y.txt"), []byte("yyy"), 0666))
|
||||||
|
|
||||||
|
require.NoError(t, Send(from, &buf))
|
||||||
|
|
||||||
|
require.NoError(t, Receive(to, &buf))
|
||||||
|
|
||||||
|
checkDir := func(path string) {
|
||||||
|
st, err := os.Stat(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, st.IsDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDir(filepath.Join(to, "a"))
|
||||||
|
checkDir(filepath.Join(to, "b", "c", "d"))
|
||||||
|
|
||||||
|
checkFile := func(path string, content []byte, mode os.FileMode) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
st, err := os.Stat(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, mode.String(), st.Mode().String())
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, content, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFile(filepath.Join(from, "a", "x.bin"), []byte("xxx"), 0755)
|
||||||
|
checkFile(filepath.Join(from, "b", "c", "y.txt"), []byte("yyy"), 0644)
|
||||||
|
}
|
6
distbuild/pkg/worker/README.md
Normal file
6
distbuild/pkg/worker/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# worker
|
||||||
|
|
||||||
|
Пакет `worker` реализует воркера в системе распределённой сборки. Воркер ходит с heartbeat-ами
|
||||||
|
к координатору, получает с него джобы, выполняет их и посылает результаты назад на координатор.
|
||||||
|
|
||||||
|
Основная функциональность воркера тестируется интеграционными тестами из пакета `disttest`.
|
35
distbuild/pkg/worker/worker.go
Normal file
35
distbuild/pkg/worker/worker.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/artifact"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Worker struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
workerID api.WorkerID,
|
||||||
|
coordinatorEndpoint string,
|
||||||
|
log *zap.Logger,
|
||||||
|
fileCache *filecache.Cache,
|
||||||
|
artifacts *artifact.Cache,
|
||||||
|
) *Worker {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) Run(ctx context.Context) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
1
disttest/.gitignore
vendored
Normal file
1
disttest/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
workdir
|
19
disttest/README.md
Normal file
19
disttest/README.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# disttest
|
||||||
|
|
||||||
|
Пакет `disttest` содержит интеграционные тесты.
|
||||||
|
|
||||||
|
Тесты запускают все компоненты внутри одного процесса. Это сделано для удобства отладки. В случае
|
||||||
|
паники в любом месте, весь тест упадёт целиком. Все логи пишутся в один файл, так что всегда сразу понятен
|
||||||
|
порядок событий. А к зависшему тесту можно подключиться в отладчике прямо из goland.
|
||||||
|
|
||||||
|
- `fixture.go` содержит код инициализации и остановки. Вам не нужно его менять. В `testdata/{{ .TestName }}`
|
||||||
|
хранится директория с исходным кодом, которую использует клиент в соответствующем тесте. `workdir/{{ .TestName }}`
|
||||||
|
сохраняет файлы после работы теста.
|
||||||
|
- `single_worker_test.go` содержит тесты с одним воркером. Каждый тест проверяет отдельную функциональность.
|
||||||
|
Отлаживайте тесты по одному, в порядке усложнения.
|
||||||
|
- `three_workers_test.go` содержит тесты с тремя воркерами. Приступайте к их отладке, после того как тесты с одним
|
||||||
|
воркером полностью пройдут.
|
||||||
|
|
||||||
|
Все тесты останавливают окружение отменяя корневой контекст. Если ваш код где-то неправильно обрабатывает
|
||||||
|
отмену контекста, то тест может зависать на остановке. Вы можете отладить такое зависание, подключившись
|
||||||
|
к зависшему тесту в дебагере, или послав SIGQUIT зависшему процессу.
|
160
disttest/fixture.go
Normal file
160
disttest/fixture.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package disttest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"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"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/dist"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/worker"
|
||||||
|
"gitlab.com/slon/shad-go/tools/testtool"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type env struct {
|
||||||
|
RootDir string
|
||||||
|
Logger *zap.Logger
|
||||||
|
|
||||||
|
Ctx context.Context
|
||||||
|
|
||||||
|
Client *client.Client
|
||||||
|
Coordinator *dist.Coordinator
|
||||||
|
Workers []*worker.Worker
|
||||||
|
|
||||||
|
HTTP *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
logToStderr = true
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WorkerCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnv(t *testing.T, config *Config) (e *env, cancel func()) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
absCWD, err := filepath.Abs(cwd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
env := &env{
|
||||||
|
RootDir: filepath.Join(absCWD, "workdir", t.Name()),
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, os.RemoveAll(env.RootDir))
|
||||||
|
require.NoError(t, os.MkdirAll(env.RootDir, 0777))
|
||||||
|
|
||||||
|
cfg := zap.NewDevelopmentConfig()
|
||||||
|
cfg.OutputPaths = []string{filepath.Join(env.RootDir, "test.log")}
|
||||||
|
|
||||||
|
if logToStderr {
|
||||||
|
cfg.OutputPaths = append(cfg.OutputPaths, "stderr")
|
||||||
|
}
|
||||||
|
|
||||||
|
env.Logger, err = cfg.Build()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("test is running inside %s; see test.log file for more info", filepath.Join("workdir", t.Name()))
|
||||||
|
|
||||||
|
port, err := testtool.GetFreePort()
|
||||||
|
require.NoError(t, err)
|
||||||
|
addr := "127.0.0.1:" + port
|
||||||
|
coordinatorEndpoint := "http://" + addr + "/coordinator"
|
||||||
|
|
||||||
|
var cancelRootContext func()
|
||||||
|
env.Ctx, cancelRootContext = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
env.Client = client.NewClient(
|
||||||
|
env.Logger.Named("client"),
|
||||||
|
coordinatorEndpoint,
|
||||||
|
filepath.Join(absCWD, "testdata", t.Name()))
|
||||||
|
|
||||||
|
coordinatorCache, err := filecache.New(filepath.Join(env.RootDir, "coordinator", "filecache"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
env.Coordinator = dist.NewCoordinator(
|
||||||
|
env.Logger.Named("coordinator"),
|
||||||
|
coordinatorCache,
|
||||||
|
)
|
||||||
|
|
||||||
|
router := http.NewServeMux()
|
||||||
|
router.Handle("/coordinator/", http.StripPrefix("/coordinator", env.Coordinator))
|
||||||
|
|
||||||
|
for i := 0; i < config.WorkerCount; i++ {
|
||||||
|
workerName := fmt.Sprintf("worker%d", i)
|
||||||
|
workerDir := filepath.Join(env.RootDir, workerName)
|
||||||
|
|
||||||
|
var fileCache *filecache.Cache
|
||||||
|
fileCache, err = filecache.New(filepath.Join(workerDir, "filecache"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var artifacts *artifact.Cache
|
||||||
|
artifacts, err = artifact.NewCache(filepath.Join(workerDir, "artifacts"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
workerPrefix := fmt.Sprintf("/worker/%d", i)
|
||||||
|
workerID := api.WorkerID("http://" + addr + workerPrefix)
|
||||||
|
|
||||||
|
w := worker.New(
|
||||||
|
workerID,
|
||||||
|
coordinatorEndpoint,
|
||||||
|
env.Logger.Named(workerName),
|
||||||
|
fileCache,
|
||||||
|
artifacts,
|
||||||
|
)
|
||||||
|
|
||||||
|
env.Workers = append(env.Workers, w)
|
||||||
|
|
||||||
|
router.Handle(workerPrefix+"/", http.StripPrefix(workerPrefix, w))
|
||||||
|
}
|
||||||
|
|
||||||
|
env.HTTP = &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
lsn, err := net.Listen("tcp", env.HTTP.Addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := env.HTTP.Serve(lsn)
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
env.Logger.Fatal("http server stopped", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, w := range env.Workers {
|
||||||
|
go func(w *worker.Worker) {
|
||||||
|
err := w.Run(env.Ctx)
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
env.Logger.Fatal("worker stopped", zap.Error(err))
|
||||||
|
}(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, func() {
|
||||||
|
cancelRootContext()
|
||||||
|
_ = env.HTTP.Shutdown(context.Background())
|
||||||
|
_ = env.Logger.Sync()
|
||||||
|
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
}
|
||||||
|
}
|
57
disttest/recorder.go
Normal file
57
disttest/recorder.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package disttest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobResult struct {
|
||||||
|
Stdout string
|
||||||
|
Stderr string
|
||||||
|
|
||||||
|
Code *int
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recorder struct {
|
||||||
|
Jobs map[build.ID]*JobResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRecorder() *Recorder {
|
||||||
|
return &Recorder{
|
||||||
|
Jobs: map[build.ID]*JobResult{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) job(jobID build.ID) *JobResult {
|
||||||
|
j, ok := r.Jobs[jobID]
|
||||||
|
if !ok {
|
||||||
|
j = &JobResult{}
|
||||||
|
r.Jobs[jobID] = j
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) OnJobStdout(jobID build.ID, stdout []byte) error {
|
||||||
|
j := r.job(jobID)
|
||||||
|
j.Stdout += string(stdout)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) OnJobStderr(jobID build.ID, stderr []byte) error {
|
||||||
|
j := r.job(jobID)
|
||||||
|
j.Stderr += string(stderr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) OnJobFinished(jobID build.ID) error {
|
||||||
|
j := r.job(jobID)
|
||||||
|
j.Code = new(int)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) OnJobFailed(jobID build.ID, code int, error string) error {
|
||||||
|
j := r.job(jobID)
|
||||||
|
j.Code = &code
|
||||||
|
j.Error = error
|
||||||
|
return nil
|
||||||
|
}
|
136
disttest/single_worker_test.go
Normal file
136
disttest/single_worker_test.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package disttest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
var singleWorkerConfig = &Config{WorkerCount: 1}
|
||||||
|
|
||||||
|
var echoGraph = build.Graph{
|
||||||
|
Jobs: []build.Job{
|
||||||
|
{
|
||||||
|
ID: build.ID{'a'},
|
||||||
|
Name: "echo",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{Exec: []string{"echo", "OK"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingleCommand(t *testing.T) {
|
||||||
|
env, cancel := newEnv(t, singleWorkerConfig)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
recorder := NewRecorder()
|
||||||
|
require.NoError(t, env.Client.Build(env.Ctx, echoGraph, recorder))
|
||||||
|
|
||||||
|
assert.Len(t, recorder.Jobs, 1)
|
||||||
|
assert.Equal(t, &JobResult{Stdout: "OK\n", Code: new(int)}, recorder.Jobs[build.ID{'a'}])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobCaching(t *testing.T) {
|
||||||
|
env, cancel := newEnv(t, singleWorkerConfig)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tmpFile, err := ioutil.TempFile("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
graph := build.Graph{
|
||||||
|
Jobs: []build.Job{
|
||||||
|
{
|
||||||
|
ID: build.ID{'a'},
|
||||||
|
Name: "echo",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{CatTemplate: "OK\n", CatOutput: tmpFile.Name()}, // No-hermetic, for testing purposes.
|
||||||
|
{Exec: []string{"echo", "OK"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewRecorder()
|
||||||
|
require.NoError(t, env.Client.Build(env.Ctx, graph, recorder))
|
||||||
|
|
||||||
|
assert.Len(t, recorder.Jobs, 1)
|
||||||
|
assert.Equal(t, &JobResult{Stdout: "OK\n", Code: new(int)}, recorder.Jobs[build.ID{'a'}])
|
||||||
|
|
||||||
|
// Second build must get results from cache.
|
||||||
|
require.NoError(t, env.Client.Build(env.Ctx, graph, NewRecorder()))
|
||||||
|
|
||||||
|
require.NoError(t, ioutil.WriteFile(tmpFile.Name(), []byte("NOTOK\n"), 0666))
|
||||||
|
|
||||||
|
output, err := ioutil.ReadAll(tmpFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []byte("NOTOK\n"), output)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceFilesGraph = build.Graph{
|
||||||
|
SourceFiles: map[build.ID]string{
|
||||||
|
{'a'}: "a.txt",
|
||||||
|
{'c'}: "b/c.txt",
|
||||||
|
},
|
||||||
|
Jobs: []build.Job{
|
||||||
|
{
|
||||||
|
ID: build.ID{'a'},
|
||||||
|
Name: "echo",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{Exec: []string{"cat", "{{.SourceDir}}/a.txt"}},
|
||||||
|
{Exec: []string{"bash", "-c", "cat {{.SourceDir}}/b/c.txt > /dev/stderr"}},
|
||||||
|
},
|
||||||
|
Inputs: []string{
|
||||||
|
"a.txt",
|
||||||
|
"b/c.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceFiles(t *testing.T) {
|
||||||
|
env, cancel := newEnv(t, singleWorkerConfig)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
recorder := NewRecorder()
|
||||||
|
require.NoError(t, env.Client.Build(env.Ctx, sourceFilesGraph, recorder))
|
||||||
|
|
||||||
|
assert.Len(t, recorder.Jobs, 1)
|
||||||
|
assert.Equal(t, &JobResult{Stdout: "foo", Stderr: "bar", Code: new(int)}, recorder.Jobs[build.ID{'a'}])
|
||||||
|
}
|
||||||
|
|
||||||
|
var artifactTransferGraph = build.Graph{
|
||||||
|
Jobs: []build.Job{
|
||||||
|
{
|
||||||
|
ID: build.ID{'a'},
|
||||||
|
Name: "write",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{CatTemplate: "OK", CatOutput: "{{.OutputDir}}/out.txt"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: build.ID{'b'},
|
||||||
|
Name: "cat",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{Exec: []string{"cat", fmt.Sprintf("{{index .Deps %q}}/out.txt", build.ID{'a'})}},
|
||||||
|
},
|
||||||
|
Deps: []build.ID{{'a'}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifactTransferBetweenJobs(t *testing.T) {
|
||||||
|
env, cancel := newEnv(t, singleWorkerConfig)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
recorder := NewRecorder()
|
||||||
|
require.NoError(t, env.Client.Build(env.Ctx, artifactTransferGraph, recorder))
|
||||||
|
|
||||||
|
assert.Len(t, recorder.Jobs, 2)
|
||||||
|
assert.Equal(t, &JobResult{Stdout: "OK", Code: new(int)}, recorder.Jobs[build.ID{'b'}])
|
||||||
|
}
|
1
disttest/testdata/TestSourceFiles/a.txt
vendored
Normal file
1
disttest/testdata/TestSourceFiles/a.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
foo
|
1
disttest/testdata/TestSourceFiles/b/c.txt
vendored
Normal file
1
disttest/testdata/TestSourceFiles/b/c.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
bar
|
64
disttest/three_workers_test.go
Normal file
64
disttest/three_workers_test.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package disttest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
var threeWorkerConfig = &Config{WorkerCount: 3}
|
||||||
|
|
||||||
|
func TestArtifactTransferBetweenWorkers(t *testing.T) {
|
||||||
|
env, cancel := newEnv(t, threeWorkerConfig)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
baseJob := build.Job{
|
||||||
|
ID: build.ID{'a'},
|
||||||
|
Name: "write",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{CatTemplate: "OK", CatOutput: "{{.OutputDir}}/out.txt"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
depJobID := build.ID{'b', byte(i)}
|
||||||
|
depJob := build.Job{
|
||||||
|
ID: depJobID,
|
||||||
|
Name: "cat",
|
||||||
|
Cmds: []build.Cmd{
|
||||||
|
{Exec: []string{"cat", fmt.Sprintf("{{index .Deps %q}}/out.txt", build.ID{'a'})}},
|
||||||
|
{Exec: []string{"sleep", "1"}, Environ: os.Environ()}, // DepTimeout is 100ms.
|
||||||
|
},
|
||||||
|
Deps: []build.ID{{'a'}},
|
||||||
|
}
|
||||||
|
|
||||||
|
graph := build.Graph{Jobs: []build.Job{baseJob, depJob}}
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
recorder := NewRecorder()
|
||||||
|
if !assert.NoError(t, env.Client.Build(env.Ctx, graph, recorder)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, recorder.Jobs, 2)
|
||||||
|
assert.Equal(t, &JobResult{Stdout: "OK", Code: new(int)}, recorder.Jobs[depJobID])
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
testDuration := time.Since(startTime)
|
||||||
|
assert.True(t, testDuration < time.Second*5/2, "test duration should be less than 2.5 seconds")
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -10,9 +10,11 @@ require (
|
||||||
github.com/gorilla/handlers v1.4.2
|
github.com/gorilla/handlers v1.4.2
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
github.com/jonboulle/clockwork v0.1.0
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.4.0
|
||||||
go.uber.org/goleak v1.0.0
|
go.uber.org/goleak v1.0.0
|
||||||
|
go.uber.org/zap v1.14.0
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
|
||||||
golang.org/x/perf v0.0.0-20191209155426-36b577b0eb03
|
golang.org/x/perf v0.0.0-20191209155426-36b577b0eb03
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
|
|
24
go.sum
24
go.sum
|
@ -1,4 +1,5 @@
|
||||||
cloud.google.com/go v0.0.0-20170206221025-ce650573d812/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.0.0-20170206221025-ce650573d812/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
|
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
|
||||||
github.com/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0/go.mod h1:55qNq4vcpkIuHowELi5C8e+1yUHtoLoOUR9QU5j7Tes=
|
github.com/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0/go.mod h1:55qNq4vcpkIuHowELi5C8e+1yUHtoLoOUR9QU5j7Tes=
|
||||||
|
@ -27,6 +28,7 @@ github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2
|
||||||
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
|
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
|
||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||||
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
|
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
|
||||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
|
@ -37,6 +39,9 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
@ -47,8 +52,10 @@ github.com/mattn/go-sqlite3 v0.0.0-20161215041557-2d44decb4941/go.mod h1:FPy6Kqz
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
@ -61,17 +68,28 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||||
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
|
go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
|
||||||
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||||
|
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||||
|
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o=
|
||||||
|
go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
@ -93,6 +111,9 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825 h1:aNQeSIHKi0RWpKA5NO0CqyLjx6Beh5l0LLUEnndEjz0=
|
golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825 h1:aNQeSIHKi0RWpKA5NO0CqyLjx6Beh5l0LLUEnndEjz0=
|
||||||
golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
@ -108,9 +129,12 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|
Loading…
Reference in a new issue