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 }