@@ -1195,5 +1195,136 @@ func TestReconciler_MultipleEventsOnNodeCancelledByUnQuarantine(t *testing.T) {
11951195
11961196 pod2 , err := setup .client .CoreV1 ().Pods ("timeout-test" ).Get (setup .ctx , "pod-2" , metav1.GetOptions {})
11971197 require .NoError (t , err )
1198- assert .Nil (t , pod2 .DeletionTimestamp , "pod-2 should not be deleted" )
1198+ return metric .Counter .GetValue ()
1199+ }
1200+
1201+ func getCounterVecValue (t * testing.T , counterVec * prometheus.CounterVec , labelValues ... string ) float64 {
1202+ t .Helper ()
1203+ counter , err := counterVec .GetMetricWithLabelValues (labelValues ... )
1204+ require .NoError (t , err )
1205+ metric := & dto.Metric {}
1206+ err = counter .Write (metric )
1207+ require .NoError (t , err )
1208+ return metric .Counter .GetValue ()
1209+ }
1210+
1211+ func getGaugeValue (t * testing.T , gauge prometheus.Gauge ) float64 {
1212+ t .Helper ()
1213+ metric := & dto.Metric {}
1214+ err := gauge .Write (metric )
1215+ require .NoError (t , err )
1216+ return metric .Gauge .GetValue ()
1217+ }
1218+
1219+ func getGaugeVecValue (t * testing.T , gaugeVec * prometheus.GaugeVec , labelValues ... string ) float64 {
1220+ t .Helper ()
1221+ gauge , err := gaugeVec .GetMetricWithLabelValues (labelValues ... )
1222+ require .NoError (t , err )
1223+ metric := & dto.Metric {}
1224+ err = gauge .Write (metric )
1225+ require .NoError (t , err )
1226+ return metric .Gauge .GetValue ()
1227+ }
1228+
1229+ func getHistogramCount (t * testing.T , histogram prometheus.Histogram ) uint64 {
1230+ t .Helper ()
1231+ metric := & dto.Metric {}
1232+ err := histogram .Write (metric )
1233+ require .NoError (t , err )
1234+ return metric .Histogram .GetSampleCount ()
1235+ }
1236+
1237+ // TestReconciler_PodListSortingConsistentEvents verifies that pod list sorting prevents duplicate Kubernetes events by ensuring consistent event messages across multiple reconciliation calls.
1238+ func TestReconciler_PodListSortingConsistentEvents (t * testing.T ) {
1239+ setup := setupDirectTest (t , []config.UserNamespace {
1240+ {Name : "test-*" , Mode : config .ModeAllowCompletion },
1241+ }, false )
1242+
1243+ nodeName := "test-node-sorting"
1244+ createNode (setup .ctx , t , setup .client , nodeName )
1245+ createNamespace (setup .ctx , t , setup .client , "test-sorting" )
1246+
1247+ for _ , podName := range []string {"ubuntu-gpu-deployment-64c54f5b4c-2c4c5" , "alloy-metrics-55d6fbdbd-vrp2h" , "alloy-gateway-0" } {
1248+ createPod (setup .ctx , t , setup .client , "test-sorting" , podName , nodeName , v1 .PodRunning )
1249+ }
1250+
1251+ // Wait for all pods to be visible in the informer cache
1252+ require .Eventually (t , func () bool {
1253+ pods , err := setup .informersInstance .FindEvictablePodsInNamespaceAndNode ("test-sorting" , nodeName )
1254+ if err != nil {
1255+ return false
1256+ }
1257+ return len (pods ) == 3
1258+ }, 30 * time .Second , 100 * time .Millisecond , "All 3 pods should be visible in informer cache before test starts" )
1259+
1260+ const numIterations = 5
1261+ var referenceMessage string
1262+ var mismatchFound bool
1263+ var mismatchIteration int
1264+ var mismatchMessage string
1265+
1266+ for i := 0 ; i < numIterations ; i ++ {
1267+ setup .client .CoreV1 ().Events (metav1 .NamespaceDefault ).DeleteCollection (
1268+ setup .ctx , metav1.DeleteOptions {}, metav1.ListOptions {})
1269+
1270+ // Wait for events to be deleted
1271+ require .Eventually (t , func () bool {
1272+ events , _ := setup .client .CoreV1 ().Events (metav1 .NamespaceDefault ).List (
1273+ setup .ctx ,
1274+ metav1.ListOptions {
1275+ FieldSelector : fmt .Sprintf ("involvedObject.name=%s,involvedObject.kind=Node" , nodeName ),
1276+ },
1277+ )
1278+ return len (events .Items ) == 0
1279+ }, 5 * time .Second , 50 * time .Millisecond , "Events should be deleted before next iteration" )
1280+
1281+ err := processHealthEvent (setup .ctx , t , setup .reconciler , setup .mockCollection , healthEventOptions {
1282+ nodeName : nodeName ,
1283+ nodeQuarantined : model .Quarantined ,
1284+ })
1285+
1286+ assert .Error (t , err )
1287+
1288+ // Wait for new event to be created with the pod list
1289+ var currentMessage string
1290+ require .Eventually (t , func () bool {
1291+ events , err := setup .client .CoreV1 ().Events (metav1 .NamespaceDefault ).List (
1292+ setup .ctx ,
1293+ metav1.ListOptions {
1294+ FieldSelector : fmt .Sprintf ("involvedObject.name=%s,involvedObject.kind=Node" , nodeName ),
1295+ },
1296+ )
1297+ if err != nil || len (events .Items ) == 0 {
1298+ return false
1299+ }
1300+ msg := events .Items [len (events .Items )- 1 ].Message
1301+ if len (msg ) > 0 && msg != "" {
1302+ currentMessage = msg
1303+ return true
1304+ }
1305+ return false
1306+ }, 5 * time .Second , 50 * time .Millisecond , "Event with pod list should be created" )
1307+
1308+ if i == 0 {
1309+ referenceMessage = currentMessage
1310+ continue
1311+ }
1312+
1313+ if currentMessage != referenceMessage {
1314+ mismatchFound = true
1315+ mismatchIteration = i
1316+ mismatchMessage = currentMessage
1317+ break
1318+ }
1319+ }
1320+
1321+ require .NotEmpty (t , referenceMessage , "No event messages were generated" )
1322+
1323+ if mismatchFound {
1324+ assert .Fail (t , fmt .Sprintf ("FAIL: Iteration %d produced different message. sort.Strings() missing from reconciler.go:252\n Expected: %s\n Got: %s" ,
1325+ mismatchIteration , referenceMessage , mismatchMessage ))
1326+ }
1327+
1328+ expectedMessage := "Waiting for following pods to finish: [test-sorting/alloy-gateway-0 test-sorting/alloy-metrics-55d6fbdbd-vrp2h test-sorting/ubuntu-gpu-deployment-64c54f5b4c-2c4c5]"
1329+ assert .Equal (t , expectedMessage , referenceMessage , "FAIL: Pods not in same order" )
11991330}
0 commit comments