vitess.io/vitess@v0.16.2/go/test/endtoend/cluster/cluster_util.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  	"os"
    23  	"path"
    24  	"reflect"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/buger/jsonparser"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  
    33  	"vitess.io/vitess/go/json2"
    34  	"vitess.io/vitess/go/mysql"
    35  	"vitess.io/vitess/go/vt/vtgate/vtgateconn"
    36  
    37  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    38  	tmc "vitess.io/vitess/go/vt/vttablet/grpctmclient"
    39  )
    40  
    41  var (
    42  	tmClient                 = tmc.NewClient()
    43  	dbCredentialFile         string
    44  	InsertTabletTemplateKsID = `insert into %s (id, msg) values (%d, '%s') /* id:%d */`
    45  	defaultOperationTimeout  = 60 * time.Second
    46  	defeaultRetryDelay       = 1 * time.Second
    47  )
    48  
    49  // Restart restarts vttablet and mysql.
    50  func (tablet *Vttablet) Restart() error {
    51  	if tablet.MysqlctlProcess.TabletUID|tablet.MysqlctldProcess.TabletUID == 0 {
    52  		return fmt.Errorf("no mysql process is running")
    53  	}
    54  
    55  	if tablet.MysqlctlProcess.TabletUID > 0 {
    56  		tablet.MysqlctlProcess.Stop()
    57  		tablet.VttabletProcess.TearDown()
    58  		os.RemoveAll(tablet.VttabletProcess.Directory)
    59  
    60  		return tablet.MysqlctlProcess.Start()
    61  	}
    62  
    63  	tablet.MysqlctldProcess.Stop()
    64  	tablet.VttabletProcess.TearDown()
    65  	os.RemoveAll(tablet.VttabletProcess.Directory)
    66  
    67  	return tablet.MysqlctldProcess.Start()
    68  }
    69  
    70  // RestartOnlyTablet restarts vttablet, but not the underlying mysql instance
    71  func (tablet *Vttablet) RestartOnlyTablet() error {
    72  	err := tablet.VttabletProcess.TearDown()
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	tablet.VttabletProcess.ServingStatus = "SERVING"
    78  
    79  	return tablet.VttabletProcess.Setup()
    80  }
    81  
    82  // ValidateTabletRestart restarts the tablet and validate error if there is any.
    83  func (tablet *Vttablet) ValidateTabletRestart(t *testing.T) {
    84  	require.Nilf(t, tablet.Restart(), "tablet restart failed")
    85  }
    86  
    87  // GetPrimaryPosition gets the executed replication position of given vttablet
    88  func GetPrimaryPosition(t *testing.T, vttablet Vttablet, hostname string) (string, string) {
    89  	ctx := context.Background()
    90  	vtablet := getTablet(vttablet.GrpcPort, hostname)
    91  	pos, err := tmClient.PrimaryPosition(ctx, vtablet)
    92  	require.Nil(t, err)
    93  	gtID := strings.SplitAfter(pos, "/")[1]
    94  	return pos, gtID
    95  }
    96  
    97  // VerifyRowsInTabletForTable verifies the total number of rows in a table.
    98  // This is used to check that replication has caught up with the changes on primary.
    99  func VerifyRowsInTabletForTable(t *testing.T, vttablet *Vttablet, ksName string, expectedRows int, tableName string) {
   100  	timeout := time.Now().Add(1 * time.Minute)
   101  	lastNumRowsFound := 0
   102  	for time.Now().Before(timeout) {
   103  		// ignoring the error check, if the newly created table is not replicated, then there might be error and we should ignore it
   104  		// but eventually it will catch up and if not caught up in required time, testcase will fail
   105  		qr, _ := vttablet.VttabletProcess.QueryTablet("select * from "+tableName, ksName, true)
   106  		if qr != nil {
   107  			if len(qr.Rows) == expectedRows {
   108  				return
   109  			}
   110  			lastNumRowsFound = len(qr.Rows)
   111  		}
   112  		time.Sleep(300 * time.Millisecond)
   113  	}
   114  	require.Equalf(t, expectedRows, lastNumRowsFound, "unexpected number of rows in %s (%s.%s)", vttablet.Alias, ksName, tableName)
   115  }
   116  
   117  // VerifyRowsInTablet Verify total number of rows in a tablet
   118  func VerifyRowsInTablet(t *testing.T, vttablet *Vttablet, ksName string, expectedRows int) {
   119  	VerifyRowsInTabletForTable(t, vttablet, ksName, expectedRows, "vt_insert_test")
   120  }
   121  
   122  // PanicHandler handles the panic in the testcase.
   123  func PanicHandler(t *testing.T) {
   124  	err := recover()
   125  	if t == nil {
   126  		return
   127  	}
   128  	require.Nilf(t, err, "panic occured in testcase %v", t.Name())
   129  }
   130  
   131  // ListBackups Lists back preset in shard
   132  func (cluster LocalProcessCluster) ListBackups(shardKsName string) ([]string, error) {
   133  	output, err := cluster.VtctlclientProcess.ExecuteCommandWithOutput("ListBackups", shardKsName)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	result := strings.Split(output, "\n")
   138  	var returnResult []string
   139  	for _, str := range result {
   140  		if str != "" {
   141  			returnResult = append(returnResult, str)
   142  		}
   143  	}
   144  	return returnResult, nil
   145  }
   146  
   147  // VerifyBackupCount compares the backup count with expected count.
   148  func (cluster LocalProcessCluster) VerifyBackupCount(t *testing.T, shardKsName string, expected int) []string {
   149  	backups, err := cluster.ListBackups(shardKsName)
   150  	require.Nil(t, err)
   151  	assert.Equalf(t, expected, len(backups), "invalid number of backups")
   152  	return backups
   153  }
   154  
   155  // RemoveAllBackups removes all the backup corresponds to list backup.
   156  func (cluster LocalProcessCluster) RemoveAllBackups(t *testing.T, shardKsName string) {
   157  	backups, err := cluster.ListBackups(shardKsName)
   158  	require.Nil(t, err)
   159  	for _, backup := range backups {
   160  		cluster.VtctlclientProcess.ExecuteCommand("RemoveBackup", shardKsName, backup)
   161  	}
   162  }
   163  
   164  // ResetTabletDirectory transitions back to tablet state (i.e. mysql process restarts with cleaned directory and tablet is off)
   165  func ResetTabletDirectory(tablet Vttablet) error {
   166  	tablet.VttabletProcess.TearDown()
   167  	tablet.MysqlctlProcess.Stop()
   168  	os.RemoveAll(tablet.VttabletProcess.Directory)
   169  
   170  	return tablet.MysqlctlProcess.Start()
   171  }
   172  
   173  func getTablet(tabletGrpcPort int, hostname string) *topodatapb.Tablet {
   174  	portMap := make(map[string]int32)
   175  	portMap["grpc"] = int32(tabletGrpcPort)
   176  	return &topodatapb.Tablet{Hostname: hostname, PortMap: portMap}
   177  }
   178  
   179  func filterResultForWarning(input string) string {
   180  	lines := strings.Split(input, "\n")
   181  	var result string
   182  	for _, line := range lines {
   183  		if strings.Contains(line, "WARNING: vtctl should only be used for VDiff v1 workflows. Please use VDiff v2 and consider using vtctldclient for all other commands.") {
   184  			continue
   185  		}
   186  		result = result + line + "\n"
   187  	}
   188  	return result
   189  }
   190  
   191  func filterResultWhenRunsForCoverage(input string) string {
   192  	if !*isCoverage {
   193  		return input
   194  	}
   195  	lines := strings.Split(input, "\n")
   196  	var result string
   197  	for _, line := range lines {
   198  		if strings.Contains(line, "=== RUN") {
   199  			continue
   200  		}
   201  		if strings.Contains(line, "--- PASS:") || strings.Contains(line, "PASS") {
   202  			break
   203  		}
   204  		result = result + line + "\n"
   205  	}
   206  	return result
   207  }
   208  
   209  // WaitForReplicationPos will wait for replication position to catch-up
   210  func WaitForReplicationPos(t *testing.T, tabletA *Vttablet, tabletB *Vttablet, hostname string, timeout float64) {
   211  	replicationPosA, _ := GetPrimaryPosition(t, *tabletA, hostname)
   212  	for {
   213  		replicationPosB, _ := GetPrimaryPosition(t, *tabletB, hostname)
   214  		if positionAtLeast(t, tabletA, replicationPosB, replicationPosA) {
   215  			break
   216  		}
   217  		msg := fmt.Sprintf("%s's replication position to catch up to %s's;currently at: %s, waiting to catch up to: %s", tabletB.Alias, tabletA.Alias, replicationPosB, replicationPosA)
   218  		waitStep(t, msg, timeout, 0.01)
   219  	}
   220  }
   221  
   222  func waitStep(t *testing.T, msg string, timeout float64, sleepTime float64) float64 {
   223  	timeout = timeout - sleepTime
   224  	if timeout < 0.0 {
   225  		t.Errorf("timeout waiting for condition '%s'", msg)
   226  	}
   227  	time.Sleep(time.Duration(sleepTime) * time.Second)
   228  	return timeout
   229  }
   230  
   231  func positionAtLeast(t *testing.T, tablet *Vttablet, a string, b string) bool {
   232  	isAtleast := false
   233  	val, err := tablet.MysqlctlProcess.ExecuteCommandWithOutput("position", "at_least", a, b)
   234  	require.NoError(t, err)
   235  	if strings.Contains(val, "true") {
   236  		isAtleast = true
   237  	}
   238  	return isAtleast
   239  }
   240  
   241  // ExecuteQueriesUsingVtgate sends query to vtgate using vtgate session.
   242  func ExecuteQueriesUsingVtgate(t *testing.T, session *vtgateconn.VTGateSession, query string) {
   243  	_, err := session.Execute(context.Background(), query, nil)
   244  	require.Nil(t, err)
   245  }
   246  
   247  // NewConnParams creates ConnParams corresponds to given arguments.
   248  func NewConnParams(port int, password, socketPath, keyspace string) mysql.ConnParams {
   249  	if port != 0 {
   250  		socketPath = ""
   251  	}
   252  	cp := mysql.ConnParams{
   253  		Uname:      "vt_dba",
   254  		Port:       port,
   255  		UnixSocket: socketPath,
   256  		Pass:       password,
   257  	}
   258  	cp.DbName = keyspace
   259  	if keyspace != "" && keyspace != "_vt" {
   260  		cp.DbName = "vt_" + keyspace
   261  	}
   262  
   263  	return cp
   264  
   265  }
   266  
   267  func filterDoubleDashArgs(args []string, version int) (filtered []string) {
   268  	if version > 13 {
   269  		return args
   270  	}
   271  
   272  	for _, arg := range args {
   273  		if arg == "--" {
   274  			continue
   275  		}
   276  
   277  		filtered = append(filtered, arg)
   278  	}
   279  
   280  	return filtered
   281  }
   282  
   283  // WriteDbCredentialToTmp writes JSON formatted db credentials to the
   284  // specified tmp directory.
   285  func WriteDbCredentialToTmp(tmpDir string) string {
   286  	data := []byte(`{
   287          "vt_dba": ["VtDbaPass"],
   288          "vt_app": ["VtAppPass"],
   289          "vt_allprivs": ["VtAllprivsPass"],
   290          "vt_repl": ["VtReplPass"],
   291          "vt_filtered": ["VtFilteredPass"]
   292  	}`)
   293  	dbCredentialFile = path.Join(tmpDir, "db_credentials.json")
   294  	os.WriteFile(dbCredentialFile, data, 0666)
   295  	return dbCredentialFile
   296  }
   297  
   298  // GetPasswordUpdateSQL returns the SQL for updating the users' passwords
   299  // to the static creds used throughout tests.
   300  func GetPasswordUpdateSQL(localCluster *LocalProcessCluster) string {
   301  	pwdChangeCmd := `
   302  					# Set real passwords for all users.
   303  					SET PASSWORD FOR 'root'@'localhost' = 'RootPass';
   304  					SET PASSWORD FOR 'vt_dba'@'localhost' = 'VtDbaPass';
   305  					SET PASSWORD FOR 'vt_app'@'localhost' = 'VtAppPass';
   306  					SET PASSWORD FOR 'vt_allprivs'@'localhost' = 'VtAllprivsPass';
   307  					SET PASSWORD FOR 'vt_repl'@'%' = 'VtReplPass';
   308  					SET PASSWORD FOR 'vt_filtered'@'localhost' = 'VtFilteredPass';
   309  					FLUSH PRIVILEGES;
   310  					`
   311  	return pwdChangeCmd
   312  }
   313  
   314  // CheckSrvKeyspace confirms that the cell and keyspace contain the expected
   315  // shard mappings.
   316  func CheckSrvKeyspace(t *testing.T, cell string, ksname string, expectedPartition map[topodatapb.TabletType][]string, ci LocalProcessCluster) {
   317  	srvKeyspace := GetSrvKeyspace(t, cell, ksname, ci)
   318  
   319  	currentPartition := map[topodatapb.TabletType][]string{}
   320  
   321  	for _, partition := range srvKeyspace.Partitions {
   322  		currentPartition[partition.ServedType] = []string{}
   323  		for _, shardRef := range partition.ShardReferences {
   324  			currentPartition[partition.ServedType] = append(currentPartition[partition.ServedType], shardRef.Name)
   325  		}
   326  	}
   327  
   328  	assert.True(t, reflect.DeepEqual(currentPartition, expectedPartition))
   329  }
   330  
   331  // GetSrvKeyspace returns the SrvKeyspace structure for the cell and keyspace.
   332  func GetSrvKeyspace(t *testing.T, cell string, ksname string, ci LocalProcessCluster) *topodatapb.SrvKeyspace {
   333  	output, err := ci.VtctlclientProcess.ExecuteCommandWithOutput("GetSrvKeyspace", cell, ksname)
   334  	require.Nil(t, err)
   335  	var srvKeyspace topodatapb.SrvKeyspace
   336  
   337  	err = json2.Unmarshal([]byte(output), &srvKeyspace)
   338  	require.Nil(t, err)
   339  	return &srvKeyspace
   340  }
   341  
   342  // ExecuteOnTablet executes a query on the specified vttablet.
   343  // It should always be called with a primary tablet for a keyspace/shard.
   344  func ExecuteOnTablet(t *testing.T, query string, vttablet Vttablet, ks string, expectFail bool) {
   345  	_, _ = vttablet.VttabletProcess.QueryTablet("begin", ks, true)
   346  	_, err := vttablet.VttabletProcess.QueryTablet(query, ks, true)
   347  	if expectFail {
   348  		require.Error(t, err)
   349  	} else {
   350  		require.Nil(t, err)
   351  	}
   352  	_, _ = vttablet.VttabletProcess.QueryTablet("commit", ks, true)
   353  }
   354  
   355  func WaitForTabletSetup(vtctlClientProcess *VtctlClientProcess, expectedTablets int, expectedStatus []string) error {
   356  	// wait for both tablet to get into replica state in topo
   357  	waitUntil := time.Now().Add(10 * time.Second)
   358  	for time.Now().Before(waitUntil) {
   359  		result, err := vtctlClientProcess.ExecuteCommandWithOutput("ListAllTablets")
   360  		if err != nil {
   361  			return err
   362  		}
   363  
   364  		tabletsFromCMD := strings.Split(result, "\n")
   365  		tabletCountFromCMD := 0
   366  
   367  		for _, line := range tabletsFromCMD {
   368  			if len(line) > 0 {
   369  				for _, status := range expectedStatus {
   370  					if strings.Contains(line, status) {
   371  						tabletCountFromCMD = tabletCountFromCMD + 1
   372  						break
   373  					}
   374  				}
   375  			}
   376  		}
   377  
   378  		if tabletCountFromCMD >= expectedTablets {
   379  			return nil
   380  		}
   381  
   382  		time.Sleep(1 * time.Second)
   383  	}
   384  
   385  	return fmt.Errorf("all %d tablet are not in expected state %s", expectedTablets, expectedStatus)
   386  }
   387  
   388  // WaitForHealthyShard waits for the given shard info record in the topo
   389  // server to list a tablet (alias and uid) as the primary serving tablet
   390  // for the shard. This is done using "vtctldclient GetShard" and parsing
   391  // its JSON output. All other watchers should then also see this shard
   392  // info status as well.
   393  func WaitForHealthyShard(vtctldclient *VtctldClientProcess, keyspace, shard string) error {
   394  	var (
   395  		tmr  = time.NewTimer(defaultOperationTimeout)
   396  		res  string
   397  		err  error
   398  		json []byte
   399  		cell string
   400  		uid  int64
   401  	)
   402  	for {
   403  		res, err = vtctldclient.ExecuteCommandWithOutput("GetShard", fmt.Sprintf("%s/%s", keyspace, shard))
   404  		if err != nil {
   405  			return err
   406  		}
   407  		json = []byte(res)
   408  
   409  		cell, err = jsonparser.GetString(json, "shard", "primary_alias", "cell")
   410  		if err != nil && err != jsonparser.KeyPathNotFoundError {
   411  			return err
   412  		}
   413  		uid, err = jsonparser.GetInt(json, "shard", "primary_alias", "uid")
   414  		if err != nil && err != jsonparser.KeyPathNotFoundError {
   415  			return err
   416  		}
   417  
   418  		if cell != "" && uid > 0 {
   419  			return nil
   420  		}
   421  
   422  		select {
   423  		case <-tmr.C:
   424  			return fmt.Errorf("timed out waiting for the %s/%s shard to become healthy in the topo after %v; last seen status: %s; last seen error: %v",
   425  				keyspace, shard, defaultOperationTimeout, res, err)
   426  		default:
   427  		}
   428  
   429  		time.Sleep(defeaultRetryDelay)
   430  	}
   431  }