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 }