diff --git a/pkg/fleets/controller.go b/pkg/fleets/controller.go index b18f26d33e..e3d0afb2b7 100644 --- a/pkg/fleets/controller.go +++ b/pkg/fleets/controller.go @@ -729,14 +729,29 @@ func (c *Controller) updateFleetStatus(ctx context.Context, fleet *agonesv1.Flee // filterGameServerSetByActive returns the active GameServerSet (or nil if it // doesn't exist) and then the rest of the GameServerSets that are controlled -// by this Fleet +// by this Fleet. +// If multiple GameServerSets match the Fleet's template (e.g. due to informer +// cache lag during creation), the oldest one by CreationTimestamp is kept as +// active and the duplicates are placed in rest so they can be scaled down and +// cleaned up by the normal deployment strategy. func (c *Controller) filterGameServerSetByActive(fleet *agonesv1.Fleet, list []*agonesv1.GameServerSet) (*agonesv1.GameServerSet, []*agonesv1.GameServerSet) { var active *agonesv1.GameServerSet var rest []*agonesv1.GameServerSet for _, gsSet := range list { if apiequality.Semantic.DeepEqual(gsSet.Spec.Template, fleet.Spec.Template) { - active = gsSet + if active == nil { + active = gsSet + } else { + // Multiple GameServerSets match the fleet template. + // Keep the oldest as active; treat the newer duplicate as rest. + if gsSet.ObjectMeta.CreationTimestamp.Before(&active.ObjectMeta.CreationTimestamp) { + rest = append(rest, active) + active = gsSet + } else { + rest = append(rest, gsSet) + } + } } else { rest = append(rest, gsSet) } diff --git a/pkg/fleets/controller_test.go b/pkg/fleets/controller_test.go index bf08627663..378e52039e 100644 --- a/pkg/fleets/controller_test.go +++ b/pkg/fleets/controller_test.go @@ -991,6 +991,61 @@ func TestControllerFilterGameServerSetByActive(t *testing.T) { assert.Equal(t, []*agonesv1.GameServerSet{gsSet1, gsSet2}, rest) } +func TestControllerFilterGameServerSetByActiveDuplicates(t *testing.T) { + t.Parallel() + + f := defaultFixture() + c, _ := newFakeController() + + now := time.Now() + + // Three GameServerSets with the same template (simulates duplicate creation + // caused by informer cache lag). + gsSet1 := f.GameServerSet() + gsSet1.ObjectMeta.Name = "gsSet1" + gsSet1.ObjectMeta.UID = "uid-1" + gsSet1.ObjectMeta.CreationTimestamp = metav1.NewTime(now.Add(-2 * time.Minute)) + + gsSet2 := f.GameServerSet() + gsSet2.ObjectMeta.Name = "gsSet2" + gsSet2.ObjectMeta.UID = "uid-2" + gsSet2.ObjectMeta.CreationTimestamp = metav1.NewTime(now.Add(-1 * time.Minute)) + + gsSet3 := f.GameServerSet() + gsSet3.ObjectMeta.Name = "gsSet3" + gsSet3.ObjectMeta.UID = "uid-3" + gsSet3.ObjectMeta.CreationTimestamp = metav1.NewTime(now) + + // Different template GameServerSet (from a previous Fleet spec). + gsSet4 := f.GameServerSet() + gsSet4.ObjectMeta.Name = "gsSet4" + gsSet4.ObjectMeta.UID = "uid-4" + gsSet4.Spec.Template.Spec.Ports = []agonesv1.GameServerPort{{HostPort: 9999}} + + // With 3 duplicate-template GSS, oldest should be active, others in rest. + active, rest := c.filterGameServerSetByActive(f, []*agonesv1.GameServerSet{gsSet1, gsSet2, gsSet3}) + assert.Equal(t, gsSet1, active, "oldest GameServerSet should be active") + assert.Len(t, rest, 2) + assert.Contains(t, rest, gsSet2) + assert.Contains(t, rest, gsSet3) + + // Order should not matter — even if newest comes first in the list, + // oldest should still be active. + active, rest = c.filterGameServerSetByActive(f, []*agonesv1.GameServerSet{gsSet3, gsSet1, gsSet2}) + assert.Equal(t, gsSet1, active, "oldest GameServerSet should be active regardless of list order") + assert.Len(t, rest, 2) + assert.Contains(t, rest, gsSet2) + assert.Contains(t, rest, gsSet3) + + // Mix of matching and non-matching templates. + active, rest = c.filterGameServerSetByActive(f, []*agonesv1.GameServerSet{gsSet3, gsSet4, gsSet1, gsSet2}) + assert.Equal(t, gsSet1, active, "oldest matching GameServerSet should be active") + assert.Len(t, rest, 3) + assert.Contains(t, rest, gsSet2) + assert.Contains(t, rest, gsSet3) + assert.Contains(t, rest, gsSet4) +} + func TestControllerRecreateDeployment(t *testing.T) { t.Parallel()