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  }