vitess.io/vitess@v0.16.2/go/test/endtoend/cluster/mysqlctl_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  	"context"
    21  	"fmt"
    22  	"html/template"
    23  	"os"
    24  	"os/exec"
    25  	"path"
    26  	"strconv"
    27  	"strings"
    28  	"syscall"
    29  	"time"
    30  
    31  	"vitess.io/vitess/go/mysql"
    32  	"vitess.io/vitess/go/vt/log"
    33  	"vitess.io/vitess/go/vt/tlstest"
    34  )
    35  
    36  // MysqlctlProcess is a generic handle for a running mysqlctl command .
    37  // It can be spawned manually
    38  type MysqlctlProcess struct {
    39  	Name            string
    40  	Binary          string
    41  	LogDirectory    string
    42  	TabletUID       int
    43  	MySQLPort       int
    44  	InitDBFile      string
    45  	ExtraArgs       []string
    46  	InitMysql       bool
    47  	SecureTransport bool
    48  }
    49  
    50  // InitDb executes mysqlctl command to add cell info
    51  func (mysqlctl *MysqlctlProcess) InitDb() (err error) {
    52  	args := []string{"--log_dir", mysqlctl.LogDirectory,
    53  		"--tablet_uid", fmt.Sprintf("%d", mysqlctl.TabletUID),
    54  		"--mysql_port", fmt.Sprintf("%d", mysqlctl.MySQLPort),
    55  		"init", "--",
    56  		"--init_db_sql_file", mysqlctl.InitDBFile}
    57  	if *isCoverage {
    58  		args = append([]string{"--test.coverprofile=" + getCoveragePath("mysql-initdb.out"), "--test.v"}, args...)
    59  	}
    60  	tmpProcess := exec.Command(
    61  		mysqlctl.Binary,
    62  		args...)
    63  	return tmpProcess.Run()
    64  }
    65  
    66  // Start executes mysqlctl command to start mysql instance
    67  func (mysqlctl *MysqlctlProcess) Start() (err error) {
    68  	tmpProcess, err := mysqlctl.startProcess(true)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	return tmpProcess.Wait()
    73  }
    74  
    75  // StartProvideInit executes mysqlctl command to start mysql instance
    76  func (mysqlctl *MysqlctlProcess) StartProvideInit(init bool) (err error) {
    77  	tmpProcess, err := mysqlctl.startProcess(init)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	return tmpProcess.Wait()
    82  }
    83  
    84  // StartProcess starts the mysqlctl and returns the process reference
    85  func (mysqlctl *MysqlctlProcess) StartProcess() (*exec.Cmd, error) {
    86  	return mysqlctl.startProcess(true)
    87  }
    88  
    89  func (mysqlctl *MysqlctlProcess) startProcess(init bool) (*exec.Cmd, error) {
    90  	tmpProcess := exec.Command(
    91  		mysqlctl.Binary,
    92  		"--log_dir", mysqlctl.LogDirectory,
    93  		"--tablet_uid", fmt.Sprintf("%d", mysqlctl.TabletUID),
    94  		"--mysql_port", fmt.Sprintf("%d", mysqlctl.MySQLPort),
    95  	)
    96  	if *isCoverage {
    97  		tmpProcess.Args = append(tmpProcess.Args, []string{"--test.coverprofile=" + getCoveragePath("mysql-start.out")}...)
    98  	}
    99  
   100  	if len(mysqlctl.ExtraArgs) > 0 {
   101  		tmpProcess.Args = append(tmpProcess.Args, mysqlctl.ExtraArgs...)
   102  	}
   103  	if mysqlctl.InitMysql {
   104  		if mysqlctl.SecureTransport {
   105  			// Set up EXTRA_MY_CNF for ssl
   106  			sslPath := path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/ssl_%010d", mysqlctl.TabletUID))
   107  			os.MkdirAll(sslPath, 0755)
   108  
   109  			// create certificates
   110  			clientServerKeyPair := tlstest.CreateClientServerCertPairs(sslPath)
   111  
   112  			// use the certificate values in template to create cnf file
   113  			sslPathData := struct {
   114  				Dir        string
   115  				ServerCert string
   116  				ServerKey  string
   117  			}{
   118  				Dir:        sslPath,
   119  				ServerCert: clientServerKeyPair.ServerCert,
   120  				ServerKey:  clientServerKeyPair.ServerKey,
   121  			}
   122  
   123  			extraMyCNF := path.Join(sslPath, "ssl.cnf")
   124  			fout, err := os.Create(extraMyCNF)
   125  			if err != nil {
   126  				log.Error(err)
   127  				return nil, err
   128  			}
   129  
   130  			template.Must(template.New(fmt.Sprintf("%010d", mysqlctl.TabletUID)).Parse(`
   131  ssl_ca={{.Dir}}/ca-cert.pem
   132  ssl_cert={{.ServerCert}}
   133  ssl_key={{.ServerKey}}
   134  `)).Execute(fout, sslPathData)
   135  			if err := fout.Close(); err != nil {
   136  				return nil, err
   137  			}
   138  
   139  			tmpProcess.Env = append(tmpProcess.Env, "EXTRA_MY_CNF="+extraMyCNF)
   140  			tmpProcess.Env = append(tmpProcess.Env, "VTDATAROOT="+os.Getenv("VTDATAROOT"))
   141  		}
   142  
   143  		if init {
   144  			tmpProcess.Args = append(tmpProcess.Args, "init", "--",
   145  				"--init_db_sql_file", mysqlctl.InitDBFile)
   146  		}
   147  	}
   148  	tmpProcess.Args = append(tmpProcess.Args, "start")
   149  	log.Infof("Starting mysqlctl with command: %v", tmpProcess.Args)
   150  	return tmpProcess, tmpProcess.Start()
   151  }
   152  
   153  // Stop executes mysqlctl command to stop mysql instance and kills the mysql instance if it doesn't shutdown in 30 seconds.
   154  func (mysqlctl *MysqlctlProcess) Stop() (err error) {
   155  	log.Infof("Shutting down MySQL: %d", mysqlctl.TabletUID)
   156  	defer log.Infof("MySQL shutdown complete: %d", mysqlctl.TabletUID)
   157  	tmpProcess, err := mysqlctl.StopProcess()
   158  	if err != nil {
   159  		return err
   160  	}
   161  	// On the CI it was noticed that MySQL shutdown hangs sometimes and
   162  	// on local investigation it was waiting on SEMI_SYNC acks for an internal command
   163  	// of Vitess even after closing the socket file.
   164  	// To prevent this process for hanging for 5 minutes, we will add a 30-second timeout.
   165  	exit := make(chan error)
   166  	go func() {
   167  		exit <- tmpProcess.Wait()
   168  	}()
   169  	select {
   170  	case <-time.After(30 * time.Second):
   171  		break
   172  	case err := <-exit:
   173  		if err == nil {
   174  			return nil
   175  		}
   176  		break
   177  	}
   178  	pidFile := path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d/mysql.pid", mysqlctl.TabletUID))
   179  	pidBytes, err := os.ReadFile(pidFile)
   180  	if err != nil {
   181  		// We can't read the file which means the PID file does not exist
   182  		// The server must have stopped
   183  		return nil
   184  	}
   185  	pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes)))
   186  	if err != nil {
   187  		return err
   188  	}
   189  	return syscall.Kill(pid, syscall.SIGKILL)
   190  }
   191  
   192  // StopProcess executes mysqlctl command to stop mysql instance and returns process reference
   193  func (mysqlctl *MysqlctlProcess) StopProcess() (*exec.Cmd, error) {
   194  	tmpProcess := exec.Command(
   195  		mysqlctl.Binary,
   196  		"--log_dir", mysqlctl.LogDirectory,
   197  		"--tablet_uid", fmt.Sprintf("%d", mysqlctl.TabletUID),
   198  	)
   199  	if *isCoverage {
   200  		tmpProcess.Args = append(tmpProcess.Args, []string{"--test.coverprofile=" + getCoveragePath("mysql-stop.out")}...)
   201  	}
   202  	if len(mysqlctl.ExtraArgs) > 0 {
   203  		tmpProcess.Args = append(tmpProcess.Args, mysqlctl.ExtraArgs...)
   204  	}
   205  	tmpProcess.Args = append(tmpProcess.Args, "shutdown")
   206  	return tmpProcess, tmpProcess.Start()
   207  }
   208  
   209  func (mysqlctl *MysqlctlProcess) BasePath() string {
   210  	return path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d", mysqlctl.TabletUID))
   211  }
   212  
   213  func (mysqlctl *MysqlctlProcess) BinaryLogsPath() string {
   214  	return path.Join(mysqlctl.BasePath(), "bin-logs")
   215  }
   216  
   217  // CleanupFiles clean the mysql files to make sure we can start the same process again
   218  func (mysqlctl *MysqlctlProcess) CleanupFiles(tabletUID int) {
   219  	os.RemoveAll(path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d/data", tabletUID)))
   220  	os.RemoveAll(path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d/relay-logs", tabletUID)))
   221  	os.RemoveAll(path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d/tmp", tabletUID)))
   222  	os.RemoveAll(path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d/bin-logs", tabletUID)))
   223  	os.RemoveAll(path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d/innodb", tabletUID)))
   224  }
   225  
   226  // Connect returns a new connection to the underlying MySQL server
   227  func (mysqlctl *MysqlctlProcess) Connect(ctx context.Context, username string) (*mysql.Conn, error) {
   228  	params := mysql.ConnParams{
   229  		Uname:      username,
   230  		UnixSocket: path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d", mysqlctl.TabletUID), "/mysql.sock"),
   231  	}
   232  
   233  	return mysql.Connect(ctx, &params)
   234  }
   235  
   236  // MysqlCtlProcessInstanceOptionalInit returns a Mysqlctl handle for mysqlctl process
   237  // configured with the given Config.
   238  func MysqlCtlProcessInstanceOptionalInit(tabletUID int, mySQLPort int, tmpDirectory string, initMySQL bool) *MysqlctlProcess {
   239  	mysqlctl := &MysqlctlProcess{
   240  		Name:         "mysqlctl",
   241  		Binary:       "mysqlctl",
   242  		LogDirectory: tmpDirectory,
   243  		InitDBFile:   path.Join(os.Getenv("VTROOT"), "/config/init_db.sql"),
   244  	}
   245  	mysqlctl.MySQLPort = mySQLPort
   246  	mysqlctl.TabletUID = tabletUID
   247  	mysqlctl.InitMysql = initMySQL
   248  	mysqlctl.SecureTransport = false
   249  	return mysqlctl
   250  }
   251  
   252  // MysqlCtlProcessInstance returns a Mysqlctl handle for mysqlctl process
   253  // configured with the given Config.
   254  func MysqlCtlProcessInstance(tabletUID int, mySQLPort int, tmpDirectory string) *MysqlctlProcess {
   255  	return MysqlCtlProcessInstanceOptionalInit(tabletUID, mySQLPort, tmpDirectory, true)
   256  }
   257  
   258  // StartMySQL starts mysqlctl process
   259  func StartMySQL(ctx context.Context, tablet *Vttablet, username string, tmpDirectory string) error {
   260  	tablet.MysqlctlProcess = *MysqlCtlProcessInstance(tablet.TabletUID, tablet.MySQLPort, tmpDirectory)
   261  	return tablet.MysqlctlProcess.Start()
   262  }
   263  
   264  // StartMySQLAndGetConnection create a connection to tablet mysql
   265  func StartMySQLAndGetConnection(ctx context.Context, tablet *Vttablet, username string, tmpDirectory string) (*mysql.Conn, error) {
   266  	tablet.MysqlctlProcess = *MysqlCtlProcessInstance(tablet.TabletUID, tablet.MySQLPort, tmpDirectory)
   267  	err := tablet.MysqlctlProcess.Start()
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	params := mysql.ConnParams{
   272  		Uname:      username,
   273  		UnixSocket: path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d", tablet.TabletUID), "/mysql.sock"),
   274  	}
   275  
   276  	return mysql.Connect(ctx, &params)
   277  }
   278  
   279  // ExecuteCommandWithOutput executes any mysqlctl command and returns output
   280  func (mysqlctl *MysqlctlProcess) ExecuteCommandWithOutput(args ...string) (result string, err error) {
   281  	tmpProcess := exec.Command(
   282  		mysqlctl.Binary,
   283  		args...,
   284  	)
   285  	log.Info(fmt.Sprintf("Executing mysqlctl with arguments %v", strings.Join(tmpProcess.Args, " ")))
   286  	resultByte, err := tmpProcess.CombinedOutput()
   287  	return string(resultByte), err
   288  }