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  }