Finish pkg/scheduler
This commit is contained in:
parent
1d5c64d8ca
commit
e56f1df9ba
8 changed files with 296 additions and 211 deletions
|
@ -38,10 +38,6 @@ type HeartbeatRequest struct {
|
||||||
// в данный момент.
|
// в данный момент.
|
||||||
RunningJobs []build.ID
|
RunningJobs []build.ID
|
||||||
|
|
||||||
DownloadingSources []build.ID
|
|
||||||
|
|
||||||
DownloadingArtifacts []build.ID
|
|
||||||
|
|
||||||
// FreeSlots сообщаяет, сколько еще процессов можно запустить на этом воркере.
|
// FreeSlots сообщаяет, сколько еще процессов можно запустить на этом воркере.
|
||||||
FreeSlots int
|
FreeSlots int
|
||||||
|
|
||||||
|
@ -51,9 +47,6 @@ type HeartbeatRequest struct {
|
||||||
|
|
||||||
// AddedArtifacts говорит, какие артефакты появились в кеше на этой итерации цикла.
|
// AddedArtifacts говорит, какие артефакты появились в кеше на этой итерации цикла.
|
||||||
AddedArtifacts []build.ID
|
AddedArtifacts []build.ID
|
||||||
|
|
||||||
// AddedSourceFiles говорит, какие файлы появились в кеше на этой итерации цикла.
|
|
||||||
AddedSourceFiles []build.ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobSpec описывает джоб, который нужно запустить.
|
// JobSpec описывает джоб, который нужно запустить.
|
||||||
|
|
2
distbuild/pkg/dist/coordinator.go
vendored
2
distbuild/pkg/dist/coordinator.go
vendored
|
@ -111,7 +111,7 @@ func (c *Coordinator) Heartbeat(ctx context.Context, req *api.HeartbeatRequest)
|
||||||
JobsToRun: map[build.ID]api.JobSpec{},
|
JobsToRun: map[build.ID]api.JobSpec{},
|
||||||
}
|
}
|
||||||
|
|
||||||
job := c.scheduler.PickJob(req.WorkerID, ctx.Done())
|
job := c.scheduler.PickJob(ctx, req.WorkerID)
|
||||||
if job != nil {
|
if job != nil {
|
||||||
rsp.JobsToRun[job.Job.ID] = *job.Job
|
rsp.JobsToRun[job.Job.ID] = *job.Job
|
||||||
}
|
}
|
||||||
|
|
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
|
|
@ -1,6 +1,7 @@
|
||||||
package scheduler
|
package scheduler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -12,8 +13,8 @@ import (
|
||||||
|
|
||||||
type PendingJob struct {
|
type PendingJob struct {
|
||||||
Job *api.JobSpec
|
Job *api.JobSpec
|
||||||
Result *api.JobResult
|
|
||||||
Finished chan struct{}
|
Finished chan struct{}
|
||||||
|
Result *api.JobResult
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
pickedUp chan struct{}
|
pickedUp chan struct{}
|
||||||
|
@ -37,29 +38,16 @@ func (p *PendingJob) pickUp() bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type jobQueue struct {
|
func (p *PendingJob) enqueue(q chan *PendingJob) {
|
||||||
mu sync.Mutex
|
select {
|
||||||
jobs []*PendingJob
|
case q <- p:
|
||||||
}
|
case <-p.pickedUp:
|
||||||
|
|
||||||
func (q *jobQueue) put(job *PendingJob) {
|
|
||||||
q.mu.Lock()
|
|
||||||
defer q.mu.Unlock()
|
|
||||||
|
|
||||||
q.jobs = append(q.jobs, job)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *jobQueue) pop() *PendingJob {
|
|
||||||
q.mu.Lock()
|
|
||||||
defer q.mu.Unlock()
|
|
||||||
|
|
||||||
if len(q.jobs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
job := q.jobs[0]
|
type workerQueue struct {
|
||||||
q.jobs = q.jobs[1:]
|
cacheQueue chan *PendingJob
|
||||||
return job
|
depQueue chan *PendingJob
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -73,12 +61,13 @@ type Scheduler struct {
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
cachedJobs map[build.ID]map[api.WorkerID]struct{}
|
cachedJobs map[build.ID]map[api.WorkerID]struct{}
|
||||||
pendingJobs map[build.ID]*PendingJob
|
|
||||||
|
|
||||||
cacheLocalQueue map[api.WorkerID]*jobQueue
|
pendingJobs map[build.ID]*PendingJob
|
||||||
depLocalQueue map[api.WorkerID]*jobQueue
|
pendingJobDeps map[build.ID]map[*PendingJob]struct{}
|
||||||
globalQueue chan *PendingJob
|
|
||||||
|
workerQueue map[api.WorkerID]*workerQueue
|
||||||
|
globalQueue chan *PendingJob
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScheduler(l *zap.Logger, config Config) *Scheduler {
|
func NewScheduler(l *zap.Logger, config Config) *Scheduler {
|
||||||
|
@ -86,12 +75,12 @@ func NewScheduler(l *zap.Logger, config Config) *Scheduler {
|
||||||
l: l,
|
l: l,
|
||||||
config: config,
|
config: config,
|
||||||
|
|
||||||
cachedJobs: make(map[build.ID]map[api.WorkerID]struct{}),
|
cachedJobs: make(map[build.ID]map[api.WorkerID]struct{}),
|
||||||
pendingJobs: make(map[build.ID]*PendingJob),
|
pendingJobs: make(map[build.ID]*PendingJob),
|
||||||
|
pendingJobDeps: make(map[build.ID]map[*PendingJob]struct{}),
|
||||||
|
|
||||||
cacheLocalQueue: make(map[api.WorkerID]*jobQueue),
|
workerQueue: make(map[api.WorkerID]*workerQueue),
|
||||||
depLocalQueue: make(map[api.WorkerID]*jobQueue),
|
globalQueue: make(chan *PendingJob),
|
||||||
globalQueue: make(chan *PendingJob),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,13 +88,15 @@ func (c *Scheduler) RegisterWorker(workerID api.WorkerID) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
_, ok := c.cacheLocalQueue[workerID]
|
_, ok := c.workerQueue[workerID]
|
||||||
if ok {
|
if ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cacheLocalQueue[workerID] = new(jobQueue)
|
c.workerQueue[workerID] = &workerQueue{
|
||||||
c.depLocalQueue[workerID] = new(jobQueue)
|
cacheQueue: make(chan *PendingJob),
|
||||||
|
depQueue: make(chan *PendingJob),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Scheduler) OnJobComplete(workerID api.WorkerID, jobID build.ID, res *api.JobResult) bool {
|
func (c *Scheduler) OnJobComplete(workerID api.WorkerID, jobID build.ID, res *api.JobResult) bool {
|
||||||
|
@ -124,6 +115,11 @@ func (c *Scheduler) OnJobComplete(workerID api.WorkerID, jobID build.ID, res *ap
|
||||||
}
|
}
|
||||||
job[workerID] = struct{}{}
|
job[workerID] = struct{}{}
|
||||||
|
|
||||||
|
workerQueue := c.workerQueue[workerID]
|
||||||
|
for waiter := range c.pendingJobDeps[jobID] {
|
||||||
|
go waiter.enqueue(workerQueue.depQueue)
|
||||||
|
}
|
||||||
|
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if !pendingFound {
|
if !pendingFound {
|
||||||
|
@ -135,41 +131,38 @@ func (c *Scheduler) OnJobComplete(workerID api.WorkerID, jobID build.ID, res *ap
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Scheduler) findOptimalWorkers(jobID build.ID, deps []build.ID) (cacheLocal, depLocal []api.WorkerID) {
|
func (c *Scheduler) enqueueCacheLocal(job *PendingJob) bool {
|
||||||
depLocalSet := map[api.WorkerID]struct{}{}
|
cached := false
|
||||||
|
|
||||||
c.mu.Lock()
|
for workerID := range c.cachedJobs[job.Job.ID] {
|
||||||
defer c.mu.Unlock()
|
cached = true
|
||||||
|
go job.enqueue(c.workerQueue[workerID].cacheQueue)
|
||||||
for workerID := range c.cachedJobs[jobID] {
|
|
||||||
cacheLocal = append(cacheLocal, workerID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dep := range deps {
|
return cached
|
||||||
for workerID := range c.cachedJobs[dep] {
|
|
||||||
if _, ok := depLocalSet[workerID]; !ok {
|
|
||||||
depLocal = append(depLocal, workerID)
|
|
||||||
depLocalSet[workerID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeAfter = time.After
|
var timeAfter = time.After
|
||||||
|
|
||||||
func (c *Scheduler) doScheduleJob(job *PendingJob) {
|
func (c *Scheduler) putDepQueue(job *PendingJob, dep build.ID) {
|
||||||
cacheLocal, depLocal := c.findOptimalWorkers(job.Job.ID, job.Job.Deps)
|
depJobs, ok := c.pendingJobDeps[dep]
|
||||||
|
if !ok {
|
||||||
|
depJobs = make(map[*PendingJob]struct{})
|
||||||
|
c.pendingJobDeps[dep] = depJobs
|
||||||
|
}
|
||||||
|
depJobs[job] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
if len(cacheLocal) != 0 {
|
func (c *Scheduler) deleteDepQueue(job *PendingJob, dep build.ID) {
|
||||||
c.mu.Lock()
|
depJobs := c.pendingJobDeps[dep]
|
||||||
for _, workerID := range cacheLocal {
|
delete(depJobs, job)
|
||||||
c.cacheLocalQueue[workerID].put(job)
|
if len(depJobs) == 0 {
|
||||||
}
|
delete(c.pendingJobDeps, dep)
|
||||||
c.mu.Unlock()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.l.Debug("job is put into cache-local queues", zap.String("job_id", job.Job.ID.String()))
|
func (c *Scheduler) doScheduleJob(job *PendingJob, cached bool) {
|
||||||
|
if cached {
|
||||||
select {
|
select {
|
||||||
case <-job.pickedUp:
|
case <-job.pickedUp:
|
||||||
c.l.Debug("job picked", zap.String("job_id", job.Job.ID.String()))
|
c.l.Debug("job picked", zap.String("job_id", job.Job.ID.String()))
|
||||||
|
@ -178,31 +171,51 @@ func (c *Scheduler) doScheduleJob(job *PendingJob) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(depLocal) != 0 {
|
c.mu.Lock()
|
||||||
|
workers := make(map[api.WorkerID]struct{})
|
||||||
|
|
||||||
|
for _, dep := range job.Job.Deps {
|
||||||
|
c.putDepQueue(job, dep)
|
||||||
|
|
||||||
|
for workerID := range c.cachedJobs[dep] {
|
||||||
|
if _, ok := workers[workerID]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go job.enqueue(c.workerQueue[workerID].depQueue)
|
||||||
|
workers[workerID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
for _, workerID := range depLocal {
|
defer c.mu.Unlock()
|
||||||
c.depLocalQueue[workerID].put(job)
|
|
||||||
}
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
c.l.Debug("job is put into dep-local queues", zap.String("job_id", job.Job.ID.String()))
|
for _, dep := range job.Job.Deps {
|
||||||
select {
|
c.deleteDepQueue(job, dep)
|
||||||
case <-job.pickedUp:
|
|
||||||
c.l.Debug("job picked", zap.String("job_id", job.Job.ID.String()))
|
|
||||||
return
|
|
||||||
case <-timeAfter(c.config.DepsTimeout):
|
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
|
c.l.Debug("job is put into dep-local queues", zap.String("job_id", job.Job.ID.String()))
|
||||||
|
|
||||||
c.l.Debug("job is put into global queue", zap.String("job_id", job.Job.ID.String()))
|
|
||||||
select {
|
select {
|
||||||
case c.globalQueue <- job:
|
|
||||||
case <-job.pickedUp:
|
case <-job.pickedUp:
|
||||||
|
c.l.Debug("job picked", zap.String("job_id", job.Job.ID.String()))
|
||||||
|
return
|
||||||
|
case <-timeAfter(c.config.DepsTimeout):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go job.enqueue(c.globalQueue)
|
||||||
|
c.l.Debug("job is put into global queue", zap.String("job_id", job.Job.ID.String()))
|
||||||
|
|
||||||
|
<-job.pickedUp
|
||||||
c.l.Debug("job picked", zap.String("job_id", job.Job.ID.String()))
|
c.l.Debug("job picked", zap.String("job_id", job.Job.ID.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Scheduler) ScheduleJob(job *api.JobSpec) *PendingJob {
|
func (c *Scheduler) ScheduleJob(job *api.JobSpec) *PendingJob {
|
||||||
|
var cached bool
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
pendingJob, running := c.pendingJobs[job.ID]
|
pendingJob, running := c.pendingJobs[job.ID]
|
||||||
if !running {
|
if !running {
|
||||||
|
@ -214,12 +227,13 @@ func (c *Scheduler) ScheduleJob(job *api.JobSpec) *PendingJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.pendingJobs[job.ID] = pendingJob
|
c.pendingJobs[job.ID] = pendingJob
|
||||||
|
cached = c.enqueueCacheLocal(pendingJob)
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if !running {
|
if !running {
|
||||||
c.l.Debug("job is scheduled", zap.String("job_id", job.ID.String()))
|
c.l.Debug("job is scheduled", zap.String("job_id", job.ID.String()))
|
||||||
go c.doScheduleJob(pendingJob)
|
go c.doScheduleJob(pendingJob, cached)
|
||||||
} else {
|
} else {
|
||||||
c.l.Debug("job is pending", zap.String("job_id", job.ID.String()))
|
c.l.Debug("job is pending", zap.String("job_id", job.ID.String()))
|
||||||
}
|
}
|
||||||
|
@ -227,50 +241,37 @@ func (c *Scheduler) ScheduleJob(job *api.JobSpec) *PendingJob {
|
||||||
return pendingJob
|
return pendingJob
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Scheduler) PickJob(workerID api.WorkerID, canceled <-chan struct{}) *PendingJob {
|
func (c *Scheduler) PickJob(ctx context.Context, workerID api.WorkerID) *PendingJob {
|
||||||
c.l.Debug("picking next job", zap.String("worker_id", workerID.String()))
|
c.l.Debug("picking next job", zap.String("worker_id", workerID.String()))
|
||||||
|
|
||||||
var cacheLocal, depLocal *jobQueue
|
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
cacheLocal = c.cacheLocalQueue[workerID]
|
local := c.workerQueue[workerID]
|
||||||
depLocal = c.depLocalQueue[workerID]
|
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
for {
|
var pg *PendingJob
|
||||||
job := cacheLocal.pop()
|
var queue string
|
||||||
if job == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if job.pickUp() {
|
|
||||||
c.l.Debug("picked job from cache-local queue", zap.String("worker_id", workerID.String()), zap.String("job_id", job.Job.ID.String()))
|
|
||||||
return job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
job := depLocal.pop()
|
|
||||||
if job == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if job.pickUp() {
|
|
||||||
c.l.Debug("picked job from dep-local queue", zap.String("worker_id", workerID.String()), zap.String("job_id", job.Job.ID.String()))
|
|
||||||
return job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case job := <-c.globalQueue:
|
case pg = <-c.globalQueue:
|
||||||
if job.pickUp() {
|
queue = "global"
|
||||||
c.l.Debug("picked job from global queue", zap.String("worker_id", workerID.String()), zap.String("job_id", job.Job.ID.String()))
|
case pg = <-local.depQueue:
|
||||||
return job
|
queue = "dep"
|
||||||
}
|
case pg = <-local.cacheQueue:
|
||||||
|
queue = "cache"
|
||||||
case <-canceled:
|
case <-ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pg.pickUp() {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.l.Debug("picked job",
|
||||||
|
zap.String("worker_id", workerID.String()),
|
||||||
|
zap.String("job_id", pg.Job.ID.String()),
|
||||||
|
zap.String("queue", queue))
|
||||||
|
|
||||||
|
return pg
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,113 +1,137 @@
|
||||||
package scheduler
|
package scheduler_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jonboulle/clockwork"
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/goleak"
|
"go.uber.org/goleak"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
workerID0 api.WorkerID = "w0"
|
workerID0 api.WorkerID = "w0"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestScheduler(t *testing.T) {
|
var (
|
||||||
defer goleak.VerifyNone(t)
|
config = scheduler.Config{
|
||||||
|
|
||||||
clock := clockwork.NewFakeClock()
|
|
||||||
timeAfter = clock.After
|
|
||||||
defer func() { timeAfter = time.After }()
|
|
||||||
|
|
||||||
config := Config{
|
|
||||||
CacheTimeout: time.Second,
|
CacheTimeout: time.Second,
|
||||||
DepsTimeout: time.Minute,
|
DepsTimeout: time.Minute,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
t.Run("SingleJob", func(t *testing.T) {
|
type testScheduler struct {
|
||||||
s := NewScheduler(zaptest.NewLogger(t), config)
|
*scheduler.Scheduler
|
||||||
|
clockwork.FakeClock
|
||||||
job0 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
}
|
||||||
pendingJob0 := s.ScheduleJob(job0)
|
|
||||||
|
func newTestScheduler(t *testing.T) *testScheduler {
|
||||||
s.RegisterWorker(workerID0)
|
log := zaptest.NewLogger(t)
|
||||||
pickerJob := s.PickJob(workerID0, nil)
|
|
||||||
|
s := &testScheduler{
|
||||||
require.Equal(t, pendingJob0, pickerJob)
|
FakeClock: clockwork.NewFakeClock(),
|
||||||
|
Scheduler: scheduler.NewScheduler(log, config),
|
||||||
result := &api.JobResult{ID: job0.ID, ExitCode: 0}
|
}
|
||||||
s.OnJobComplete(workerID0, job0.ID, result)
|
|
||||||
|
*scheduler.TimeAfter = s.FakeClock.After
|
||||||
select {
|
return s
|
||||||
case <-pendingJob0.Finished:
|
}
|
||||||
require.Equal(t, pendingJob0.Result, result)
|
|
||||||
|
func (s *testScheduler) stop(t *testing.T) {
|
||||||
default:
|
*scheduler.TimeAfter = time.After
|
||||||
t.Fatalf("job0 is not finished")
|
goleak.VerifyNone(t)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
func TestScheduler_SingleJob(t *testing.T) {
|
||||||
t.Run("PickJobTimeout", func(t *testing.T) {
|
s := newTestScheduler(t)
|
||||||
s := NewScheduler(zaptest.NewLogger(t), config)
|
defer s.stop(t)
|
||||||
|
|
||||||
canceled := make(chan struct{})
|
job0 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
close(canceled)
|
pendingJob0 := s.ScheduleJob(job0)
|
||||||
|
|
||||||
s.RegisterWorker(workerID0)
|
s.BlockUntil(1)
|
||||||
require.Nil(t, s.PickJob(workerID0, canceled))
|
s.Advance(config.DepsTimeout) // At this point job must be in global queue.
|
||||||
})
|
|
||||||
|
s.RegisterWorker(workerID0)
|
||||||
t.Run("CacheLocalScheduling", func(t *testing.T) {
|
pickerJob := s.PickJob(context.Background(), workerID0)
|
||||||
s := NewScheduler(zaptest.NewLogger(t), config)
|
|
||||||
|
require.Equal(t, pendingJob0, pickerJob)
|
||||||
job0 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
|
||||||
job1 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
result := &api.JobResult{ID: job0.ID, ExitCode: 0}
|
||||||
|
s.OnJobComplete(workerID0, job0.ID, result)
|
||||||
s.RegisterWorker(workerID0)
|
|
||||||
s.OnJobComplete(workerID0, job0.ID, &api.JobResult{})
|
select {
|
||||||
|
case <-pendingJob0.Finished:
|
||||||
pendingJob1 := s.ScheduleJob(job1)
|
require.Equal(t, pendingJob0.Result, result)
|
||||||
pendingJob0 := s.ScheduleJob(job0)
|
|
||||||
|
default:
|
||||||
// job0 scheduling should be blocked on CacheTimeout
|
t.Fatalf("job0 is not finished")
|
||||||
clock.BlockUntil(1)
|
}
|
||||||
|
}
|
||||||
pickedJob := s.PickJob(workerID0, nil)
|
|
||||||
require.Equal(t, pendingJob0, pickedJob)
|
func TestScheduler_PickJobCancelation(t *testing.T) {
|
||||||
|
s := newTestScheduler(t)
|
||||||
pickedJob = s.PickJob(workerID0, nil)
|
defer s.stop(t)
|
||||||
require.Equal(t, pendingJob1, pickedJob)
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
clock.Advance(time.Hour)
|
cancel()
|
||||||
})
|
|
||||||
|
s.RegisterWorker(workerID0)
|
||||||
t.Run("DependencyLocalScheduling", func(t *testing.T) {
|
require.Nil(t, s.PickJob(ctx, workerID0))
|
||||||
s := NewScheduler(zaptest.NewLogger(t), config)
|
}
|
||||||
|
|
||||||
job0 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
func TestScheduler_CacheLocalScheduling(t *testing.T) {
|
||||||
job1 := &api.JobSpec{Job: build.Job{ID: build.NewID(), Deps: []build.ID{job0.ID}}}
|
s := newTestScheduler(t)
|
||||||
job2 := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
defer s.stop(t)
|
||||||
|
|
||||||
s.RegisterWorker(workerID0)
|
cachedJob := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
s.OnJobComplete(workerID0, job0.ID, &api.JobResult{})
|
uncachedJob := &api.JobSpec{Job: build.Job{ID: build.NewID()}}
|
||||||
|
|
||||||
pendingJob2 := s.ScheduleJob(job2)
|
s.RegisterWorker(workerID0)
|
||||||
pendingJob1 := s.ScheduleJob(job1)
|
s.OnJobComplete(workerID0, cachedJob.ID, &api.JobResult{})
|
||||||
|
|
||||||
// job1 should be blocked on DepsTimeout
|
pendingUncachedJob := s.ScheduleJob(uncachedJob)
|
||||||
clock.BlockUntil(1)
|
pendingCachedJob := s.ScheduleJob(cachedJob)
|
||||||
|
|
||||||
pickedJob := s.PickJob(workerID0, nil)
|
s.BlockUntil(2) // both jobs should be blocked
|
||||||
require.Equal(t, pendingJob1, pickedJob)
|
|
||||||
|
firstPickedJob := s.PickJob(context.Background(), workerID0)
|
||||||
pickedJob = s.PickJob(workerID0, nil)
|
assert.Equal(t, pendingCachedJob, firstPickedJob)
|
||||||
require.Equal(t, pendingJob2, pickedJob)
|
|
||||||
|
s.Advance(config.DepsTimeout) // At this point uncachedJob is put into global queue.
|
||||||
clock.Advance(time.Hour)
|
|
||||||
})
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/api"
|
||||||
|
"gitlab.com/slon/shad-go/distbuild/pkg/artifact"
|
||||||
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
"gitlab.com/slon/shad-go/distbuild/pkg/build"
|
||||||
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
"gitlab.com/slon/shad-go/distbuild/pkg/filecache"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (w *Worker) pullFiles(ctx context.Context, files map[build.ID]string) error {
|
func (w *Worker) downloadFiles(ctx context.Context, files map[build.ID]string) error {
|
||||||
for id := range files {
|
for id := range files {
|
||||||
_, unlock, err := w.fileCache.Get(id)
|
_, unlock, err := w.fileCache.Get(id)
|
||||||
if errors.Is(err, filecache.ErrNotFound) {
|
if errors.Is(err, filecache.ErrNotFound) {
|
||||||
|
@ -24,3 +26,20 @@ func (w *Worker) pullFiles(ctx context.Context, files map[build.ID]string) error
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Worker) downloadArtifacts(ctx context.Context, artifacts map[build.ID]api.WorkerID) error {
|
||||||
|
for id, worker := range artifacts {
|
||||||
|
_, unlock, err := w.artifacts.Get(id)
|
||||||
|
if errors.Is(err, artifact.ErrNotFound) {
|
||||||
|
if err = artifact.Download(ctx, worker.String(), w.artifacts, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -165,7 +165,11 @@ func (w *Worker) runJob(ctx context.Context, spec *api.JobSpec) (*api.JobResult,
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = w.pullFiles(ctx, spec.SourceFiles); err != nil {
|
if err = w.downloadFiles(ctx, spec.SourceFiles); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.downloadArtifacts(ctx, spec.Artifacts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue