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  }