vitess.io/vitess@v0.16.2/go/test/endtoend/cluster/vtgate_process.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cluster
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"os"
    25  	"os/exec"
    26  	"path"
    27  	"reflect"
    28  	"strconv"
    29  	"strings"
    30  	"syscall"
    31  	"time"
    32  
    33  	"vitess.io/vitess/go/vt/log"
    34  	"vitess.io/vitess/go/vt/mysqlctl"
    35  	"vitess.io/vitess/go/vt/vtgate/planbuilder"
    36  	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
    37  )
    38  
    39  // VtgateProcess is a generic handle for a running vtgate .
    40  // It can be spawned manually
    41  type VtgateProcess struct {
    42  	Name                  string
    43  	Binary                string
    44  	CommonArg             VtctlProcess
    45  	LogDir                string
    46  	FileToLogQueries      string
    47  	Port                  int
    48  	GrpcPort              int
    49  	MySQLServerPort       int
    50  	MySQLServerSocketPath string
    51  	Cell                  string
    52  	CellsToWatch          string
    53  	TabletTypesToWait     string
    54  	ServiceMap            string
    55  	MySQLAuthServerImpl   string
    56  	Directory             string
    57  	VerifyURL             string
    58  	VSchemaURL            string
    59  	SysVarSetEnabled      bool
    60  	PlannerVersion        plancontext.PlannerVersion
    61  	// Extra Args to be set before starting the vtgate process
    62  	ExtraArgs []string
    63  
    64  	proc *exec.Cmd
    65  	exit chan error
    66  }
    67  
    68  const defaultVtGatePlannerVersion = planbuilder.Gen4CompareV3
    69  
    70  // Setup starts Vtgate process with required arguements
    71  func (vtgate *VtgateProcess) Setup() (err error) {
    72  	args := []string{
    73  		"--topo_implementation", vtgate.CommonArg.TopoImplementation,
    74  		"--topo_global_server_address", vtgate.CommonArg.TopoGlobalAddress,
    75  		"--topo_global_root", vtgate.CommonArg.TopoGlobalRoot,
    76  		"--log_dir", vtgate.LogDir,
    77  		"--log_queries_to_file", vtgate.FileToLogQueries,
    78  		"--port", fmt.Sprintf("%d", vtgate.Port),
    79  		"--grpc_port", fmt.Sprintf("%d", vtgate.GrpcPort),
    80  		"--mysql_server_port", fmt.Sprintf("%d", vtgate.MySQLServerPort),
    81  		"--mysql_server_socket_path", vtgate.MySQLServerSocketPath,
    82  		"--cell", vtgate.Cell,
    83  		"--cells_to_watch", vtgate.CellsToWatch,
    84  		"--tablet_types_to_wait", vtgate.TabletTypesToWait,
    85  		"--service_map", vtgate.ServiceMap,
    86  		"--mysql_auth_server_impl", vtgate.MySQLAuthServerImpl,
    87  	}
    88  	// If no explicit mysql_server_version has been specified then we autodetect
    89  	// the MySQL version that will be used for the test and base the vtgate's
    90  	// mysql server version on that.
    91  	msvflag := false
    92  	for _, f := range vtgate.ExtraArgs {
    93  		if strings.Contains(f, "mysql_server_version") {
    94  			msvflag = true
    95  			break
    96  		}
    97  	}
    98  	if !msvflag {
    99  		version, err := mysqlctl.GetVersionString()
   100  		if err != nil {
   101  			return err
   102  		}
   103  		_, vers, err := mysqlctl.ParseVersionString(version)
   104  		if err != nil {
   105  			return err
   106  		}
   107  		mysqlvers := fmt.Sprintf("%d.%d.%d-vitess", vers.Major, vers.Minor, vers.Patch)
   108  		args = append(args, "--mysql_server_version", mysqlvers)
   109  	}
   110  	if vtgate.PlannerVersion > 0 {
   111  		args = append(args, "--planner-version", vtgate.PlannerVersion.String())
   112  	}
   113  	if vtgate.SysVarSetEnabled {
   114  		args = append(args, "--enable_system_settings")
   115  	}
   116  	vtgate.proc = exec.Command(
   117  		vtgate.Binary,
   118  		args...,
   119  	)
   120  	if *isCoverage {
   121  		vtgate.proc.Args = append(vtgate.proc.Args, "--test.coverprofile="+getCoveragePath("vtgate.out"))
   122  	}
   123  
   124  	vtgate.proc.Args = append(vtgate.proc.Args, vtgate.ExtraArgs...)
   125  
   126  	errFile, _ := os.Create(path.Join(vtgate.LogDir, "vtgate-stderr.txt"))
   127  	vtgate.proc.Stderr = errFile
   128  
   129  	vtgate.proc.Env = append(vtgate.proc.Env, os.Environ()...)
   130  
   131  	log.Infof("Running vtgate with command: %v", strings.Join(vtgate.proc.Args, " "))
   132  
   133  	err = vtgate.proc.Start()
   134  	if err != nil {
   135  		return
   136  	}
   137  	vtgate.exit = make(chan error)
   138  	go func() {
   139  		if vtgate.proc != nil {
   140  			vtgate.exit <- vtgate.proc.Wait()
   141  			close(vtgate.exit)
   142  		}
   143  	}()
   144  
   145  	timeout := time.Now().Add(60 * time.Second)
   146  	for time.Now().Before(timeout) {
   147  		if vtgate.WaitForStatus() {
   148  			return nil
   149  		}
   150  		select {
   151  		case err := <-vtgate.exit:
   152  			return fmt.Errorf("process '%s' exited prematurely (err: %s)", vtgate.Name, err)
   153  		default:
   154  			time.Sleep(300 * time.Millisecond)
   155  		}
   156  	}
   157  
   158  	return fmt.Errorf("process '%s' timed out after 60s (err: %s)", vtgate.Name, <-vtgate.exit)
   159  }
   160  
   161  // WaitForStatus function checks if vtgate process is up and running
   162  func (vtgate *VtgateProcess) WaitForStatus() bool {
   163  	resp, err := http.Get(vtgate.VerifyURL)
   164  	if err != nil {
   165  		return false
   166  	}
   167  	defer resp.Body.Close()
   168  
   169  	return resp.StatusCode == 200
   170  }
   171  
   172  // GetStatusForTabletOfShard function gets status for a specific tablet of a shard in keyspace
   173  // endPointsCount : number of endpoints
   174  func (vtgate *VtgateProcess) GetStatusForTabletOfShard(name string, endPointsCount int) bool {
   175  	resp, err := http.Get(vtgate.VerifyURL)
   176  	if err != nil {
   177  		return false
   178  	}
   179  	defer resp.Body.Close()
   180  
   181  	if resp.StatusCode == 200 {
   182  		resultMap := make(map[string]any)
   183  		respByte, _ := io.ReadAll(resp.Body)
   184  		err := json.Unmarshal(respByte, &resultMap)
   185  		if err != nil {
   186  			panic(err)
   187  		}
   188  		object := reflect.ValueOf(resultMap["HealthcheckConnections"])
   189  		if object.Kind() == reflect.Map {
   190  			for _, key := range object.MapKeys() {
   191  				if key.String() == name {
   192  					value := fmt.Sprintf("%v", object.MapIndex(key))
   193  					countStr := strconv.Itoa(endPointsCount)
   194  					return value == countStr
   195  				}
   196  			}
   197  		}
   198  	}
   199  	return false
   200  }
   201  
   202  // WaitForStatusOfTabletInShard function waits till status of a tablet in shard is 1
   203  // endPointsCount: how many endpoints to wait for
   204  func (vtgate *VtgateProcess) WaitForStatusOfTabletInShard(name string, endPointsCount int, timeout time.Duration) error {
   205  	log.Infof("Waiting for healthy status of %d %s tablets in cell %s",
   206  		endPointsCount, name, vtgate.Cell)
   207  	deadline := time.Now().Add(timeout)
   208  	for time.Now().Before(deadline) {
   209  		if vtgate.GetStatusForTabletOfShard(name, endPointsCount) {
   210  			return nil
   211  		}
   212  		select {
   213  		case err := <-vtgate.exit:
   214  			return fmt.Errorf("process '%s' exited prematurely (err: %s)", vtgate.Name, err)
   215  		default:
   216  			time.Sleep(300 * time.Millisecond)
   217  		}
   218  	}
   219  	return fmt.Errorf("wait for %s failed", name)
   220  }
   221  
   222  // TearDown shuts down the running vtgate service
   223  func (vtgate *VtgateProcess) TearDown() error {
   224  	if vtgate.proc == nil || vtgate.exit == nil {
   225  		return nil
   226  	}
   227  	// graceful shutdown is not currently working with vtgate, attempting a force-kill to make tests less flaky
   228  	// Attempt graceful shutdown with SIGTERM first
   229  	vtgate.proc.Process.Signal(syscall.SIGTERM)
   230  
   231  	// We are not checking vtgate's exit code because it sometimes
   232  	// returns exit code 2, even though vtgate terminates cleanly.
   233  	select {
   234  	case <-vtgate.exit:
   235  		vtgate.proc = nil
   236  		return nil
   237  
   238  	case <-time.After(30 * time.Second):
   239  		vtgate.proc.Process.Kill()
   240  		err := <-vtgate.exit
   241  		vtgate.proc = nil
   242  		return err
   243  	}
   244  }
   245  
   246  // VtgateProcessInstance returns a Vtgate handle for vtgate process
   247  // configured with the given Config.
   248  // The process must be manually started by calling setup()
   249  func VtgateProcessInstance(
   250  	port, grpcPort, mySQLServerPort int,
   251  	cell, cellsToWatch, hostname, tabletTypesToWait string,
   252  	topoPort int,
   253  	tmpDirectory string,
   254  	extraArgs []string,
   255  	plannerVersion plancontext.PlannerVersion,
   256  ) *VtgateProcess {
   257  	vtctl := VtctlProcessInstance(topoPort, hostname)
   258  	vtgate := &VtgateProcess{
   259  		Name:                  "vtgate",
   260  		Binary:                "vtgate",
   261  		FileToLogQueries:      path.Join(tmpDirectory, "/vtgate_querylog.txt"),
   262  		Directory:             os.Getenv("VTDATAROOT"),
   263  		ServiceMap:            "grpc-tabletmanager,grpc-throttler,grpc-queryservice,grpc-updatestream,grpc-vtctl,grpc-vtgateservice",
   264  		LogDir:                tmpDirectory,
   265  		Port:                  port,
   266  		GrpcPort:              grpcPort,
   267  		MySQLServerPort:       mySQLServerPort,
   268  		MySQLServerSocketPath: path.Join(tmpDirectory, "mysql.sock"),
   269  		Cell:                  cell,
   270  		CellsToWatch:          cellsToWatch,
   271  		TabletTypesToWait:     tabletTypesToWait,
   272  		CommonArg:             *vtctl,
   273  		MySQLAuthServerImpl:   "none",
   274  		ExtraArgs:             extraArgs,
   275  		PlannerVersion:        plannerVersion,
   276  	}
   277  
   278  	vtgate.VerifyURL = fmt.Sprintf("http://%s:%d/debug/vars", hostname, port)
   279  	vtgate.VSchemaURL = fmt.Sprintf("http://%s:%d/debug/vschema", hostname, port)
   280  
   281  	return vtgate
   282  }
   283  
   284  // GetVars returns map of vars
   285  func (vtgate *VtgateProcess) GetVars() (map[string]any, error) {
   286  	resultMap := make(map[string]any)
   287  	resp, err := http.Get(vtgate.VerifyURL)
   288  	if err != nil {
   289  		return nil, fmt.Errorf("error getting response from %s", vtgate.VerifyURL)
   290  	}
   291  	defer resp.Body.Close()
   292  
   293  	if resp.StatusCode == 200 {
   294  		respByte, _ := io.ReadAll(resp.Body)
   295  		err := json.Unmarshal(respByte, &resultMap)
   296  		if err != nil {
   297  			return nil, fmt.Errorf("not able to parse response body")
   298  		}
   299  		return resultMap, nil
   300  	}
   301  	return nil, fmt.Errorf("unsuccessful response")
   302  }
   303  
   304  // ReadVSchema reads the vschema from the vtgate endpoint for it and returns
   305  // a pointer to the interface. To read this vschema, the caller must convert it to a map
   306  func (vtgate *VtgateProcess) ReadVSchema() (*interface{}, error) {
   307  	httpClient := &http.Client{Timeout: 5 * time.Second}
   308  	resp, err := httpClient.Get(vtgate.VSchemaURL)
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	defer resp.Body.Close()
   313  	res, err := io.ReadAll(resp.Body)
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  	var results interface{}
   318  	err = json.Unmarshal(res, &results)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	return &results, nil
   323  }