vitess.io/vitess@v0.16.2/go/test/endtoend/vreplication/vdiff_helper_test.go (about)

     1  /*
     2  Copyright 2022 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 vreplication
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/buger/jsonparser"
    27  	"github.com/stretchr/testify/require"
    28  
    29  	"vitess.io/vitess/go/sqlescape"
    30  	"vitess.io/vitess/go/sqltypes"
    31  	"vitess.io/vitess/go/test/endtoend/cluster"
    32  	"vitess.io/vitess/go/vt/log"
    33  	vdiff2 "vitess.io/vitess/go/vt/vttablet/tabletmanager/vdiff"
    34  	"vitess.io/vitess/go/vt/wrangler"
    35  )
    36  
    37  const (
    38  	vdiffTimeout = time.Second * 90 // we can leverage auto retry on error with this longer-than-usual timeout
    39  )
    40  
    41  var (
    42  	runVDiffsSideBySide = true
    43  )
    44  
    45  func vdiff(t *testing.T, keyspace, workflow, cells string, v1, v2 bool, wantV2Result *expectedVDiff2Result) {
    46  	ksWorkflow := fmt.Sprintf("%s.%s", keyspace, workflow)
    47  	if v1 {
    48  		doVDiff1(t, ksWorkflow, cells)
    49  	}
    50  	if v2 {
    51  		doVdiff2(t, keyspace, workflow, cells, wantV2Result)
    52  	}
    53  }
    54  
    55  func vdiff1(t *testing.T, ksWorkflow, cells string) {
    56  	if !runVDiffsSideBySide {
    57  		doVDiff1(t, ksWorkflow, cells)
    58  		return
    59  	}
    60  	arr := strings.Split(ksWorkflow, ".")
    61  	keyspace := arr[0]
    62  	workflowName := arr[1]
    63  	vdiff(t, keyspace, workflowName, cells, true, true, nil)
    64  }
    65  
    66  func doVDiff1(t *testing.T, ksWorkflow, cells string) {
    67  	t.Run(fmt.Sprintf("vdiff1 %s", ksWorkflow), func(t *testing.T) {
    68  		output, err := vc.VtctlClient.ExecuteCommandWithOutput("VDiff", "--", "--v1", "--tablet_types=primary", "--source_cell="+cells, "--format", "json", ksWorkflow)
    69  		log.Infof("vdiff1 err: %+v, output: %+v", err, output)
    70  		require.Nil(t, err)
    71  		require.NotNil(t, output)
    72  		diffReports := make(map[string]*wrangler.DiffReport)
    73  		err = json.Unmarshal([]byte(output), &diffReports)
    74  		require.Nil(t, err)
    75  		if len(diffReports) < 1 {
    76  			t.Fatal("VDiff did not return a valid json response " + output + "\n")
    77  		}
    78  		require.True(t, len(diffReports) > 0)
    79  		for key, diffReport := range diffReports {
    80  			if diffReport.ProcessedRows != diffReport.MatchingRows {
    81  				require.Failf(t, "vdiff1 failed", "Table %d : %#v\n", key, diffReport)
    82  			}
    83  		}
    84  	})
    85  }
    86  
    87  func waitForVDiff2ToComplete(t *testing.T, ksWorkflow, cells, uuid string, completedAtMin time.Time) *vdiffInfo {
    88  	var info *vdiffInfo
    89  	first := true
    90  	previousProgress := vdiff2.ProgressReport{}
    91  	ch := make(chan bool)
    92  	go func() {
    93  		for {
    94  			time.Sleep(1 * time.Second)
    95  			_, jsonStr := performVDiff2Action(t, ksWorkflow, cells, "show", uuid, false)
    96  			info = getVDiffInfo(jsonStr)
    97  			if info.State == "completed" {
    98  				if !completedAtMin.IsZero() {
    99  					ca := info.CompletedAt
   100  					completedAt, _ := time.Parse(vdiff2.TimestampFormat, ca)
   101  					if !completedAt.After(completedAtMin) {
   102  						continue
   103  					}
   104  				}
   105  				ch <- true
   106  				return
   107  			} else if info.State == "started" { // test the progress report
   108  				// The ETA should always be in the future -- when we're able to estimate
   109  				// it -- and the progress percentage should only increase.
   110  				// The timestamp format allows us to compare them lexicographically.
   111  				// We don't test that the ETA always increases as it can decrease based on how
   112  				// quickly we're doing work.
   113  				if info.Progress.ETA != "" {
   114  					// If we're operating at the second boundary then the ETA can be up
   115  					// to 1 second in the past due to using second based precision.
   116  					loc, _ := time.LoadLocation("UTC")
   117  					require.GreaterOrEqual(t, info.Progress.ETA, time.Now().Add(-time.Second).In(loc).Format(vdiff2.TimestampFormat))
   118  				}
   119  				if !first {
   120  					require.GreaterOrEqual(t, info.Progress.Percentage, previousProgress.Percentage)
   121  				}
   122  				previousProgress.Percentage = info.Progress.Percentage
   123  				first = false
   124  			}
   125  		}
   126  	}()
   127  
   128  	select {
   129  	case <-ch:
   130  		return info
   131  	case <-time.After(vdiffTimeout):
   132  		require.FailNow(t, fmt.Sprintf("VDiff never completed for UUID %s", uuid))
   133  		return nil
   134  	}
   135  }
   136  
   137  type expectedVDiff2Result struct {
   138  	state       string
   139  	shards      []string
   140  	hasMismatch bool
   141  }
   142  
   143  func doVdiff2(t *testing.T, keyspace, workflow, cells string, want *expectedVDiff2Result) {
   144  	ksWorkflow := fmt.Sprintf("%s.%s", keyspace, workflow)
   145  	t.Run(fmt.Sprintf("vdiff2 %s", ksWorkflow), func(t *testing.T) {
   146  		uuid, _ := performVDiff2Action(t, ksWorkflow, cells, "create", "", false, "--auto-retry")
   147  		info := waitForVDiff2ToComplete(t, ksWorkflow, cells, uuid, time.Time{})
   148  
   149  		require.Equal(t, workflow, info.Workflow)
   150  		require.Equal(t, keyspace, info.Keyspace)
   151  		if want != nil {
   152  			require.Equal(t, want.state, info.State)
   153  			require.Equal(t, strings.Join(want.shards, ","), info.Shards)
   154  			require.Equal(t, want.hasMismatch, info.HasMismatch)
   155  		} else {
   156  			require.Equal(t, "completed", info.State)
   157  			require.False(t, info.HasMismatch)
   158  		}
   159  		if strings.Contains(t.Name(), "AcrossDBVersions") {
   160  			log.Errorf("VDiff resume cannot be guaranteed between major MySQL versions due to implied collation differences, skipping resume test...")
   161  			return
   162  		}
   163  	})
   164  }
   165  
   166  func performVDiff2Action(t *testing.T, ksWorkflow, cells, action, actionArg string, expectError bool, extraFlags ...string) (uuid string, output string) {
   167  	var err error
   168  	args := []string{"VDiff", "--", "--tablet_types=primary", "--source_cell=" + cells, "--format=json"}
   169  	if len(extraFlags) > 0 {
   170  		args = append(args, extraFlags...)
   171  	}
   172  	args = append(args, ksWorkflow, action, actionArg)
   173  	output, err = vc.VtctlClient.ExecuteCommandWithOutput(args...)
   174  	log.Infof("vdiff2 output: %+v (err: %+v)", output, err)
   175  	if !expectError {
   176  		require.Nil(t, err)
   177  		uuid, err = jsonparser.GetString([]byte(output), "UUID")
   178  		if action != "delete" && !(action == "show" && actionArg == "all") { // a UUID is not required
   179  			require.NoError(t, err)
   180  			require.NotEmpty(t, uuid)
   181  		}
   182  	}
   183  	return uuid, output
   184  }
   185  
   186  type vdiffInfo struct {
   187  	Workflow, Keyspace string
   188  	State, Shards      string
   189  	RowsCompared       int64
   190  	StartedAt          string
   191  	CompletedAt        string
   192  	HasMismatch        bool
   193  	Progress           vdiff2.ProgressReport
   194  }
   195  
   196  func getVDiffInfo(jsonStr string) *vdiffInfo {
   197  	var info vdiffInfo
   198  	json := []byte(jsonStr)
   199  	info.Workflow, _ = jsonparser.GetString(json, "Workflow")
   200  	info.Keyspace, _ = jsonparser.GetString(json, "Keyspace")
   201  	info.State, _ = jsonparser.GetString(json, "State")
   202  	info.Shards, _ = jsonparser.GetString(json, "Shards")
   203  	info.RowsCompared, _ = jsonparser.GetInt(json, "RowsCompared")
   204  	info.StartedAt, _ = jsonparser.GetString(json, "StartedAt")
   205  	info.CompletedAt, _ = jsonparser.GetString(json, "CompletedAt")
   206  	info.HasMismatch, _ = jsonparser.GetBoolean(json, "HasMismatch")
   207  	info.Progress.Percentage, _ = jsonparser.GetFloat(json, "Progress", "Percentage")
   208  	info.Progress.ETA, _ = jsonparser.GetString(json, "Progress", "ETA")
   209  	return &info
   210  }
   211  
   212  func encodeString(in string) string {
   213  	var buf strings.Builder
   214  	sqltypes.NewVarChar(in).EncodeSQL(&buf)
   215  	return buf.String()
   216  }
   217  
   218  // updateTableStats runs ANALYZE TABLE on each table involved in the workflow.
   219  // You should execute this if you leverage table information from e.g.
   220  // information_schema.tables in your test.
   221  func updateTableStats(t *testing.T, tablet *cluster.VttabletProcess, tables string) {
   222  	dbName := "vt_" + tablet.Keyspace
   223  	tableList := strings.Split(strings.TrimSpace(tables), ",")
   224  	if len(tableList) == 0 {
   225  		// we need to get all of the tables in the keyspace
   226  		res, err := tablet.QueryTabletWithDB("show tables", dbName)
   227  		require.NoError(t, err)
   228  		for _, row := range res.Rows {
   229  			tableList = append(tableList, row[0].String())
   230  		}
   231  	}
   232  	for _, table := range tableList {
   233  		table = strings.TrimSpace(table)
   234  		if table != "" {
   235  			res, err := tablet.QueryTabletWithDB(fmt.Sprintf(sqlAnalyzeTable, sqlescape.EscapeID(table)), dbName)
   236  			require.NoError(t, err)
   237  			require.Equal(t, 1, len(res.Rows))
   238  		}
   239  	}
   240  }
   241  
   242  // generateMoreCustomers creates additional test data for better tests
   243  // when needed.
   244  func generateMoreCustomers(t *testing.T, keyspace string, numCustomers int64) {
   245  	log.Infof("Generating more test data with an additional %d customers", numCustomers)
   246  	res := execVtgateQuery(t, vtgateConn, keyspace, "select max(cid) from customer")
   247  	startingID, _ := res.Rows[0][0].ToInt64()
   248  	insert := strings.Builder{}
   249  	insert.WriteString("insert into customer(cid, name, typ) values ")
   250  	i := int64(0)
   251  	for i < numCustomers {
   252  		i++
   253  		insert.WriteString(fmt.Sprintf("(%d, 'Testy (Bot) McTester', 'soho')", startingID+i))
   254  		if i != numCustomers {
   255  			insert.WriteString(", ")
   256  		}
   257  	}
   258  	execVtgateQuery(t, vtgateConn, keyspace, insert.String())
   259  }