github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/allocrunner/taskrunner/stats_hook_test.go (about)

     1  package taskrunner
     2  
     3  import (
     4  	"context"
     5  	"sync/atomic"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    10  	cstructs "github.com/hashicorp/nomad/client/structs"
    11  	"github.com/hashicorp/nomad/helper/testlog"
    12  	"github.com/stretchr/testify/require"
    13  )
    14  
    15  // Statically assert the stats hook implements the expected interfaces
    16  var _ interfaces.TaskPoststartHook = (*statsHook)(nil)
    17  var _ interfaces.TaskExitedHook = (*statsHook)(nil)
    18  var _ interfaces.ShutdownHook = (*statsHook)(nil)
    19  
    20  type mockStatsUpdater struct {
    21  	// Ch is sent task resource usage updates if not nil
    22  	Ch chan *cstructs.TaskResourceUsage
    23  }
    24  
    25  // newMockStatsUpdater returns a mockStatsUpdater that blocks on Ch for every
    26  // call to UpdateStats
    27  func newMockStatsUpdater() *mockStatsUpdater {
    28  	return &mockStatsUpdater{
    29  		Ch: make(chan *cstructs.TaskResourceUsage),
    30  	}
    31  }
    32  
    33  func (m *mockStatsUpdater) UpdateStats(ru *cstructs.TaskResourceUsage) {
    34  	if m.Ch != nil {
    35  		m.Ch <- ru
    36  	}
    37  }
    38  
    39  type mockDriverStats struct {
    40  	called uint32
    41  
    42  	// err is returned by Stats if it is non-nil
    43  	err error
    44  }
    45  
    46  func (m *mockDriverStats) Stats(ctx context.Context, interval time.Duration) (<-chan *cstructs.TaskResourceUsage, error) {
    47  	atomic.AddUint32(&m.called, 1)
    48  
    49  	if m.err != nil {
    50  		return nil, m.err
    51  	}
    52  	ru := &cstructs.TaskResourceUsage{
    53  		ResourceUsage: &cstructs.ResourceUsage{
    54  			MemoryStats: &cstructs.MemoryStats{
    55  				RSS:      1,
    56  				Measured: []string{"RSS"},
    57  			},
    58  			CpuStats: &cstructs.CpuStats{
    59  				SystemMode: 1,
    60  				Measured:   []string{"System Mode"},
    61  			},
    62  		},
    63  		Timestamp: time.Now().UnixNano(),
    64  		Pids:      map[string]*cstructs.ResourceUsage{},
    65  	}
    66  	ru.Pids["task"] = ru.ResourceUsage
    67  	ch := make(chan *cstructs.TaskResourceUsage)
    68  	go func() {
    69  		defer close(ch)
    70  		select {
    71  		case <-ctx.Done():
    72  		case ch <- ru:
    73  		}
    74  	}()
    75  	return ch, nil
    76  }
    77  
    78  func (m *mockDriverStats) Called() int {
    79  	return int(atomic.LoadUint32(&m.called))
    80  }
    81  
    82  // TestTaskRunner_StatsHook_PoststartExited asserts the stats hook starts and
    83  // stops.
    84  func TestTaskRunner_StatsHook_PoststartExited(t *testing.T) {
    85  	t.Parallel()
    86  
    87  	require := require.New(t)
    88  	logger := testlog.HCLogger(t)
    89  	su := newMockStatsUpdater()
    90  	ds := new(mockDriverStats)
    91  
    92  	poststartReq := &interfaces.TaskPoststartRequest{DriverStats: ds}
    93  
    94  	// Create hook
    95  	h := newStatsHook(su, time.Minute, logger)
    96  
    97  	// Always call Exited to cleanup goroutines
    98  	defer h.Exited(context.Background(), nil, nil)
    99  
   100  	// Run prestart
   101  	require.NoError(h.Poststart(context.Background(), poststartReq, nil))
   102  
   103  	// An initial stats collection should run and call the updater
   104  	select {
   105  	case ru := <-su.Ch:
   106  		require.Equal(uint64(1), ru.ResourceUsage.MemoryStats.RSS)
   107  	case <-time.After(10 * time.Second):
   108  		t.Fatalf("timeout waiting for initial stats collection")
   109  	}
   110  
   111  	require.NoError(h.Exited(context.Background(), nil, nil))
   112  }
   113  
   114  // TestTaskRunner_StatsHook_Periodic asserts the stats hook collects stats on
   115  // an interval.
   116  func TestTaskRunner_StatsHook_Periodic(t *testing.T) {
   117  	t.Parallel()
   118  
   119  	require := require.New(t)
   120  	logger := testlog.HCLogger(t)
   121  	su := newMockStatsUpdater()
   122  
   123  	ds := new(mockDriverStats)
   124  	poststartReq := &interfaces.TaskPoststartRequest{DriverStats: ds}
   125  
   126  	// interval needs to be high enough that even on a slow/busy VM
   127  	// Exited() can complete within the interval.
   128  	const interval = 500 * time.Millisecond
   129  
   130  	h := newStatsHook(su, interval, logger)
   131  	defer h.Exited(context.Background(), nil, nil)
   132  
   133  	// Run prestart
   134  	require.NoError(h.Poststart(context.Background(), poststartReq, nil))
   135  
   136  	// An initial stats collection should run and call the updater
   137  	var firstrun int64
   138  	select {
   139  	case ru := <-su.Ch:
   140  		if ru.Timestamp <= 0 {
   141  			t.Fatalf("expected nonzero timestamp (%v)", ru.Timestamp)
   142  		}
   143  		firstrun = ru.Timestamp
   144  	case <-time.After(10 * time.Second):
   145  		t.Fatalf("timeout waiting for initial stats collection")
   146  	}
   147  
   148  	// Should get another update in ~500ms (see interval above)
   149  	select {
   150  	case ru := <-su.Ch:
   151  		if ru.Timestamp <= firstrun {
   152  			t.Fatalf("expected timestamp (%v) after first run (%v)", ru.Timestamp, firstrun)
   153  		}
   154  	case <-time.After(10 * time.Second):
   155  		t.Fatalf("timeout waiting for second stats collection")
   156  	}
   157  
   158  	// Exiting should prevent further updates
   159  	require.NoError(h.Exited(context.Background(), nil, nil))
   160  
   161  	// Should *not* get another update in ~500ms (see interval above)
   162  	// we may get a single update due to race with exit
   163  	timeout := time.After(2 * interval)
   164  	firstUpdate := true
   165  
   166  WAITING:
   167  	select {
   168  	case ru := <-su.Ch:
   169  		if firstUpdate {
   170  			firstUpdate = false
   171  			goto WAITING
   172  		}
   173  		t.Fatalf("unexpected update after exit (firstrun=%v; update=%v", firstrun, ru.Timestamp)
   174  	case <-timeout:
   175  		// Ok! No update after exit as expected.
   176  	}
   177  }
   178  
   179  // TestTaskRunner_StatsHook_NotImplemented asserts the stats hook stops if the
   180  // driver returns NotImplemented.
   181  func TestTaskRunner_StatsHook_NotImplemented(t *testing.T) {
   182  	t.Parallel()
   183  
   184  	require := require.New(t)
   185  	logger := testlog.HCLogger(t)
   186  	su := newMockStatsUpdater()
   187  	ds := &mockDriverStats{
   188  		err: cstructs.DriverStatsNotImplemented,
   189  	}
   190  
   191  	poststartReq := &interfaces.TaskPoststartRequest{DriverStats: ds}
   192  
   193  	h := newStatsHook(su, 1, logger)
   194  	defer h.Exited(context.Background(), nil, nil)
   195  
   196  	// Run prestart
   197  	require.NoError(h.Poststart(context.Background(), poststartReq, nil))
   198  
   199  	// An initial stats collection should run and *not* call the updater
   200  	select {
   201  	case ru := <-su.Ch:
   202  		t.Fatalf("unexpected resource update (timestamp=%v)", ru.Timestamp)
   203  	case <-time.After(500 * time.Millisecond):
   204  		// Ok! No update received because error was returned
   205  	}
   206  }
   207  
   208  // TestTaskRunner_StatsHook_Backoff asserts that stats hook does some backoff
   209  // even if the driver doesn't support intervals well
   210  func TestTaskRunner_StatsHook_Backoff(t *testing.T) {
   211  	t.Parallel()
   212  
   213  	logger := testlog.HCLogger(t)
   214  	su := newMockStatsUpdater()
   215  	ds := &mockDriverStats{}
   216  
   217  	poststartReq := &interfaces.TaskPoststartRequest{DriverStats: ds}
   218  
   219  	h := newStatsHook(su, time.Minute, logger)
   220  	defer h.Exited(context.Background(), nil, nil)
   221  
   222  	// Run prestart
   223  	require.NoError(t, h.Poststart(context.Background(), poststartReq, nil))
   224  
   225  	timeout := time.After(500 * time.Millisecond)
   226  
   227  DRAIN:
   228  	for {
   229  		select {
   230  		case <-su.Ch:
   231  		case <-timeout:
   232  			break DRAIN
   233  		}
   234  	}
   235  
   236  	require.Equal(t, ds.Called(), 1)
   237  }