blob: 263606965ac277e6d7b202b3c75d092602254461 [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.13
// +build linux darwin
package main
import (
"context"
"fmt"
"runtime"
"testing"
"time"
"golang.org/x/build/buildlet"
"golang.org/x/build/cmd/coordinator/spanlog"
"golang.org/x/build/dashboard"
)
func TestSchedLess(t *testing.T) {
t1, t2 := time.Unix(1, 0), time.Unix(2, 0)
tests := []struct {
name string
a, b *SchedItem
want bool
}{
{
name: "gomote over reg",
a: &SchedItem{
IsGomote: true,
requestTime: t2,
},
b: &SchedItem{
requestTime: t1,
},
want: true,
},
{
name: "gomote over try",
a: &SchedItem{
IsGomote: true,
requestTime: t2,
},
b: &SchedItem{
IsTry: true,
requestTime: t1,
},
want: true,
},
{
name: "try over reg",
a: &SchedItem{
IsTry: true,
requestTime: t2,
},
b: &SchedItem{
requestTime: t1,
},
want: true,
},
{
name: "try FIFO, less",
a: &SchedItem{
IsTry: true,
requestTime: t1,
},
b: &SchedItem{
IsTry: true,
requestTime: t2,
},
want: true,
},
{
name: "try FIFO, greater",
a: &SchedItem{
IsTry: true,
requestTime: t2,
},
b: &SchedItem{
IsTry: true,
requestTime: t1,
},
want: false,
},
{
name: "reg LIFO, less",
a: &SchedItem{
CommitTime: t2,
requestTime: t1, // shouldn't be used
},
b: &SchedItem{
CommitTime: t1,
requestTime: t2, // shouldn't be used
},
want: true,
},
{
name: "reg LIFO, greater",
a: &SchedItem{
CommitTime: t1,
requestTime: t2, // shouldn't be used
},
b: &SchedItem{
CommitTime: t2,
requestTime: t1, // shouldn't be used
},
want: false,
},
}
for _, tt := range tests {
got := schedLess(tt.a, tt.b)
if got != tt.want {
t.Errorf("%s: got %v; want %v", tt.name, got, tt.want)
}
}
}
type discardLogger struct{}
func (discardLogger) LogEventTime(event string, optText ...string) {}
func (discardLogger) CreateSpan(event string, optText ...string) spanlog.Span {
return createSpan(discardLogger{}, event, optText...)
}
// step is a test step for TestScheduler
type step func(*testing.T, *Scheduler)
// getBuildletCall represents a call to GetBuildlet.
type getBuildletCall struct {
si *SchedItem
ctx context.Context
ctxCancel context.CancelFunc
done chan struct{} // closed when call done
gotClient *buildlet.Client
gotErr error
}
func newGetBuildletCall(si *SchedItem) *getBuildletCall {
c := &getBuildletCall{
si: si,
done: make(chan struct{}),
}
c.ctx, c.ctxCancel = context.WithCancel(context.Background())
return c
}
func (c *getBuildletCall) cancel(t *testing.T, s *Scheduler) { c.ctxCancel() }
// start is a step (assignable to type step) that starts a
// s.GetBuildlet call and waits for it to either succeed or get
// blocked in the scheduler.
func (c *getBuildletCall) start(t *testing.T, s *Scheduler) {
t.Logf("starting buildlet call for SchedItem=%p", c.si)
go func() {
c.gotClient, c.gotErr = s.GetBuildlet(c.ctx, c.si)
close(c.done)
}()
// Wait for si to be enqueued, or this call to be satisified.
if !trueSoon(func() bool {
select {
case <-c.done:
return true
default:
return s.hasWaiter(c.si)
}
}) {
t.Fatalf("timeout waiting for GetBuildlet call to run to its blocking point")
}
}
func trueSoon(f func() bool) bool {
deadline := time.Now().Add(5 * time.Second)
for {
if time.Now().After(deadline) {
return false
}
if f() {
return true
}
time.Sleep(5 * time.Millisecond)
}
}
// wantGetBuildlet is a step (assignable to type step) that) that expects
// the GetBuildlet call to succeed.
func (c *getBuildletCall) wantGetBuildlet(t *testing.T, s *Scheduler) {
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
t.Logf("waiting on sched.getBuildlet(%q) ...", c.si.HostType)
select {
case <-c.done:
t.Logf("got sched.getBuildlet(%q).", c.si.HostType)
if c.gotErr != nil {
t.Fatalf("GetBuildlet(%q): %v", c.si.HostType, c.gotErr)
}
case <-timer.C:
stack := make([]byte, 1<<20)
stack = stack[:runtime.Stack(stack, true)]
t.Fatalf("timeout waiting for buildlet of type %q; stacks:\n%s", c.si.HostType, stack)
}
}
type poolChan map[string]chan interface{} // hostType -> { *buildlet.Client | error}
func (m poolChan) GetBuildlet(ctx context.Context, hostType string, lg logger) (*buildlet.Client, error) {
c, ok := m[hostType]
if !ok {
return nil, fmt.Errorf("pool doesn't support host type %q", hostType)
}
select {
case v := <-c:
if c, ok := v.(*buildlet.Client); ok {
return c, nil
}
return nil, v.(error)
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (poolChan) String() string { return "testing poolChan" }
func TestScheduler(t *testing.T) {
defer func() { testPoolHook = nil }()
var pool poolChan // initialized per test below
// buildletAvailable is a step that creates a buildlet to the pool.
buildletAvailable := func(hostType string) step {
return func(t *testing.T, s *Scheduler) {
bc := buildlet.NewClient("127.0.0.1:9999", buildlet.NoKeyPair) // dummy
t.Logf("adding buildlet to pool for %q...", hostType)
ch := pool[hostType]
ch <- bc
t.Logf("added buildlet to pool for %q (ch=%p)", hostType, ch)
}
}
tests := []struct {
name string
steps func() []step
}{
{
name: "simple-get-before-available",
steps: func() []step {
si := &SchedItem{HostType: "test-host-foo"}
fooGet := newGetBuildletCall(si)
return []step{
fooGet.start,
buildletAvailable("test-host-foo"),
fooGet.wantGetBuildlet,
}
},
},
{
name: "simple-get-already-available",
steps: func() []step {
si := &SchedItem{HostType: "test-host-foo"}
fooGet := newGetBuildletCall(si)
return []step{
buildletAvailable("test-host-foo"),
fooGet.start,
fooGet.wantGetBuildlet,
}
},
},
{
name: "try-bot-trumps-regular", // really that prioritization works at all; TestSchedLess tests actual policy
steps: func() []step {
tryItem := &SchedItem{HostType: "test-host-foo", IsTry: true}
regItem := &SchedItem{HostType: "test-host-foo"}
tryGet := newGetBuildletCall(tryItem)
regGet := newGetBuildletCall(regItem)
return []step{
regGet.start,
tryGet.start,
buildletAvailable("test-host-foo"),
tryGet.wantGetBuildlet,
buildletAvailable("test-host-foo"),
regGet.wantGetBuildlet,
}
},
},
{
name: "cancel-context-removes-waiter",
steps: func() []step {
si := &SchedItem{HostType: "test-host-foo"}
get := newGetBuildletCall(si)
return []step{
get.start,
get.cancel,
func(t *testing.T, s *Scheduler) {
if !trueSoon(func() bool { return !s.hasWaiter(si) }) {
t.Errorf("still have SchedItem in waiting set")
}
},
}
},
},
}
for _, tt := range tests {
pool = make(poolChan)
pool["test-host-foo"] = make(chan interface{}, 1)
pool["test-host-bar"] = make(chan interface{}, 1)
testPoolHook = func(*dashboard.HostConfig) BuildletPool { return pool }
t.Run(tt.name, func(t *testing.T) {
s := NewScheduler()
for i, st := range tt.steps() {
t.Logf("step %v...", i)
st(t, s)
}
})
}
}