vitess.io/vitess@v0.16.2/go/test/endtoend/migration/migration_test.go (about) 1 /* 2 Copyright 2020 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 migration 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "os" 24 "path" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 32 "vitess.io/vitess/go/mysql" 33 "vitess.io/vitess/go/sqltypes" 34 "vitess.io/vitess/go/test/endtoend/cluster" 35 36 binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata" 37 ) 38 39 var ( 40 clusterInstance *cluster.LocalProcessCluster 41 vtParams *mysql.ConnParams 42 cell = "test" 43 keyspaces = make(map[string]*cluster.Keyspace) 44 45 legacyProduct = cluster.Keyspace{ 46 Name: "product", 47 SchemaSQL: ` 48 create table product(pid bigint, description varbinary(128), primary key(pid)); 49 `, 50 } 51 legacyProductData = ` 52 insert into vt_product.product(pid, description) values(1, 'keyboard'), (2, 'monitor'); 53 ` 54 55 legacyCustomer = cluster.Keyspace{ 56 Name: "customer", 57 SchemaSQL: ` 58 create table customer(cid bigint, name varbinary(128), primary key(cid)); 59 create table orders(oid bigint, cid bigint, pid bigint, mname varchar(128), price bigint, primary key(oid)); 60 `, 61 } 62 legacyCustomerData = ` 63 insert into vt_customer.customer(cid, name) values(1, 'john'), (2, 'paul'), (3, 'ringo'); 64 insert into vt_customer.orders(oid, cid, mname, pid, price) values(1, 1, 'monoprice', 1, 10), (2, 1, 'newegg', 2, 15); 65 ` 66 67 commerce = cluster.Keyspace{ 68 Name: "commerce", 69 SchemaSQL: ` 70 create table product(pid bigint, description varbinary(128), primary key(pid)); 71 create table customer(cid bigint, name varbinary(128), primary key(cid)); 72 create table orders(oid bigint, cid bigint, pid bigint, mname varchar(128), price bigint, primary key(oid)); 73 `, 74 VSchema: `{ 75 "tables": { 76 "product": {}, 77 "customer": {}, 78 "orders": {} 79 } 80 }`, 81 } 82 83 connFormat = `externalConnections: 84 product: 85 socket: %s 86 dbName: vt_product 87 app: 88 user: vt_app 89 dba: 90 user: vt_dba 91 customer: 92 flavor: FilePos 93 socket: %s 94 dbName: vt_customer 95 app: 96 user: vt_app 97 dba: 98 user: vt_dba 99 ` 100 ) 101 102 /* 103 TestMigration demonstrates how to setup vreplication to import data from multiple external 104 mysql sources. 105 We use vitess to bring up two databases that we'll treat as external. We'll directly access 106 the mysql instances instead of going through any vitess layer. 107 The product database contains a product table. 108 The customer database contains a customer and an orders table. 109 We create a new "commerce" keyspace, which will be the target. The commerce keyspace will 110 take a yaml config that defines these external sources. it will look like this: 111 112 externalConnections: 113 114 product: 115 socket: /home/sougou/dev/src/vitess.io/vitess/vtdataroot/vtroot_15201/vt_0000000622/mysql.sock 116 dbName: vt_product 117 app: 118 user: vt_app 119 dba: 120 user: vt_dba 121 customer: 122 flavor: FilePos 123 socket: /home/sougou/dev/src/vitess.io/vitess/vtdataroot/vtroot_15201/vt_0000000620/mysql.sock 124 dbName: vt_customer 125 app: 126 user: vt_app 127 dba: 128 user: vt_dba 129 130 We then execute the following vreplication inserts to initiate the import. The test uses 131 three streams although only two are required. This is to show that there can exist multiple 132 streams from the same source. The main difference between an external source vs a vitess 133 source is that the source proto contains an "external_mysql" field instead of keyspace and shard. 134 That field is the key into the externalConnections section of the input yaml. 135 136 VReplicationExec: insert into _vt.vreplication (workflow, db_name, source, pos, max_tps, max_replication_lag, tablet_types, time_updated, transaction_timestamp, state) values('product', 'vt_commerce', 'filter:<rules:<match:\"product\" > > external_mysql:\"product\" ', ”, 9999, 9999, 'primary', 0, 0, 'Running') 137 VReplicationExec: insert into _vt.vreplication (workflow, db_name, source, pos, max_tps, max_replication_lag, tablet_types, time_updated, transaction_timestamp, state) values('customer', 'vt_commerce', 'filter:<rules:<match:\"customer\" > > external_mysql:\"customer\" ', ”, 9999, 9999, 'primary', 0, 0, 'Running') 138 VReplicationExec: insert into _vt.vreplication (workflow, db_name, source, pos, max_tps, max_replication_lag, tablet_types, time_updated, transaction_timestamp, state) values('orders', 'vt_commerce', 'filter:<rules:<match:\"orders\" > > external_mysql:\"customer\" ', ”, 9999, 9999, 'primary', 0, 0, 'Running') 139 */ 140 func TestMigration(t *testing.T) { 141 yamlFile := startCluster(t) 142 defer clusterInstance.Teardown() 143 144 tabletConfig := func(vt *cluster.VttabletProcess) { 145 vt.ExtraArgs = append(vt.ExtraArgs, "--tablet_config", yamlFile) 146 } 147 createKeyspace(t, commerce, []string{"0"}, tabletConfig) 148 err := clusterInstance.VtctlclientProcess.ExecuteCommand("RebuildKeyspaceGraph", "commerce") 149 require.NoError(t, err) 150 151 err = clusterInstance.StartVtgate() 152 require.NoError(t, err) 153 154 migrate(t, "product", "commerce", []string{"product"}) 155 migrate(t, "customer", "commerce", []string{"customer"}) 156 migrate(t, "customer", "commerce", []string{"orders"}) 157 vttablet := keyspaces["commerce"].Shards[0].Vttablets[0].VttabletProcess 158 waitForVReplicationToCatchup(t, vttablet, 1*time.Second) 159 160 testcases := []struct { 161 query string 162 result *sqltypes.Result 163 }{{ 164 query: "select * from product", 165 result: sqltypes.MakeTestResult(sqltypes.MakeTestFields( 166 "pid|description", 167 "int64|varbinary"), 168 "1|keyboard", 169 "2|monitor", 170 ), 171 }, { 172 query: "select * from customer", 173 result: sqltypes.MakeTestResult(sqltypes.MakeTestFields( 174 "cid|name", 175 "int64|varbinary"), 176 "1|john", 177 "2|paul", 178 "3|ringo", 179 ), 180 }, { 181 query: "select * from orders", 182 result: sqltypes.MakeTestResult(sqltypes.MakeTestFields( 183 "oid|cid|mname|pid|price", 184 "int64|int64|int64|varchar|int64"), 185 "1|1|1|monoprice|10", 186 "2|1|2|newegg|15", 187 ), 188 }} 189 190 vtParams = &mysql.ConnParams{ 191 Host: clusterInstance.Hostname, 192 Port: clusterInstance.VtgateMySQLPort, 193 } 194 conn, err := mysql.Connect(context.Background(), vtParams) 195 require.NoError(t, err) 196 defer conn.Close() 197 198 execQuery(t, conn, "use `commerce`") 199 for _, tcase := range testcases { 200 result := execQuery(t, conn, tcase.query) 201 // nil out the fields because they're too detailed. 202 result.Fields = nil 203 tcase.result.Fields = nil 204 assert.Equal(t, tcase.result, result, tcase.query) 205 } 206 } 207 208 func migrate(t *testing.T, fromdb, toks string, tables []string) { 209 bls := &binlogdatapb.BinlogSource{ 210 ExternalMysql: fromdb, 211 Filter: &binlogdatapb.Filter{}, 212 } 213 for _, table := range tables { 214 bls.Filter.Rules = append(bls.Filter.Rules, &binlogdatapb.Rule{Match: table}) 215 } 216 val := sqltypes.NewVarBinary(fmt.Sprintf("%v", bls)) 217 var sqlEscaped bytes.Buffer 218 val.EncodeSQL(&sqlEscaped) 219 query := fmt.Sprintf("insert into _vt.vreplication "+ 220 "(workflow, db_name, source, pos, max_tps, max_replication_lag, tablet_types, time_updated, transaction_timestamp, state) values"+ 221 "('%s', '%s', %s, '', 9999, 9999, 'primary', 0, 0, 'Running')", tables[0], "vt_"+toks, sqlEscaped.String()) 222 fmt.Printf("VReplicationExec: %s\n", query) 223 vttablet := keyspaces[toks].Shards[0].Vttablets[0].VttabletProcess 224 err := clusterInstance.VtctlclientProcess.ExecuteCommand("VReplicationExec", vttablet.TabletPath, query) 225 require.NoError(t, err) 226 } 227 228 func startCluster(t *testing.T) string { 229 clusterInstance = cluster.NewCluster(cell, "localhost") 230 231 err := clusterInstance.StartTopo() 232 if err != nil { 233 t.Fatal(err) 234 } 235 236 createKeyspace(t, legacyCustomer, []string{"0"}) 237 createKeyspace(t, legacyProduct, []string{"0"}) 238 239 productSocket := path.Join(keyspaces["product"].Shards[0].Vttablets[0].VttabletProcess.Directory, "mysql.sock") 240 customerSocket := path.Join(keyspaces["customer"].Shards[0].Vttablets[0].VttabletProcess.Directory, "mysql.sock") 241 242 populate(t, productSocket, legacyProductData) 243 populate(t, customerSocket, legacyCustomerData) 244 245 buf := &bytes.Buffer{} 246 fmt.Fprintf(buf, "externalConnections:\n") 247 tabletConfig := fmt.Sprintf(connFormat, productSocket, customerSocket) 248 fmt.Printf("tablet_config:\n%s\n", tabletConfig) 249 yamlFile := path.Join(clusterInstance.TmpDirectory, "external.yaml") 250 err = os.WriteFile(yamlFile, []byte(tabletConfig), 0644) 251 require.NoError(t, err) 252 return yamlFile 253 } 254 255 func createKeyspace(t *testing.T, ks cluster.Keyspace, shards []string, customizers ...any) { 256 t.Helper() 257 258 err := clusterInstance.StartKeyspace(ks, shards, 1, false, customizers...) 259 require.NoError(t, err) 260 keyspaces[ks.Name] = &clusterInstance.Keyspaces[len(clusterInstance.Keyspaces)-1] 261 } 262 263 func populate(t *testing.T, socket, sql string) { 264 t.Helper() 265 266 params := &mysql.ConnParams{ 267 UnixSocket: socket, 268 Uname: "vt_app", 269 } 270 conn, err := mysql.Connect(context.Background(), params) 271 require.NoError(t, err) 272 defer conn.Close() 273 lines := strings.Split(sql, "\n") 274 for _, line := range lines { 275 if line == "" { 276 continue 277 } 278 execQuery(t, conn, line) 279 } 280 } 281 282 // waitForVReplicationToCatchup: logic copied from go/test/endtoend/vreplication/cluster.go 283 func waitForVReplicationToCatchup(t *testing.T, vttablet *cluster.VttabletProcess, duration time.Duration) { 284 queries := []string{ 285 `select count(*) from _vt.vreplication where pos = ''`, 286 "select count(*) from information_schema.tables where table_schema='_vt' and table_name='copy_state' limit 1;", 287 `select count(*) from _vt.copy_state`, 288 } 289 results := []string{"[INT64(0)]", "[INT64(1)]", "[INT64(0)]"} 290 for ind, query := range queries { 291 waitDuration := 100 * time.Millisecond 292 for { 293 qr, err := vttablet.QueryTablet(query, "", false) 294 if err != nil { 295 t.Fatal(err) 296 } 297 if len(qr.Rows) > 0 && fmt.Sprintf("%v", qr.Rows[0]) == string(results[ind]) { 298 break 299 } 300 time.Sleep(waitDuration) 301 duration -= waitDuration 302 if duration <= 0 { 303 t.Fatalf("waitForVReplicationToCatchup timed out, query: %v, result: %v", query, qr) 304 } 305 } 306 } 307 } 308 309 func execQuery(t *testing.T, conn *mysql.Conn, query string) *sqltypes.Result { 310 t.Helper() 311 qr, err := conn.ExecuteFetch(query, 10000, true) 312 require.Nil(t, err) 313 return qr 314 }