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  }