agones.dev/agones@v1.53.0/pkg/gameserversets/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 gameserversets 16 17 import ( 18 "context" 19 "encoding/json" 20 "sync" 21 "time" 22 23 "agones.dev/agones/pkg/apis" 24 "agones.dev/agones/pkg/apis/agones" 25 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 26 "agones.dev/agones/pkg/client/clientset/versioned" 27 getterv1 "agones.dev/agones/pkg/client/clientset/versioned/typed/agones/v1" 28 "agones.dev/agones/pkg/client/informers/externalversions" 29 listerv1 "agones.dev/agones/pkg/client/listers/agones/v1" 30 "agones.dev/agones/pkg/gameservers" 31 "agones.dev/agones/pkg/util/crd" 32 "agones.dev/agones/pkg/util/logfields" 33 "agones.dev/agones/pkg/util/runtime" 34 "agones.dev/agones/pkg/util/webhooks" 35 "agones.dev/agones/pkg/util/workerqueue" 36 "github.com/google/go-cmp/cmp" 37 "github.com/heptiolabs/healthcheck" 38 "github.com/pkg/errors" 39 "github.com/sirupsen/logrus" 40 "go.opencensus.io/tag" 41 admissionv1 "k8s.io/api/admission/v1" 42 corev1 "k8s.io/api/core/v1" 43 extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 44 apiextclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" 45 k8serrors "k8s.io/apimachinery/pkg/api/errors" 46 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 47 runtimeschema "k8s.io/apimachinery/pkg/runtime/schema" 48 "k8s.io/client-go/kubernetes" 49 "k8s.io/client-go/kubernetes/scheme" 50 typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" 51 "k8s.io/client-go/tools/cache" 52 "k8s.io/client-go/tools/record" 53 ) 54 55 var ( 56 // ErrNoGameServerSetOwner is returned when a GameServerSet can't be found as an owner 57 // for a GameServer 58 ErrNoGameServerSetOwner = errors.New("No GameServerSet owner for this GameServer") 59 ) 60 61 const ( 62 // gameServerErrorDeletionDelay is the minimum amount of time to delay the deletion 63 // of a GameServer in Error state. 64 gameServerErrorDeletionDelay = 30 * time.Second 65 ) 66 67 // Extensions struct contains what is needed to bind webhook handlers 68 type Extensions struct { 69 baseLogger *logrus.Entry 70 apiHooks agonesv1.APIHooks 71 } 72 73 func init() { 74 registerViews() 75 } 76 77 // Controller is a GameServerSet controller 78 type Controller struct { 79 baseLogger *logrus.Entry 80 counter *gameservers.PerNodeCounter 81 crdGetter apiextclientv1.CustomResourceDefinitionInterface 82 gameServerGetter getterv1.GameServersGetter 83 gameServerLister listerv1.GameServerLister 84 gameServerSynced cache.InformerSynced 85 gameServerSetGetter getterv1.GameServerSetsGetter 86 gameServerSetLister listerv1.GameServerSetLister 87 gameServerSetSynced cache.InformerSynced 88 workerqueue *workerqueue.WorkerQueue 89 recorder record.EventRecorder 90 stateCache *gameServerStateCache 91 allocationController *AllocationOverflowController 92 maxCreationParallelism int 93 maxGameServerCreationsPerBatch int 94 maxDeletionParallelism int 95 maxGameServerDeletionsPerBatch int 96 maxPodPendingCount int 97 } 98 99 // NewController returns a new gameserverset crd controller 100 func NewController( 101 health healthcheck.Handler, 102 counter *gameservers.PerNodeCounter, 103 kubeClient kubernetes.Interface, 104 extClient extclientset.Interface, 105 agonesClient versioned.Interface, 106 agonesInformerFactory externalversions.SharedInformerFactory, 107 maxCreationParallelism int, 108 maxDeletionParallelism int, 109 maxGameServerCreationsPerBatch int, 110 maxGameServerDeletionsPerBatch int, 111 maxPodPendingCount int) *Controller { 112 113 gameServers := agonesInformerFactory.Agones().V1().GameServers() 114 gsInformer := gameServers.Informer() 115 gameServerSets := agonesInformerFactory.Agones().V1().GameServerSets() 116 gsSetInformer := gameServerSets.Informer() 117 118 c := &Controller{ 119 crdGetter: extClient.ApiextensionsV1().CustomResourceDefinitions(), 120 counter: counter, 121 gameServerGetter: agonesClient.AgonesV1(), 122 gameServerLister: gameServers.Lister(), 123 gameServerSynced: gsInformer.HasSynced, 124 gameServerSetGetter: agonesClient.AgonesV1(), 125 gameServerSetLister: gameServerSets.Lister(), 126 gameServerSetSynced: gsSetInformer.HasSynced, 127 maxCreationParallelism: maxCreationParallelism, 128 maxDeletionParallelism: maxDeletionParallelism, 129 maxGameServerCreationsPerBatch: maxGameServerCreationsPerBatch, 130 maxGameServerDeletionsPerBatch: maxGameServerDeletionsPerBatch, 131 maxPodPendingCount: maxPodPendingCount, 132 stateCache: &gameServerStateCache{}, 133 } 134 135 c.baseLogger = runtime.NewLoggerWithType(c) 136 c.workerqueue = workerqueue.NewWorkerQueueWithRateLimiter(c.syncGameServerSet, c.baseLogger, logfields.GameServerSetKey, agones.GroupName+".GameServerSetController", workerqueue.FastRateLimiter(3*time.Second)) 137 health.AddLivenessCheck("gameserverset-workerqueue", healthcheck.Check(c.workerqueue.Healthy)) 138 139 eventBroadcaster := record.NewBroadcaster() 140 eventBroadcaster.StartLogging(c.baseLogger.Debugf) 141 eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) 142 c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "gameserverset-controller"}) 143 144 c.allocationController = NewAllocatorOverflowController(health, counter, agonesClient, agonesInformerFactory) 145 146 _, _ = gsSetInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 147 AddFunc: c.workerqueue.Enqueue, 148 UpdateFunc: func(oldObj, newObj interface{}) { 149 oldGss := oldObj.(*agonesv1.GameServerSet) 150 newGss := newObj.(*agonesv1.GameServerSet) 151 if oldGss.Spec.Replicas != newGss.Spec.Replicas { 152 c.workerqueue.Enqueue(newGss) 153 } 154 }, 155 DeleteFunc: func(gsSet interface{}) { 156 c.stateCache.deleteGameServerSet(gsSet.(*agonesv1.GameServerSet)) 157 }, 158 }) 159 160 _, _ = gsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 161 AddFunc: c.gameServerEventHandler, 162 UpdateFunc: func(_, newObj interface{}) { 163 gs := newObj.(*agonesv1.GameServer) 164 // ignore if already being deleted 165 if gs.ObjectMeta.DeletionTimestamp == nil { 166 c.gameServerEventHandler(gs) 167 } 168 }, 169 DeleteFunc: c.gameServerEventHandler, 170 }) 171 172 return c 173 } 174 175 // NewExtensions binds the handlers to the webhook outside the initialization of the controller 176 // initializes a new logger for extensions. 177 func NewExtensions(apiHooks agonesv1.APIHooks, wh *webhooks.WebHook) *Extensions { 178 ext := &Extensions{apiHooks: apiHooks} 179 180 ext.baseLogger = runtime.NewLoggerWithType(ext) 181 182 wh.AddHandler("/validate", agonesv1.Kind("GameServerSet"), admissionv1.Create, ext.creationValidationHandler) 183 wh.AddHandler("/validate", agonesv1.Kind("GameServerSet"), admissionv1.Update, ext.updateValidationHandler) 184 185 return ext 186 } 187 188 // Run the GameServerSet controller. Will block until stop is closed. 189 // Runs threadiness number workers to process the rate limited queue 190 func (c *Controller) Run(ctx context.Context, workers int) error { 191 err := crd.WaitForEstablishedCRD(ctx, c.crdGetter, "gameserversets."+agones.GroupName, c.baseLogger) 192 if err != nil { 193 return err 194 } 195 196 c.baseLogger.Debug("Wait for cache sync") 197 if !cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced, c.gameServerSetSynced) { 198 return errors.New("failed to wait for caches to sync") 199 } 200 201 go func() { 202 if err := c.allocationController.Run(ctx); err != nil { 203 c.baseLogger.WithError(err).Error("error running allocation overflow controller") 204 } 205 }() 206 207 c.workerqueue.Run(ctx, workers) 208 return nil 209 } 210 211 // updateValidationHandler that validates a GameServerSet when is updated 212 // Should only be called on gameserverset update operations. 213 func (ext *Extensions) updateValidationHandler(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) { 214 ext.baseLogger.WithField("review", review).Debug("updateValidationHandler") 215 216 newGss := &agonesv1.GameServerSet{} 217 oldGss := &agonesv1.GameServerSet{} 218 219 newObj := review.Request.Object 220 if err := json.Unmarshal(newObj.Raw, newGss); err != nil { 221 return review, errors.Wrapf(err, "error unmarshalling new GameServerSet json: %s", newObj.Raw) 222 } 223 224 oldObj := review.Request.OldObject 225 if err := json.Unmarshal(oldObj.Raw, oldGss); err != nil { 226 return review, errors.Wrapf(err, "error unmarshalling old GameServerSet json: %s", oldObj.Raw) 227 } 228 229 if errs := oldGss.ValidateUpdate(newGss); len(errs) > 0 { 230 kind := runtimeschema.GroupKind{ 231 Group: review.Request.Kind.Group, 232 Kind: review.Request.Kind.Kind, 233 } 234 statusErr := k8serrors.NewInvalid(kind, review.Request.Name, errs) 235 review.Response.Allowed = false 236 review.Response.Result = &statusErr.ErrStatus 237 loggerForGameServerSet(ext.baseLogger, newGss).WithField("review", review).Debug("Invalid GameServerSet update") 238 } 239 240 return review, nil 241 } 242 243 // creationValidationHandler that validates a GameServerSet when is created 244 // Should only be called on gameserverset create operations. 245 func (ext *Extensions) creationValidationHandler(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) { 246 ext.baseLogger.WithField("review", review).Debug("creationValidationHandler") 247 248 newGss := &agonesv1.GameServerSet{} 249 250 newObj := review.Request.Object 251 if err := json.Unmarshal(newObj.Raw, newGss); err != nil { 252 return review, errors.Wrapf(err, "error unmarshalling GameServerSet json after schema validation: %s", newObj.Raw) 253 } 254 255 if errs := newGss.Validate(ext.apiHooks); len(errs) > 0 { 256 kind := runtimeschema.GroupKind{ 257 Group: review.Request.Kind.Group, 258 Kind: review.Request.Kind.Kind, 259 } 260 statusErr := k8serrors.NewInvalid(kind, review.Request.Name, errs) 261 review.Response.Allowed = false 262 review.Response.Result = &statusErr.ErrStatus 263 loggerForGameServerSet(ext.baseLogger, newGss).WithField("review", review).Debug("Invalid GameServerSet update") 264 } 265 266 return review, nil 267 } 268 269 func (c *Controller) gameServerEventHandler(obj interface{}) { 270 gs, ok := obj.(*agonesv1.GameServer) 271 if !ok { 272 return 273 } 274 275 ref := metav1.GetControllerOf(gs) 276 if ref == nil { 277 return 278 } 279 gsSet, err := c.gameServerSetLister.GameServerSets(gs.ObjectMeta.Namespace).Get(ref.Name) 280 if err != nil { 281 if k8serrors.IsNotFound(err) { 282 c.baseLogger.WithField("ref", ref).Debug("Owner GameServerSet no longer available for syncing") 283 } else { 284 runtime.HandleError(c.baseLogger.WithField("gsKey", gs.ObjectMeta.Namespace+"/"+gs.ObjectMeta.Name).WithField("ref", ref), 285 errors.Wrap(err, "error retrieving GameServer owner")) 286 } 287 return 288 } 289 c.workerqueue.EnqueueImmediately(gsSet) 290 } 291 292 // syncGameServer synchronises the GameServers for the Set, 293 // making sure there are aways as many GameServers as requested 294 func (c *Controller) syncGameServerSet(ctx context.Context, key string) error { 295 // Convert the namespace/name string into a distinct namespace and name 296 namespace, name, err := cache.SplitMetaNamespaceKey(key) 297 if err != nil { 298 // don't return an error, as we don't want this retried 299 runtime.HandleError(loggerForGameServerSetKey(c.baseLogger, key), errors.Wrapf(err, "invalid resource key")) 300 return nil 301 } 302 303 gsSet, err := c.gameServerSetLister.GameServerSets(namespace).Get(name) 304 if err != nil { 305 if k8serrors.IsNotFound(err) { 306 loggerForGameServerSetKey(c.baseLogger, key).Debug("GameServerSet is no longer available for syncing") 307 return nil 308 } 309 return errors.Wrapf(err, "error retrieving GameServerSet %s from namespace %s", name, namespace) 310 } 311 312 list, err := ListGameServersByGameServerSetOwner(c.gameServerLister, gsSet) 313 if err != nil { 314 return err 315 } 316 317 list = c.stateCache.forGameServerSet(gsSet).reconcileWithUpdatedServerList(list) 318 319 numServersToAdd, toDelete, isPartial := computeReconciliationAction(gsSet.Spec.Scheduling, list, c.counter.Counts(), 320 int(gsSet.Spec.Replicas), c.maxGameServerCreationsPerBatch, c.maxGameServerDeletionsPerBatch, c.maxPodPendingCount, gsSet.Spec.Priorities) 321 322 // GameserverSet is marked for deletion then don't add gameservers. 323 if !gsSet.DeletionTimestamp.IsZero() { 324 numServersToAdd = 0 325 } 326 327 status := computeStatus(gsSet, list) 328 fields := logrus.Fields{} 329 330 for _, gs := range list { 331 key := "gsCount" + string(gs.Status.State) 332 if gs.ObjectMeta.DeletionTimestamp != nil { 333 key += "Deleted" 334 } 335 v, ok := fields[key] 336 if !ok { 337 v = 0 338 } 339 340 fields[key] = v.(int) + 1 341 } 342 loggerForGameServerSet(c.baseLogger, gsSet). 343 WithField("targetReplicaCount", gsSet.Spec.Replicas). 344 WithField("numServersToAdd", numServersToAdd). 345 WithField("numServersToDelete", len(toDelete)). 346 WithField("isPartial", isPartial). 347 WithField("status", status). 348 WithFields(fields). 349 Debug("Reconciling GameServerSet") 350 if isPartial { 351 // we've determined that there's work to do, but we've decided not to do all the work in one shot 352 // make sure we get a follow-up, by re-scheduling this GSS in the worker queue immediately before this 353 // function returns 354 defer c.workerqueue.EnqueueImmediately(gsSet) 355 } 356 357 if numServersToAdd > 0 { 358 if err := c.addMoreGameServers(ctx, gsSet, numServersToAdd); err != nil { 359 loggerForGameServerSet(c.baseLogger, gsSet).WithError(err).Warning("error adding game servers") 360 return errors.Wrap(err, "error adding game servers") 361 } 362 } 363 364 if len(toDelete) > 0 { 365 if err := c.deleteGameServers(ctx, gsSet, toDelete); err != nil { 366 loggerForGameServerSet(c.baseLogger, gsSet).WithError(err).Warning("error deleting game servers") 367 return errors.Wrap(err, "error deleting game servers") 368 } 369 } 370 371 return c.syncGameServerSetStatus(ctx, gsSet, list) 372 } 373 374 // computeReconciliationAction computes the action to take to reconcile a game server set set given 375 // the list of game servers that were found and target replica count. 376 func computeReconciliationAction(strategy apis.SchedulingStrategy, list []*agonesv1.GameServer, 377 counts map[string]gameservers.NodeCount, targetReplicaCount int, maxCreations int, maxDeletions int, 378 maxPending int, priorities []agonesv1.Priority) (int, []*agonesv1.GameServer, bool) { 379 var upCount int // up == Ready or will become ready 380 var deleteCount int // number of gameservers to delete 381 382 // track the number of pods that are being created at any given moment by the GameServerSet 383 // so we can limit it at a throughput that Kubernetes can handle 384 var podPendingCount int // podPending == "up" but don't have a Pod running yet 385 386 var potentialDeletions []*agonesv1.GameServer 387 var toDelete []*agonesv1.GameServer 388 389 scheduleDeletion := func(gs *agonesv1.GameServer) { 390 toDelete = append(toDelete, gs) 391 deleteCount-- 392 } 393 394 handleGameServerUp := func(gs *agonesv1.GameServer) { 395 if upCount >= targetReplicaCount { 396 deleteCount++ 397 } else { 398 upCount++ 399 } 400 401 // Track gameservers that could be potentially deleted 402 potentialDeletions = append(potentialDeletions, gs) 403 } 404 405 // pass 1 - count allocated/reserved servers only, since those can't be touched 406 for _, gs := range list { 407 if !gs.IsDeletable() { 408 upCount++ 409 } 410 } 411 412 // pass 2 - handle all other statuses 413 for _, gs := range list { 414 if !gs.IsDeletable() { 415 // already handled above 416 continue 417 } 418 419 // GS being deleted don't count. 420 if gs.IsBeingDeleted() { 421 continue 422 } 423 424 switch gs.Status.State { 425 case agonesv1.GameServerStatePortAllocation: 426 podPendingCount++ 427 handleGameServerUp(gs) 428 case agonesv1.GameServerStateCreating: 429 podPendingCount++ 430 handleGameServerUp(gs) 431 case agonesv1.GameServerStateStarting: 432 podPendingCount++ 433 handleGameServerUp(gs) 434 case agonesv1.GameServerStateScheduled: 435 podPendingCount++ 436 handleGameServerUp(gs) 437 case agonesv1.GameServerStateRequestReady: 438 handleGameServerUp(gs) 439 case agonesv1.GameServerStateReady: 440 handleGameServerUp(gs) 441 case agonesv1.GameServerStateReserved: 442 handleGameServerUp(gs) 443 444 // GameServerStateShutdown - already handled above 445 // GameServerStateAllocated - already handled above 446 case agonesv1.GameServerStateError: 447 if !shouldDeleteErroredGameServer(gs) { 448 // The GameServer is in an Error state and should not be deleted yet. 449 // To stop an ever-increasing number of GameServers from being created, 450 // consider the Error state GameServers as up and pending. This stops high 451 // churn rate that can negatively impact Kubernetes. 452 podPendingCount++ 453 handleGameServerUp(gs) 454 } else { 455 scheduleDeletion(gs) 456 } 457 458 case agonesv1.GameServerStateUnhealthy: 459 scheduleDeletion(gs) 460 default: 461 // unrecognized state, assume it's up. 462 handleGameServerUp(gs) 463 } 464 } 465 466 var partialReconciliation bool 467 var numServersToAdd int 468 469 if upCount < targetReplicaCount { 470 numServersToAdd = targetReplicaCount - upCount 471 originalNumServersToAdd := numServersToAdd 472 473 if numServersToAdd > maxCreations { 474 numServersToAdd = maxCreations 475 } 476 477 if numServersToAdd+podPendingCount > maxPending { 478 numServersToAdd = maxPending - podPendingCount 479 if numServersToAdd < 0 { 480 numServersToAdd = 0 481 } 482 } 483 484 if originalNumServersToAdd != numServersToAdd { 485 partialReconciliation = true 486 } 487 } 488 489 if deleteCount > 0 { 490 potentialDeletions = SortGameServersByStrategy(strategy, potentialDeletions, counts, priorities) 491 toDelete = append(toDelete, potentialDeletions[0:deleteCount]...) 492 } 493 494 if len(toDelete) > maxDeletions { 495 toDelete = toDelete[0:maxDeletions] 496 partialReconciliation = true 497 } 498 499 return numServersToAdd, toDelete, partialReconciliation 500 } 501 502 func shouldDeleteErroredGameServer(gs *agonesv1.GameServer) bool { 503 erroredAtStr := gs.Annotations[agonesv1.GameServerErroredAtAnnotation] 504 if erroredAtStr == "" { 505 return true 506 } 507 508 erroredAt, err := time.Parse(time.RFC3339, erroredAtStr) 509 if err != nil { 510 // The annotation is in the wrong format, delete the GameServer. 511 return true 512 } 513 514 if time.Since(erroredAt) >= gameServerErrorDeletionDelay { 515 return true 516 } 517 return false 518 } 519 520 // addMoreGameServers adds diff more GameServers to the set 521 func (c *Controller) addMoreGameServers(ctx context.Context, gsSet *agonesv1.GameServerSet, count int) (err error) { 522 loggerForGameServerSet(c.baseLogger, gsSet).WithField("count", count).Debug("Adding more gameservers") 523 latency := c.newMetrics(ctx) 524 latency.setRequest(count) 525 526 defer func() { 527 if err != nil { 528 latency.setError("error") 529 } 530 latency.record() 531 532 }() 533 534 return parallelize(newGameServersChannel(count, gsSet), c.maxCreationParallelism, func(gs *agonesv1.GameServer) error { 535 gs, err := c.gameServerGetter.GameServers(gs.Namespace).Create(ctx, gs, metav1.CreateOptions{}) 536 if err != nil { 537 return errors.Wrapf(err, "error creating gameserver for gameserverset %s", gsSet.ObjectMeta.Name) 538 } 539 540 c.stateCache.forGameServerSet(gsSet).created(gs) 541 c.recorder.Eventf(gsSet, corev1.EventTypeNormal, "SuccessfulCreate", "Created gameserver: %s", gs.ObjectMeta.Name) 542 return nil 543 }) 544 } 545 546 func (c *Controller) deleteGameServers(ctx context.Context, gsSet *agonesv1.GameServerSet, toDelete []*agonesv1.GameServer) error { 547 loggerForGameServerSet(c.baseLogger, gsSet).WithField("diff", len(toDelete)).Debug("Deleting gameservers") 548 549 return parallelize(gameServerListToChannel(toDelete), c.maxDeletionParallelism, func(gs *agonesv1.GameServer) error { 550 // We should not delete the gameservers directly buy set their state to shutdown and let the gameserver controller to delete 551 gsCopy := gs.DeepCopy() 552 gsCopy.Status.State = agonesv1.GameServerStateShutdown 553 _, err := c.gameServerGetter.GameServers(gs.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{}) 554 if err != nil { 555 return errors.Wrapf(err, "error updating gameserver %s from status %s to Shutdown status", gs.ObjectMeta.Name, gs.Status.State) 556 } 557 558 c.stateCache.forGameServerSet(gsSet).deleted(gs) 559 c.recorder.Eventf(gsSet, corev1.EventTypeNormal, "SuccessfulDelete", "Deleted gameserver in state %s: %v", gs.Status.State, gs.ObjectMeta.Name) 560 return nil 561 }) 562 } 563 564 func newGameServersChannel(n int, gsSet *agonesv1.GameServerSet) chan *agonesv1.GameServer { 565 gameServers := make(chan *agonesv1.GameServer) 566 go func() { 567 defer close(gameServers) 568 569 for i := 0; i < n; i++ { 570 gameServers <- gsSet.GameServer() 571 } 572 }() 573 574 return gameServers 575 } 576 577 func gameServerListToChannel(list []*agonesv1.GameServer) chan *agonesv1.GameServer { 578 gameServers := make(chan *agonesv1.GameServer) 579 go func() { 580 defer close(gameServers) 581 582 for _, gs := range list { 583 gameServers <- gs 584 } 585 }() 586 587 return gameServers 588 } 589 590 // parallelize processes a channel of game server objects, invoking the provided callback for items in the channel with the specified degree of parallelism up to a limit. 591 // Returns nil if all callbacks returned nil or one of the error responses, not necessarily the first one. 592 func parallelize(gameServers chan *agonesv1.GameServer, parallelism int, work func(gs *agonesv1.GameServer) error) error { 593 errch := make(chan error, parallelism) 594 595 var wg sync.WaitGroup 596 597 for i := 0; i < parallelism; i++ { 598 wg.Add(1) 599 600 go func() { 601 defer wg.Done() 602 for it := range gameServers { 603 err := work(it) 604 if err != nil { 605 errch <- err 606 break 607 } 608 } 609 }() 610 } 611 wg.Wait() 612 close(errch) 613 614 for range gameServers { 615 // drain any remaining game servers in the channel, in case we did not consume them all 616 continue 617 } 618 619 // return first error from the channel, or nil if all successful. 620 return <-errch 621 } 622 623 // syncGameServerSetStatus synchronises the GameServerSet State with active GameServer counts 624 func (c *Controller) syncGameServerSetStatus(ctx context.Context, gsSet *agonesv1.GameServerSet, list []*agonesv1.GameServer) error { 625 return c.updateStatusIfChanged(ctx, gsSet, computeStatus(gsSet, list)) 626 } 627 628 // updateStatusIfChanged updates GameServerSet status if it's different than provided. 629 func (c *Controller) updateStatusIfChanged(ctx context.Context, gsSet *agonesv1.GameServerSet, status agonesv1.GameServerSetStatus) error { 630 if !cmp.Equal(gsSet.Status, status) { 631 gsSetCopy := gsSet.DeepCopy() 632 gsSetCopy.Status = status 633 _, err := c.gameServerSetGetter.GameServerSets(gsSet.ObjectMeta.Namespace).UpdateStatus(ctx, gsSetCopy, metav1.UpdateOptions{}) 634 if err != nil { 635 return errors.Wrapf(err, "error updating status on GameServerSet %s", gsSet.ObjectMeta.Name) 636 } 637 } 638 return nil 639 } 640 641 // computeStatus computes the status of the game server set. 642 func computeStatus(gsSet *agonesv1.GameServerSet, list []*agonesv1.GameServer) agonesv1.GameServerSetStatus { 643 var status agonesv1.GameServerSetStatus 644 645 // Initialize list status with empty lists from spec 646 if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 647 status.Lists = createInitialListStatus(gsSet) 648 status.Counters = createInitialCounterStatus(gsSet) 649 } 650 for _, gs := range list { 651 if gs.IsBeingDeleted() { 652 // don't count GS that are being deleted 653 status.ShutdownReplicas++ 654 continue 655 } 656 657 status.Replicas++ 658 switch gs.Status.State { 659 case agonesv1.GameServerStateReady: 660 status.ReadyReplicas++ 661 case agonesv1.GameServerStateAllocated: 662 status.AllocatedReplicas++ 663 case agonesv1.GameServerStateReserved: 664 status.ReservedReplicas++ 665 } 666 667 // Drop Counters and Lists status if the feature flag has been set to false 668 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 669 if len(status.Counters) != 0 || len(status.Lists) != 0 { 670 status.Counters = map[string]agonesv1.AggregatedCounterStatus{} 671 status.Lists = map[string]agonesv1.AggregatedListStatus{} 672 } 673 } 674 // Aggregates all Counters and Lists only for GameServer all states (except IsBeingDeleted) 675 if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 676 status.Counters = aggregateCounters(status.Counters, gs.Status.Counters, gs.Status.State) 677 status.Lists = aggregateLists(status.Lists, gs.Status.Lists, gs.Status.State) 678 } 679 } 680 681 if runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { 682 // to make this code simpler, while the feature gate is in place, 683 // we will loop around the gs list twice. 684 status.Players = &agonesv1.AggregatedPlayerStatus{} 685 // TODO: integrate this extra loop into the above for loop when PlayerTracking moves to GA 686 for _, gs := range list { 687 if gs.ObjectMeta.DeletionTimestamp.IsZero() && 688 (gs.Status.State == agonesv1.GameServerStateReady || 689 gs.Status.State == agonesv1.GameServerStateReserved || 690 gs.Status.State == agonesv1.GameServerStateAllocated) { 691 if gs.Status.Players != nil { 692 status.Players.Capacity += gs.Status.Players.Capacity 693 status.Players.Count += gs.Status.Players.Count 694 } 695 } 696 } 697 } 698 699 return status 700 } 701 702 func createInitialListStatus(gsSet *agonesv1.GameServerSet) map[string]agonesv1.AggregatedListStatus { 703 list := make(map[string]agonesv1.AggregatedListStatus) 704 for name := range gsSet.Spec.Template.Spec.Lists { 705 list[name] = agonesv1.AggregatedListStatus{} 706 } 707 return list 708 } 709 710 func createInitialCounterStatus(gsSet *agonesv1.GameServerSet) map[string]agonesv1.AggregatedCounterStatus { 711 counters := make(map[string]agonesv1.AggregatedCounterStatus) 712 for name := range gsSet.Spec.Template.Spec.Counters { 713 counters[name] = agonesv1.AggregatedCounterStatus{} 714 } 715 return counters 716 } 717 718 // aggregateCounters adds the contents of a CounterStatus map to an AggregatedCounterStatus map. 719 func aggregateCounters(aggCounterStatus map[string]agonesv1.AggregatedCounterStatus, 720 counterStatus map[string]agonesv1.CounterStatus, 721 gsState agonesv1.GameServerState) map[string]agonesv1.AggregatedCounterStatus { 722 723 if aggCounterStatus == nil { 724 aggCounterStatus = make(map[string]agonesv1.AggregatedCounterStatus) 725 } 726 727 for key, val := range counterStatus { 728 // If the Counter exists in both maps, aggregate the values. 729 if counter, ok := aggCounterStatus[key]; ok { 730 // Aggregate for all game server statuses (expected IsBeingDeleted) 731 counter.Count = agonesv1.SafeAdd(counter.Count, val.Count) 732 counter.Capacity = agonesv1.SafeAdd(counter.Capacity, val.Capacity) 733 734 // Aggregate for Allocated game servers only 735 if gsState == agonesv1.GameServerStateAllocated { 736 counter.AllocatedCount = agonesv1.SafeAdd(counter.AllocatedCount, val.Count) 737 counter.AllocatedCapacity = agonesv1.SafeAdd(counter.AllocatedCapacity, val.Capacity) 738 } 739 aggCounterStatus[key] = counter 740 } else { 741 tmp := val.DeepCopy() 742 allocatedCount := int64(0) 743 allocatedCapacity := int64(0) 744 if gsState == agonesv1.GameServerStateAllocated { 745 allocatedCount = tmp.Count 746 allocatedCapacity = tmp.Capacity 747 } 748 aggCounterStatus[key] = agonesv1.AggregatedCounterStatus{ 749 AllocatedCount: allocatedCount, 750 AllocatedCapacity: allocatedCapacity, 751 Capacity: tmp.Capacity, 752 Count: tmp.Count, 753 } 754 } 755 } 756 757 return aggCounterStatus 758 } 759 760 // aggregateLists adds the contents of a ListStatus map to an AggregatedListStatus map. 761 func aggregateLists(aggListStatus map[string]agonesv1.AggregatedListStatus, 762 listStatus map[string]agonesv1.ListStatus, 763 gsState agonesv1.GameServerState) map[string]agonesv1.AggregatedListStatus { 764 765 if aggListStatus == nil { 766 aggListStatus = make(map[string]agonesv1.AggregatedListStatus) 767 } 768 769 for key, val := range listStatus { 770 // If the List exists in both maps, aggregate the values. 771 if list, ok := aggListStatus[key]; ok { 772 list.Capacity += val.Capacity 773 // We do include duplicates in the Count. 774 list.Count += int64(len(val.Values)) 775 if gsState == agonesv1.GameServerStateAllocated { 776 list.AllocatedCount += int64(len(val.Values)) 777 list.AllocatedCapacity += val.Capacity 778 } 779 aggListStatus[key] = list 780 } else { 781 tmp := val.DeepCopy() 782 allocatedCount := int64(0) 783 allocatedCapacity := int64(0) 784 if gsState == agonesv1.GameServerStateAllocated { 785 allocatedCount = int64(len(tmp.Values)) 786 allocatedCapacity = tmp.Capacity 787 } 788 aggListStatus[key] = agonesv1.AggregatedListStatus{ 789 AllocatedCount: allocatedCount, 790 AllocatedCapacity: allocatedCapacity, 791 Capacity: tmp.Capacity, 792 Count: int64(len(tmp.Values)), 793 } 794 } 795 } 796 797 return aggListStatus 798 } 799 800 // newMetrics creates a new gss latency recorder. 801 func (c *Controller) newMetrics(ctx context.Context) *metrics { 802 ctx, err := tag.New(ctx, latencyTags...) 803 if err != nil { 804 c.baseLogger.WithError(err).Warn("failed to tag latency recorder.") 805 } 806 return &metrics{ 807 ctx: ctx, 808 gameServerLister: c.gameServerLister, 809 logger: c.baseLogger, 810 start: time.Now(), 811 } 812 }