Merge branch 'distbuild'

This commit is contained in:
Fedor Korotkiy 2020-04-09 14:23:21 +03:00
commit 7784d2bc9d
55 changed files with 2492 additions and 0 deletions

View file

@ -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
start: 02-04-2020 18:00
deadline: 12-04-2020 23:59

145
distbuild/README.md Normal file
View 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`, сделайте коммит добавляющий незначащий перенос строки в какой-нибудь файл
из этой директории.

View 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`. Запишите в лог события
получения/отправки запроса и все ошибки. Это поможет вам отлаживать интеграционные тесты
в следующей части задания.

View 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)
}

View 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")
}

View 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")
}

View 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)
}

View 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)
}

View 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")
}

View 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")
}

View 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")
}

View 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)
}

View 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)
}

View 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`. Запишите в этот логгер интересные события,
это поможет при отладке в следующих частях задачи.

View 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")
}

View 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)
}

View 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")
}

View 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)
}

View 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")
}

View file

@ -0,0 +1,4 @@
# build
Пакет `build` содержит описание графа сборки и набор хелпер-функций для работы с графом. Вам не нужно
писать новый код в этом пакете, но нужно научиться пользоваться тем кодом который вам дан.

View 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
}

View 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)
}

View 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
View 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
}

View 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
}

View 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)
}

View file

@ -0,0 +1,12 @@
# client
Пакет `client` реализует клиента системы распределённой сборки. Клиент запускается локально, и имеет доступ к
директории с исходным кодом.
Клиент получает на вход `build.Graph` и запускает сборку на координаторе.
После того, как координатор создал новую сборку, клиент заливает недостающие файлы и посылает сигнал о завершении стадии заливки.
После этого, клиент следит за прогрессом сборки, дожидается завершения и выходит.
Клиент тестируется интеграционными тестами из пакета `disttest`.

View 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
View file

@ -0,0 +1,5 @@
# dist
Пакет `dist` реализует координатора системы распределённой сборки.
Основная функциональность воркера тестируется интеграционными тестами из пакета `disttest`.

32
distbuild/pkg/dist/coordinator.go vendored Normal file
View 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")
}

View 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).

View 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")
}

View 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)
}

View 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")
}

View 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)
}

View 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")
}

View 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`, и ожидание этих
таймаутов происходит последовательно в одной горутине.

View file

@ -0,0 +1,3 @@
package scheduler
var TimeAfter = &timeAfter

View 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")
}

View 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)
}

View 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/) для рекурсивного обхода.

View 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")
}

View 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)
}

View file

@ -0,0 +1,6 @@
# worker
Пакет `worker` реализует воркера в системе распределённой сборки. Воркер ходит с heartbeat-ами
к координатору, получает с него джобы, выполняет их и посылает результаты назад на координатор.
Основная функциональность воркера тестируется интеграционными тестами из пакета `disttest`.

View 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
View file

@ -0,0 +1 @@
workdir

19
disttest/README.md Normal file
View 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
View 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
View 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
}

View 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'}])
}

View file

@ -0,0 +1 @@
foo

View file

@ -0,0 +1 @@
bar

View 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
View file

@ -10,9 +10,11 @@ require (
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.4
github.com/gorilla/websocket v1.4.2
github.com/jonboulle/clockwork v0.1.0
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.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/perf v0.0.0-20191209155426-36b577b0eb03
golang.org/x/sync v0.0.0-20190423024810-112230192c58

24
go.sum
View file

@ -1,4 +1,5 @@
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/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=
@ -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/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/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/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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/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/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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=
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/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-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/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/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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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/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-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-20200125223703-d33eef8e6825 h1:aNQeSIHKi0RWpKA5NO0CqyLjx6Beh5l0LLUEnndEjz0=
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 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/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
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/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=