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 }