github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/tasklet.go (about) 1 package taskrunner 2 3 import ( 4 "context" 5 "time" 6 7 metrics "github.com/armon/go-metrics" 8 log "github.com/hashicorp/go-hclog" 9 10 "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces" 11 ) 12 13 // contextExec allows canceling a interfaces.ScriptExecutor with a context. 14 type contextExec struct { 15 // pctx is the parent context. A subcontext will be created with Exec's 16 // timeout. 17 pctx context.Context 18 19 // exec to be wrapped in a context 20 exec interfaces.ScriptExecutor 21 } 22 23 func newContextExec(ctx context.Context, exec interfaces.ScriptExecutor) *contextExec { 24 return &contextExec{ 25 pctx: ctx, 26 exec: exec, 27 } 28 } 29 30 // execResult are the outputs of an Exec 31 type execResult struct { 32 output []byte 33 code int 34 err error 35 } 36 37 // Exec a command until the timeout expires, the context is canceled, or the 38 // underlying Exec returns. 39 func (c *contextExec) Exec(timeout time.Duration, cmd string, args []string) ([]byte, int, error) { 40 resCh := make(chan execResult, 1) 41 42 // Don't trust the underlying implementation to obey timeout 43 ctx, cancel := context.WithTimeout(c.pctx, timeout) 44 defer cancel() 45 46 go func() { 47 output, code, err := c.exec.Exec(timeout, cmd, args) 48 select { 49 case resCh <- execResult{output, code, err}: 50 case <-ctx.Done(): 51 } 52 }() 53 54 select { 55 case res := <-resCh: 56 return res.output, res.code, res.err 57 case <-ctx.Done(): 58 return nil, 0, ctx.Err() 59 } 60 } 61 62 // tasklet is an abstraction around periodically running a script within 63 // the context of a Task. The interfaces.ScriptExecutor is fired at least 64 // once and on each interval, and fires a callback whenever the script 65 // is complete. 66 type tasklet struct { 67 Command string // Command is the command to run for tasklet 68 Args []string // Args is a list of arguments for tasklet 69 Interval time.Duration // Interval of the tasklet 70 Timeout time.Duration // Timeout of the tasklet 71 exec interfaces.ScriptExecutor 72 callback taskletCallback 73 logger log.Logger 74 shutdownCh <-chan struct{} 75 } 76 77 // taskletHandle is returned by tasklet.run by cancelling a tasklet and 78 // waiting for it to shutdown. 79 type taskletHandle struct { 80 // cancel the script 81 cancel func() 82 exitCh chan struct{} 83 } 84 85 // wait returns a chan that's closed when the tasklet exits 86 func (t taskletHandle) wait() <-chan struct{} { 87 return t.exitCh 88 } 89 90 // taskletCallback is called with a cancellation context and the output of a 91 // tasklet's Exec whenever it runs. 92 type taskletCallback func(context.Context, execResult) 93 94 // run this tasklet check and return its cancel func. The tasklet's 95 // callback will be called each time it completes. If the shutdownCh is 96 // closed the check will be run once more before exiting. 97 func (t *tasklet) run() *taskletHandle { 98 ctx, cancel := context.WithCancel(context.Background()) 99 exitCh := make(chan struct{}) 100 101 // Wrap the original interfaces.ScriptExecutor in one that obeys context 102 // cancelation. 103 ctxExec := newContextExec(ctx, t.exec) 104 105 go func() { 106 defer close(exitCh) 107 timer := time.NewTimer(0) 108 defer timer.Stop() 109 for { 110 // Block until tasklet is removed, Nomad is shutting 111 // down, or the tasklet interval is up 112 select { 113 case <-ctx.Done(): 114 // tasklet has been removed 115 return 116 case <-t.shutdownCh: 117 // unblock but don't exit until after we run once more 118 case <-timer.C: 119 timer.Reset(t.Interval) 120 } 121 122 metrics.IncrCounter([]string{ 123 "client", "allocrunner", "taskrunner", "tasklet_runs"}, 1) 124 125 // Execute check script with timeout 126 t.logger.Trace("tasklet executing") 127 output, code, err := ctxExec.Exec(t.Timeout, t.Command, t.Args) 128 switch err { 129 case context.Canceled: 130 // check removed during execution; exit 131 return 132 case context.DeadlineExceeded: 133 metrics.IncrCounter([]string{ 134 "client", "allocrunner", "taskrunner", 135 "tasklet_timeouts"}, 1) 136 // If no error was returned, set one to make sure the tasklet 137 // is marked as failed 138 if err == nil { 139 err = context.DeadlineExceeded 140 } 141 142 // Log deadline exceeded every time as it's a 143 // distinct issue from the tasklet returning failure 144 t.logger.Warn("tasklet timed out", "timeout", t.Timeout) 145 } 146 147 t.callback(ctx, execResult{output, code, err}) 148 149 select { 150 case <-t.shutdownCh: 151 // We've been told to exit and just ran so exit 152 return 153 default: 154 } 155 } 156 }() 157 return &taskletHandle{cancel: cancel, exitCh: exitCh} 158 }