github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/tasklet_test.go (about) 1 package taskrunner 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "os/exec" 8 "sync/atomic" 9 "testing" 10 "time" 11 12 hclog "github.com/hashicorp/go-hclog" 13 "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces" 14 "github.com/hashicorp/nomad/helper/testlog" 15 "github.com/hashicorp/nomad/helper/testtask" 16 "github.com/stretchr/testify/assert" 17 ) 18 19 func TestMain(m *testing.M) { 20 if !testtask.Run() { 21 os.Exit(m.Run()) 22 } 23 } 24 25 func TestTasklet_Exec_HappyPath(t *testing.T) { 26 results := []execResult{ 27 {[]byte("output"), 0, nil}, 28 {[]byte("output"), 1, nil}, 29 {[]byte("output"), 0, context.DeadlineExceeded}, 30 {[]byte("<ignored output>"), 2, fmt.Errorf("some error")}, 31 {[]byte("error9000"), 9000, nil}, 32 } 33 exec := newScriptedExec(results) 34 tm := newTaskletMock(exec, testlog.HCLogger(t), time.Nanosecond, 3*time.Second) 35 36 handle := tm.run() 37 defer handle.cancel() // just-in-case cleanup 38 39 deadline := time.After(3 * time.Second) 40 for i := 0; i <= 4; i++ { 41 select { 42 case result := <-tm.calls: 43 // for the happy path without cancelations or shutdowns, we expect 44 // to get the results passed to the callback in order and without 45 // modification 46 assert.Equal(t, result, results[i]) 47 case <-deadline: 48 t.Fatalf("timed out waiting for all script checks to finish") 49 } 50 } 51 } 52 53 // TestTasklet_Exec_Cancel asserts cancelling a tasklet short-circuits 54 // any running executions the tasklet 55 func TestTasklet_Exec_Cancel(t *testing.T) { 56 exec, cancel := newBlockingScriptExec() 57 defer cancel() 58 tm := newTaskletMock(exec, testlog.HCLogger(t), time.Hour, time.Hour) 59 60 handle := tm.run() 61 <-exec.running // wait until Exec is called 62 handle.cancel() // cancel now that we're blocked in exec 63 64 select { 65 case <-handle.wait(): 66 case <-time.After(3 * time.Second): 67 t.Fatalf("timed out waiting for tasklet check to exit") 68 } 69 70 // The underlying ScriptExecutor (newBlockScriptExec) *cannot* be 71 // canceled. Only a wrapper around it obeys the context cancelation. 72 if atomic.LoadInt32(&exec.exited) == 1 { 73 t.Errorf("expected script executor to still be running after timeout") 74 } 75 // No tasklets finished, so no callbacks should have gotten a 76 // chance to fire 77 select { 78 case call := <-tm.calls: 79 t.Errorf("expected 0 calls of tasklet, got %v", call) 80 default: 81 break 82 } 83 } 84 85 // TestTasklet_Exec_Timeout asserts a tasklet script will be killed 86 // when the timeout is reached. 87 func TestTasklet_Exec_Timeout(t *testing.T) { 88 t.Parallel() 89 exec, cancel := newBlockingScriptExec() 90 defer cancel() 91 92 tm := newTaskletMock(exec, testlog.HCLogger(t), time.Hour, time.Second) 93 94 handle := tm.run() 95 defer handle.cancel() // just-in-case cleanup 96 <-exec.running // wait until Exec is called 97 98 // We should get a timeout 99 select { 100 case update := <-tm.calls: 101 if update.err != context.DeadlineExceeded { 102 t.Errorf("expected context.DeadlineExceeed but received %+v", update) 103 } 104 case <-time.After(3 * time.Second): 105 t.Fatalf("timed out waiting for script check to exit") 106 } 107 108 // The underlying ScriptExecutor (newBlockScriptExec) *cannot* be 109 // canceled. Only a wrapper around it obeys the context cancelation. 110 if atomic.LoadInt32(&exec.exited) == 1 { 111 t.Errorf("expected executor to still be running after timeout") 112 } 113 114 // Cancel and watch for exit 115 handle.cancel() 116 select { 117 case <-handle.wait(): // ok! 118 case update := <-tm.calls: 119 t.Errorf("unexpected extra callback on exit with status=%v", update) 120 case <-time.After(3 * time.Second): 121 t.Fatalf("timed out waiting for tasklet to exit") 122 } 123 } 124 125 // TestTasklet_Exec_Shutdown asserts a script will be executed once more 126 // when told to shutdown. 127 func TestTasklet_Exec_Shutdown(t *testing.T) { 128 exec := newSimpleExec(0, nil) 129 shutdown := make(chan struct{}) 130 tm := newTaskletMock(exec, testlog.HCLogger(t), time.Hour, 3*time.Second) 131 tm.shutdownCh = shutdown 132 handle := tm.run() 133 134 defer handle.cancel() // just-in-case cleanup 135 close(shutdown) // tell script to exit 136 137 select { 138 case update := <-tm.calls: 139 if update.err != nil { 140 t.Errorf("expected clean shutdown but received %q", update.err) 141 } 142 case <-time.After(3 * time.Second): 143 t.Fatalf("timed out waiting for script check to exit") 144 } 145 146 select { 147 case <-handle.wait(): // ok 148 case <-time.After(3 * time.Second): 149 t.Fatalf("timed out waiting for script check to exit") 150 } 151 } 152 153 // test helpers 154 155 type taskletMock struct { 156 tasklet 157 calls chan execResult 158 } 159 160 func newTaskletMock(exec interfaces.ScriptExecutor, logger hclog.Logger, interval, timeout time.Duration) *taskletMock { 161 tm := &taskletMock{calls: make(chan execResult)} 162 tm.exec = exec 163 tm.logger = logger 164 tm.Interval = interval 165 tm.Timeout = timeout 166 tm.callback = func(ctx context.Context, params execResult) { 167 tm.calls <- params 168 } 169 return tm 170 } 171 172 // blockingScriptExec implements ScriptExec by running a subcommand that never 173 // exits. 174 type blockingScriptExec struct { 175 // pctx is canceled *only* for test cleanup. Just like real 176 // ScriptExecutors its Exec method cannot be canceled directly -- only 177 // with a timeout. 178 pctx context.Context 179 180 // running is ticked before blocking to allow synchronizing operations 181 running chan struct{} 182 183 // set to 1 with atomics if Exec is called and has exited 184 exited int32 185 } 186 187 // newBlockingScriptExec returns a ScriptExecutor that blocks Exec() until the 188 // caller recvs on the b.running chan. It also returns a CancelFunc for test 189 // cleanup only. The runtime cannot cancel ScriptExecutors before their timeout 190 // expires. 191 func newBlockingScriptExec() (*blockingScriptExec, context.CancelFunc) { 192 ctx, cancel := context.WithCancel(context.Background()) 193 exec := &blockingScriptExec{ 194 pctx: ctx, 195 running: make(chan struct{}), 196 } 197 return exec, cancel 198 } 199 200 func (b *blockingScriptExec) Exec(dur time.Duration, _ string, _ []string) ([]byte, int, error) { 201 b.running <- struct{}{} 202 ctx, cancel := context.WithTimeout(b.pctx, dur) 203 defer cancel() 204 cmd := exec.CommandContext(ctx, testtask.Path(), "sleep", "9000h") 205 testtask.SetCmdEnv(cmd) 206 err := cmd.Run() 207 code := 0 208 if exitErr, ok := err.(*exec.ExitError); ok { 209 if !exitErr.Success() { 210 code = 1 211 } 212 } 213 atomic.StoreInt32(&b.exited, 1) 214 return []byte{}, code, err 215 } 216 217 // sleeperExec sleeps for 100ms but returns successfully to allow testing timeout conditions 218 type sleeperExec struct{} 219 220 func (sleeperExec) Exec(time.Duration, string, []string) ([]byte, int, error) { 221 time.Sleep(100 * time.Millisecond) 222 return []byte{}, 0, nil 223 } 224 225 // simpleExec is a fake ScriptExecutor that returns whatever is specified. 226 type simpleExec struct { 227 code int 228 err error 229 } 230 231 func (s simpleExec) Exec(time.Duration, string, []string) ([]byte, int, error) { 232 return []byte(fmt.Sprintf("code=%d err=%v", s.code, s.err)), s.code, s.err 233 } 234 235 // newSimpleExec creates a new ScriptExecutor that returns the given code and err. 236 func newSimpleExec(code int, err error) simpleExec { 237 return simpleExec{code: code, err: err} 238 } 239 240 // scriptedExec is a fake ScriptExecutor with a predetermined sequence 241 // of results. 242 type scriptedExec struct { 243 fn func() ([]byte, int, error) 244 } 245 246 // For each call to Exec, scriptedExec returns the next result in its 247 // sequence of results 248 func (s scriptedExec) Exec(time.Duration, string, []string) ([]byte, int, error) { 249 return s.fn() 250 } 251 252 func newScriptedExec(results []execResult) scriptedExec { 253 index := 0 254 s := scriptedExec{} 255 // we have to close over the index because the interface we're 256 // mocking expects a value and not a pointer, which prevents 257 // us from updating the index 258 fn := func() ([]byte, int, error) { 259 result := results[index] 260 // prevents us from iterating off the end of the results 261 if index+1 < len(results) { 262 index = index + 1 263 } 264 return result.output, result.code, result.err 265 } 266 s.fn = fn 267 return s 268 }