golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/pool/ledger_test.go (about)

     1  // Copyright 2020 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 pool
     8  
     9  import (
    10  	"context"
    11  	"sort"
    12  	"testing"
    13  	"time"
    14  
    15  	"golang.org/x/build/internal/cloud"
    16  	"golang.org/x/build/internal/coordinator/pool/queue"
    17  )
    18  
    19  func canceledContext() context.Context {
    20  	ctx, cancel := context.WithCancel(context.Background())
    21  	cancel()
    22  	return ctx
    23  }
    24  
    25  func TestLedgerReserveResources(t *testing.T) {
    26  	testCases := []struct {
    27  		desc      string
    28  		ctx       context.Context
    29  		instName  string
    30  		vmType    string
    31  		instTypes []*cloud.InstanceType
    32  		cpuLimit  int64
    33  		cpuUsed   int64
    34  		wantErr   bool
    35  	}{
    36  		{
    37  			desc:     "success",
    38  			ctx:      context.Background(),
    39  			instName: "small-instance",
    40  			vmType:   "aa.small",
    41  			instTypes: []*cloud.InstanceType{
    42  				{
    43  					Type: "aa.small",
    44  					CPU:  5,
    45  				},
    46  			},
    47  			cpuLimit: 20,
    48  			cpuUsed:  5,
    49  			wantErr:  false,
    50  		},
    51  		{
    52  			desc:     "cancelled-context",
    53  			ctx:      canceledContext(),
    54  			instName: "small-instance",
    55  			vmType:   "aa.small",
    56  			instTypes: []*cloud.InstanceType{
    57  				{
    58  					Type: "aa.small",
    59  					CPU:  5,
    60  				},
    61  			},
    62  			cpuLimit: 20,
    63  			cpuUsed:  20,
    64  			wantErr:  true,
    65  		},
    66  		{
    67  			desc:      "unknown-instance-type",
    68  			ctx:       context.Background(),
    69  			instName:  "small-instance",
    70  			vmType:    "aa.small",
    71  			instTypes: []*cloud.InstanceType{},
    72  			cpuLimit:  20,
    73  			cpuUsed:   5,
    74  			wantErr:   true,
    75  		},
    76  		{
    77  			desc:     "instance-already-exists",
    78  			ctx:      context.Background(),
    79  			instName: "large-instance",
    80  			vmType:   "aa.small",
    81  			instTypes: []*cloud.InstanceType{
    82  				{
    83  					Type: "aa.small",
    84  					CPU:  5,
    85  				},
    86  			},
    87  			cpuLimit: 20,
    88  			cpuUsed:  5,
    89  			wantErr:  true,
    90  		},
    91  	}
    92  	for _, tc := range testCases {
    93  		t.Run(tc.desc, func(t *testing.T) {
    94  			l := newLedger()
    95  			l.entries = map[string]*entry{
    96  				"large-instance": {},
    97  			}
    98  			l.SetCPULimit(tc.cpuLimit)
    99  			l.types = make(map[string]*cloud.InstanceType)
   100  			l.UpdateInstanceTypes(tc.instTypes)
   101  			gotErr := l.ReserveResources(tc.ctx, tc.instName, tc.vmType, new(queue.SchedItem))
   102  			if (gotErr != nil) != tc.wantErr {
   103  				t.Errorf("ledger.ReserveResources(%+v, %s, %s) = %s; want error %t", tc.ctx, tc.instName, tc.vmType, gotErr, tc.wantErr)
   104  			}
   105  		})
   106  	}
   107  }
   108  
   109  func TestLedgerReleaseResources(t *testing.T) {
   110  	testCases := []struct {
   111  		desc        string
   112  		instName    string
   113  		entry       *entry
   114  		cpuUsed     int64
   115  		a1Used      int64
   116  		wantCPUUsed int64
   117  		wantErr     bool
   118  	}{
   119  		{
   120  			desc:     "success",
   121  			instName: "inst-x",
   122  			entry: &entry{
   123  				instanceName: "inst-x",
   124  				vCPUCount:    10,
   125  			},
   126  			cpuUsed:     20,
   127  			a1Used:      0,
   128  			wantCPUUsed: 10,
   129  			wantErr:     false,
   130  		},
   131  		{
   132  			desc:     "entry-not-found",
   133  			instName: "inst-x",
   134  			entry: &entry{
   135  				instanceName: "inst-w",
   136  				vCPUCount:    10,
   137  			},
   138  			cpuUsed:     20,
   139  			a1Used:      0,
   140  			wantCPUUsed: 20,
   141  			wantErr:     true,
   142  		},
   143  	}
   144  	for _, tc := range testCases {
   145  		t.Run(tc.desc, func(t *testing.T) {
   146  			l := newLedger()
   147  			l.cpuQueue.UpdateQuotas(int(tc.cpuUsed-tc.entry.vCPUCount), 20)
   148  			l.entries = map[string]*entry{
   149  				tc.entry.instanceName: tc.entry,
   150  			}
   151  			item := l.cpuQueue.Enqueue(int(tc.entry.vCPUCount), new(queue.SchedItem))
   152  			if err := item.Await(context.Background()); err != nil {
   153  				t.Fatalf("item.Await() = %q, wanted no error", err)
   154  			}
   155  			tc.entry.quota = item
   156  			gotErr := l.releaseResources(tc.instName)
   157  			if (gotErr != nil) != tc.wantErr {
   158  				t.Errorf("ledger.releaseResources(%s) = %s; want error %t", tc.instName, gotErr, tc.wantErr)
   159  			}
   160  			usage := l.cpuQueue.Quotas()
   161  			if int64(usage.Used) != tc.wantCPUUsed {
   162  				t.Errorf("ledger.cpuUsed = %d; wanted %d", usage.Used, tc.wantCPUUsed)
   163  			}
   164  		})
   165  	}
   166  }
   167  
   168  func TestReserveResourcesEntries(t *testing.T) {
   169  	testCases := []struct {
   170  		desc        string
   171  		numCPU      int64
   172  		cpuLimit    int64
   173  		cpuUsed     int64
   174  		instName    string
   175  		instType    string
   176  		wantErr     bool
   177  		wantCPUUsed int
   178  		wantA1Used  int
   179  	}{
   180  		{
   181  			desc:        "reservation-success",
   182  			numCPU:      10,
   183  			cpuLimit:    10,
   184  			cpuUsed:     0,
   185  			instName:    "chacha",
   186  			instType:    "x.type",
   187  			wantErr:     false,
   188  			wantCPUUsed: 10,
   189  			wantA1Used:  0,
   190  		},
   191  		{
   192  			desc:        "failed-to-reserve",
   193  			numCPU:      10,
   194  			cpuLimit:    5,
   195  			cpuUsed:     0,
   196  			instName:    "pasa",
   197  			instType:    "x.type",
   198  			wantErr:     true,
   199  			wantCPUUsed: 0,
   200  			wantA1Used:  0,
   201  		},
   202  		{
   203  			desc:        "invalid-cpu-count",
   204  			numCPU:      0,
   205  			cpuLimit:    50,
   206  			cpuUsed:     20,
   207  			instName:    "double",
   208  			instType:    "x.type",
   209  			wantErr:     true,
   210  			wantCPUUsed: 20,
   211  			wantA1Used:  0,
   212  		},
   213  	}
   214  	for _, tc := range testCases {
   215  		t.Run(tc.desc, func(t *testing.T) {
   216  			l := newLedger()
   217  			l.types = make(map[string]*cloud.InstanceType)
   218  			l.UpdateInstanceTypes([]*cloud.InstanceType{{Type: tc.instType, CPU: tc.numCPU}})
   219  			l.cpuQueue.UpdateQuotas(int(tc.cpuUsed), int(tc.cpuLimit))
   220  			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   221  			defer cancel()
   222  			err := l.ReserveResources(ctx, tc.instName, tc.instType, new(queue.SchedItem))
   223  			if (err != nil) != tc.wantErr {
   224  				t.Errorf("ledger.allocateResources(%d) = %v, wantErr: %v", tc.numCPU, err, tc.wantErr)
   225  			}
   226  			usage := l.cpuQueue.Quotas()
   227  			if usage.Used != tc.wantCPUUsed {
   228  				t.Errorf("ledger.cpuUsed = %d; want %d", usage.Used, tc.wantCPUUsed)
   229  			}
   230  			if _, ok := l.entries[tc.instName]; !tc.wantErr && !ok {
   231  				t.Fatalf("ledger.entries[%s] = nil; want it to exist", tc.instName)
   232  			}
   233  			if e, _ := l.entries[tc.instName]; !tc.wantErr && e.vCPUCount != tc.numCPU {
   234  				t.Fatalf("ledger.entries[%s].vCPUCount = %d; want %d", tc.instName, e.vCPUCount, tc.numCPU)
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestLedgerUpdateReservation(t *testing.T) {
   241  	testCases := []struct {
   242  		desc     string
   243  		instName string
   244  		instID   string
   245  		entry    *entry
   246  		wantErr  bool
   247  	}{
   248  		{
   249  			desc:     "success",
   250  			instName: "inst-x",
   251  			instID:   "id-foo-x",
   252  			entry: &entry{
   253  				instanceName: "inst-x",
   254  			},
   255  			wantErr: false,
   256  		},
   257  		{
   258  			desc:     "success",
   259  			instName: "inst-x",
   260  			instID:   "id-foo-x",
   261  			entry: &entry{
   262  				instanceName: "inst-w",
   263  			},
   264  			wantErr: true,
   265  		},
   266  	}
   267  	for _, tc := range testCases {
   268  		t.Run(tc.desc, func(t *testing.T) {
   269  			l := newLedger()
   270  			l.entries = map[string]*entry{
   271  				tc.entry.instanceName: tc.entry,
   272  			}
   273  			if gotErr := l.UpdateReservation(tc.instName, tc.instID); (gotErr != nil) != tc.wantErr {
   274  				t.Errorf("ledger.updateReservation(%s, %s) = %s; want error %t", tc.instName, tc.instID, gotErr, tc.wantErr)
   275  			}
   276  			e, ok := l.entries[tc.instName]
   277  			if !tc.wantErr && !ok {
   278  				t.Fatalf("ledger.entries[%s] does not exist", tc.instName)
   279  			}
   280  			if !tc.wantErr && e.createdAt.IsZero() {
   281  				t.Errorf("ledger.entries[%s].createdAt = %s; time not set", tc.instName, e.createdAt)
   282  			}
   283  		})
   284  	}
   285  }
   286  
   287  func TestLedgerRemove(t *testing.T) {
   288  	testCases := []struct {
   289  		desc        string
   290  		instName    string
   291  		entry       *entry
   292  		cpuUsed     int
   293  		wantCPUUsed int
   294  		wantErr     bool
   295  	}{
   296  		{
   297  			desc:     "success",
   298  			instName: "inst-x",
   299  			entry: &entry{
   300  				instanceName: "inst-x",
   301  				vCPUCount:    10,
   302  			},
   303  			cpuUsed:     100,
   304  			wantCPUUsed: 90,
   305  			wantErr:     false,
   306  		},
   307  		{
   308  			desc:     "entry-does-not-exist",
   309  			instName: "inst-x",
   310  			entry: &entry{
   311  				instanceName: "inst-w",
   312  				vCPUCount:    10,
   313  			},
   314  			cpuUsed:     100,
   315  			wantCPUUsed: 100,
   316  			wantErr:     true,
   317  		},
   318  	}
   319  	for _, tc := range testCases {
   320  		t.Run(tc.desc, func(t *testing.T) {
   321  			l := newLedger()
   322  			l.cpuQueue.UpdateQuotas(tc.cpuUsed-int(tc.entry.vCPUCount), 100)
   323  			l.entries = map[string]*entry{
   324  				tc.entry.instanceName: tc.entry,
   325  			}
   326  			item := l.cpuQueue.Enqueue(int(tc.entry.vCPUCount), new(queue.SchedItem))
   327  			if err := item.Await(context.Background()); err != nil {
   328  				t.Fatalf("item.Await() = %q, wanted no error", err)
   329  			}
   330  			tc.entry.quota = item
   331  			l.cpuQueue.UpdateQuotas(tc.cpuUsed, 20)
   332  			if gotErr := l.Remove(tc.instName); (gotErr != nil) != tc.wantErr {
   333  				t.Errorf("ledger.remove(%s) = %s; want error %t", tc.instName, gotErr, tc.wantErr)
   334  			}
   335  			if gotE, ok := l.entries[tc.instName]; ok {
   336  				t.Errorf("ledger.entries[%s] = %+v; want it not to exist", tc.instName, gotE)
   337  			}
   338  			usage := l.cpuQueue.Quotas()
   339  			if usage.Used != tc.wantCPUUsed {
   340  				t.Errorf("ledger.cpuUsed = %d; want %d", usage.Used, tc.wantCPUUsed)
   341  			}
   342  		})
   343  	}
   344  }
   345  
   346  func TestLedgerSetCPULimit(t *testing.T) {
   347  	l := newLedger()
   348  	want := 300
   349  	l.SetCPULimit(int64(want))
   350  	usage := l.cpuQueue.Quotas()
   351  	if usage.Limit != want {
   352  		t.Errorf("ledger.cpuLimit = %d; want %d", want, want)
   353  	}
   354  }
   355  
   356  func TestLedgerUpdateInstanceTypes(t *testing.T) {
   357  	testCases := []struct {
   358  		desc  string
   359  		types []*cloud.InstanceType
   360  	}{
   361  		{"no-type", []*cloud.InstanceType{}},
   362  		{"single-type", []*cloud.InstanceType{{"x", 15}}},
   363  	}
   364  	for _, tc := range testCases {
   365  		t.Run(tc.desc, func(t *testing.T) {
   366  			l := newLedger()
   367  			l.UpdateInstanceTypes(tc.types)
   368  			for _, it := range tc.types {
   369  				if gotV, ok := l.types[it.Type]; !ok || gotV != it {
   370  					t.Errorf("ledger.types[%s] = %v; want %v", it.Type, gotV, it)
   371  				}
   372  			}
   373  			if len(l.types) != len(tc.types) {
   374  				t.Errorf("len(ledger.types) = %d; want %d", len(l.types), len(tc.types))
   375  			}
   376  		})
   377  	}
   378  }
   379  
   380  func TestLedgerResources(t *testing.T) {
   381  	testCases := []struct {
   382  		desc          string
   383  		entries       map[string]*entry
   384  		cpuCount      int64
   385  		cpuLimit      int64
   386  		wantInstCount int64
   387  	}{
   388  		{"no-instances", map[string]*entry{}, 2, 3, 0},
   389  		{"single-instance", map[string]*entry{"x": {}}, 2, 3, 1},
   390  	}
   391  	for _, tc := range testCases {
   392  		t.Run(tc.desc, func(t *testing.T) {
   393  			l := newLedger()
   394  			l.entries = tc.entries
   395  			l.cpuQueue.UpdateQuotas(int(tc.cpuCount), int(tc.cpuLimit))
   396  			gotR := l.Resources()
   397  			if gotR.InstCount != tc.wantInstCount {
   398  				t.Errorf("ledger.instCount = %d; want %d", gotR.InstCount, tc.wantInstCount)
   399  			}
   400  			if gotR.CPUUsed != tc.cpuCount {
   401  				t.Errorf("ledger.cpuCount = %d; want %d", gotR.CPUUsed, tc.cpuCount)
   402  			}
   403  			if gotR.CPULimit != tc.cpuLimit {
   404  				t.Errorf("ledger.cpuLimit = %d; want %d", gotR.CPULimit, tc.cpuLimit)
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  func TestLedgerResourceTime(t *testing.T) {
   411  	ct := time.Now()
   412  
   413  	testCases := []struct {
   414  		desc    string
   415  		entries map[string]*entry
   416  	}{
   417  		{"no-instances", map[string]*entry{}},
   418  		{"single-instance", map[string]*entry{
   419  			"inst-x": {
   420  				createdAt:    ct,
   421  				instanceID:   "id-x",
   422  				instanceName: "inst-x",
   423  				vCPUCount:    1,
   424  			},
   425  		}},
   426  		{"multiple-instances", map[string]*entry{
   427  			"inst-z": {
   428  				createdAt:    ct.Add(2 * time.Second),
   429  				instanceID:   "id-z",
   430  				instanceName: "inst-z",
   431  				vCPUCount:    1,
   432  			},
   433  			"inst-y": {
   434  				createdAt:    ct.Add(time.Second),
   435  				instanceID:   "id-y",
   436  				instanceName: "inst-y",
   437  				vCPUCount:    1,
   438  			},
   439  			"inst-x": {
   440  				createdAt:    ct,
   441  				instanceID:   "id-x",
   442  				instanceName: "inst-x",
   443  				vCPUCount:    1,
   444  			},
   445  		}},
   446  	}
   447  	for _, tc := range testCases {
   448  		t.Run(tc.desc, func(t *testing.T) {
   449  			l := newLedger()
   450  			l.entries = tc.entries
   451  			gotRT := l.ResourceTime()
   452  			if !sort.SliceIsSorted(gotRT, func(i, j int) bool { return gotRT[i].Creation.Before(gotRT[j].Creation) }) {
   453  				t.Errorf("resource time is not sorted")
   454  			}
   455  			if len(l.entries) != len(gotRT) {
   456  				t.Errorf("mismatch in items returned")
   457  			}
   458  			for _, rt := range gotRT {
   459  				delete(l.entries, rt.Name)
   460  			}
   461  			if len(l.entries) != 0 {
   462  				t.Errorf("mismatch")
   463  			}
   464  		})
   465  	}
   466  }