1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
10 "git.curoverse.com/arvados.git/lib/cloud"
11 "git.curoverse.com/arvados.git/lib/dispatchcloud/test"
12 "git.curoverse.com/arvados.git/sdk/go/arvados"
13 "github.com/sirupsen/logrus"
14 check "gopkg.in/check.v1"
17 const GiB arvados.ByteSize = 1 << 30
19 var _ = check.Suite(&PoolSuite{})
21 type lessChecker struct {
25 func (*lessChecker) Check(params []interface{}, names []string) (result bool, error string) {
26 return params[0].(int) < params[1].(int), ""
29 var less = &lessChecker{&check.CheckerInfo{Name: "less", Params: []string{"obtained", "expected"}}}
31 type PoolSuite struct{}
33 func (suite *PoolSuite) SetUpSuite(c *check.C) {
34 logrus.StandardLogger().SetLevel(logrus.DebugLevel)
37 func (suite *PoolSuite) TestStartContainer(c *check.C) {
38 // TODO: use an instanceSet stub with an SSH server
41 func (suite *PoolSuite) TestVerifyHostKey(c *check.C) {
42 // TODO: use an instanceSet stub with an SSH server
45 func (suite *PoolSuite) TestCreateUnallocShutdown(c *check.C) {
46 lameInstanceSet := &test.LameInstanceSet{Hold: make(chan bool)}
47 type1 := arvados.InstanceType{Name: "a1s", ProviderType: "a1.small", VCPUs: 1, RAM: 1 * GiB, Price: .01}
48 type2 := arvados.InstanceType{Name: "a2m", ProviderType: "a2.medium", VCPUs: 2, RAM: 2 * GiB, Price: .02}
49 type3 := arvados.InstanceType{Name: "a2l", ProviderType: "a2.large", VCPUs: 4, RAM: 4 * GiB, Price: .04}
51 logger: logrus.StandardLogger(),
52 newExecutor: func(cloud.Instance) Executor { return stubExecutor{} },
53 instanceSet: &throttledInstanceSet{InstanceSet: lameInstanceSet},
54 instanceTypes: arvados.InstanceTypeMap{
60 notify := pool.Subscribe()
61 defer pool.Unsubscribe(notify)
62 notify2 := pool.Subscribe()
63 defer pool.Unsubscribe(notify2)
65 c.Check(pool.Unallocated()[type1], check.Equals, 0)
66 c.Check(pool.Unallocated()[type2], check.Equals, 0)
67 c.Check(pool.Unallocated()[type3], check.Equals, 0)
72 c.Check(pool.Unallocated()[type1], check.Equals, 1)
73 c.Check(pool.Unallocated()[type2], check.Equals, 2)
74 c.Check(pool.Unallocated()[type3], check.Equals, 1)
76 // Unblock the pending Create calls.
77 go lameInstanceSet.Release(4)
79 // Wait for each instance to either return from its Create
80 // call, or show up in a poll.
81 suite.wait(c, pool, notify, func() bool {
83 defer pool.mtx.RUnlock()
84 return len(pool.workers) == 4
87 // Place type3 node on admin-hold
88 ivs := suite.instancesByType(pool, type3)
89 c.Assert(ivs, check.HasLen, 1)
90 type3instanceID := ivs[0].Instance
91 err := pool.SetIdleBehavior(type3instanceID, IdleBehaviorHold)
92 c.Check(err, check.IsNil)
94 // Check admin-hold behavior: refuse to shutdown, and don't
95 // report as Unallocated ("available now or soon").
96 c.Check(pool.Shutdown(type3), check.Equals, false)
97 suite.wait(c, pool, notify, func() bool {
98 return pool.Unallocated()[type3] == 0
100 c.Check(suite.instancesByType(pool, type3), check.HasLen, 1)
102 // Shutdown both type2 nodes
103 c.Check(pool.Shutdown(type2), check.Equals, true)
104 suite.wait(c, pool, notify, func() bool {
105 return pool.Unallocated()[type1] == 1 && pool.Unallocated()[type2] == 1
107 c.Check(pool.Shutdown(type2), check.Equals, true)
108 suite.wait(c, pool, notify, func() bool {
109 return pool.Unallocated()[type1] == 1 && pool.Unallocated()[type2] == 0
111 c.Check(pool.Shutdown(type2), check.Equals, false)
113 // Consume any waiting notifications to ensure the
114 // next one we get is from Shutdown.
123 // Shutdown type1 node
124 c.Check(pool.Shutdown(type1), check.Equals, true)
125 suite.wait(c, pool, notify, func() bool {
126 return pool.Unallocated()[type1] == 0 && pool.Unallocated()[type2] == 0 && pool.Unallocated()[type3] == 0
130 case <-time.After(time.Second):
131 c.Error("notify did not receive")
134 // Put type3 node back in service.
135 err = pool.SetIdleBehavior(type3instanceID, IdleBehaviorRun)
136 c.Check(err, check.IsNil)
137 suite.wait(c, pool, notify, func() bool {
138 return pool.Unallocated()[type3] == 1
141 // Check admin-drain behavior: shut down right away, and don't
142 // report as Unallocated.
143 err = pool.SetIdleBehavior(type3instanceID, IdleBehaviorDrain)
144 c.Check(err, check.IsNil)
145 suite.wait(c, pool, notify, func() bool {
146 return pool.Unallocated()[type3] == 0
148 suite.wait(c, pool, notify, func() bool {
149 ivs := suite.instancesByType(pool, type3)
150 return len(ivs) == 1 && ivs[0].WorkerState == StateShutdown.String()
153 // Unblock all pending Destroy calls. Pool calls Destroy again
154 // if a node still appears in the provider list after a
155 // previous attempt, so there might be more than 4 Destroy
157 go lameInstanceSet.Release(4444)
159 // Sync until all instances disappear from the provider list.
160 suite.wait(c, pool, notify, func() bool {
161 pool.getInstancesAndSync()
162 return len(pool.Instances()) == 0
166 func (suite *PoolSuite) instancesByType(pool *Pool, it arvados.InstanceType) []InstanceView {
167 var ivs []InstanceView
168 for _, iv := range pool.Instances() {
169 if iv.ArvadosInstanceType == it.Name {
170 ivs = append(ivs, iv)
176 func (suite *PoolSuite) wait(c *check.C, pool *Pool, notify <-chan struct{}, ready func() bool) {
177 timeout := time.NewTimer(time.Second).C
186 c.Check(ready(), check.Equals, true)