1
16
17 package eviction
18
19 import (
20 "fmt"
21 "strings"
22 "sync"
23 "testing"
24 "time"
25
26 gomock "github.com/golang/mock/gomock"
27 "k8s.io/apimachinery/pkg/api/resource"
28 statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
29 evictionapi "k8s.io/kubernetes/pkg/kubelet/eviction/api"
30 )
31
32 const testCgroupPath = "/sys/fs/cgroups/memory"
33
34 func nodeSummary(available, workingSet, usage resource.Quantity, allocatable bool) *statsapi.Summary {
35 availableBytes := uint64(available.Value())
36 workingSetBytes := uint64(workingSet.Value())
37 usageBytes := uint64(usage.Value())
38 memoryStats := statsapi.MemoryStats{
39 AvailableBytes: &availableBytes,
40 WorkingSetBytes: &workingSetBytes,
41 UsageBytes: &usageBytes,
42 }
43 if allocatable {
44 return &statsapi.Summary{
45 Node: statsapi.NodeStats{
46 SystemContainers: []statsapi.ContainerStats{
47 {
48 Name: statsapi.SystemContainerPods,
49 Memory: &memoryStats,
50 },
51 },
52 },
53 }
54 }
55 return &statsapi.Summary{
56 Node: statsapi.NodeStats{
57 Memory: &memoryStats,
58 },
59 }
60 }
61
62 func newTestMemoryThresholdNotifier(threshold evictionapi.Threshold, factory NotifierFactory, handler func(string)) *memoryThresholdNotifier {
63 return &memoryThresholdNotifier{
64 threshold: threshold,
65 cgroupPath: testCgroupPath,
66 events: make(chan struct{}),
67 factory: factory,
68 handler: handler,
69 }
70 }
71
72 func TestUpdateThreshold(t *testing.T) {
73 testCases := []struct {
74 description string
75 available resource.Quantity
76 workingSet resource.Quantity
77 usage resource.Quantity
78 evictionThreshold evictionapi.Threshold
79 expectedThreshold resource.Quantity
80 updateThresholdErr error
81 expectErr bool
82 }{
83 {
84 description: "node level threshold",
85 available: resource.MustParse("3Gi"),
86 usage: resource.MustParse("2Gi"),
87 workingSet: resource.MustParse("1Gi"),
88 evictionThreshold: evictionapi.Threshold{
89 Signal: evictionapi.SignalMemoryAvailable,
90 Operator: evictionapi.OpLessThan,
91 Value: evictionapi.ThresholdValue{
92 Quantity: quantityMustParse("1Gi"),
93 },
94 },
95 expectedThreshold: resource.MustParse("4Gi"),
96 updateThresholdErr: nil,
97 expectErr: false,
98 },
99 {
100 description: "allocatable threshold",
101 available: resource.MustParse("4Gi"),
102 usage: resource.MustParse("3Gi"),
103 workingSet: resource.MustParse("1Gi"),
104 evictionThreshold: evictionapi.Threshold{
105 Signal: evictionapi.SignalAllocatableMemoryAvailable,
106 Operator: evictionapi.OpLessThan,
107 Value: evictionapi.ThresholdValue{
108 Quantity: quantityMustParse("1Gi"),
109 },
110 },
111 expectedThreshold: resource.MustParse("6Gi"),
112 updateThresholdErr: nil,
113 expectErr: false,
114 },
115 {
116 description: "error updating node level threshold",
117 available: resource.MustParse("3Gi"),
118 usage: resource.MustParse("2Gi"),
119 workingSet: resource.MustParse("1Gi"),
120 evictionThreshold: evictionapi.Threshold{
121 Signal: evictionapi.SignalMemoryAvailable,
122 Operator: evictionapi.OpLessThan,
123 Value: evictionapi.ThresholdValue{
124 Quantity: quantityMustParse("1Gi"),
125 },
126 },
127 expectedThreshold: resource.MustParse("4Gi"),
128 updateThresholdErr: fmt.Errorf("unexpected error"),
129 expectErr: true,
130 },
131 }
132
133 mockCtrl := gomock.NewController(t)
134 defer mockCtrl.Finish()
135
136 for _, tc := range testCases {
137 t.Run(tc.description, func(t *testing.T) {
138 notifierFactory := NewMockNotifierFactory(mockCtrl)
139 notifier := NewMockCgroupNotifier(mockCtrl)
140
141 m := newTestMemoryThresholdNotifier(tc.evictionThreshold, notifierFactory, nil)
142 notifierFactory.EXPECT().NewCgroupNotifier(testCgroupPath, memoryUsageAttribute, tc.expectedThreshold.Value()).Return(notifier, tc.updateThresholdErr).Times(1)
143 var events chan<- struct{} = m.events
144 notifier.EXPECT().Start(events).Return().AnyTimes()
145 err := m.UpdateThreshold(nodeSummary(tc.available, tc.workingSet, tc.usage, isAllocatableEvictionThreshold(tc.evictionThreshold)))
146 if err != nil && !tc.expectErr {
147 t.Errorf("Unexpected error updating threshold: %v", err)
148 } else if err == nil && tc.expectErr {
149 t.Errorf("Expected error updating threshold, but got nil")
150 }
151 })
152 }
153 }
154
155 func TestStart(t *testing.T) {
156 noResources := resource.MustParse("0")
157 threshold := evictionapi.Threshold{
158 Signal: evictionapi.SignalMemoryAvailable,
159 Operator: evictionapi.OpLessThan,
160 Value: evictionapi.ThresholdValue{
161 Quantity: &noResources,
162 },
163 }
164 mockCtrl := gomock.NewController(t)
165 defer mockCtrl.Finish()
166 notifierFactory := NewMockNotifierFactory(mockCtrl)
167 notifier := NewMockCgroupNotifier(mockCtrl)
168
169 var wg sync.WaitGroup
170 wg.Add(4)
171 m := newTestMemoryThresholdNotifier(threshold, notifierFactory, func(string) {
172 wg.Done()
173 })
174 notifierFactory.EXPECT().NewCgroupNotifier(testCgroupPath, memoryUsageAttribute, int64(0)).Return(notifier, nil).Times(1)
175
176 var events chan<- struct{} = m.events
177 notifier.EXPECT().Start(events).DoAndReturn(func(events chan<- struct{}) {
178 for i := 0; i < 4; i++ {
179 events <- struct{}{}
180 }
181 })
182
183 err := m.UpdateThreshold(nodeSummary(noResources, noResources, noResources, isAllocatableEvictionThreshold(threshold)))
184 if err != nil {
185 t.Errorf("Unexpected error updating threshold: %v", err)
186 }
187
188 go m.Start()
189
190 wg.Wait()
191 }
192
193 func TestThresholdDescription(t *testing.T) {
194 testCases := []struct {
195 description string
196 evictionThreshold evictionapi.Threshold
197 expectedSubstrings []string
198 omittedSubstrings []string
199 }{
200 {
201 description: "hard node level threshold",
202 evictionThreshold: evictionapi.Threshold{
203 Signal: evictionapi.SignalMemoryAvailable,
204 Operator: evictionapi.OpLessThan,
205 Value: evictionapi.ThresholdValue{
206 Quantity: quantityMustParse("2Gi"),
207 },
208 },
209 expectedSubstrings: []string{"hard"},
210 omittedSubstrings: []string{"allocatable", "soft"},
211 },
212 {
213 description: "soft node level threshold",
214 evictionThreshold: evictionapi.Threshold{
215 Signal: evictionapi.SignalMemoryAvailable,
216 Operator: evictionapi.OpLessThan,
217 Value: evictionapi.ThresholdValue{
218 Quantity: quantityMustParse("2Gi"),
219 },
220 GracePeriod: time.Minute * 2,
221 },
222 expectedSubstrings: []string{"soft"},
223 omittedSubstrings: []string{"allocatable", "hard"},
224 },
225 {
226 description: "hard allocatable threshold",
227 evictionThreshold: evictionapi.Threshold{
228 Signal: evictionapi.SignalAllocatableMemoryAvailable,
229 Operator: evictionapi.OpLessThan,
230 Value: evictionapi.ThresholdValue{
231 Quantity: quantityMustParse("2Gi"),
232 },
233 GracePeriod: time.Minute * 2,
234 },
235 expectedSubstrings: []string{"soft", "allocatable"},
236 omittedSubstrings: []string{"hard"},
237 },
238 {
239 description: "soft allocatable threshold",
240 evictionThreshold: evictionapi.Threshold{
241 Signal: evictionapi.SignalAllocatableMemoryAvailable,
242 Operator: evictionapi.OpLessThan,
243 Value: evictionapi.ThresholdValue{
244 Quantity: quantityMustParse("2Gi"),
245 },
246 },
247 expectedSubstrings: []string{"hard", "allocatable"},
248 omittedSubstrings: []string{"soft"},
249 },
250 }
251
252 for _, tc := range testCases {
253 t.Run(tc.description, func(t *testing.T) {
254 m := &memoryThresholdNotifier{
255 notifier: &MockCgroupNotifier{},
256 threshold: tc.evictionThreshold,
257 cgroupPath: testCgroupPath,
258 }
259 desc := m.Description()
260 for _, expected := range tc.expectedSubstrings {
261 if !strings.Contains(desc, expected) {
262 t.Errorf("expected description for notifier with threshold %+v to contain %s, but it did not", tc.evictionThreshold, expected)
263 }
264 }
265 for _, omitted := range tc.omittedSubstrings {
266 if strings.Contains(desc, omitted) {
267 t.Errorf("expected description for notifier with threshold %+v NOT to contain %s, but it did", tc.evictionThreshold, omitted)
268 }
269 }
270 })
271 }
272 }
273
View as plain text