github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/stats_hook_test.go (about)

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