agones.dev/agones@v1.54.0/pkg/fleetautoscalers/controller.go (about)

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fleetautoscalers
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"sync"
    23  	"time"
    24  
    25  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    26  	"agones.dev/agones/pkg/apis/autoscaling"
    27  	autoscalingv1 "agones.dev/agones/pkg/apis/autoscaling/v1"
    28  	"agones.dev/agones/pkg/client/clientset/versioned"
    29  	typedagonesv1 "agones.dev/agones/pkg/client/clientset/versioned/typed/agones/v1"
    30  	typedautoscalingv1 "agones.dev/agones/pkg/client/clientset/versioned/typed/autoscaling/v1"
    31  	"agones.dev/agones/pkg/client/informers/externalversions"
    32  	listeragonesv1 "agones.dev/agones/pkg/client/listers/agones/v1"
    33  	listerautoscalingv1 "agones.dev/agones/pkg/client/listers/autoscaling/v1"
    34  	"agones.dev/agones/pkg/gameservers"
    35  	"agones.dev/agones/pkg/util/crd"
    36  	"agones.dev/agones/pkg/util/logfields"
    37  	"agones.dev/agones/pkg/util/runtime"
    38  	"agones.dev/agones/pkg/util/webhooks"
    39  	"agones.dev/agones/pkg/util/workerqueue"
    40  	extism "github.com/extism/go-sdk"
    41  	"github.com/heptiolabs/healthcheck"
    42  	"github.com/pkg/errors"
    43  	"github.com/sirupsen/logrus"
    44  	"gomodules.xyz/jsonpatch/v2"
    45  	admissionv1 "k8s.io/api/admission/v1"
    46  	corev1 "k8s.io/api/core/v1"
    47  	extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    48  	apiextclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
    49  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    50  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    51  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    52  	"k8s.io/apimachinery/pkg/labels"
    53  	runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
    54  	"k8s.io/apimachinery/pkg/types"
    55  	"k8s.io/apimachinery/pkg/util/wait"
    56  	"k8s.io/client-go/kubernetes"
    57  	"k8s.io/client-go/kubernetes/scheme"
    58  	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    59  	"k8s.io/client-go/tools/cache"
    60  	"k8s.io/client-go/tools/record"
    61  	"k8s.io/utils/clock"
    62  )
    63  
    64  // fasState is the state for each Fleet Autoscaler instance
    65  // expand this structure if extra state is needed for other autoscaler
    66  // strategies.
    67  type fasState struct {
    68  	wasmPlugin *extism.Plugin
    69  	httpClient *http.Client
    70  }
    71  
    72  // fasThread is used for tracking each Fleet's autoscaling jobs
    73  type fasThread struct {
    74  	cancel     context.CancelFunc
    75  	state      fasState
    76  	generation int64
    77  }
    78  
    79  // close cancels the context and cleans up any resources
    80  func (ft *fasThread) close(ctx context.Context) {
    81  	ft.cancel()
    82  	if ft.state.wasmPlugin != nil {
    83  		_ = ft.state.wasmPlugin.Close(ctx)
    84  	}
    85  	if ft.state.httpClient != nil {
    86  		ft.state.httpClient.CloseIdleConnections()
    87  	}
    88  }
    89  
    90  // Extensions struct contains what is needed to bind webhook handlers
    91  type Extensions struct {
    92  	baseLogger *logrus.Entry
    93  }
    94  
    95  // FasLogger helps log and record events related to FleetAutoscaler.
    96  type FasLogger struct {
    97  	fas            *autoscalingv1.FleetAutoscaler
    98  	baseLogger     *logrus.Entry
    99  	recorder       record.EventRecorder
   100  	currChainEntry *autoscalingv1.FleetAutoscalerPolicyType
   101  }
   102  
   103  // Controller is the FleetAutoscaler controller
   104  //
   105  //nolint:govet // ignore fieldalignment, singleton
   106  type Controller struct {
   107  	baseLogger            *logrus.Entry
   108  	clock                 clock.WithTickerAndDelayedExecution
   109  	counter               *gameservers.PerNodeCounter
   110  	crdGetter             apiextclientv1.CustomResourceDefinitionInterface
   111  	fasThreads            map[types.UID]fasThread
   112  	fasThreadMutex        sync.Mutex
   113  	fleetGetter           typedagonesv1.FleetsGetter
   114  	fleetLister           listeragonesv1.FleetLister
   115  	fleetSynced           cache.InformerSynced
   116  	fleetAutoscalerGetter typedautoscalingv1.FleetAutoscalersGetter
   117  	fleetAutoscalerLister listerautoscalingv1.FleetAutoscalerLister
   118  	fleetAutoscalerSynced cache.InformerSynced
   119  	workerqueue           *workerqueue.WorkerQueue
   120  	recorder              record.EventRecorder
   121  	gameServerLister      listeragonesv1.GameServerLister
   122  }
   123  
   124  // NewController returns a controller for a FleetAutoscaler
   125  func NewController(
   126  	health healthcheck.Handler,
   127  	kubeClient kubernetes.Interface,
   128  	extClient extclientset.Interface,
   129  	agonesClient versioned.Interface,
   130  	agonesInformerFactory externalversions.SharedInformerFactory,
   131  	counter *gameservers.PerNodeCounter) *Controller {
   132  
   133  	autoscaler := agonesInformerFactory.Autoscaling().V1().FleetAutoscalers()
   134  	fleetInformer := agonesInformerFactory.Agones().V1().Fleets()
   135  	gameServers := agonesInformerFactory.Agones().V1().GameServers()
   136  
   137  	c := &Controller{
   138  		clock:                 clock.RealClock{},
   139  		counter:               counter,
   140  		crdGetter:             extClient.ApiextensionsV1().CustomResourceDefinitions(),
   141  		fasThreads:            map[types.UID]fasThread{},
   142  		fasThreadMutex:        sync.Mutex{},
   143  		fleetGetter:           agonesClient.AgonesV1(),
   144  		fleetLister:           fleetInformer.Lister(),
   145  		fleetSynced:           fleetInformer.Informer().HasSynced,
   146  		fleetAutoscalerGetter: agonesClient.AutoscalingV1(),
   147  		fleetAutoscalerLister: autoscaler.Lister(),
   148  		fleetAutoscalerSynced: autoscaler.Informer().HasSynced,
   149  		gameServerLister:      gameServers.Lister(),
   150  	}
   151  	c.baseLogger = runtime.NewLoggerWithType(c)
   152  	c.workerqueue = workerqueue.NewWorkerQueueWithRateLimiter(c.syncFleetAutoscaler, c.baseLogger, logfields.FleetAutoscalerKey, autoscaling.GroupName+".FleetAutoscalerController", workerqueue.FastRateLimiter(3*time.Second))
   153  	health.AddLivenessCheck("fleetautoscaler-workerqueue", c.workerqueue.Healthy)
   154  
   155  	eventBroadcaster := record.NewBroadcaster()
   156  	eventBroadcaster.StartLogging(c.baseLogger.Debugf)
   157  	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
   158  	c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "fleetautoscaler-controller"})
   159  
   160  	ctx := context.Background()
   161  	_, _ = autoscaler.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
   162  		AddFunc: func(obj interface{}) {
   163  			c.addFasThread(obj.(*autoscalingv1.FleetAutoscaler), true)
   164  		},
   165  		UpdateFunc: func(_, newObj interface{}) {
   166  			c.updateFasThread(ctx, newObj.(*autoscalingv1.FleetAutoscaler))
   167  		},
   168  		DeleteFunc: func(obj interface{}) {
   169  			// Could be a DeletedFinalStateUnknown, in which case, just ignore it
   170  			fas, ok := obj.(*autoscalingv1.FleetAutoscaler)
   171  			if !ok {
   172  				return
   173  			}
   174  			c.deleteFasThread(ctx, fas, true)
   175  		},
   176  	})
   177  
   178  	return c
   179  }
   180  
   181  // NewExtensions binds the handlers to the webhook outside the initialization of the controller
   182  // initializes a new logger for extensions.
   183  func NewExtensions(wh *webhooks.WebHook) *Extensions {
   184  	ext := &Extensions{}
   185  
   186  	ext.baseLogger = runtime.NewLoggerWithType(ext)
   187  
   188  	kind := autoscalingv1.Kind("FleetAutoscaler")
   189  	wh.AddHandler("/mutate", kind, admissionv1.Create, ext.mutationHandler)
   190  	wh.AddHandler("/mutate", kind, admissionv1.Update, ext.mutationHandler)
   191  	wh.AddHandler("/validate", kind, admissionv1.Create, ext.validationHandler)
   192  	wh.AddHandler("/validate", kind, admissionv1.Update, ext.validationHandler)
   193  
   194  	return ext
   195  }
   196  
   197  // Run the FleetAutoscaler controller. Will block until stop is closed.
   198  // Runs threadiness number workers to process the rate limited queue
   199  func (c *Controller) Run(ctx context.Context, workers int) error {
   200  	err := crd.WaitForEstablishedCRD(ctx, c.crdGetter, "fleetautoscalers."+autoscaling.GroupName, c.baseLogger)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	c.baseLogger.Debug("Wait for cache sync")
   206  	if !cache.WaitForCacheSync(ctx.Done(), c.fleetSynced, c.fleetAutoscalerSynced) {
   207  		return errors.New("failed to wait for caches to sync")
   208  	}
   209  
   210  	go func() {
   211  		// clean all go routines when ctx is Done
   212  		<-ctx.Done()
   213  		c.fasThreadMutex.Lock()
   214  		defer c.fasThreadMutex.Unlock()
   215  		for _, thread := range c.fasThreads {
   216  			thread.close(ctx)
   217  		}
   218  	}()
   219  
   220  	c.workerqueue.Run(ctx, workers)
   221  	return nil
   222  }
   223  
   224  func loggerForFleetAutoscalerKey(key string, logger *logrus.Entry) *logrus.Entry {
   225  	return logfields.AugmentLogEntry(logger, logfields.FleetAutoscalerKey, key)
   226  }
   227  
   228  func loggerForFleetAutoscaler(fas *autoscalingv1.FleetAutoscaler, logger *logrus.Entry) *logrus.Entry {
   229  	fasName := "NilFleetAutoScaler"
   230  	if fas != nil {
   231  		fasName = fas.Namespace + "/" + fas.Name
   232  	}
   233  	return loggerForFleetAutoscalerKey(fasName, logger).WithField("fas", fas)
   234  }
   235  
   236  func (c *Controller) loggerForFleetAutoscalerKey(key string) *logrus.Entry {
   237  	return loggerForFleetAutoscalerKey(key, c.baseLogger)
   238  }
   239  
   240  func (c *Controller) loggerForFleetAutoscaler(fas *autoscalingv1.FleetAutoscaler) *logrus.Entry {
   241  	return loggerForFleetAutoscaler(fas, c.baseLogger)
   242  }
   243  
   244  // creationMutationHandler is the handler for the mutating webhook that sets
   245  // the default values on the FleetAutoscaler
   246  func (ext *Extensions) mutationHandler(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) {
   247  	obj := review.Request.Object
   248  	fas := &autoscalingv1.FleetAutoscaler{}
   249  	err := json.Unmarshal(obj.Raw, fas)
   250  	if err != nil {
   251  		// If the JSON is invalid during mutation, fall through to validation. This allows OpenAPI schema validation
   252  		// to proceed, resulting in a more user friendly error message.
   253  		return review, nil
   254  	}
   255  
   256  	fas.ApplyDefaults()
   257  
   258  	newFas, err := json.Marshal(fas)
   259  	if err != nil {
   260  		return review, errors.Wrapf(err, "error marshalling default applied FleetAutoscaler %s to json", fas.ObjectMeta.Name)
   261  	}
   262  
   263  	patch, err := jsonpatch.CreatePatch(obj.Raw, newFas)
   264  	if err != nil {
   265  		return review, errors.Wrapf(err, "error creating patch for FleetAutoscaler %s", fas.ObjectMeta.Name)
   266  	}
   267  
   268  	jsonPatch, err := json.Marshal(patch)
   269  	if err != nil {
   270  		return review, errors.Wrapf(err, "error creating json for patch for FleetAutoScaler %s", fas.ObjectMeta.Name)
   271  	}
   272  
   273  	pt := admissionv1.PatchTypeJSONPatch
   274  	review.Response.PatchType = &pt
   275  	review.Response.Patch = jsonPatch
   276  
   277  	return review, nil
   278  }
   279  
   280  // validationHandler will intercept when a FleetAutoscaler is created, and
   281  // validate its settings.
   282  func (ext *Extensions) validationHandler(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) {
   283  	obj := review.Request.Object
   284  	fas := &autoscalingv1.FleetAutoscaler{}
   285  	err := json.Unmarshal(obj.Raw, fas)
   286  	if err != nil {
   287  		ext.baseLogger.WithField("review", review).WithError(err).Error("validationHandler")
   288  		return review, errors.Wrapf(err, "error unmarshalling FleetAutoscaler json after schema validation: %s", obj.Raw)
   289  	}
   290  	fas.ApplyDefaults()
   291  
   292  	if errs := fas.Validate(); len(errs) > 0 {
   293  		kind := runtimeschema.GroupKind{
   294  			Group: review.Request.Kind.Group,
   295  			Kind:  review.Request.Kind.Kind,
   296  		}
   297  		statusErr := k8serrors.NewInvalid(kind, review.Request.Name, errs)
   298  		review.Response.Allowed = false
   299  		review.Response.Result = &statusErr.ErrStatus
   300  		loggerForFleetAutoscaler(fas, ext.baseLogger).WithField("review", review).Debug("Invalid FleetAutoscaler")
   301  	}
   302  
   303  	return review, nil
   304  }
   305  
   306  // syncFleetAutoscaler syncs FleetAutoScale according to different sync type
   307  func (c *Controller) syncFleetAutoscaler(ctx context.Context, key string) error {
   308  	c.loggerForFleetAutoscalerKey(key).Debug("Synchronising")
   309  
   310  	fas, err := c.getFleetAutoscalerByKey(key)
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	if fas == nil {
   316  		return c.cleanFasThreads(ctx, key)
   317  	}
   318  
   319  	// The state is protected by the workerqueue ensuring that we don't process the same queue item concurrently,
   320  	// and we're only going to get here if the fasThread for this FleetAutoscaler exists,
   321  	// so we can safely read the state without a lock over the whole function.
   322  	c.fasThreadMutex.Lock()
   323  	thread, ok := c.fasThreads[fas.ObjectMeta.UID]
   324  	c.fasThreadMutex.Unlock()
   325  	if !ok {
   326  		return errors.New("There should be a fasThread for the FleetAutoscaler, but it was not found")
   327  	}
   328  
   329  	// Retrieve the fleet by spec name
   330  	fleet, err := c.fleetLister.Fleets(fas.Namespace).Get(fas.Spec.FleetName)
   331  	if err != nil {
   332  		if k8serrors.IsNotFound(err) {
   333  			c.loggerForFleetAutoscaler(fas).Debug("Could not find fleet for autoscaler. Skipping.")
   334  
   335  			c.recorder.Eventf(fas, corev1.EventTypeWarning, "FailedGetFleet",
   336  				"could not fetch fleet: %s", fas.Spec.FleetName)
   337  
   338  			// don't retry. Pick it up next sync.
   339  			err = nil
   340  		}
   341  
   342  		if err := c.updateStatusUnableToScale(ctx, fas); err != nil {
   343  			return err
   344  		}
   345  
   346  		return err
   347  	}
   348  
   349  	// Don't do anything, the fleet is marked for deletion
   350  	if !fleet.DeletionTimestamp.IsZero() {
   351  		return nil
   352  	}
   353  
   354  	fasLog := FasLogger{
   355  		fas:            fas,
   356  		baseLogger:     c.baseLogger,
   357  		recorder:       c.recorder,
   358  		currChainEntry: &fas.Status.LastAppliedPolicy,
   359  	}
   360  
   361  	currentReplicas := fleet.Status.Replicas
   362  	gameServerNamespacedLister := c.gameServerLister.GameServers(fleet.ObjectMeta.Namespace)
   363  	desiredReplicas, scalingLimited, err := computeDesiredFleetSize(ctx, &thread.state, fas.Spec.Policy, fleet, gameServerNamespacedLister, c.counter.Counts(), &fasLog)
   364  
   365  	// If the err is not nil and not an inactive schedule error (ignorable in this case), then record the event
   366  	if err != nil {
   367  		if !errors.Is(err, InactiveScheduleError{}) {
   368  			c.recorder.Eventf(fas, corev1.EventTypeWarning, "FleetAutoscaler",
   369  				"Error calculating desired fleet size on FleetAutoscaler %s. Error: %s", fas.ObjectMeta.Name, err.Error())
   370  
   371  			if err := c.updateStatusUnableToScale(ctx, fas); err != nil {
   372  				return err
   373  			}
   374  		}
   375  		return errors.Wrapf(err, "error calculating autoscaling fleet: %s", fleet.ObjectMeta.Name)
   376  	}
   377  
   378  	// Log the desired replicas and scaling status
   379  	c.loggerForFleetAutoscalerKey(key).Debugf("Computed desired fleet size: %d, Scaling limited: %v", desiredReplicas, scalingLimited)
   380  
   381  	// Scale the fleet to the new size
   382  	if err = c.scaleFleet(ctx, fas, fleet, desiredReplicas); err != nil {
   383  		return errors.Wrapf(err, "error autoscaling fleet %s to %d replicas", fas.Spec.FleetName, desiredReplicas)
   384  	}
   385  
   386  	return c.updateStatus(ctx, fas, currentReplicas, desiredReplicas, desiredReplicas != fleet.Spec.Replicas, scalingLimited, *fasLog.currChainEntry)
   387  }
   388  
   389  // getFleetAutoscalerByKey gets the Fleet Autoscaler by key
   390  // a nil FleetAutoscaler returned indicates that an attempt to sync should not be retried, e.g.  if the FleetAutoscaler no longer exists.
   391  func (c *Controller) getFleetAutoscalerByKey(key string) (*autoscalingv1.FleetAutoscaler, error) {
   392  	// Convert the namespace/name string into a distinct namespace and name
   393  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   394  	if err != nil {
   395  		// don't return an error, as we don't want this retried
   396  		runtime.HandleError(c.loggerForFleetAutoscalerKey(key), errors.Wrapf(err, "invalid resource key"))
   397  		return nil, nil
   398  	}
   399  	fas, err := c.fleetAutoscalerLister.FleetAutoscalers(namespace).Get(name)
   400  	if err != nil {
   401  		if k8serrors.IsNotFound(err) {
   402  			c.loggerForFleetAutoscalerKey(key).Debug(fmt.Sprintf("FleetAutoscaler %s from namespace %s is no longer available for syncing", name, namespace))
   403  			return nil, nil
   404  		}
   405  		return nil, errors.Wrapf(err, "error retrieving FleetAutoscaler %s from namespace %s", name, namespace)
   406  	}
   407  	return fas, nil
   408  }
   409  
   410  // scaleFleet scales the fleet of the autoscaler to a new number of replicas
   411  func (c *Controller) scaleFleet(ctx context.Context, fas *autoscalingv1.FleetAutoscaler, f *agonesv1.Fleet, replicas int32) error {
   412  	if replicas != f.Spec.Replicas {
   413  		fCopy := f.DeepCopy()
   414  		fCopy.Spec.Replicas = replicas
   415  		fCopy, err := c.fleetGetter.Fleets(f.ObjectMeta.Namespace).Update(ctx, fCopy, metav1.UpdateOptions{})
   416  		if err != nil {
   417  			c.recorder.Eventf(fas, corev1.EventTypeWarning, "AutoScalingFleetError",
   418  				"Error on scaling fleet %s from %d to %d. Error: %s", fCopy.ObjectMeta.Name, f.Spec.Replicas, fCopy.Spec.Replicas, err.Error())
   419  			return errors.Wrapf(err, "error updating replicas for fleet %s", f.ObjectMeta.Name)
   420  		}
   421  
   422  		c.recorder.Eventf(fas, corev1.EventTypeNormal, "AutoScalingFleet",
   423  			"Scaling fleet %s from %d to %d", fCopy.ObjectMeta.Name, f.Spec.Replicas, fCopy.Spec.Replicas)
   424  	}
   425  
   426  	return nil
   427  }
   428  
   429  // updateStatus updates the status of the given FleetAutoscaler
   430  func (c *Controller) updateStatus(ctx context.Context, fas *autoscalingv1.FleetAutoscaler, currentReplicas int32, desiredReplicas int32, scaled bool, scalingLimited bool, chainEntry autoscalingv1.FleetAutoscalerPolicyType) error {
   431  	fasCopy := fas.DeepCopy()
   432  	fasCopy.Status.AbleToScale = true
   433  	fasCopy.Status.ScalingLimited = scalingLimited
   434  	fasCopy.Status.CurrentReplicas = currentReplicas
   435  	fasCopy.Status.DesiredReplicas = desiredReplicas
   436  
   437  	if fas.Spec.Policy.Type == autoscalingv1.ChainPolicyType {
   438  		fasCopy.Status.LastAppliedPolicy = chainEntry
   439  	} else {
   440  		fasCopy.Status.LastAppliedPolicy = fas.Spec.Policy.Type
   441  	}
   442  
   443  	if scaled {
   444  		now := metav1.NewTime(time.Now())
   445  		fasCopy.Status.LastScaleTime = &now
   446  	}
   447  
   448  	if !apiequality.Semantic.DeepEqual(fas.Status, fasCopy.Status) {
   449  		if scalingLimited {
   450  			// scalingLimited indicates that the calculated scale would be above or below the range defined by MinReplicas and MaxReplicas
   451  			msg := "Scaling fleet %s was limited to minimum size of %d"
   452  			if currentReplicas > desiredReplicas {
   453  				msg = "Scaling fleet %s was limited to maximum size of %d"
   454  			}
   455  
   456  			c.recorder.Eventf(fas, corev1.EventTypeWarning, "ScalingLimited", msg, fas.Spec.FleetName, desiredReplicas)
   457  		}
   458  
   459  		_, err := c.fleetAutoscalerGetter.FleetAutoscalers(fas.ObjectMeta.Namespace).UpdateStatus(ctx, fasCopy, metav1.UpdateOptions{})
   460  		if err != nil {
   461  			return errors.Wrapf(err, "error updating status for fleetautoscaler %s", fas.ObjectMeta.Name)
   462  		}
   463  	}
   464  
   465  	return nil
   466  }
   467  
   468  // updateStatus updates the status of the given FleetAutoscaler in the case we're not able to scale
   469  func (c *Controller) updateStatusUnableToScale(ctx context.Context, fas *autoscalingv1.FleetAutoscaler) error {
   470  	fasCopy := fas.DeepCopy()
   471  	fasCopy.Status.AbleToScale = false
   472  	fasCopy.Status.ScalingLimited = false
   473  	fasCopy.Status.CurrentReplicas = 0
   474  	fasCopy.Status.DesiredReplicas = 0
   475  	fasCopy.Status.LastAppliedPolicy = autoscalingv1.FleetAutoscalerPolicyType("")
   476  
   477  	if !apiequality.Semantic.DeepEqual(fas.Status, fasCopy.Status) {
   478  		_, err := c.fleetAutoscalerGetter.FleetAutoscalers(fas.ObjectMeta.Namespace).UpdateStatus(ctx, fasCopy, metav1.UpdateOptions{})
   479  		if err != nil {
   480  			return errors.Wrapf(err, "error updating status for fleetautoscaler %s", fas.ObjectMeta.Name)
   481  		}
   482  	}
   483  
   484  	return nil
   485  }
   486  
   487  // addFasThread creates a ticker that enqueues the FleetAutoscaler for it's configured interval.
   488  // If `lock` is set to true, the function will do appropriate locking for this operation. If set to `false`
   489  // make sure to lock the operation with the c.fasThreadMutex for the execution of this command.
   490  func (c *Controller) addFasThread(fas *autoscalingv1.FleetAutoscaler, lock bool) {
   491  	log := c.loggerForFleetAutoscaler(fas)
   492  	log.WithField("seconds", fas.Spec.Sync.FixedInterval.Seconds).Debug("Thread for Autoscaler created")
   493  
   494  	duration := time.Duration(fas.Spec.Sync.FixedInterval.Seconds) * time.Second
   495  
   496  	// store against the UID, as there is no guarantee the name is unique over time.
   497  	ctx, cancel := context.WithCancel(context.Background())
   498  	thread := fasThread{
   499  		cancel:     cancel,
   500  		generation: fas.Generation,
   501  		state:      fasState{},
   502  	}
   503  
   504  	if lock {
   505  		c.fasThreadMutex.Lock()
   506  		defer c.fasThreadMutex.Unlock()
   507  	}
   508  
   509  	// Seems unlikely that concurrent events could fire at the same time for the same UID,
   510  	// but just in case, let's check.
   511  	if _, ok := c.fasThreads[fas.ObjectMeta.UID]; ok {
   512  		return
   513  	}
   514  	c.fasThreads[fas.ObjectMeta.UID] = thread
   515  
   516  	// do immediate enqueue on addition to have an autoscale fire on addition.
   517  	c.workerqueue.Enqueue(fas)
   518  	// Add to queue for each duration period, until cancellation occurs.
   519  	// Workerqueue will handle if multiple attempts are made to add an existing item to the queue, and retries on failure
   520  	// etc.
   521  	go func() {
   522  		wait.Until(func() {
   523  			c.workerqueue.Enqueue(fas)
   524  		}, duration, ctx.Done())
   525  	}()
   526  }
   527  
   528  // updateFasThread will replace the queueing thread if the generation has changes on the FleetAutoscaler.
   529  func (c *Controller) updateFasThread(ctx context.Context, fas *autoscalingv1.FleetAutoscaler) {
   530  	c.fasThreadMutex.Lock()
   531  	defer c.fasThreadMutex.Unlock()
   532  
   533  	thread, ok := c.fasThreads[fas.ObjectMeta.UID]
   534  	if !ok {
   535  		// maybe the controller crashed and we are only getting update events at this point, so let's add
   536  		// the thread back in
   537  		c.addFasThread(fas, false)
   538  		return
   539  	}
   540  
   541  	if fas.Generation != thread.generation {
   542  		c.loggerForFleetAutoscaler(fas).WithField("generation", thread.generation).
   543  			Debug("Fleet autoscaler generation updated, recreating thread")
   544  		c.deleteFasThread(ctx, fas, false)
   545  		c.addFasThread(fas, false)
   546  	}
   547  }
   548  
   549  // deleteFasThread removes a FleetAutoScaler sync routine.
   550  // If `lock` is set to true, the function will do appropriate locking for this operation. If set to `false`
   551  // make sure to lock the operation with the c.fasThreadMutex for the execution of this command.
   552  func (c *Controller) deleteFasThread(ctx context.Context, fas *autoscalingv1.FleetAutoscaler, lock bool) {
   553  	c.loggerForFleetAutoscaler(fas).Debug("Thread for Autoscaler removed")
   554  
   555  	if lock {
   556  		c.fasThreadMutex.Lock()
   557  		defer c.fasThreadMutex.Unlock()
   558  	}
   559  
   560  	if thread, ok := c.fasThreads[fas.ObjectMeta.UID]; ok {
   561  		thread.close(ctx)
   562  		delete(c.fasThreads, fas.ObjectMeta.UID)
   563  	}
   564  }
   565  
   566  // cleanFasThreads will delete any fasThread that no longer
   567  // can be tied to a FleetAutoscaler instance.
   568  func (c *Controller) cleanFasThreads(ctx context.Context, key string) error {
   569  	c.baseLogger.WithField("key", key).Debug("Doing full autoscaler thread cleanup")
   570  	namespace, _, err := cache.SplitMetaNamespaceKey(key)
   571  	if err != nil {
   572  		return errors.Wrap(err, "attempting to clean all fleet autoscaler threads")
   573  	}
   574  
   575  	fasList, err := c.fleetAutoscalerLister.FleetAutoscalers(namespace).List(labels.Everything())
   576  	if err != nil {
   577  		return errors.Wrap(err, "attempting to clean all fleet autoscaler threads")
   578  	}
   579  
   580  	c.fasThreadMutex.Lock()
   581  	defer c.fasThreadMutex.Unlock()
   582  
   583  	keys := map[types.UID]bool{}
   584  	for k := range c.fasThreads {
   585  		keys[k] = true
   586  	}
   587  
   588  	for _, fas := range fasList {
   589  		delete(keys, fas.ObjectMeta.UID)
   590  	}
   591  
   592  	// any key that doesn't match to an existing UID, stop it.
   593  	for k := range keys {
   594  		thread := c.fasThreads[k]
   595  		thread.close(ctx)
   596  		delete(c.fasThreads, k)
   597  	}
   598  
   599  	return nil
   600  }