From 840fa8ba5837042380b463643df5df54eea4de08 Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Tue, 30 Jul 2024 17:10:07 +0000 Subject: [PATCH 1/7] Remove unnecessary mustParseDate calls within validation test --- pkg/apis/autoscaling/v1/fleetautoscaler_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/apis/autoscaling/v1/fleetautoscaler_test.go b/pkg/apis/autoscaling/v1/fleetautoscaler_test.go index 957cd07153..05cd149cc1 100644 --- a/pkg/apis/autoscaling/v1/fleetautoscaler_test.go +++ b/pkg/apis/autoscaling/v1/fleetautoscaler_test.go @@ -488,8 +488,6 @@ func TestFleetAutoscalerScheduleValidateUpdate(t *testing.T) { }, "end time before start time": { fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) { - mustParseDate("3999-06-15T15:59:59Z") - mustParseDate("3999-05-15T15:59:59Z") fap.Schedule.Between.Start = mustParseDate("3999-06-15T15:59:59Z") fap.Schedule.Between.End = mustParseDate("3999-05-15T15:59:59Z") }), From f50716f248b1775e5383f3d06f29dcfca9c52644 Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Tue, 30 Jul 2024 17:12:12 +0000 Subject: [PATCH 2/7] Add application logic for Schedule and Chain Policy within the autoscaler --- pkg/fleetautoscalers/controller.go | 2 +- pkg/fleetautoscalers/fleetautoscalers.go | 99 +++++++++++++++++-- pkg/fleetautoscalers/fleetautoscalers_test.go | 2 +- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/pkg/fleetautoscalers/controller.go b/pkg/fleetautoscalers/controller.go index bfbfb5e4ed..8fe969b9b7 100644 --- a/pkg/fleetautoscalers/controller.go +++ b/pkg/fleetautoscalers/controller.go @@ -313,7 +313,7 @@ func (c *Controller) syncFleetAutoscaler(ctx context.Context, key string) error } currentReplicas := fleet.Status.Replicas - desiredReplicas, scalingLimited, err := computeDesiredFleetSize(fas, fleet, c.gameServerLister, c.counter.Counts()) + desiredReplicas, scalingLimited, err := computeDesiredFleetSize(fas.Spec.Policy, fleet, c.gameServerLister, c.counter.Counts()) if err != nil { c.recorder.Eventf(fas, corev1.EventTypeWarning, "FleetAutoscaler", "Error calculating desired fleet size on FleetAutoscaler %s. Error: %s", fas.ObjectMeta.Name, err.Error()) diff --git a/pkg/fleetautoscalers/fleetautoscalers.go b/pkg/fleetautoscalers/fleetautoscalers.go index 5ef77e0edf..2df38e552f 100644 --- a/pkg/fleetautoscalers/fleetautoscalers.go +++ b/pkg/fleetautoscalers/fleetautoscalers.go @@ -29,6 +29,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/robfig/cron/v3" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/uuid" @@ -50,17 +51,21 @@ var client = http.Client{ } // computeDesiredFleetSize computes the new desired size of the given fleet -func computeDesiredFleetSize(fas *autoscalingv1.FleetAutoscaler, f *agonesv1.Fleet, +func computeDesiredFleetSize(pol autoscalingv1.FleetAutoscalerPolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { - switch fas.Spec.Policy.Type { + switch pol.Type { case autoscalingv1.BufferPolicyType: - return applyBufferPolicy(fas.Spec.Policy.Buffer, f) + return applyBufferPolicy(pol.Buffer, f) case autoscalingv1.WebhookPolicyType: - return applyWebhookPolicy(fas.Spec.Policy.Webhook, f) + return applyWebhookPolicy(pol.Webhook, f) case autoscalingv1.CounterPolicyType: - return applyCounterOrListPolicy(fas.Spec.Policy.Counter, nil, f, gameServerLister, nodeCounts) + return applyCounterOrListPolicy(pol.Counter, nil, f, gameServerLister, nodeCounts) case autoscalingv1.ListPolicyType: - return applyCounterOrListPolicy(nil, fas.Spec.Policy.List, f, gameServerLister, nodeCounts) + return applyCounterOrListPolicy(nil, pol.List, f, gameServerLister, nodeCounts) + case autoscalingv1.SchedulePolicyType: + return applySchedulePolicy(pol.Schedule, f, gameServerLister, nodeCounts) + case autoscalingv1.ChainPolicyType: + return applyChainPolicy(pol.Chain, f, gameServerLister, nodeCounts) } return 0, false, errors.New("wrong policy type, should be one of: Buffer, Webhook, Counter, List") @@ -362,6 +367,88 @@ func applyCounterOrListPolicy(c *autoscalingv1.CounterPolicy, l *autoscalingv1.L return 0, false, errors.Errorf("unable to apply ListPolicy %v", l) } +func applySchedulePolicy(s *autoscalingv1.SchedulePolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { + // Ensure the scheduled autoscaler feature gate is enabled + if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) { + return 0, false, errors.Errorf("cannot apply SchedulePolicy unless feature flag %s is enabled", runtime.FeatureScheduledAutoscaler) + } + + if isScheduleActive(s) { + return computeDesiredFleetSize(s.Policy, f, gameServerLister, nodeCounts) + } + + return f.Status.Replicas, false, nil +} + +func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { + // Ensure the scheduled autoscaler feature gate is enabled + if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) { + return 0, false, errors.Errorf("cannot apply ChainPolicy unless feature flag %s is enabled", runtime.FeatureScheduledAutoscaler) + } + + // Loop over all entries in the chain + for _, entry := range c { + switch entry.Type { + case autoscalingv1.SchedulePolicyType: + schedRep, schedLim, schedErr := applySchedulePolicy(entry.Schedule, f, gameServerLister, nodeCounts) + // If the schedule is active and no error was returned from the policy, then return the replicas, limited and error + if isScheduleActive(entry.Schedule) && schedErr == nil { + return schedRep, schedLim, schedErr + } + case autoscalingv1.WebhookPolicyType: + webhookRep, webhookLim, webhookErr := applyWebhookPolicy(entry.Webhook, f) + if webhookErr == nil { + return webhookRep, webhookLim, webhookErr + } + default: + return computeDesiredFleetSize(entry.FleetAutoscalerPolicy, f, gameServerLister, nodeCounts) + } + } + + return f.Status.Replicas, false, nil +} + +// isScheduleActive checks if a chain entry's is active and returns a boolean, true if active, false otherwise +func isScheduleActive(s *autoscalingv1.SchedulePolicy) bool { + now := time.Now() + scheduleDelta := time.Minute * -1 + + // If a start time is present and the current time is before the start time, the schedule is inactive so return false + startTime := s.Between.Start.Time + if !startTime.IsZero() && now.Before(startTime) { + return false + } + + // If an end time is present and the current time is after the end time, the schedule is inactive so return false + endTime := s.Between.End.Time + if !endTime.IsZero() && now.After(endTime) { + return false + } + + // If no startCron field is specified, then it's automatically true (duration is no longer relevant since we're always running) + if s.ActivePeriod.StartCron == "" { + return true + } + + location, _ := time.LoadLocation(s.ActivePeriod.Timezone) + startCron, _ := cron.ParseStandard(s.ActivePeriod.StartCron) + nextStart := startCron.Next(now.In(location)).Add(scheduleDelta) + duration, err := time.ParseDuration(s.ActivePeriod.Duration) + + // If there's an err, then the duration field is empty, meaning duration is indefinite + if err != nil { + duration = 0 // Indefinite duration if not set + } + + // If the current time is after the next start time, and the duration is indefinite or the current time is before the next start time + duration, + // then return true + if now.After(nextStart) && (duration == 0 || now.Before(nextStart.Add(duration))) { + return true + } + + return false +} + // getSortedGameServers returns the list of Game Servers for the Fleet in the order in which the // Game Servers would be deleted. func getSortedGameServers(f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, diff --git a/pkg/fleetautoscalers/fleetautoscalers_test.go b/pkg/fleetautoscalers/fleetautoscalers_test.go index 0b1f9bee74..1aeffb6b48 100644 --- a/pkg/fleetautoscalers/fleetautoscalers_test.go +++ b/pkg/fleetautoscalers/fleetautoscalers_test.go @@ -187,7 +187,7 @@ func TestComputeDesiredFleetSize(t *testing.T) { _, cancel := agtesting.StartInformers(m, gameServers.Informer().HasSynced) defer cancel() - replicas, limited, err := computeDesiredFleetSize(fas, f, gameServers.Lister(), nc) + replicas, limited, err := computeDesiredFleetSize(fas.Spec.Policy, f, gameServers.Lister(), nc) if tc.expected.err != "" && assert.NotNil(t, err) { assert.Equal(t, tc.expected.err, err.Error()) From e56c9923161da648a05a2830185d68f04b27d5c4 Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Tue, 30 Jul 2024 17:20:19 +0000 Subject: [PATCH 3/7] replace err w/ nil --- pkg/fleetautoscalers/fleetautoscalers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/fleetautoscalers/fleetautoscalers.go b/pkg/fleetautoscalers/fleetautoscalers.go index 2df38e552f..f8389f0263 100644 --- a/pkg/fleetautoscalers/fleetautoscalers.go +++ b/pkg/fleetautoscalers/fleetautoscalers.go @@ -393,12 +393,12 @@ func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServer schedRep, schedLim, schedErr := applySchedulePolicy(entry.Schedule, f, gameServerLister, nodeCounts) // If the schedule is active and no error was returned from the policy, then return the replicas, limited and error if isScheduleActive(entry.Schedule) && schedErr == nil { - return schedRep, schedLim, schedErr + return schedRep, schedLim, nil } case autoscalingv1.WebhookPolicyType: webhookRep, webhookLim, webhookErr := applyWebhookPolicy(entry.Webhook, f) if webhookErr == nil { - return webhookRep, webhookLim, webhookErr + return webhookRep, webhookLim, nil } default: return computeDesiredFleetSize(entry.FleetAutoscalerPolicy, f, gameServerLister, nodeCounts) From dcc8250416d9c78744986cf906479b6f0b300adc Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Wed, 31 Jul 2024 01:13:28 +0000 Subject: [PATCH 4/7] scaling tests --- pkg/fleetautoscalers/fleetautoscalers_test.go | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) diff --git a/pkg/fleetautoscalers/fleetautoscalers_test.go b/pkg/fleetautoscalers/fleetautoscalers_test.go index 1aeffb6b48..825efd6498 100644 --- a/pkg/fleetautoscalers/fleetautoscalers_test.go +++ b/pkg/fleetautoscalers/fleetautoscalers_test.go @@ -23,6 +23,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" admregv1 "k8s.io/api/admissionregistration/v1" @@ -2134,3 +2135,273 @@ func TestApplyListPolicy(t *testing.T) { }) } } + +// nolint:dupl // Linter errors on lines are duplicate of TestApplySchedulePolicy +// NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateSchedulePolicy) +func TestApplySchedulePolicy(t *testing.T) { + t.Parallel() + + nc := map[string]gameservers.NodeCount{ + "n1": {Ready: 1, Allocated: 1}, + } + modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { + _, fleet := defaultFixtures() + f(fleet) + return fleet + } + + type expected struct { + replicas int32 + limited bool + err string + } + + testCases := map[string]struct { + fleet *agonesv1.Fleet + featureFlags string + specReplicas int32 + statusReplicas int32 + statusAllocatedReplicas int32 + statusReadyReplicas int32 + sp *autoscalingv1.SchedulePolicy + gsList []agonesv1.GameServer + now func() time.Time + want expected + }{ + "differing start and end timezones": {}, + "daylight saving time start": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + sp: &autoscalingv1.SchedulePolicy{}, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "daylight saving time end": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + sp: &autoscalingv1.SchedulePolicy{}, + }, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "daylight saving time cron time": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + sp: &autoscalingv1.SchedulePolicy{}, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "leap year time change": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + sp: &autoscalingv1.SchedulePolicy{}, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "new year": {}, + } + + utilruntime.FeatureTestMutex.Lock() + defer utilruntime.FeatureTestMutex.Unlock() + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := utilruntime.ParseFeatures(tc.featureFlags) + assert.NoError(t, err) + + // For Counters and Lists + m := agtesting.NewMocks() + m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &agonesv1.GameServerList{Items: tc.gsList}, nil + }) + + informer := m.AgonesInformerFactory.Agones().V1() + _, cancel := agtesting.StartInformers(m, + informer.GameServers().Informer().HasSynced) + defer cancel() + + replicas, limited, err := applySchedulePolicy(*tc.sp, tc.fleet, informer.GameServers().Lister(), nc) + + if tc.want.err != "" && assert.NotNil(t, err) { + assert.Equal(t, tc.want.err, err.Error()) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.want.replicas, replicas) + assert.Equal(t, tc.want.limited, limited) + } + }) + } +} + +// nolint:dupl // Linter errors on lines are duplicate of TestApplyChainPolicy +// NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateChainPolicy) +func TestApplyChainPolicy(t *testing.T) { + t.Parallel() + + nc := map[string]gameservers.NodeCount{ + "n1": {Ready: 1, Allocated: 1}, + } + modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { + _, fleet := defaultFixtures() + f(fleet) + return fleet + } + + type expected struct { + replicas int32 + limited bool + err string + } + + testCases := map[string]struct { + fleet *agonesv1.Fleet + featureFlags string + specReplicas int32 + statusReplicas int32 + statusAllocatedReplicas int32 + statusReadyReplicas int32 + cp *autoscalingv1.ChainPolicy + gsList []agonesv1.GameServer + now func() time.Time + want expected + }{ + "scheduled autoscaler feature flag not enabled": { + fleet: modifiedFleet(func(f *agonesv1.Fleet) { + f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) + f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ + Values: []string{}, + Capacity: 7} + f.Status.Replicas = 10 + f.Status.ReadyReplicas = 5 + f.Status.AllocatedReplicas = 5 + f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) + f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ + Count: 31, + Capacity: 70, + } + }), + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", + cp: &autoscalingv1.ChainPolicy{}, + now: func() time.Time { + return time.Time{} + }, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "nil chain policy": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: nil, + now: func() time.Time { + return time.Time{} + }, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "default policy": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{ + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, + }, + }, + want: expected{ + replicas: 5, + limited: false, + err: "", + }, + }, + "one webhook policy, no default policy": {}, + "one webhook policy, one default policy": {}, + "two inactive schedule entries, no default": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{ + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + // Between: autoscalingv1.Between{ + // Start: "00:00", + // }, + }, + }, + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, + }, + }, + want: expected{ + replicas: 0, + limited: false, + err: "", + }, + }, + "two inactive schedules entries, one default": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{ + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, + }, + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, + }, + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, + }, + }, + want: expected{ + replicas: 5, + limited: false, + err: "", + }, + }, + "two default entries": {}, + } + + utilruntime.FeatureTestMutex.Lock() + defer utilruntime.FeatureTestMutex.Unlock() + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := utilruntime.ParseFeatures(tc.featureFlags) + assert.NoError(t, err) + + // For Counters and Lists + m := agtesting.NewMocks() + m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &agonesv1.GameServerList{Items: tc.gsList}, nil + }) + + informer := m.AgonesInformerFactory.Agones().V1() + _, cancel := agtesting.StartInformers(m, + informer.GameServers().Informer().HasSynced) + defer cancel() + + replicas, limited, err := applyChainPolicy(*tc.cp, tc.fleet, informer.GameServers().Lister(), nc) + + if tc.want.err != "" && assert.NotNil(t, err) { + assert.Equal(t, tc.want.err, err.Error()) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.want.replicas, replicas) + assert.Equal(t, tc.want.limited, limited) + } + }) + } +} From 0f645e15af208ea68d707c54e74209640f09df69 Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Tue, 30 Jul 2024 22:43:45 -0700 Subject: [PATCH 5/7] flesh out tests for autoscaling logic --- pkg/fleetautoscalers/fleetautoscalers_test.go | 379 +++++++++++++++--- 1 file changed, 334 insertions(+), 45 deletions(-) diff --git a/pkg/fleetautoscalers/fleetautoscalers_test.go b/pkg/fleetautoscalers/fleetautoscalers_test.go index 825efd6498..e4456be2d0 100644 --- a/pkg/fleetautoscalers/fleetautoscalers_test.go +++ b/pkg/fleetautoscalers/fleetautoscalers_test.go @@ -2153,7 +2153,7 @@ func TestApplySchedulePolicy(t *testing.T) { type expected struct { replicas int32 limited bool - err string + wantErr bool } testCases := map[string]struct { @@ -2163,50 +2163,299 @@ func TestApplySchedulePolicy(t *testing.T) { statusReplicas int32 statusAllocatedReplicas int32 statusReadyReplicas int32 + now func() time.Time sp *autoscalingv1.SchedulePolicy gsList []agonesv1.GameServer - now func() time.Time want expected }{ - "differing start and end timezones": {}, + "scheduled autoscaler feature flag not enabled": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", + now: func() time.Time { + return mustParseTime("2021-01-01T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{}, + want: expected{ + replicas: 0, + limited: false, + wantErr: true, + }, + }, + "no start time": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: func() time.Time { + return mustParseTime("2021-01-01T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + End: mustParseMetav1Time("2021-01-01T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "* * * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 5, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, + want: expected{ + replicas: 5, + limited: false, + wantErr: false, + }, + }, + "no end time": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "* * * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 3, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, + want: expected{ + replicas: 3, + limited: false, + wantErr: false, + }, + }, + "no cron time": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: func() time.Time { + return mustParseTime("2021-01-01T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), + End: mustParseMetav1Time("2021-01-01T01:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 4, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, + want: expected{ + replicas: 4, + limited: false, + wantErr: false, + }, + }, + "no duration": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), + End: mustParseMetav1Time("2021-01-01T01:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "* * * * *", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 5, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, + want: expected{ + replicas: 5, + limited: false, + wantErr: false, + }, + }, + "no start time, end time, cron time, duration": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: func() time.Time { + return mustParseTime("2021-01-01T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 6, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, + want: expected{ + replicas: 6, + limited: false, + wantErr: false, + }, + }, "daylight saving time start": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - sp: &autoscalingv1.SchedulePolicy{}, + now: func() time.Time { + return mustParseTime("2021-03-14T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-03-14T00:00:00Z"), + End: mustParseMetav1Time("2021-03-15T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "0 0 * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 7, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, want: expected{ - replicas: 0, + replicas: 7, limited: false, - err: "", + wantErr: false, }, }, "daylight saving time end": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - sp: &autoscalingv1.SchedulePolicy{}, + now: func() time.Time { + return mustParseTime("2021-11-07T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-11-07T00:00:00Z"), + End: mustParseMetav1Time("2021-11-08T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "0 0 * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 8, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, }, want: expected{ - replicas: 0, + replicas: 8, limited: false, - err: "", + wantErr: false, }, }, "daylight saving time cron time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - sp: &autoscalingv1.SchedulePolicy{}, + now: func() time.Time { + return mustParseTime("2021-11-07T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-11-07T00:00:00Z"), + End: mustParseMetav1Time("2021-11-08T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "0 0 * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 9, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, want: expected{ replicas: 0, limited: false, - err: "", + wantErr: false, }, }, "leap year time change": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - sp: &autoscalingv1.SchedulePolicy{}, + now: func() time.Time { + return mustParseTime("2020-03-08T00:00:00Z") + }, + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2020-03-08T00:00:00Z"), + End: mustParseMetav1Time("2020-03-09T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "0 0 * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 10, + MaxReplicas: 11, + BufferSize: intstr.FromInt(1), + }, + }, + }, want: expected{ - replicas: 0, + replicas: 10, limited: false, - err: "", + wantErr: false, + }, + }, + "new year": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: func() time.Time { + return mustParseTime("2021-01-01T00:00:00Z") + } + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), + End: mustParseMetav1Time("2021-01-02T00:00:00Z"), + }, + ActivePeriod: { + Timezone: "UTC", + StartCron: "0 0 * * *", + Duration: "1h", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 11, + MaxReplicas: 12, + BufferSize: intstr.FromInt(1), + }, + }, }, }, - "new year": {}, } utilruntime.FeatureTestMutex.Lock() @@ -2228,10 +2477,10 @@ func TestApplySchedulePolicy(t *testing.T) { informer.GameServers().Informer().HasSynced) defer cancel() - replicas, limited, err := applySchedulePolicy(*tc.sp, tc.fleet, informer.GameServers().Lister(), nc) + replicas, limited, err := applySchedulePolicy(tc.sp, tc.fleet, informer.GameServers().Lister(), nc) - if tc.want.err != "" && assert.NotNil(t, err) { - assert.Equal(t, tc.want.err, err.Error()) + if tc.want.wantErr { + assert.NotNil(t, err) } else { assert.Nil(t, err) assert.Equal(t, tc.want.replicas, replicas) @@ -2246,6 +2495,7 @@ func TestApplySchedulePolicy(t *testing.T) { func TestApplyChainPolicy(t *testing.T) { t.Parallel() + // For Counters and Lists nc := map[string]gameservers.NodeCount{ "n1": {Ready: 1, Allocated: 1}, } @@ -2255,10 +2505,14 @@ func TestApplyChainPolicy(t *testing.T) { return fleet } + // For Webhook Policy + url := "scale" + invalidURL := ")1golang.org/" + type expected struct { replicas int32 limited bool - err string + wantErr bool } testCases := map[string]struct { @@ -2268,26 +2522,12 @@ func TestApplyChainPolicy(t *testing.T) { statusReplicas int32 statusAllocatedReplicas int32 statusReadyReplicas int32 + now func() time.Time cp *autoscalingv1.ChainPolicy gsList []agonesv1.GameServer - now func() time.Time want expected }{ "scheduled autoscaler feature flag not enabled": { - fleet: modifiedFleet(func(f *agonesv1.Fleet) { - f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) - f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ - Values: []string{}, - Capacity: 7} - f.Status.Replicas = 10 - f.Status.ReadyReplicas = 5 - f.Status.AllocatedReplicas = 5 - f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) - f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ - Count: 31, - Capacity: 70, - } - }), featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", cp: &autoscalingv1.ChainPolicy{}, now: func() time.Time { @@ -2296,7 +2536,7 @@ func TestApplyChainPolicy(t *testing.T) { want: expected{ replicas: 0, limited: false, - err: "", + wantErr: false, }, }, "nil chain policy": { @@ -2308,7 +2548,7 @@ func TestApplyChainPolicy(t *testing.T) { want: expected{ replicas: 0, limited: false, - err: "", + wantErr: false, }, }, "default policy": { @@ -2316,17 +2556,55 @@ func TestApplyChainPolicy(t *testing.T) { cp: &autoscalingv1.ChainPolicy{ { ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 5, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, }, }, want: expected{ replicas: 5, limited: false, - err: "", + wantErr: false, + }, + }, + "one webhook policy, no default policy": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{ + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + + + }, + }, + }, + }, + "one webhook policy, one default policy": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{ + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + }, + }, + { + ID: "", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + MinReplicas: 5, + MaxReplicas: 10, + BufferSize: intstr.FromInt(1), + }, + }, + }, }, }, - "one webhook policy, no default policy": {}, - "one webhook policy, one default policy": {}, "two inactive schedule entries, no default": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", cp: &autoscalingv1.ChainPolicy{ @@ -2346,7 +2624,7 @@ func TestApplyChainPolicy(t *testing.T) { want: expected{ replicas: 0, limited: false, - err: "", + wantErr: false, }, }, "two inactive schedules entries, one default": { @@ -2368,10 +2646,9 @@ func TestApplyChainPolicy(t *testing.T) { want: expected{ replicas: 5, limited: false, - err: "", + wantErr: false, }, }, - "two default entries": {}, } utilruntime.FeatureTestMutex.Lock() @@ -2395,8 +2672,8 @@ func TestApplyChainPolicy(t *testing.T) { replicas, limited, err := applyChainPolicy(*tc.cp, tc.fleet, informer.GameServers().Lister(), nc) - if tc.want.err != "" && assert.NotNil(t, err) { - assert.Equal(t, tc.want.err, err.Error()) + if tc.want.wantErr { + assert.NotNil(t, err) } else { assert.Nil(t, err) assert.Equal(t, tc.want.replicas, replicas) @@ -2405,3 +2682,15 @@ func TestApplyChainPolicy(t *testing.T) { }) } } + +// Parse a time string and return a metav1.Time +func mustParseMetav1Time(timeStr string) metav1.Time { + t, _ := time.Parse(time.RFC3339, timeStr) + return metav1.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) +} + +// Parse a time string and return a time.Time +func mustParseTime(timeStr string) time.Time { + t, _ := time.Parse(time.RFC3339, timeStr) + return t +} From fcc966cff8cd49fb97b68953942f3abf2fc1ed72 Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Wed, 31 Jul 2024 22:37:41 +0000 Subject: [PATCH 6/7] schedule tests --- pkg/fleetautoscalers/fleetautoscalers.go | 42 +- pkg/fleetautoscalers/fleetautoscalers_test.go | 560 ++++++++---------- 2 files changed, 271 insertions(+), 331 deletions(-) diff --git a/pkg/fleetautoscalers/fleetautoscalers.go b/pkg/fleetautoscalers/fleetautoscalers.go index f8389f0263..d3f95f3e34 100644 --- a/pkg/fleetautoscalers/fleetautoscalers.go +++ b/pkg/fleetautoscalers/fleetautoscalers.go @@ -63,7 +63,7 @@ func computeDesiredFleetSize(pol autoscalingv1.FleetAutoscalerPolicy, f *agonesv case autoscalingv1.ListPolicyType: return applyCounterOrListPolicy(nil, pol.List, f, gameServerLister, nodeCounts) case autoscalingv1.SchedulePolicyType: - return applySchedulePolicy(pol.Schedule, f, gameServerLister, nodeCounts) + return applySchedulePolicy(pol.Schedule, f, gameServerLister, nodeCounts, time.Now()) case autoscalingv1.ChainPolicyType: return applyChainPolicy(pol.Chain, f, gameServerLister, nodeCounts) } @@ -367,17 +367,25 @@ func applyCounterOrListPolicy(c *autoscalingv1.CounterPolicy, l *autoscalingv1.L return 0, false, errors.Errorf("unable to apply ListPolicy %v", l) } -func applySchedulePolicy(s *autoscalingv1.SchedulePolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { +func applySchedulePolicy(s *autoscalingv1.SchedulePolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount, currentTime time.Time) (int32, bool, error) { // Ensure the scheduled autoscaler feature gate is enabled if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) { return 0, false, errors.Errorf("cannot apply SchedulePolicy unless feature flag %s is enabled", runtime.FeatureScheduledAutoscaler) } - if isScheduleActive(s) { + fmt.Println("Above") + if isScheduleActive(s, currentTime) { + fmt.Println("Inside") return computeDesiredFleetSize(s.Policy, f, gameServerLister, nodeCounts) } - return f.Status.Replicas, false, nil + var replicas int32 + fmt.Println("Outside") + if f != nil { + replicas = f.Status.Replicas + } + + return replicas, false, nil } func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { @@ -390,9 +398,9 @@ func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServer for _, entry := range c { switch entry.Type { case autoscalingv1.SchedulePolicyType: - schedRep, schedLim, schedErr := applySchedulePolicy(entry.Schedule, f, gameServerLister, nodeCounts) + schedRep, schedLim, schedErr := applySchedulePolicy(entry.Schedule, f, gameServerLister, nodeCounts, time.Now()) // If the schedule is active and no error was returned from the policy, then return the replicas, limited and error - if isScheduleActive(entry.Schedule) && schedErr == nil { + if isScheduleActive(entry.Schedule, time.Now()) && schedErr == nil { return schedRep, schedLim, nil } case autoscalingv1.WebhookPolicyType: @@ -409,30 +417,34 @@ func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServer } // isScheduleActive checks if a chain entry's is active and returns a boolean, true if active, false otherwise -func isScheduleActive(s *autoscalingv1.SchedulePolicy) bool { - now := time.Now() - scheduleDelta := time.Minute * -1 +func isScheduleActive(s *autoscalingv1.SchedulePolicy, currentTime time.Time) bool { + // var now = time.Now() + // fmt.Printf("Time Now Is: %s\n", now) + cronDelta := time.Minute * -2 // If a start time is present and the current time is before the start time, the schedule is inactive so return false startTime := s.Between.Start.Time - if !startTime.IsZero() && now.Before(startTime) { + if !startTime.IsZero() && currentTime.Before(startTime) { + fmt.Println("ONE") return false } // If an end time is present and the current time is after the end time, the schedule is inactive so return false endTime := s.Between.End.Time - if !endTime.IsZero() && now.After(endTime) { + if !endTime.IsZero() && currentTime.After(endTime) { + fmt.Println("TWO") return false } // If no startCron field is specified, then it's automatically true (duration is no longer relevant since we're always running) if s.ActivePeriod.StartCron == "" { + fmt.Println("THREE") return true } location, _ := time.LoadLocation(s.ActivePeriod.Timezone) startCron, _ := cron.ParseStandard(s.ActivePeriod.StartCron) - nextStart := startCron.Next(now.In(location)).Add(scheduleDelta) + nextStartTime := startCron.Next(currentTime.In(location)).Add(cronDelta) duration, err := time.ParseDuration(s.ActivePeriod.Duration) // If there's an err, then the duration field is empty, meaning duration is indefinite @@ -442,10 +454,14 @@ func isScheduleActive(s *autoscalingv1.SchedulePolicy) bool { // If the current time is after the next start time, and the duration is indefinite or the current time is before the next start time + duration, // then return true - if now.After(nextStart) && (duration == 0 || now.Before(nextStart.Add(duration))) { + fmt.Printf("Current Time: %s\n", currentTime) + fmt.Printf("Next Start Time: %s\n", nextStartTime) + if currentTime.After(nextStartTime) && (duration == 0 || currentTime.Before(nextStartTime.Add(duration))) { + fmt.Println("FOUR") return true } + fmt.Println("FIVE") return false } diff --git a/pkg/fleetautoscalers/fleetautoscalers_test.go b/pkg/fleetautoscalers/fleetautoscalers_test.go index e4456be2d0..7f6a4ff6b2 100644 --- a/pkg/fleetautoscalers/fleetautoscalers_test.go +++ b/pkg/fleetautoscalers/fleetautoscalers_test.go @@ -2141,14 +2141,9 @@ func TestApplyListPolicy(t *testing.T) { func TestApplySchedulePolicy(t *testing.T) { t.Parallel() - nc := map[string]gameservers.NodeCount{ - "n1": {Ready: 1, Allocated: 1}, - } - modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { - _, fleet := defaultFixtures() - f(fleet) - return fleet - } + // nc := map[string]gameservers.NodeCount{ + // "n1": {Ready: 1, Allocated: 1}, + // } type expected struct { replicas int32 @@ -2157,23 +2152,19 @@ func TestApplySchedulePolicy(t *testing.T) { } testCases := map[string]struct { - fleet *agonesv1.Fleet featureFlags string specReplicas int32 statusReplicas int32 statusAllocatedReplicas int32 statusReadyReplicas int32 - now func() time.Time + now time.Time sp *autoscalingv1.SchedulePolicy gsList []agonesv1.GameServer want expected }{ "scheduled autoscaler feature flag not enabled": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", - now: func() time.Time { - return mustParseTime("2021-01-01T00:00:00Z") - }, - sp: &autoscalingv1.SchedulePolicy{}, + sp: &autoscalingv1.SchedulePolicy{}, want: expected{ replicas: 0, limited: false, @@ -2182,9 +2173,7 @@ func TestApplySchedulePolicy(t *testing.T) { }, "no start time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-01-01T00:00:00Z") - }, + now: mustParseTime("2021-01-00T00:00:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ End: mustParseMetav1Time("2021-01-01T00:00:00Z"), @@ -2192,25 +2181,26 @@ func TestApplySchedulePolicy(t *testing.T) { ActivePeriod: autoscalingv1.ActivePeriod{ Timezone: "UTC", StartCron: "* * * * *", - Duration: "1h", + Duration: "48h", }, Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 5, - MaxReplicas: 10, BufferSize: intstr.FromInt(1), + MinReplicas: 3, + MaxReplicas: 10, }, }, }, want: expected{ - replicas: 5, + replicas: 3, limited: false, wantErr: false, }, }, "no end time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: mustParseTime("2021-01-02T00:00:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), @@ -2223,23 +2213,21 @@ func TestApplySchedulePolicy(t *testing.T) { Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 3, - MaxReplicas: 10, BufferSize: intstr.FromInt(1), + MinReplicas: 4, + MaxReplicas: 7, }, }, }, want: expected{ - replicas: 3, - limited: false, + replicas: 4, + limited: true, wantErr: false, }, }, "no cron time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-01-01T00:00:00Z") - }, + now: mustParseTime("2021-01-01T0:30:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), @@ -2252,20 +2240,21 @@ func TestApplySchedulePolicy(t *testing.T) { Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 4, + MinReplicas: 5, MaxReplicas: 10, BufferSize: intstr.FromInt(1), }, }, }, want: expected{ - replicas: 4, - limited: false, + replicas: 5, + limited: true, wantErr: false, }, }, "no duration": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: mustParseTime("2021-01-01T0:30:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), @@ -2278,74 +2267,68 @@ func TestApplySchedulePolicy(t *testing.T) { Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 5, - MaxReplicas: 10, - BufferSize: intstr.FromInt(1), + BufferSize: intstr.FromInt(8), + MinReplicas: 11, + MaxReplicas: 30, }, }, }, want: expected{ - replicas: 5, - limited: false, + replicas: 11, + limited: true, wantErr: false, }, }, "no start time, end time, cron time, duration": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-01-01T00:00:00Z") - }, + now: mustParseTime("2021-01-01T00:00:00Z"), sp: &autoscalingv1.SchedulePolicy{ Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ + BufferSize: intstr.FromInt(1), MinReplicas: 6, MaxReplicas: 10, - BufferSize: intstr.FromInt(1), }, }, }, want: expected{ replicas: 6, - limited: false, + limited: true, wantErr: false, }, }, "daylight saving time start": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-03-14T00:00:00Z") - }, + now: mustParseTime("2021-03-14T02:00:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ - Start: mustParseMetav1Time("2021-03-14T00:00:00Z"), + Start: mustParseMetav1Time("2021-03-13T00:00:00Z"), End: mustParseMetav1Time("2021-03-15T00:00:00Z"), }, ActivePeriod: autoscalingv1.ActivePeriod{ Timezone: "UTC", - StartCron: "0 0 * * *", + StartCron: "* 2 * * *", Duration: "1h", }, Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ + BufferSize: intstr.FromInt(1), MinReplicas: 7, MaxReplicas: 10, - BufferSize: intstr.FromInt(1), }, }, }, want: expected{ replicas: 7, - limited: false, + limited: true, wantErr: false, }, }, "daylight saving time end": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-11-07T00:00:00Z") - }, + now: mustParseTime("2021-11-07T01:59:59Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ Start: mustParseMetav1Time("2021-11-07T00:00:00Z"), @@ -2353,7 +2336,7 @@ func TestApplySchedulePolicy(t *testing.T) { }, ActivePeriod: autoscalingv1.ActivePeriod{ Timezone: "UTC", - StartCron: "0 0 * * *", + StartCron: "0 2 * * *", Duration: "1h", }, Policy: autoscalingv1.FleetAutoscalerPolicy{ @@ -2367,95 +2350,38 @@ func TestApplySchedulePolicy(t *testing.T) { }, want: expected{ replicas: 8, - limited: false, - wantErr: false, - }, - }, - "daylight saving time cron time": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-11-07T00:00:00Z") - }, - sp: &autoscalingv1.SchedulePolicy{ - Between: autoscalingv1.Between{ - Start: mustParseMetav1Time("2021-11-07T00:00:00Z"), - End: mustParseMetav1Time("2021-11-08T00:00:00Z"), - }, - ActivePeriod: autoscalingv1.ActivePeriod{ - Timezone: "UTC", - StartCron: "0 0 * * *", - Duration: "1h", - }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 9, - MaxReplicas: 10, - BufferSize: intstr.FromInt(1), - }, - }, - }, - want: expected{ - replicas: 0, - limited: false, + limited: true, wantErr: false, }, }, - "leap year time change": { + "new year": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2020-03-08T00:00:00Z") - }, + now: mustParseTime("2021-01-01T00:00:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ - Start: mustParseMetav1Time("2020-03-08T00:00:00Z"), - End: mustParseMetav1Time("2020-03-09T00:00:00Z"), + Start: mustParseMetav1Time("2020-12-31T24:59:59Z"), + End: mustParseMetav1Time("2021-01-02T00:00:00Z"), }, ActivePeriod: autoscalingv1.ActivePeriod{ Timezone: "UTC", - StartCron: "0 0 * * *", + StartCron: "* 0 * * *", Duration: "1h", }, Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 10, - MaxReplicas: 11, BufferSize: intstr.FromInt(1), + MinReplicas: 11, + MaxReplicas: 12, }, }, }, want: expected{ - replicas: 10, - limited: false, + replicas: 11, + limited: true, wantErr: false, }, }, - "new year": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: func() time.Time { - return mustParseTime("2021-01-01T00:00:00Z") - } - sp: &autoscalingv1.SchedulePolicy{ - Between: autoscalingv1.Between{ - Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), - End: mustParseMetav1Time("2021-01-02T00:00:00Z"), - }, - ActivePeriod: { - Timezone: "UTC", - StartCron: "0 0 * * *", - Duration: "1h", - }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 11, - MaxReplicas: 12, - BufferSize: intstr.FromInt(1), - }, - }, - }, - }, } utilruntime.FeatureTestMutex.Lock() @@ -2467,22 +2393,24 @@ func TestApplySchedulePolicy(t *testing.T) { assert.NoError(t, err) // For Counters and Lists - m := agtesting.NewMocks() - m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { - return true, &agonesv1.GameServerList{Items: tc.gsList}, nil - }) + // m := agtesting.NewMocks() + // m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + // return true, &agonesv1.GameServerList{Items: tc.gsList}, nil + // }) - informer := m.AgonesInformerFactory.Agones().V1() - _, cancel := agtesting.StartInformers(m, - informer.GameServers().Informer().HasSynced) - defer cancel() + // informer := m.AgonesInformerFactory.Agones().V1() + // _, cancel := agtesting.StartInformers(m, + // informer.GameServers().Informer().HasSynced) + // defer cancel() - replicas, limited, err := applySchedulePolicy(tc.sp, tc.fleet, informer.GameServers().Lister(), nc) + _, f := defaultFixtures() + replicas, limited, err := applySchedulePolicy(tc.sp, f, nil, nil, tc.now) if tc.want.wantErr { assert.NotNil(t, err) } else { assert.Nil(t, err) + // fmt.Printf("Err: %s\n", err.Error()) assert.Equal(t, tc.want.replicas, replicas) assert.Equal(t, tc.want.limited, limited) } @@ -2492,196 +2420,192 @@ func TestApplySchedulePolicy(t *testing.T) { // nolint:dupl // Linter errors on lines are duplicate of TestApplyChainPolicy // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateChainPolicy) -func TestApplyChainPolicy(t *testing.T) { - t.Parallel() - - // For Counters and Lists - nc := map[string]gameservers.NodeCount{ - "n1": {Ready: 1, Allocated: 1}, - } - modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { - _, fleet := defaultFixtures() - f(fleet) - return fleet - } - - // For Webhook Policy - url := "scale" - invalidURL := ")1golang.org/" - - type expected struct { - replicas int32 - limited bool - wantErr bool - } - - testCases := map[string]struct { - fleet *agonesv1.Fleet - featureFlags string - specReplicas int32 - statusReplicas int32 - statusAllocatedReplicas int32 - statusReadyReplicas int32 - now func() time.Time - cp *autoscalingv1.ChainPolicy - gsList []agonesv1.GameServer - want expected - }{ - "scheduled autoscaler feature flag not enabled": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", - cp: &autoscalingv1.ChainPolicy{}, - now: func() time.Time { - return time.Time{} - }, - want: expected{ - replicas: 0, - limited: false, - wantErr: false, - }, - }, - "nil chain policy": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - cp: nil, - now: func() time.Time { - return time.Time{} - }, - want: expected{ - replicas: 0, - limited: false, - wantErr: false, - }, - }, - "default policy": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - cp: &autoscalingv1.ChainPolicy{ - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 5, - MaxReplicas: 10, - BufferSize: intstr.FromInt(1), - }, - }, - }, - }, - want: expected{ - replicas: 5, - limited: false, - wantErr: false, - }, - }, - "one webhook policy, no default policy": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - cp: &autoscalingv1.ChainPolicy{ - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ - - - }, - }, - }, - }, - "one webhook policy, one default policy": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - cp: &autoscalingv1.ChainPolicy{ - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ - }, - }, - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 5, - MaxReplicas: 10, - BufferSize: intstr.FromInt(1), - }, - }, - }, - }, - }, - "two inactive schedule entries, no default": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - cp: &autoscalingv1.ChainPolicy{ - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ - // Between: autoscalingv1.Between{ - // Start: "00:00", - // }, - }, - }, - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, - }, - }, - want: expected{ - replicas: 0, - limited: false, - wantErr: false, - }, - }, - "two inactive schedules entries, one default": { - featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - cp: &autoscalingv1.ChainPolicy{ - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, - }, - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, - }, - { - ID: "", - FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, - }, - }, - want: expected{ - replicas: 5, - limited: false, - wantErr: false, - }, - }, - } - - utilruntime.FeatureTestMutex.Lock() - defer utilruntime.FeatureTestMutex.Unlock() - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - err := utilruntime.ParseFeatures(tc.featureFlags) - assert.NoError(t, err) - - // For Counters and Lists - m := agtesting.NewMocks() - m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { - return true, &agonesv1.GameServerList{Items: tc.gsList}, nil - }) - - informer := m.AgonesInformerFactory.Agones().V1() - _, cancel := agtesting.StartInformers(m, - informer.GameServers().Informer().HasSynced) - defer cancel() - - replicas, limited, err := applyChainPolicy(*tc.cp, tc.fleet, informer.GameServers().Lister(), nc) - - if tc.want.wantErr { - assert.NotNil(t, err) - } else { - assert.Nil(t, err) - assert.Equal(t, tc.want.replicas, replicas) - assert.Equal(t, tc.want.limited, limited) - } - }) - } -} +// func TestApplyChainPolicy(t *testing.T) { +// t.Parallel() + +// // For Counters and Lists +// nc := map[string]gameservers.NodeCount{ +// "n1": {Ready: 1, Allocated: 1}, +// } +// modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { +// _, fleet := defaultFixtures() +// f(fleet) +// return fleet +// } + +// // For Webhook Policy +// url := "scale" +// invalidURL := ")1golang.org/" + +// type expected struct { +// replicas int32 +// limited bool +// wantErr bool +// } + +// testCases := map[string]struct { +// fleet *agonesv1.Fleet +// featureFlags string +// specReplicas int32 +// statusReplicas int32 +// statusAllocatedReplicas int32 +// statusReadyReplicas int32 +// now func() time.Time +// cp *autoscalingv1.ChainPolicy +// gsList []agonesv1.GameServer +// want expected +// }{ +// "scheduled autoscaler feature flag not enabled": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", +// cp: &autoscalingv1.ChainPolicy{}, +// now: func() time.Time { +// return time.Time{} +// }, +// want: expected{ +// replicas: 0, +// limited: false, +// wantErr: false, +// }, +// }, +// "nil chain policy": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", +// cp: nil, +// now: func() time.Time { +// return time.Time{} +// }, +// want: expected{ +// replicas: 0, +// limited: false, +// wantErr: false, +// }, +// }, +// "default policy": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", +// cp: &autoscalingv1.ChainPolicy{ +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ +// Type: autoscalingv1.BufferPolicyType, +// Buffer: &autoscalingv1.BufferPolicy{ +// MinReplicas: 5, +// MaxReplicas: 10, +// BufferSize: intstr.FromInt(1), +// }, +// }, +// }, +// }, +// want: expected{ +// replicas: 5, +// limited: false, +// wantErr: false, +// }, +// }, +// "one webhook policy, no default policy": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", +// cp: &autoscalingv1.ChainPolicy{ +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, +// }, +// }, +// }, +// "one webhook policy, one default policy": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", +// cp: &autoscalingv1.ChainPolicy{ +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, +// }, +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ +// Type: autoscalingv1.BufferPolicyType, +// Buffer: &autoscalingv1.BufferPolicy{ +// MinReplicas: 5, +// MaxReplicas: 10, +// BufferSize: intstr.FromInt(1), +// }, +// }, +// }, +// }, +// }, +// "two inactive schedule entries, no default": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", +// cp: &autoscalingv1.ChainPolicy{ +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ +// // Between: autoscalingv1.Between{ +// // Start: "00:00", +// // }, +// }, +// }, +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, +// }, +// }, +// want: expected{ +// replicas: 0, +// limited: false, +// wantErr: false, +// }, +// }, +// "two inactive schedules entries, one default": { +// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", +// cp: &autoscalingv1.ChainPolicy{ +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, +// }, +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, +// }, +// { +// ID: "", +// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, +// }, +// }, +// want: expected{ +// replicas: 5, +// limited: false, +// wantErr: false, +// }, +// }, +// } + +// utilruntime.FeatureTestMutex.Lock() +// defer utilruntime.FeatureTestMutex.Unlock() + +// for name, tc := range testCases { +// t.Run(name, func(t *testing.T) { +// err := utilruntime.ParseFeatures(tc.featureFlags) +// assert.NoError(t, err) + +// // For Counters and Lists +// m := agtesting.NewMocks() +// m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { +// return true, &agonesv1.GameServerList{Items: tc.gsList}, nil +// }) + +// informer := m.AgonesInformerFactory.Agones().V1() +// _, cancel := agtesting.StartInformers(m, +// informer.GameServers().Informer().HasSynced) +// defer cancel() + +// replicas, limited, err := applyChainPolicy(*tc.cp, tc.fleet, informer.GameServers().Lister(), nc) + +// if tc.want.wantErr { +// assert.NotNil(t, err) +// } else { +// assert.Nil(t, err) +// assert.Equal(t, tc.want.replicas, replicas) +// assert.Equal(t, tc.want.limited, limited) +// } +// }) +// } +// } // Parse a time string and return a metav1.Time func mustParseMetav1Time(timeStr string) metav1.Time { From fa2043664d959eefbbccad633eec875bc0f65999 Mon Sep 17 00:00:00 2001 From: indexjoseph Date: Wed, 31 Jul 2024 22:57:54 -0700 Subject: [PATCH 7/7] Add schedule and chain policy tests and add calculation for cronStart and cronEnd times Add custom error for inactive schedule & prevent logging inactive schedule errors as events f --- pkg/apis/autoscaling/v1/fleetautoscaler.go | 10 +- .../autoscaling/v1/fleetautoscaler_test.go | 2 +- pkg/fleetautoscalers/controller.go | 3 +- pkg/fleetautoscalers/fleetautoscalers.go | 91 +-- pkg/fleetautoscalers/fleetautoscalers_test.go | 534 ++++++++---------- 5 files changed, 290 insertions(+), 350 deletions(-) diff --git a/pkg/apis/autoscaling/v1/fleetautoscaler.go b/pkg/apis/autoscaling/v1/fleetautoscaler.go index a8e1fe2de3..14e96c4447 100644 --- a/pkg/apis/autoscaling/v1/fleetautoscaler.go +++ b/pkg/apis/autoscaling/v1/fleetautoscaler.go @@ -581,13 +581,9 @@ func (c *ChainPolicy) ValidateChainPolicy(fldPath *field.Path) field.ErrorList { seenIDs[entry.ID] = true } // Ensure that chain entry has a policy - hasValidPolicy := entry.Buffer == nil && entry.Webhook == nil && entry.Counter == nil && entry.List == nil && entry.Schedule == nil - if entry.Type == "" || hasValidPolicy { - allErrs = append(allErrs, field.Required(fldPath.Index(i), "policy is missing")) - } - // Ensure the chain entry's policy is not a chain policy (to avoid nested chain policies) - if entry.Chain != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Index(i), entry.FleetAutoscalerPolicy.Type, "chain policy cannot be used in chain policy")) + hasValidPolicy := entry.Buffer != nil || entry.Webhook != nil || entry.Counter != nil || entry.List != nil || entry.Schedule != nil + if entry.Type == "" || !hasValidPolicy { + allErrs = append(allErrs, field.Required(fldPath.Index(i), "valid policy is missing")) } // Validate the chain entry's policy allErrs = append(allErrs, entry.FleetAutoscalerPolicy.ValidatePolicy(fldPath.Index(i).Child("policy"))...) diff --git a/pkg/apis/autoscaling/v1/fleetautoscaler_test.go b/pkg/apis/autoscaling/v1/fleetautoscaler_test.go index 05cd149cc1..60a73786f9 100644 --- a/pkg/apis/autoscaling/v1/fleetautoscaler_test.go +++ b/pkg/apis/autoscaling/v1/fleetautoscaler_test.go @@ -586,7 +586,7 @@ func TestFleetAutoscalerChainValidateUpdate(t *testing.T) { } }), featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true", - wantLength: 2, + wantLength: 1, wantField: "spec.policy.chain[1]", }, "invalid nested policy format": { diff --git a/pkg/fleetautoscalers/controller.go b/pkg/fleetautoscalers/controller.go index 8fe969b9b7..abc1e61945 100644 --- a/pkg/fleetautoscalers/controller.go +++ b/pkg/fleetautoscalers/controller.go @@ -314,7 +314,8 @@ func (c *Controller) syncFleetAutoscaler(ctx context.Context, key string) error currentReplicas := fleet.Status.Replicas desiredReplicas, scalingLimited, err := computeDesiredFleetSize(fas.Spec.Policy, fleet, c.gameServerLister, c.counter.Counts()) - if err != nil { + // If there err is nil and not an inactive schedule error (ignorable in this case), then record the event + if err != nil && !errors.Is(err, InactiveScheduleError{}) { c.recorder.Eventf(fas, corev1.EventTypeWarning, "FleetAutoscaler", "Error calculating desired fleet size on FleetAutoscaler %s. Error: %s", fas.ObjectMeta.Name, err.Error()) diff --git a/pkg/fleetautoscalers/fleetautoscalers.go b/pkg/fleetautoscalers/fleetautoscalers.go index d3f95f3e34..8663be99ad 100644 --- a/pkg/fleetautoscalers/fleetautoscalers.go +++ b/pkg/fleetautoscalers/fleetautoscalers.go @@ -42,6 +42,8 @@ import ( "agones.dev/agones/pkg/util/runtime" ) +const maxDuration = "2540400h" // 290 Years + var tlsConfig = &tls.Config{} var client = http.Client{ Timeout: 15 * time.Second, @@ -50,6 +52,13 @@ var client = http.Client{ }, } +// InactiveScheduleError denotes an error for schedules that are not currently active. +type InactiveScheduleError struct{} + +func (InactiveScheduleError) Error() string { + return "inactive schedule, policy not applicable" +} + // computeDesiredFleetSize computes the new desired size of the given fleet func computeDesiredFleetSize(pol autoscalingv1.FleetAutoscalerPolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { @@ -65,7 +74,7 @@ func computeDesiredFleetSize(pol autoscalingv1.FleetAutoscalerPolicy, f *agonesv case autoscalingv1.SchedulePolicyType: return applySchedulePolicy(pol.Schedule, f, gameServerLister, nodeCounts, time.Now()) case autoscalingv1.ChainPolicyType: - return applyChainPolicy(pol.Chain, f, gameServerLister, nodeCounts) + return applyChainPolicy(pol.Chain, f, gameServerLister, nodeCounts, time.Now()) } return 0, false, errors.New("wrong policy type, should be one of: Buffer, Webhook, Counter, List") @@ -373,95 +382,101 @@ func applySchedulePolicy(s *autoscalingv1.SchedulePolicy, f *agonesv1.Fleet, gam return 0, false, errors.Errorf("cannot apply SchedulePolicy unless feature flag %s is enabled", runtime.FeatureScheduledAutoscaler) } - fmt.Println("Above") if isScheduleActive(s, currentTime) { - fmt.Println("Inside") return computeDesiredFleetSize(s.Policy, f, gameServerLister, nodeCounts) } - var replicas int32 - fmt.Println("Outside") - if f != nil { - replicas = f.Status.Replicas - } - - return replicas, false, nil + // If the schedule wasn't active then return the current replica amount of the fleet + return f.Status.Replicas, false, &InactiveScheduleError{} } -func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount) (int32, bool, error) { +func applyChainPolicy(c autoscalingv1.ChainPolicy, f *agonesv1.Fleet, gameServerLister listeragonesv1.GameServerLister, nodeCounts map[string]gameservers.NodeCount, currentTime time.Time) (int32, bool, error) { // Ensure the scheduled autoscaler feature gate is enabled if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) { return 0, false, errors.Errorf("cannot apply ChainPolicy unless feature flag %s is enabled", runtime.FeatureScheduledAutoscaler) } + replicas := f.Status.Replicas + var limited bool + var err error + // Loop over all entries in the chain for _, entry := range c { switch entry.Type { case autoscalingv1.SchedulePolicyType: - schedRep, schedLim, schedErr := applySchedulePolicy(entry.Schedule, f, gameServerLister, nodeCounts, time.Now()) - // If the schedule is active and no error was returned from the policy, then return the replicas, limited and error - if isScheduleActive(entry.Schedule, time.Now()) && schedErr == nil { - return schedRep, schedLim, nil + replicas, limited, err = applySchedulePolicy(entry.Schedule, f, gameServerLister, nodeCounts, currentTime) + // If no error was returned from the schedule policy (schedule is active and/or webhook policy within schedule was successful), then return the values given + if err == nil { + return replicas, limited, nil } case autoscalingv1.WebhookPolicyType: - webhookRep, webhookLim, webhookErr := applyWebhookPolicy(entry.Webhook, f) - if webhookErr == nil { - return webhookRep, webhookLim, nil + replicas, limited, err = applyWebhookPolicy(entry.Webhook, f) + // If no error was returned from the webhook policy, then return the values given + if err == nil { + return replicas, limited, nil } default: + // Every other policy type we just want to compute the desired fleet and return it return computeDesiredFleetSize(entry.FleetAutoscalerPolicy, f, gameServerLister, nodeCounts) } + } - return f.Status.Replicas, false, nil + // Fall off the chain + return replicas, limited, err } // isScheduleActive checks if a chain entry's is active and returns a boolean, true if active, false otherwise func isScheduleActive(s *autoscalingv1.SchedulePolicy, currentTime time.Time) bool { - // var now = time.Now() - // fmt.Printf("Time Now Is: %s\n", now) - cronDelta := time.Minute * -2 + // Used for checking ahead of the schedule for daylight savings purposes + cronDelta := (time.Minute * -1) + (time.Second * -30) - // If a start time is present and the current time is before the start time, the schedule is inactive so return false + // If the current time is before the start time, the schedule is inactive so return false startTime := s.Between.Start.Time - if !startTime.IsZero() && currentTime.Before(startTime) { - fmt.Println("ONE") + if currentTime.Before(startTime) { return false } // If an end time is present and the current time is after the end time, the schedule is inactive so return false endTime := s.Between.End.Time if !endTime.IsZero() && currentTime.After(endTime) { - fmt.Println("TWO") return false } // If no startCron field is specified, then it's automatically true (duration is no longer relevant since we're always running) if s.ActivePeriod.StartCron == "" { - fmt.Println("THREE") return true } + // Ignore the error as validation is already done within the validateChainPolicy after being unmarshalled location, _ := time.LoadLocation(s.ActivePeriod.Timezone) + + // Ignore the error as validation is already done within the validateChainPolicy after being unmarshalled startCron, _ := cron.ParseStandard(s.ActivePeriod.StartCron) - nextStartTime := startCron.Next(currentTime.In(location)).Add(cronDelta) - duration, err := time.ParseDuration(s.ActivePeriod.Duration) - // If there's an err, then the duration field is empty, meaning duration is indefinite - if err != nil { - duration = 0 // Indefinite duration if not set + // Ignore the error as validation is already done within the validateChainPolicy after being unmarshalled. + // If the duration is empty set it to the largest duration possible (290 years) + duration, _ := time.ParseDuration(s.ActivePeriod.Duration) + if s.ActivePeriod.Duration == "" { + duration, _ = time.ParseDuration(maxDuration) } - // If the current time is after the next start time, and the duration is indefinite or the current time is before the next start time + duration, + // Get the current time - duration + currentTimeMinusDuration := currentTime.Add(duration * -1) + // Take (current time - duration) to get the first available start time + cronStartTime := startCron.Next(currentTimeMinusDuration.In(location)) + // Take the (cronStartTime + duration) to get the end time + cronEndTime := cronStartTime.Add(duration) + + // If the current time is after the cronStartTime - 90 seconds (for daylight saving purposes) AND the current time before the cronEndTime // then return true - fmt.Printf("Current Time: %s\n", currentTime) - fmt.Printf("Next Start Time: %s\n", nextStartTime) - if currentTime.After(nextStartTime) && (duration == 0 || currentTime.Before(nextStartTime.Add(duration))) { - fmt.Println("FOUR") + // Example: startCron = 0 14 * * * // 2:00 PM Everyday | duration = 1 hr | cronDelta = 90 seconds | currentTime = 2024-08-01T14:30:00Z | currentTimeMinusDuration = 2024-08-01T13:30:00Z + // then cronStartTime = 2024-08-01T14:00:00Z and cronEndTime = 2024-08-01T15:00:00Z + // and since currentTime > cronStartTime + cronDelta AND currentTime < cronEndTime, we return true + if currentTime.After(cronStartTime.Add(cronDelta)) && currentTime.Before(cronEndTime) { return true } - fmt.Println("FIVE") return false } diff --git a/pkg/fleetautoscalers/fleetautoscalers_test.go b/pkg/fleetautoscalers/fleetautoscalers_test.go index 7f6a4ff6b2..2104d2fc27 100644 --- a/pkg/fleetautoscalers/fleetautoscalers_test.go +++ b/pkg/fleetautoscalers/fleetautoscalers_test.go @@ -41,6 +41,7 @@ import ( const ( scaleFactor = 2 + webhookURL = "scale" ) type testServer struct{} @@ -351,7 +352,7 @@ func TestApplyWebhookPolicy(t *testing.T) { defer server.Close() _, f := defaultWebhookFixtures() - url := "scale" + url := webhookURL emptyString := "" invalidURL := ")1golang.org/" wrongServerURL := "http://127.0.0.1:1" @@ -581,7 +582,7 @@ func TestApplyWebhookPolicy(t *testing.T) { func TestApplyWebhookPolicyNilFleet(t *testing.T) { t.Parallel() - url := "scale" + url := webhookURL w := &autoscalingv1.WebhookPolicy{ Service: &admregv1.ServiceReference{ Name: "service1", @@ -2141,16 +2142,31 @@ func TestApplyListPolicy(t *testing.T) { func TestApplySchedulePolicy(t *testing.T) { t.Parallel() - // nc := map[string]gameservers.NodeCount{ - // "n1": {Ready: 1, Allocated: 1}, - // } - type expected struct { replicas int32 limited bool wantErr bool } + bufferPolicy := autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + BufferSize: intstr.FromInt(1), + MinReplicas: 3, + MaxReplicas: 10, + }, + } + expectedWhenActive := expected{ + replicas: 3, + limited: false, + wantErr: false, + } + expectedWhenInactive := expected{ + replicas: 0, + limited: false, + wantErr: true, + } + testCases := map[string]struct { featureFlags string specReplicas int32 @@ -2173,7 +2189,7 @@ func TestApplySchedulePolicy(t *testing.T) { }, "no start time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", - now: mustParseTime("2021-01-00T00:00:00Z"), + now: mustParseTime("2020-12-26T08:30:00Z"), sp: &autoscalingv1.SchedulePolicy{ Between: autoscalingv1.Between{ End: mustParseMetav1Time("2021-01-01T00:00:00Z"), @@ -2183,20 +2199,9 @@ func TestApplySchedulePolicy(t *testing.T) { StartCron: "* * * * *", Duration: "48h", }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - BufferSize: intstr.FromInt(1), - MinReplicas: 3, - MaxReplicas: 10, - }, - }, - }, - want: expected{ - replicas: 3, - limited: false, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "no end time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", @@ -2210,20 +2215,9 @@ func TestApplySchedulePolicy(t *testing.T) { StartCron: "* * * * *", Duration: "1h", }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - BufferSize: intstr.FromInt(1), - MinReplicas: 4, - MaxReplicas: 7, - }, - }, - }, - want: expected{ - replicas: 4, - limited: true, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "no cron time": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", @@ -2237,20 +2231,9 @@ func TestApplySchedulePolicy(t *testing.T) { Timezone: "UTC", Duration: "1h", }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 5, - MaxReplicas: 10, - BufferSize: intstr.FromInt(1), - }, - }, - }, - want: expected{ - replicas: 5, - limited: true, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "no duration": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", @@ -2264,39 +2247,17 @@ func TestApplySchedulePolicy(t *testing.T) { Timezone: "UTC", StartCron: "* * * * *", }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - BufferSize: intstr.FromInt(8), - MinReplicas: 11, - MaxReplicas: 30, - }, - }, - }, - want: expected{ - replicas: 11, - limited: true, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "no start time, end time, cron time, duration": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", now: mustParseTime("2021-01-01T00:00:00Z"), sp: &autoscalingv1.SchedulePolicy{ - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - BufferSize: intstr.FromInt(1), - MinReplicas: 6, - MaxReplicas: 10, - }, - }, - }, - want: expected{ - replicas: 6, - limited: true, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "daylight saving time start": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", @@ -2311,20 +2272,9 @@ func TestApplySchedulePolicy(t *testing.T) { StartCron: "* 2 * * *", Duration: "1h", }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - BufferSize: intstr.FromInt(1), - MinReplicas: 7, - MaxReplicas: 10, - }, - }, - }, - want: expected{ - replicas: 7, - limited: true, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "daylight saving time end": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", @@ -2339,20 +2289,9 @@ func TestApplySchedulePolicy(t *testing.T) { StartCron: "0 2 * * *", Duration: "1h", }, - Policy: autoscalingv1.FleetAutoscalerPolicy{ - Type: autoscalingv1.BufferPolicyType, - Buffer: &autoscalingv1.BufferPolicy{ - MinReplicas: 8, - MaxReplicas: 10, - BufferSize: intstr.FromInt(1), - }, - }, - }, - want: expected{ - replicas: 8, - limited: true, - wantErr: false, + Policy: bufferPolicy, }, + want: expectedWhenActive, }, "new year": { featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", @@ -2367,17 +2306,207 @@ func TestApplySchedulePolicy(t *testing.T) { StartCron: "* 0 * * *", Duration: "1h", }, + Policy: bufferPolicy, + }, + want: expectedWhenActive, + }, + "inactive schedule": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: mustParseTime("2023-12-12T03:49:00Z"), + sp: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2022-12-31T24:59:59Z"), + End: mustParseMetav1Time("2023-03-02T00:00:00Z"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "UTC", + StartCron: "* 0 * 3 *", + Duration: "", + }, + Policy: bufferPolicy, + }, + want: expectedWhenInactive, + }, + } + + utilruntime.FeatureTestMutex.Lock() + defer utilruntime.FeatureTestMutex.Unlock() + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := utilruntime.ParseFeatures(tc.featureFlags) + assert.NoError(t, err) + + _, f := defaultFixtures() + replicas, limited, err := applySchedulePolicy(tc.sp, f, nil, nil, tc.now) + + if tc.want.wantErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.want.replicas, replicas) + assert.Equal(t, tc.want.limited, limited) + } + }) + } +} + +// nolint:dupl // Linter errors on lines are duplicate of TestApplyChainPolicy +// NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateChainPolicy) +func TestApplyChainPolicy(t *testing.T) { + t.Parallel() + + // For Webhook Policy + ts := testServer{} + server := httptest.NewServer(ts) + defer server.Close() + url := webhookURL + + type expected struct { + replicas int32 + limited bool + wantErr bool + } + + scheduleOne := autoscalingv1.ChainEntry{ + ID: "schedule-1", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.SchedulePolicyType, + Schedule: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + Start: mustParseMetav1Time("2024-08-01T10:07:36-06:00"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "America/Chicago", + StartCron: "* * * * *", + Duration: "", + }, Policy: autoscalingv1.FleetAutoscalerPolicy{ Type: autoscalingv1.BufferPolicyType, Buffer: &autoscalingv1.BufferPolicy{ BufferSize: intstr.FromInt(1), - MinReplicas: 11, - MaxReplicas: 12, + MinReplicas: 10, + MaxReplicas: 10, }, }, }, + }, + } + scheduleTwo := autoscalingv1.ChainEntry{ + ID: "schedule-2", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.SchedulePolicyType, + Schedule: &autoscalingv1.SchedulePolicy{ + Between: autoscalingv1.Between{ + End: mustParseMetav1Time("2021-01-02T4:53:00-05:00"), + }, + ActivePeriod: autoscalingv1.ActivePeriod{ + Timezone: "America/New_York", + StartCron: "0 1 3 * *", + Duration: "", + }, + Policy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + BufferSize: intstr.FromInt(1), + MinReplicas: 3, + MaxReplicas: 10, + }, + }, + }, + }, + } + webhookEntry := autoscalingv1.ChainEntry{ + ID: "webhook policy", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.WebhookPolicyType, + Webhook: &autoscalingv1.WebhookPolicy{ + Service: &admregv1.ServiceReference{ + Name: "service1", + Namespace: "default", + Path: &url, + }, + CABundle: []byte("invalid-value"), + }, + }, + } + defaultEntry := autoscalingv1.ChainEntry{ + ID: "default", + FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ + Type: autoscalingv1.BufferPolicyType, + Buffer: &autoscalingv1.BufferPolicy{ + BufferSize: intstr.FromInt(1), + MinReplicas: 6, + MaxReplicas: 10, + }, + }, + } + + testCases := map[string]struct { + fleet *agonesv1.Fleet + featureFlags string + specReplicas int32 + statusReplicas int32 + statusAllocatedReplicas int32 + statusReadyReplicas int32 + now time.Time + cp *autoscalingv1.ChainPolicy + gsList []agonesv1.GameServer + want expected + }{ + "scheduled autoscaler feature flag not enabled": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", + cp: &autoscalingv1.ChainPolicy{}, + want: expected{ + replicas: 0, + limited: false, + wantErr: true, + }, + }, + "default policy": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{defaultEntry}, + want: expected{ + replicas: 6, + limited: true, + wantErr: false, + }, + }, + "one invalid webhook policy, one default (fallthrough)": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + cp: &autoscalingv1.ChainPolicy{webhookEntry, defaultEntry}, + want: expected{ + replicas: 6, + limited: true, + wantErr: false, + }, + }, + "two inactive schedule entries, no default (fall off chain)": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: mustParseTime("2021-01-01T0:30:00Z"), + cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleOne}, + want: expected{ + replicas: 5, + limited: false, + wantErr: true, + }, + }, + "two inactive schedules entries, one default (fallthrough)": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: mustParseTime("2021-11-05T5:30:00Z"), + cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleTwo, defaultEntry}, want: expected{ - replicas: 11, + replicas: 6, + limited: true, + wantErr: false, + }, + }, + "two overlapping/active schedule entries, schedule-1 applied": { + featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", + now: mustParseTime("2024-08-01T10:07:36-06:00"), + cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleTwo}, + want: expected{ + replicas: 10, limited: true, wantErr: false, }, @@ -2392,25 +2521,13 @@ func TestApplySchedulePolicy(t *testing.T) { err := utilruntime.ParseFeatures(tc.featureFlags) assert.NoError(t, err) - // For Counters and Lists - // m := agtesting.NewMocks() - // m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { - // return true, &agonesv1.GameServerList{Items: tc.gsList}, nil - // }) - - // informer := m.AgonesInformerFactory.Agones().V1() - // _, cancel := agtesting.StartInformers(m, - // informer.GameServers().Informer().HasSynced) - // defer cancel() - _, f := defaultFixtures() - replicas, limited, err := applySchedulePolicy(tc.sp, f, nil, nil, tc.now) + replicas, limited, err := applyChainPolicy(*tc.cp, f, nil, nil, tc.now) if tc.want.wantErr { assert.NotNil(t, err) } else { assert.Nil(t, err) - // fmt.Printf("Err: %s\n", err.Error()) assert.Equal(t, tc.want.replicas, replicas) assert.Equal(t, tc.want.limited, limited) } @@ -2418,195 +2535,6 @@ func TestApplySchedulePolicy(t *testing.T) { } } -// nolint:dupl // Linter errors on lines are duplicate of TestApplyChainPolicy -// NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateChainPolicy) -// func TestApplyChainPolicy(t *testing.T) { -// t.Parallel() - -// // For Counters and Lists -// nc := map[string]gameservers.NodeCount{ -// "n1": {Ready: 1, Allocated: 1}, -// } -// modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { -// _, fleet := defaultFixtures() -// f(fleet) -// return fleet -// } - -// // For Webhook Policy -// url := "scale" -// invalidURL := ")1golang.org/" - -// type expected struct { -// replicas int32 -// limited bool -// wantErr bool -// } - -// testCases := map[string]struct { -// fleet *agonesv1.Fleet -// featureFlags string -// specReplicas int32 -// statusReplicas int32 -// statusAllocatedReplicas int32 -// statusReadyReplicas int32 -// now func() time.Time -// cp *autoscalingv1.ChainPolicy -// gsList []agonesv1.GameServer -// want expected -// }{ -// "scheduled autoscaler feature flag not enabled": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", -// cp: &autoscalingv1.ChainPolicy{}, -// now: func() time.Time { -// return time.Time{} -// }, -// want: expected{ -// replicas: 0, -// limited: false, -// wantErr: false, -// }, -// }, -// "nil chain policy": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", -// cp: nil, -// now: func() time.Time { -// return time.Time{} -// }, -// want: expected{ -// replicas: 0, -// limited: false, -// wantErr: false, -// }, -// }, -// "default policy": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", -// cp: &autoscalingv1.ChainPolicy{ -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ -// Type: autoscalingv1.BufferPolicyType, -// Buffer: &autoscalingv1.BufferPolicy{ -// MinReplicas: 5, -// MaxReplicas: 10, -// BufferSize: intstr.FromInt(1), -// }, -// }, -// }, -// }, -// want: expected{ -// replicas: 5, -// limited: false, -// wantErr: false, -// }, -// }, -// "one webhook policy, no default policy": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", -// cp: &autoscalingv1.ChainPolicy{ -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, -// }, -// }, -// }, -// "one webhook policy, one default policy": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", -// cp: &autoscalingv1.ChainPolicy{ -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, -// }, -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ -// Type: autoscalingv1.BufferPolicyType, -// Buffer: &autoscalingv1.BufferPolicy{ -// MinReplicas: 5, -// MaxReplicas: 10, -// BufferSize: intstr.FromInt(1), -// }, -// }, -// }, -// }, -// }, -// "two inactive schedule entries, no default": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", -// cp: &autoscalingv1.ChainPolicy{ -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ -// // Between: autoscalingv1.Between{ -// // Start: "00:00", -// // }, -// }, -// }, -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, -// }, -// }, -// want: expected{ -// replicas: 0, -// limited: false, -// wantErr: false, -// }, -// }, -// "two inactive schedules entries, one default": { -// featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", -// cp: &autoscalingv1.ChainPolicy{ -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, -// }, -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, -// }, -// { -// ID: "", -// FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{}, -// }, -// }, -// want: expected{ -// replicas: 5, -// limited: false, -// wantErr: false, -// }, -// }, -// } - -// utilruntime.FeatureTestMutex.Lock() -// defer utilruntime.FeatureTestMutex.Unlock() - -// for name, tc := range testCases { -// t.Run(name, func(t *testing.T) { -// err := utilruntime.ParseFeatures(tc.featureFlags) -// assert.NoError(t, err) - -// // For Counters and Lists -// m := agtesting.NewMocks() -// m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { -// return true, &agonesv1.GameServerList{Items: tc.gsList}, nil -// }) - -// informer := m.AgonesInformerFactory.Agones().V1() -// _, cancel := agtesting.StartInformers(m, -// informer.GameServers().Informer().HasSynced) -// defer cancel() - -// replicas, limited, err := applyChainPolicy(*tc.cp, tc.fleet, informer.GameServers().Lister(), nc) - -// if tc.want.wantErr { -// assert.NotNil(t, err) -// } else { -// assert.Nil(t, err) -// assert.Equal(t, tc.want.replicas, replicas) -// assert.Equal(t, tc.want.limited, limited) -// } -// }) -// } -// } - // Parse a time string and return a metav1.Time func mustParseMetav1Time(timeStr string) metav1.Time { t, _ := time.Parse(time.RFC3339, timeStr)