anycast: drop pods from nexthop set on DeletionTimestamp
Build flock Image / build (push) Has been cancelled

Previously the AnycastReconciler kept a pod in the nexthop set as long as
its PodReady condition was True. During a rolling restart that produces a
window after kubelet has accepted SIGTERM (DeletionTimestamp set, pod
still Ready until probes observe shutdown) where BGP still advertises a
path through the dying pod's veth — in-flight requests get RST'd when
the container actually exits.

Fix: introduce podAnycastEligible(pod) = !DeletionTimestamp && Ready,
swap it in at the AnycastReconciler's isReady callback, and fire the
ready-change callback when DeletionTimestamp transitions (the informer
UpdateFunc previously only fired on Ready transitions).

Result: as soon as the apiserver marks a pod for deletion, the
reconciler withdraws the local nexthop and BIRD reannounces the route
without it. Sibling replicas absorb traffic before the pod's
terminationGracePeriod elapses.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-25 22:24:50 -05:00
parent e9d3eef2cc
commit c61b12204c
3 changed files with 62 additions and 3 deletions
+46
View File
@@ -0,0 +1,46 @@
package agent
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func readyPod(deletionTimestamp *metav1.Time) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: deletionTimestamp},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
},
},
}
}
func TestPodAnycastEligible(t *testing.T) {
now := metav1.Now()
cases := []struct {
name string
pod *corev1.Pod
want bool
}{
{"ready, not deleting", readyPod(nil), true},
{"ready, but deleting", readyPod(&now), false},
{
"not ready, not deleting",
&corev1.Pod{Status: corev1.PodStatus{Conditions: []corev1.PodCondition{
{Type: corev1.PodReady, Status: corev1.ConditionFalse},
}}},
false,
},
{"no conditions, not deleting", &corev1.Pod{}, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := podAnycastEligible(c.pod); got != c.want {
t.Fatalf("got %v want %v", got, c.want)
}
})
}
}