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 }