vitess.io/vitess@v0.16.2/go/vt/vttest/vtprocess.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 vttest contains helpers to set up Vitess for testing.
    18  package vttest
    19  
    20  import (
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"os"
    25  	"os/exec"
    26  	"strings"
    27  	"syscall"
    28  	"time"
    29  
    30  	"google.golang.org/protobuf/encoding/prototext"
    31  
    32  	"vitess.io/vitess/go/vt/log"
    33  	"vitess.io/vitess/go/vt/servenv"
    34  )
    35  
    36  // HealthChecker is a callback that impements a service-specific health check
    37  // It must return true if the service at the given `addr` is reachable, false
    38  // otherwerise.
    39  type HealthChecker func(addr string) bool
    40  
    41  // VtProcess is a generic handle for a running Vitess process.
    42  // It can be spawned manually or through one of the available
    43  // helper methods.
    44  type VtProcess struct {
    45  	Name         string
    46  	Directory    string
    47  	LogDirectory string
    48  	Binary       string
    49  	ExtraArgs    []string
    50  	Env          []string
    51  	Port         int
    52  	PortGrpc     int
    53  	HealthCheck  HealthChecker
    54  
    55  	proc *exec.Cmd
    56  	exit chan error
    57  }
    58  
    59  // getVars returns the JSON contents of the `/debug/vars` endpoint
    60  // of this Vitess-based process. If an error is returned, it probably
    61  // means that the Vitess service has not started successfully.
    62  func getVars(addr string) ([]byte, error) {
    63  	url := fmt.Sprintf("http://%s/debug/vars", addr)
    64  	resp, err := http.Get(url)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	defer resp.Body.Close()
    69  	return io.ReadAll(resp.Body)
    70  }
    71  
    72  // defaultHealthCheck checks the health of the Vitess process using getVars.
    73  // It is used when VtProcess.HealthCheck is nil.
    74  func defaultHealthCheck(addr string) bool {
    75  	_, err := getVars(addr)
    76  	return err == nil
    77  }
    78  
    79  // IsHealthy returns whether the monitored Vitess process has started
    80  // successfully.
    81  func (vtp *VtProcess) IsHealthy() bool {
    82  	healthCheck := vtp.HealthCheck
    83  	if healthCheck == nil {
    84  		healthCheck = defaultHealthCheck
    85  	}
    86  	return healthCheck(vtp.Address())
    87  }
    88  
    89  // Address returns the main address for this Vitess process.
    90  // This is usually the main HTTP endpoint for the service.
    91  func (vtp *VtProcess) Address() string {
    92  	return fmt.Sprintf("localhost:%d", vtp.Port)
    93  }
    94  
    95  // WaitTerminate attempts to gracefully shutdown the Vitess process by sending
    96  // a SIGTERM, then wait for up to 10s for it to exit. If the process hasn't
    97  // exited cleanly after 10s, a SIGKILL is forced and the corresponding exit
    98  // error is returned to the user
    99  func (vtp *VtProcess) WaitTerminate() error {
   100  	if vtp.proc == nil || vtp.exit == nil {
   101  		return nil
   102  	}
   103  
   104  	// Attempt graceful shutdown with SIGTERM first
   105  	vtp.proc.Process.Signal(syscall.SIGTERM)
   106  
   107  	select {
   108  	case err := <-vtp.exit:
   109  		vtp.proc = nil
   110  		return err
   111  
   112  	case <-time.After(10 * time.Second):
   113  		vtp.proc.Process.Kill()
   114  		vtp.proc = nil
   115  		return <-vtp.exit
   116  	}
   117  }
   118  
   119  // WaitStart spawns this Vitess process and waits for it to be up
   120  // and running. The process is considered "up" when it starts serving
   121  // its debug HTTP endpoint -- this means the process was successfully
   122  // started.
   123  // If the process is not healthy after 60s, this method will timeout and
   124  // return an error.
   125  func (vtp *VtProcess) WaitStart() (err error) {
   126  	vtp.proc = exec.Command(
   127  		vtp.Binary,
   128  		"--port", fmt.Sprintf("%d", vtp.Port),
   129  		"--log_dir", vtp.LogDirectory,
   130  		"--alsologtostderr",
   131  	)
   132  
   133  	if vtp.PortGrpc != 0 {
   134  		vtp.proc.Args = append(vtp.proc.Args, "--grpc_port")
   135  		vtp.proc.Args = append(vtp.proc.Args, fmt.Sprintf("%d", vtp.PortGrpc))
   136  	}
   137  
   138  	vtp.proc.Args = append(vtp.proc.Args, vtp.ExtraArgs...)
   139  	vtp.proc.Env = append(vtp.proc.Env, os.Environ()...)
   140  	vtp.proc.Env = append(vtp.proc.Env, vtp.Env...)
   141  
   142  	vtp.proc.Stderr = os.Stderr
   143  	vtp.proc.Stdout = os.Stdout
   144  
   145  	log.Infof("%v %v", strings.Join(vtp.proc.Args, " "))
   146  	err = vtp.proc.Start()
   147  	if err != nil {
   148  		return
   149  	}
   150  
   151  	vtp.exit = make(chan error)
   152  	go func() {
   153  		vtp.exit <- vtp.proc.Wait()
   154  	}()
   155  
   156  	timeout := time.Now().Add(60 * time.Second)
   157  	for time.Now().Before(timeout) {
   158  		if vtp.IsHealthy() {
   159  			return nil
   160  		}
   161  
   162  		select {
   163  		case err := <-vtp.exit:
   164  			return fmt.Errorf("process '%s' exited prematurely (err: %s)", vtp.Name, err)
   165  		default:
   166  			time.Sleep(300 * time.Millisecond)
   167  		}
   168  	}
   169  
   170  	vtp.proc.Process.Kill()
   171  	return fmt.Errorf("process '%s' timed out after 60s (err: %s)", vtp.Name, <-vtp.exit)
   172  }
   173  
   174  const (
   175  	// DefaultCharset is the default charset used by MySQL instances
   176  	DefaultCharset = "utf8mb4"
   177  )
   178  
   179  // QueryServerArgs are the default arguments passed to all Vitess query servers
   180  var QueryServerArgs = []string{
   181  	"--queryserver-config-pool-size", "4",
   182  	"--queryserver-config-query-timeout", "300",
   183  	"--queryserver-config-schema-reload-time", "60",
   184  	"--queryserver-config-stream-pool-size", "4",
   185  	"--queryserver-config-transaction-cap", "4",
   186  	"--queryserver-config-transaction-timeout", "300",
   187  	"--queryserver-config-txpool-timeout", "300",
   188  }
   189  
   190  // VtcomboProcess returns a VtProcess handle for a local `vtcombo` service,
   191  // configured with the given Config.
   192  // The process must be manually started by calling WaitStart()
   193  func VtcomboProcess(environment Environment, args *Config, mysql MySQLManager) (*VtProcess, error) {
   194  	vt := &VtProcess{
   195  		Name:         "vtcombo",
   196  		Directory:    environment.Directory(),
   197  		LogDirectory: environment.LogDirectory(),
   198  		Binary:       environment.BinaryPath("vtcombo"),
   199  		Port:         environment.PortForProtocol("vtcombo", ""),
   200  		PortGrpc:     environment.PortForProtocol("vtcombo", "grpc"),
   201  		HealthCheck:  environment.ProcessHealthCheck("vtcombo"),
   202  		Env:          environment.EnvVars(),
   203  	}
   204  
   205  	user, pass := mysql.Auth()
   206  	socket := mysql.UnixSocket()
   207  	charset := args.Charset
   208  	if charset == "" {
   209  		charset = DefaultCharset
   210  	}
   211  	protoTopo, _ := prototext.Marshal(args.Topology)
   212  	vt.ExtraArgs = append(vt.ExtraArgs, []string{
   213  		"--db_charset", charset,
   214  		"--db_app_user", user,
   215  		"--db_app_password", pass,
   216  		"--db_dba_user", user,
   217  		"--db_dba_password", pass,
   218  		"--proto_topo", string(protoTopo),
   219  		"--mycnf_server_id", "1",
   220  		"--mycnf_socket_file", socket,
   221  		"--normalize_queries",
   222  		"--enable_query_plan_field_caching=false",
   223  		"--dbddl_plugin", "vttest",
   224  		"--foreign_key_mode", args.ForeignKeyMode,
   225  		"--planner-version", args.PlannerVersion,
   226  		fmt.Sprintf("--enable_online_ddl=%t", args.EnableOnlineDDL),
   227  		fmt.Sprintf("--enable_direct_ddl=%t", args.EnableDirectDDL),
   228  		fmt.Sprintf("--enable_system_settings=%t", args.EnableSystemSettings),
   229  	}...)
   230  
   231  	// If topo tablet refresh interval is not defined then we will give it value of 10s. Please note
   232  	// that the default value is 1 minute, but we are keeping it low to make vttestserver perform faster.
   233  	// Less value might result in high pressure on topo but for testing purpose that should not be a concern.
   234  	if args.VtgateTabletRefreshInterval <= 0 {
   235  		vt.ExtraArgs = append(vt.ExtraArgs, fmt.Sprintf("--tablet_refresh_interval=%v", 10*time.Second))
   236  	} else {
   237  		vt.ExtraArgs = append(vt.ExtraArgs, fmt.Sprintf("--tablet_refresh_interval=%v", args.VtgateTabletRefreshInterval))
   238  	}
   239  
   240  	vt.ExtraArgs = append(vt.ExtraArgs, QueryServerArgs...)
   241  	vt.ExtraArgs = append(vt.ExtraArgs, environment.VtcomboArguments()...)
   242  
   243  	if args.SchemaDir != "" {
   244  		vt.ExtraArgs = append(vt.ExtraArgs, []string{"--schema_dir", args.SchemaDir}...)
   245  	}
   246  	if args.TransactionMode != "" {
   247  		vt.ExtraArgs = append(vt.ExtraArgs, []string{"--transaction_mode", args.TransactionMode}...)
   248  	}
   249  	if args.TransactionTimeout != 0 {
   250  		vt.ExtraArgs = append(vt.ExtraArgs, "--queryserver-config-transaction-timeout", fmt.Sprintf("%f", args.TransactionTimeout))
   251  	}
   252  	if args.TabletHostName != "" {
   253  		vt.ExtraArgs = append(vt.ExtraArgs, []string{"--tablet_hostname", args.TabletHostName}...)
   254  	}
   255  	if servenv.GRPCAuth() == "mtls" {
   256  		vt.ExtraArgs = append(vt.ExtraArgs, []string{"--grpc_auth_mode", servenv.GRPCAuth(), "--grpc_key", servenv.GRPCKey(), "--grpc_cert", servenv.GRPCCert(), "--grpc_ca", servenv.GRPCCertificateAuthority(), "--grpc_auth_mtls_allowed_substrings", servenv.ClientCertSubstrings()}...)
   257  	}
   258  	if args.VSchemaDDLAuthorizedUsers != "" {
   259  		vt.ExtraArgs = append(vt.ExtraArgs, []string{"--vschema_ddl_authorized_users", args.VSchemaDDLAuthorizedUsers}...)
   260  	}
   261  	vt.ExtraArgs = append(vt.ExtraArgs, "--mysql_server_version", servenv.MySQLServerVersion())
   262  	if socket != "" {
   263  		vt.ExtraArgs = append(vt.ExtraArgs, []string{
   264  			"--db_socket", socket,
   265  		}...)
   266  	} else {
   267  		hostname, p := mysql.Address()
   268  		port := fmt.Sprintf("%d", p)
   269  
   270  		vt.ExtraArgs = append(vt.ExtraArgs, []string{
   271  			"--db_host", hostname,
   272  			"--db_port", port,
   273  		}...)
   274  	}
   275  
   276  	vtcomboMysqlPort := environment.PortForProtocol("vtcombo_mysql_port", "")
   277  	vtcomboMysqlBindAddress := "localhost"
   278  	if args.MySQLBindHost != "" {
   279  		vtcomboMysqlBindAddress = args.MySQLBindHost
   280  	}
   281  
   282  	vt.ExtraArgs = append(vt.ExtraArgs, []string{
   283  		"--mysql_auth_server_impl", "none",
   284  		"--mysql_server_port", fmt.Sprintf("%d", vtcomboMysqlPort),
   285  		"--mysql_server_bind_address", vtcomboMysqlBindAddress,
   286  	}...)
   287  
   288  	if args.ExternalTopoImplementation != "" {
   289  		vt.ExtraArgs = append(vt.ExtraArgs, []string{
   290  			"--external_topo_server",
   291  			"--topo_implementation", args.ExternalTopoImplementation,
   292  			"--topo_global_server_address", args.ExternalTopoGlobalServerAddress,
   293  			"--topo_global_root", args.ExternalTopoGlobalRoot,
   294  		}...)
   295  	}
   296  
   297  	return vt, nil
   298  }