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