agones.dev/agones@v1.53.0/test/load/allocation/runscenario/runscenario.go (about) 1 // Copyright 2022 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 //nolint:typecheck 16 package main 17 18 import ( 19 "bufio" 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "errors" 24 "flag" 25 "log" 26 "os" 27 "strconv" 28 "strings" 29 "sync" 30 "sync/atomic" 31 "time" 32 33 pb "agones.dev/agones/pkg/allocation/go" 34 "google.golang.org/grpc" 35 "google.golang.org/grpc/credentials" 36 ) 37 38 type allocErrorCode string 39 40 const ( 41 noerror allocErrorCode = "Noerror" 42 unknown allocErrorCode = "Unknown" 43 objectHasBeenModified allocErrorCode = "ObjectHasBeenModified" 44 tooManyConcurrentRequests allocErrorCode = "TooManyConcurrentRequests" 45 noAvailableGameServer allocErrorCode = "NoAvailableGameServer" 46 storageError allocErrorCode = "StorageError" 47 deadLineExceeded allocErrorCode = "DeadLineExceeded" 48 connectionTimedOut allocErrorCode = "ConnectionTimedOut" 49 connectionRefused allocErrorCode = "ConnectionRefused" 50 errReadingFromServer allocErrorCode = "ErrReadingFromServer" 51 ) 52 53 var ( 54 logger = log.New(os.Stdout, "", 0) 55 errorMsgs = map[allocErrorCode]string{ 56 objectHasBeenModified: "the object has been modified", 57 tooManyConcurrentRequests: "too many concurrent requests", 58 noAvailableGameServer: "no available GameServer to allocate", 59 storageError: "storage error", 60 deadLineExceeded: "context deadline exceeded", 61 connectionTimedOut: "connection timed out", 62 connectionRefused: "connection refused", 63 errReadingFromServer: "allocator seems crashed and restarted", 64 } 65 ) 66 67 var ( 68 externalIP = flag.String("ip", "missing external IP", "the external IP for allocator server") 69 port = flag.String("port", "443", "the port for allocator server") 70 certFile = flag.String("cert", "missing cert", "the public key file for the client certificate in PEM format") 71 keyFile = flag.String("key", "missing key", "the private key file for the client certificate in PEM format") 72 cacertFile = flag.String("cacert", "missing cacert", "the CA cert file for server signing certificate in PEM format") 73 scenariosFile = flag.String("scenariosFile", "", "Scenario File that have duration of each allocations with different number of clients and allocations") 74 namespace = flag.String("namespace", "default", "the game server kubernetes namespace") 75 multicluster = flag.Bool("multicluster", false, "set to true to enable the multi-cluster allocation") 76 displaySuccessfulResponses = flag.Bool("displaySuccessfulResponses", false, "Display successful responses") 77 displayFailedResponses = flag.Bool("displayFailedResponses", false, "Display failed responses") 78 ) 79 80 type scenario struct { 81 duration time.Duration 82 numOfClients int 83 intervalMillisecond int 84 } 85 86 func main() { 87 flag.Parse() 88 89 endpoint := *externalIP + ":" + *port 90 dialOpts, err := dialOptions(*certFile, *keyFile, *cacertFile) 91 if err != nil { 92 logger.Fatalf("%v", err) 93 } 94 95 scenarios := readScenarios(*scenariosFile) 96 var totalAllocCnt uint64 97 var totalFailureCnt uint64 98 var totalDuration float64 99 100 totalFailureDtls := allocErrorCodeCntMap() 101 for i, sc := range *scenarios { 102 logger.Printf("\n\n%v :Running Scenario %v with %v clients submitting requests every %vms for %v\n===================\n", time.Now(), i+1, sc.numOfClients, sc.intervalMillisecond, sc.duration) 103 104 var wg sync.WaitGroup 105 failureCnts := make([]uint64, sc.numOfClients) 106 allocCnts := make([]uint64, sc.numOfClients) 107 failureDtls := make([]map[allocErrorCode]uint64, sc.numOfClients) 108 for k := 0; k < sc.numOfClients; k++ { 109 wg.Add(1) 110 go func(clientID int) { 111 defer wg.Done() 112 failureDtls[clientID] = allocErrorCodeCntMap() 113 durCtx, cancel := context.WithTimeout(context.Background(), sc.duration) 114 defer cancel() 115 conn, err := grpc.NewClient(endpoint, dialOpts) 116 if err != nil { 117 logger.Printf("Failed to dial for client %v: %v", clientID, err) 118 return 119 } 120 client := pb.NewAllocationServiceClient(conn) 121 var wgc sync.WaitGroup 122 for durCtx.Err() == nil { 123 wgc.Add(1) 124 go func() { 125 defer wgc.Done() 126 if err := allocate(client); err != noerror { 127 tmpVar := failureDtls[clientID][err] 128 atomic.AddUint64(&tmpVar, 1) 129 atomic.AddUint64(&failureCnts[clientID], 1) 130 failureDtls[clientID][err] = tmpVar 131 } 132 }() 133 atomic.AddUint64(&allocCnts[clientID], 1) 134 time.Sleep(time.Duration(sc.intervalMillisecond) * time.Millisecond) 135 } 136 wgc.Wait() 137 _ = conn.Close() // Ignore error handling because the connection will be closed when the main func exits anyway. 138 }(k) 139 } 140 wg.Wait() 141 logger.Printf("\nFinished Scenario %v\n", i+1) 142 143 var scnFailureCnt uint64 144 var scnAllocCnt uint64 145 scnErrDtls := allocErrorCodeCntMap() 146 for j := 0; j < sc.numOfClients; j++ { 147 scnAllocCnt += atomic.LoadUint64(&allocCnts[j]) 148 scnFailureCnt += atomic.LoadUint64(&failureCnts[j]) 149 for k, v := range failureDtls[j] { 150 totalFailureDtls[k] += v 151 scnErrDtls[k] += v 152 } 153 } 154 totalAllocCnt += scnAllocCnt 155 totalFailureCnt += scnFailureCnt 156 totalDuration += sc.duration.Seconds() 157 for k, v := range scnErrDtls { 158 if k != noerror { 159 logger.Printf("Count: %v\t\tError: %v", v, k) 160 } 161 } 162 logger.Printf("\n\n%v\nScenario Failure Count: %v, Allocation Count: %v, Failure rate: %v, allocation rate: %v", time.Now(), scnFailureCnt, scnAllocCnt, float64(scnFailureCnt)/float64(scnAllocCnt), float64(scnAllocCnt-scnFailureCnt)/sc.duration.Seconds()) 163 } 164 165 logger.Print("\nFinal Error Totals\n") 166 for k, v := range totalFailureDtls { 167 if k != noerror { 168 logger.Printf("Count: %v\t\tError: %v", v, k) 169 } 170 } 171 logger.Printf("\n\n%v\nFinal Total Failure Count: %v, Total Allocation Count: %v, Failure rate: %v, allocation rate: %v", time.Now(), totalFailureCnt, totalAllocCnt, float64(totalFailureCnt)/float64(totalAllocCnt), float64(totalAllocCnt-totalFailureCnt)/totalDuration) 172 } 173 174 func dialOptions(certFile, keyFile, cacertFile string) (grpc.DialOption, error) { 175 clientCert, err := os.ReadFile(certFile) 176 if err != nil { 177 return nil, err 178 } 179 clientKey, err := os.ReadFile(keyFile) 180 if err != nil { 181 return nil, err 182 } 183 cacert, err := os.ReadFile(cacertFile) 184 if err != nil { 185 return nil, err 186 } 187 188 // Load client cert 189 cert, err := tls.X509KeyPair(clientCert, clientKey) 190 if err != nil { 191 return nil, err 192 } 193 194 tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}} 195 if len(cacert) != 0 { 196 // Load CA cert, if provided and trust the server certificate. 197 // This is required for self-signed certs. 198 tlsConfig.RootCAs = x509.NewCertPool() 199 if !tlsConfig.RootCAs.AppendCertsFromPEM(cacert) { 200 return nil, errors.New("only PEM format is accepted for server CA") 201 } 202 } 203 204 return grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), nil 205 } 206 207 func allocate(client pb.AllocationServiceClient) allocErrorCode { 208 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 209 defer cancel() 210 resp, err := client.Allocate(ctx, &pb.AllocationRequest{ 211 Namespace: *namespace, 212 MultiClusterSetting: &pb.MultiClusterSetting{Enabled: *multicluster}, 213 }) 214 if err != nil { 215 alErrType := errorType(err) 216 if *displayFailedResponses || alErrType == unknown { 217 logger.Printf("Error received: %v", err) 218 } 219 return alErrType 220 } 221 if *displaySuccessfulResponses { 222 gsAddress := resp.GetAddress() 223 gsName := resp.GetGameServerName() 224 portName := resp.GetPorts()[0].GetName() 225 port := resp.GetPorts()[0].GetPort() 226 logger.Println("Allocate Response") 227 logger.Printf(" Received GS:\n Addreess: %s\n Name: %s\n Port Name: %s\n Port: %v", gsAddress, gsName, portName, port) 228 } 229 return noerror 230 } 231 232 func readScenarios(file string) *[]scenario { 233 fp, err := os.Open(file) 234 if err != nil { 235 logger.Fatalf("Failed opening the scenario file %s: %v", file, err) 236 } 237 defer func() { _ = fp.Close() }() 238 239 var scenarios []scenario 240 241 scanner := bufio.NewScanner(fp) 242 scanner.Split(bufio.ScanLines) 243 for scanner.Scan() { 244 line := scanner.Text() 245 if strings.HasPrefix(line, "#") { 246 continue 247 } 248 lineParts := strings.Split(line, ",") 249 if len(lineParts) != 3 { 250 logger.Fatalf("There should be 3 parts for each scenario but there is %d for %q", len(lineParts), line) 251 } 252 duration, err := time.ParseDuration(lineParts[0]) 253 if err != nil { 254 logger.Fatalf("Failed parsing duration %s: %v", lineParts[0], err) 255 } 256 numClients, err := strconv.Atoi(lineParts[1]) 257 if err != nil { 258 logger.Fatalf("Failed parsing number of clients %s: %v", lineParts[1], err) 259 } 260 intervalMillisecond, err := strconv.Atoi(lineParts[2]) 261 if err != nil { 262 logger.Fatalf("Failed parsing intervalMillisecond %s: %v", lineParts[2], err) 263 } 264 scenarios = append(scenarios, scenario{duration, numClients, intervalMillisecond}) 265 } 266 267 return &scenarios 268 } 269 270 func errorType(err error) allocErrorCode { 271 for ec, em := range errorMsgs { 272 if strings.Contains(err.Error(), em) { 273 return ec 274 } 275 } 276 return unknown 277 } 278 279 func allocErrorCodeCntMap() map[allocErrorCode]uint64 { 280 return map[allocErrorCode]uint64{ 281 noerror: 0, 282 unknown: 0, 283 objectHasBeenModified: 0, 284 tooManyConcurrentRequests: 0, 285 noAvailableGameServer: 0, 286 storageError: 0, 287 deadLineExceeded: 0, 288 connectionTimedOut: 0, 289 connectionRefused: 0, 290 errReadingFromServer: 0, 291 } 292 }