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  }