agones.dev/agones@v1.54.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 }