golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/schedule/schedule_test.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build linux || darwin 6 7 package schedule 8 9 import ( 10 "context" 11 "fmt" 12 "runtime" 13 "testing" 14 "time" 15 16 "golang.org/x/build/buildlet" 17 "golang.org/x/build/dashboard" 18 cpool "golang.org/x/build/internal/coordinator/pool" 19 "golang.org/x/build/internal/coordinator/pool/queue" 20 "golang.org/x/build/internal/spanlog" 21 ) 22 23 type discardLogger struct{} 24 25 func (discardLogger) LogEventTime(event string, optText ...string) {} 26 27 func (discardLogger) CreateSpan(event string, optText ...string) spanlog.Span { 28 return CreateSpan(discardLogger{}, event, optText...) 29 } 30 31 // step is a test step for TestScheduler 32 type step func(*testing.T, *Scheduler) 33 34 // getBuildletCall represents a call to GetBuildlet. 35 type getBuildletCall struct { 36 si *queue.SchedItem 37 ctx context.Context 38 ctxCancel context.CancelFunc 39 40 done chan struct{} // closed when call done 41 gotClient buildlet.Client 42 gotErr error 43 } 44 45 func newGetBuildletCall(si *queue.SchedItem) *getBuildletCall { 46 c := &getBuildletCall{ 47 si: si, 48 done: make(chan struct{}), 49 } 50 c.ctx, c.ctxCancel = context.WithCancel(context.Background()) 51 return c 52 } 53 54 func (c *getBuildletCall) cancel(t *testing.T, s *Scheduler) { c.ctxCancel() } 55 56 // start is a step (assignable to type step) that starts a 57 // s.GetBuildlet call and waits for it to either succeed or get 58 // blocked in the scheduler. 59 func (c *getBuildletCall) start(t *testing.T, s *Scheduler) { 60 t.Logf("starting buildlet call for SchedItem=%p", c.si) 61 go func() { 62 c.gotClient, c.gotErr = s.GetBuildlet(c.ctx, c.si) 63 close(c.done) 64 }() 65 66 // Wait for si to be enqueued, or this call to be satisfied. 67 if !trueSoon(func() bool { 68 select { 69 case <-c.done: 70 return true 71 default: 72 return s.hasWaiter(c.si) 73 } 74 }) { 75 t.Fatalf("timeout waiting for GetBuildlet call to run to its blocking point") 76 } 77 } 78 79 func trueSoon(f func() bool) bool { 80 deadline := time.Now().Add(5 * time.Second) 81 for { 82 if time.Now().After(deadline) { 83 return false 84 } 85 if f() { 86 return true 87 } 88 time.Sleep(5 * time.Millisecond) 89 } 90 } 91 92 // wantGetBuildlet is a step (assignable to type step) that) that expects 93 // the GetBuildlet call to succeed. 94 func (c *getBuildletCall) wantGetBuildlet(t *testing.T, s *Scheduler) { 95 timer := time.NewTimer(5 * time.Second) 96 defer timer.Stop() 97 t.Logf("waiting on sched.getBuildlet(%q) ...", c.si.HostType) 98 select { 99 case <-c.done: 100 t.Logf("got sched.getBuildlet(%q).", c.si.HostType) 101 if c.gotErr != nil { 102 t.Fatalf("GetBuildlet(%q): %v", c.si.HostType, c.gotErr) 103 } 104 case <-timer.C: 105 stack := make([]byte, 1<<20) 106 stack = stack[:runtime.Stack(stack, true)] 107 t.Fatalf("timeout waiting for buildlet of type %q; stacks:\n%s", c.si.HostType, stack) 108 } 109 } 110 111 type fakePool struct { 112 poolChan map[string]chan interface{} // hostType -> { buildlet.Client | error} 113 } 114 115 func (f *fakePool) GetBuildlet(ctx context.Context, hostType string, lg cpool.Logger, item *queue.SchedItem) (buildlet.Client, error) { 116 c, ok := f.poolChan[hostType] 117 if !ok { 118 return nil, fmt.Errorf("pool doesn't support host type %q", hostType) 119 } 120 select { 121 case v := <-c: 122 if c, ok := v.(buildlet.Client); ok { 123 return c, nil 124 } 125 return nil, v.(error) 126 case <-ctx.Done(): 127 return nil, ctx.Err() 128 } 129 } 130 131 func (fakePool) String() string { return "testing poolChan" } 132 133 func TestScheduler(t *testing.T) { 134 defer func() { cpool.TestPoolHook = nil }() 135 136 var pool *fakePool // initialized per test below 137 // buildletAvailable is a step that creates a buildlet to the pool. 138 buildletAvailable := func(hostType string) step { 139 return func(t *testing.T, s *Scheduler) { 140 bc := buildlet.NewClient("127.0.0.1:9999", buildlet.NoKeyPair) // dummy 141 t.Logf("adding buildlet to pool for %q...", hostType) 142 ch := pool.poolChan[hostType] 143 ch <- bc 144 t.Logf("added buildlet to pool for %q (ch=%p)", hostType, ch) 145 } 146 } 147 148 tests := []struct { 149 name string 150 steps func() []step 151 }{ 152 { 153 name: "simple-get-before-available", 154 steps: func() []step { 155 si := &queue.SchedItem{HostType: "test-host-foo"} 156 fooGet := newGetBuildletCall(si) 157 return []step{ 158 fooGet.start, 159 buildletAvailable("test-host-foo"), 160 fooGet.wantGetBuildlet, 161 } 162 }, 163 }, 164 { 165 name: "simple-get-already-available", 166 steps: func() []step { 167 si := &queue.SchedItem{HostType: "test-host-foo"} 168 fooGet := newGetBuildletCall(si) 169 return []step{ 170 buildletAvailable("test-host-foo"), 171 fooGet.start, 172 fooGet.wantGetBuildlet, 173 } 174 }, 175 }, 176 { 177 name: "cancel-context-removes-waiter", 178 steps: func() []step { 179 si := &queue.SchedItem{HostType: "test-host-foo"} 180 get := newGetBuildletCall(si) 181 return []step{ 182 get.start, 183 get.cancel, 184 func(t *testing.T, s *Scheduler) { 185 if !trueSoon(func() bool { return !s.hasWaiter(si) }) { 186 t.Errorf("still have SchedItem in waiting set") 187 } 188 }, 189 } 190 }, 191 }, 192 } 193 for _, tt := range tests { 194 pool = &fakePool{poolChan: map[string]chan interface{}{}} 195 pool.poolChan["test-host-foo"] = make(chan interface{}, 1) 196 pool.poolChan["test-host-bar"] = make(chan interface{}, 1) 197 198 cpool.TestPoolHook = func(*dashboard.HostConfig) cpool.Buildlet { return pool } 199 t.Run(tt.name, func(t *testing.T) { 200 s := NewScheduler() 201 for i, st := range tt.steps() { 202 t.Logf("step %v...", i) 203 st(t, s) 204 } 205 }) 206 } 207 }