agones.dev/agones@v1.53.0/pkg/gameserverallocations/allocation_cache.go (about)

     1  // Copyright 2021 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 gameserverallocations
    16  
    17  import (
    18  	"context"
    19  	"sort"
    20  
    21  	"agones.dev/agones/pkg/apis/agones"
    22  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    23  	allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
    24  	informerv1 "agones.dev/agones/pkg/client/informers/externalversions/agones/v1"
    25  	listerv1 "agones.dev/agones/pkg/client/listers/agones/v1"
    26  	"agones.dev/agones/pkg/gameservers"
    27  	"agones.dev/agones/pkg/util/logfields"
    28  	"agones.dev/agones/pkg/util/runtime"
    29  	"agones.dev/agones/pkg/util/workerqueue"
    30  	"github.com/heptiolabs/healthcheck"
    31  	"github.com/pkg/errors"
    32  	"github.com/sirupsen/logrus"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/client-go/tools/cache"
    35  )
    36  
    37  type matcher func(*agonesv1.GameServer) bool
    38  
    39  // readyOrAllocatedGameServerMatcher returns true when a GameServer is in a Ready or Allocated state.
    40  func readyOrAllocatedGameServerMatcher(gs *agonesv1.GameServer) bool {
    41  	return gs.Status.State == agonesv1.GameServerStateReady || gs.Status.State == agonesv1.GameServerStateAllocated
    42  }
    43  
    44  // AllocationCache maintains a cache of GameServers that could potentially be allocated.
    45  type AllocationCache struct {
    46  	baseLogger       *logrus.Entry
    47  	cache            gameServerCache
    48  	gameServerLister listerv1.GameServerLister
    49  	gameServerSynced cache.InformerSynced
    50  	workerqueue      *workerqueue.WorkerQueue
    51  	counter          *gameservers.PerNodeCounter
    52  	matcher          matcher
    53  }
    54  
    55  // NewAllocationCache creates a new instance of AllocationCache
    56  func NewAllocationCache(informer informerv1.GameServerInformer, counter *gameservers.PerNodeCounter, health healthcheck.Handler) *AllocationCache {
    57  	c := &AllocationCache{
    58  		gameServerSynced: informer.Informer().HasSynced,
    59  		gameServerLister: informer.Lister(),
    60  		counter:          counter,
    61  		matcher:          readyOrAllocatedGameServerMatcher,
    62  	}
    63  
    64  	_, _ = informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    65  		UpdateFunc: func(oldObj, newObj interface{}) {
    66  			// only interested in if the old / new state was/is Ready
    67  			oldGs := oldObj.(*agonesv1.GameServer)
    68  			newGs := newObj.(*agonesv1.GameServer)
    69  			key, ok := c.getKey(newGs)
    70  			if !ok {
    71  				return
    72  			}
    73  			if oldGs.ObjectMeta.ResourceVersion == newGs.ObjectMeta.ResourceVersion {
    74  				return
    75  			}
    76  			switch {
    77  			case newGs.IsBeingDeleted():
    78  				c.cache.Delete(key)
    79  			case c.matcher(newGs):
    80  				c.cache.Store(key, newGs)
    81  			case c.matcher(oldGs):
    82  				c.cache.Delete(key)
    83  			}
    84  		},
    85  		DeleteFunc: func(obj interface{}) {
    86  			gs, ok := obj.(*agonesv1.GameServer)
    87  			if !ok {
    88  				return
    89  			}
    90  			var key string
    91  			if key, ok = c.getKey(gs); ok {
    92  				c.cache.Delete(key)
    93  			}
    94  		},
    95  	})
    96  
    97  	c.baseLogger = runtime.NewLoggerWithType(c)
    98  	c.workerqueue = workerqueue.NewWorkerQueue(c.SyncGameServers, c.baseLogger, logfields.GameServerKey, agones.GroupName+".AllocationCache")
    99  	health.AddLivenessCheck("allocationcache-workerqueue", healthcheck.Check(c.workerqueue.Healthy))
   100  
   101  	return c
   102  }
   103  
   104  func (c *AllocationCache) loggerForGameServerKey(key string) *logrus.Entry {
   105  	return logfields.AugmentLogEntry(c.baseLogger, logfields.GameServerKey, key)
   106  }
   107  
   108  // RemoveGameServer removes a gameserver from the cache of game servers
   109  func (c *AllocationCache) RemoveGameServer(gs *agonesv1.GameServer) error {
   110  	key, _ := cache.MetaNamespaceKeyFunc(gs)
   111  	if ok := c.cache.Delete(key); !ok {
   112  		return ErrConflictInGameServerSelection
   113  	}
   114  	return nil
   115  }
   116  
   117  // Sync builds the initial cache from the current set GameServers in the cluster
   118  func (c *AllocationCache) Sync(ctx context.Context) error {
   119  	c.baseLogger.Debug("Wait for AllocationCache cache sync")
   120  	if !cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced) {
   121  		return errors.New("failed to wait for caches to sync")
   122  	}
   123  
   124  	// build the cache
   125  	return c.syncCache()
   126  }
   127  
   128  // Resync enqueues an empty game server to be synced. Using queue helps avoiding multiple threads syncing at the same time.
   129  func (c *AllocationCache) Resync() {
   130  	// this will trigger syncing of the cache (assuming cache might not be up to date)
   131  	c.workerqueue.EnqueueImmediately(&agonesv1.GameServer{})
   132  }
   133  
   134  // Run prepares cache to start
   135  func (c *AllocationCache) Run(ctx context.Context) error {
   136  	if err := c.Sync(ctx); err != nil {
   137  		return err
   138  	}
   139  	// we don't want mutiple workers refresh cache at the same time so one worker will be better.
   140  	// Also we don't expect to have too many failures when allocating
   141  	go c.workerqueue.Run(ctx, 1)
   142  	return nil
   143  }
   144  
   145  // AddGameServer adds a gameserver to the cache of allocatable GameServers
   146  func (c *AllocationCache) AddGameServer(gs *agonesv1.GameServer) {
   147  	key, _ := cache.MetaNamespaceKeyFunc(gs)
   148  
   149  	c.cache.Store(key, gs)
   150  }
   151  
   152  // getGameServers returns a list of game servers in the cache.
   153  func (c *AllocationCache) getGameServers() []*agonesv1.GameServer {
   154  	length := c.cache.Len()
   155  	if length == 0 {
   156  		return nil
   157  	}
   158  
   159  	list := make([]*agonesv1.GameServer, 0, length)
   160  	c.cache.Range(func(_ string, gs *agonesv1.GameServer) bool {
   161  		list = append(list, gs)
   162  		return true
   163  	})
   164  	return list
   165  }
   166  
   167  // ListSortedGameServers returns a list of the cached gameservers
   168  // sorted by most allocated to least.
   169  func (c *AllocationCache) ListSortedGameServers(gsa *allocationv1.GameServerAllocation) []*agonesv1.GameServer {
   170  	list := c.getGameServers()
   171  	if list == nil {
   172  		return []*agonesv1.GameServer{}
   173  	}
   174  	counts := c.counter.Counts()
   175  
   176  	sort.Slice(list, func(i, j int) bool {
   177  		gs1 := list[i]
   178  		gs2 := list[j]
   179  
   180  		// Search Allocated GameServers first.
   181  		if gs1.Status.State != gs2.Status.State {
   182  			return gs1.Status.State == agonesv1.GameServerStateAllocated
   183  		}
   184  
   185  		c1, ok := counts[gs1.Status.NodeName]
   186  		if !ok {
   187  			return false
   188  		}
   189  
   190  		c2, ok := counts[gs2.Status.NodeName]
   191  		if !ok {
   192  			return true
   193  		}
   194  
   195  		if c1.Allocated > c2.Allocated {
   196  			return true
   197  		}
   198  		if c1.Allocated < c2.Allocated {
   199  			return false
   200  		}
   201  
   202  		// prefer nodes that have the most Ready gameservers on them - they are most likely to be
   203  		// completely filled and least likely target for scale down.
   204  		if c1.Ready < c2.Ready {
   205  			return false
   206  		}
   207  		if c1.Ready > c2.Ready {
   208  			return true
   209  		}
   210  
   211  		// if player tracking is enabled, prefer game servers with the least amount of room left
   212  		if runtime.FeatureEnabled(runtime.FeaturePlayerAllocationFilter) {
   213  			if gs1.Status.Players != nil && gs2.Status.Players != nil {
   214  				cap1 := gs1.Status.Players.Capacity - gs1.Status.Players.Count
   215  				cap2 := gs2.Status.Players.Capacity - gs2.Status.Players.Count
   216  
   217  				// if they are equal, pass the comparison through.
   218  				if cap1 < cap2 {
   219  					return true
   220  				} else if cap2 < cap1 {
   221  					return false
   222  				}
   223  			}
   224  		}
   225  
   226  		// if we end up here, then break the tie with Counter or List Priority.
   227  		if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) && (gsa != nil) {
   228  			if res := gs1.CompareCountAndListPriorities(gsa.Spec.Priorities, gs2); res != nil {
   229  				return *res
   230  			}
   231  		}
   232  
   233  		// finally sort lexicographically, so we have a stable order
   234  		return gs1.GetObjectMeta().GetName() < gs2.GetObjectMeta().GetName()
   235  	})
   236  
   237  	return list
   238  }
   239  
   240  // ListSortedGameServersPriorities sorts and returns a list of game servers based on the
   241  // list of Priorities.
   242  func (c *AllocationCache) ListSortedGameServersPriorities(gsa *allocationv1.GameServerAllocation) []*agonesv1.GameServer {
   243  	list := c.getGameServers()
   244  	if list == nil {
   245  		return []*agonesv1.GameServer{}
   246  	}
   247  
   248  	sort.Slice(list, func(i, j int) bool {
   249  		gs1 := list[i]
   250  		gs2 := list[j]
   251  
   252  		if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) && (gsa != nil) {
   253  			if res := gs1.CompareCountAndListPriorities(gsa.Spec.Priorities, gs2); res != nil {
   254  				return *res
   255  			}
   256  		}
   257  
   258  		// finally sort lexicographically, so we have a stable order
   259  		return gs1.GetObjectMeta().GetName() < gs2.GetObjectMeta().GetName()
   260  	})
   261  
   262  	return list
   263  }
   264  
   265  // SyncGameServers synchronises the GameServers to Gameserver cache. This is called when a failure
   266  // happened during the allocation. This method will sync and make sure the cache is up to date.
   267  func (c *AllocationCache) SyncGameServers(_ context.Context, key string) error {
   268  	c.loggerForGameServerKey(key).Debug("Refreshing Allocation Gameserver cache")
   269  
   270  	return c.syncCache()
   271  }
   272  
   273  // syncCache syncs the gameserver cache and updates the local cache for any changes.
   274  func (c *AllocationCache) syncCache() error {
   275  	// build the cache
   276  	gsList, err := c.gameServerLister.List(labels.Everything())
   277  	if err != nil {
   278  		return errors.Wrap(err, "could not list GameServers")
   279  	}
   280  
   281  	// convert list of current gameservers to map for faster access
   282  	currGameservers := make(map[string]*agonesv1.GameServer)
   283  	for _, gs := range gsList {
   284  		if key, ok := c.getKey(gs); ok {
   285  			currGameservers[key] = gs
   286  		}
   287  	}
   288  
   289  	// first remove the gameservers are not in the list anymore
   290  	tobeDeletedGSInCache := make([]string, 0)
   291  	c.cache.Range(func(key string, _ *agonesv1.GameServer) bool {
   292  		if _, ok := currGameservers[key]; !ok {
   293  			tobeDeletedGSInCache = append(tobeDeletedGSInCache, key)
   294  		}
   295  		return true
   296  	})
   297  
   298  	for _, staleGSKey := range tobeDeletedGSInCache {
   299  		c.cache.Delete(staleGSKey)
   300  	}
   301  
   302  	// refresh the cache of possible allocatable GameServers
   303  	for key, gs := range currGameservers {
   304  		if gsCache, ok := c.cache.Load(key); ok {
   305  			if !(gs.DeletionTimestamp.IsZero() && c.matcher(gs)) {
   306  				c.cache.Delete(key)
   307  			} else if gs.ObjectMeta.ResourceVersion != gsCache.ObjectMeta.ResourceVersion {
   308  				c.cache.Store(key, gs)
   309  			}
   310  		} else if gs.DeletionTimestamp.IsZero() && c.matcher(gs) {
   311  			c.cache.Store(key, gs)
   312  		}
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  // getKey extract the key of gameserver object
   319  func (c *AllocationCache) getKey(gs *agonesv1.GameServer) (string, bool) {
   320  	var key string
   321  	ok := true
   322  	var err error
   323  	if key, err = cache.MetaNamespaceKeyFunc(gs); err != nil {
   324  		ok = false
   325  		err = errors.Wrap(err, "Error creating key for object")
   326  		runtime.HandleError(c.baseLogger.WithField("obj", gs), err)
   327  	}
   328  	return key, ok
   329  }