vitess.io/vitess@v0.16.2/go/test/endtoend/onlineddl/ghost/onlineddl_ghost_test.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 ghost
    18  
    19  import (
    20  	"flag"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	"vitess.io/vitess/go/mysql"
    30  	"vitess.io/vitess/go/vt/schema"
    31  
    32  	"vitess.io/vitess/go/test/endtoend/cluster"
    33  	"vitess.io/vitess/go/test/endtoend/onlineddl"
    34  
    35  	"github.com/stretchr/testify/assert"
    36  	"github.com/stretchr/testify/require"
    37  )
    38  
    39  var (
    40  	clusterInstance       *cluster.LocalProcessCluster
    41  	shards                []cluster.Shard
    42  	vtParams              mysql.ConnParams
    43  	hostname              = "localhost"
    44  	keyspaceName          = "ks"
    45  	cell                  = "zone1"
    46  	schemaChangeDirectory = ""
    47  	totalTableCount       = 4
    48  
    49  	normalMigrationWait = 20 * time.Second
    50  
    51  	createTable = `
    52  		CREATE TABLE %s (
    53  			id bigint(20) NOT NULL,
    54  			msg varchar(64),
    55  			PRIMARY KEY (id)
    56  		) ENGINE=InnoDB;`
    57  	insertStatements = []string{
    58  		`insert into %s (id, msg) values (3, 'three')`,
    59  		`insert into %s (id, msg) values (5, 'five')`,
    60  		`insert into %s (id, msg) values (7, 'seven')`,
    61  		`insert into %s (id, msg) values (11, 'eleven')`,
    62  		`insert into %s (id, msg) values (13, 'thirteen')`,
    63  	}
    64  	// To verify non online-DDL behavior
    65  	alterTableNormalStatement = `
    66  		ALTER TABLE %s
    67  			ADD COLUMN non_online int UNSIGNED NOT NULL`
    68  	// A trivial statement which must succeed and does not change the schema
    69  	alterTableTrivialStatement = `
    70  		ALTER TABLE %s
    71  			ENGINE=InnoDB`
    72  	// The following statement is valid
    73  	alterTableSuccessfulStatement = `
    74  		ALTER TABLE %s
    75  			MODIFY id bigint UNSIGNED NOT NULL,
    76  			ADD COLUMN ghost_col int NOT NULL,
    77  			ADD INDEX idx_msg(msg)`
    78  	// The following statement will fail because gh-ost requires some shared unique key
    79  	alterTableFailedStatement = `
    80  		ALTER TABLE %s
    81  			DROP PRIMARY KEY,
    82  			DROP COLUMN ghost_col`
    83  	// We will run this query with "gh-ost --max-load=Threads_running=1"
    84  	alterTableThrottlingStatement = `
    85  		ALTER TABLE %s
    86  			DROP COLUMN ghost_col`
    87  	onlineDDLCreateTableStatement = `
    88  		CREATE TABLE %s (
    89  			id bigint NOT NULL,
    90  			online_ddl_create_col INT NOT NULL,
    91  			PRIMARY KEY (id)
    92  		) ENGINE=InnoDB;`
    93  	noPKCreateTableStatement = `
    94  		CREATE TABLE %s (
    95  			online_ddl_create_col INT NOT NULL
    96  		) ENGINE=InnoDB;`
    97  	onlineDDLDropTableStatement = `
    98  		DROP TABLE %s`
    99  	onlineDDLDropTableIfExistsStatement = `
   100  		DROP TABLE IF EXISTS %s`
   101  
   102  	vSchema = `
   103  		{
   104  			"sharded": true,
   105  			"vindexes": {
   106  				"hash_index": {
   107  					"type": "hash"
   108  				}
   109  			},
   110  			"tables": {
   111  				"vt_onlineddl_test_00": {
   112  					"column_vindexes": [
   113  						{
   114  							"column": "id",
   115  							"name": "hash_index"
   116  						}
   117  					]
   118  				},
   119  				"vt_onlineddl_test_01": {
   120  					"column_vindexes": [
   121  						{
   122  							"column": "id",
   123  							"name": "hash_index"
   124  						}
   125  					]
   126  				},
   127  				"vt_onlineddl_test_02": {
   128  					"column_vindexes": [
   129  						{
   130  							"column": "id",
   131  							"name": "hash_index"
   132  						}
   133  					]
   134  				},
   135  				"vt_onlineddl_test_03": {
   136  					"column_vindexes": [
   137  						{
   138  							"column": "id",
   139  							"name": "hash_index"
   140  						}
   141  					]
   142  				}
   143  			}
   144  		}
   145  		`
   146  )
   147  
   148  func TestMain(m *testing.M) {
   149  	defer cluster.PanicHandler(nil)
   150  	flag.Parse()
   151  
   152  	exitcode, err := func() (int, error) {
   153  		clusterInstance = cluster.NewCluster(cell, hostname)
   154  		schemaChangeDirectory = path.Join("/tmp", fmt.Sprintf("schema_change_dir_%d", clusterInstance.GetAndReserveTabletUID()))
   155  		defer os.RemoveAll(schemaChangeDirectory)
   156  		defer clusterInstance.Teardown()
   157  
   158  		if _, err := os.Stat(schemaChangeDirectory); os.IsNotExist(err) {
   159  			_ = os.Mkdir(schemaChangeDirectory, 0700)
   160  		}
   161  
   162  		clusterInstance.VtctldExtraArgs = []string{
   163  			"--schema_change_dir", schemaChangeDirectory,
   164  			"--schema_change_controller", "local",
   165  			"--schema_change_check_interval", "1",
   166  		}
   167  
   168  		clusterInstance.VtTabletExtraArgs = []string{
   169  			"--enable-lag-throttler",
   170  			"--throttle_threshold", "1s",
   171  			"--heartbeat_enable",
   172  			"--heartbeat_interval", "250ms",
   173  			"--heartbeat_on_demand_duration", "5s",
   174  			"--migration_check_interval", "5s",
   175  			"--gh-ost-path", os.Getenv("VITESS_ENDTOEND_GH_OST_PATH"), // leave env variable empty/unset to get the default behavior. Override in Mac.
   176  		}
   177  		clusterInstance.VtGateExtraArgs = []string{
   178  			"--ddl_strategy", "gh-ost",
   179  		}
   180  
   181  		if err := clusterInstance.StartTopo(); err != nil {
   182  			return 1, err
   183  		}
   184  
   185  		keyspace := &cluster.Keyspace{
   186  			Name:    keyspaceName,
   187  			VSchema: vSchema,
   188  		}
   189  
   190  		if err := clusterInstance.StartKeyspace(*keyspace, []string{"-80", "80-"}, 1, false); err != nil {
   191  			return 1, err
   192  		}
   193  
   194  		vtgateInstance := clusterInstance.NewVtgateInstance()
   195  		// Start vtgate
   196  		if err := vtgateInstance.Setup(); err != nil {
   197  			return 1, err
   198  		}
   199  		// ensure it is torn down during cluster TearDown
   200  		clusterInstance.VtgateProcess = *vtgateInstance
   201  		vtParams = mysql.ConnParams{
   202  			Host: clusterInstance.Hostname,
   203  			Port: clusterInstance.VtgateMySQLPort,
   204  		}
   205  
   206  		return m.Run(), nil
   207  	}()
   208  	if err != nil {
   209  		fmt.Printf("%v\n", err)
   210  		os.Exit(1)
   211  	} else {
   212  		os.Exit(exitcode)
   213  	}
   214  
   215  }
   216  
   217  func TestSchemaChange(t *testing.T) {
   218  	defer cluster.PanicHandler(t)
   219  	shards = clusterInstance.Keyspaces[0].Shards
   220  	assert.Equal(t, 2, len(shards))
   221  	testWithInitialSchema(t)
   222  	t.Run("create non_online", func(t *testing.T) {
   223  		_ = testOnlineDDLStatement(t, alterTableNormalStatement, string(schema.DDLStrategyDirect), "vtctl", "non_online", "")
   224  	})
   225  	t.Run("successful online alter, vtgate", func(t *testing.T) {
   226  		uuid := testOnlineDDLStatement(t, alterTableSuccessfulStatement, "gh-ost", "vtgate", "ghost_col", "")
   227  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   228  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   229  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   230  
   231  		var totalRowsCopied uint64
   232  		// count sum of rows copied in all shards, that should be the total number of rows inserted to the table
   233  		rs := onlineddl.ReadMigrations(t, &vtParams, uuid)
   234  		require.NotNil(t, rs)
   235  		for _, row := range rs.Named().Rows {
   236  			rowsCopied := row.AsUint64("rows_copied", 0)
   237  			totalRowsCopied += rowsCopied
   238  		}
   239  		require.Equal(t, uint64(len(insertStatements)), totalRowsCopied)
   240  
   241  		// See that we're able to read logs after successful migration:
   242  		expectedMessage := "starting gh-ost"
   243  		logs := onlineddl.ReadMigrationLogs(t, &vtParams, uuid)
   244  		assert.Equal(t, len(shards), len(logs))
   245  		for i := range logs {
   246  			require.Contains(t, logs[i], expectedMessage)
   247  		}
   248  
   249  	})
   250  	t.Run("successful online alter, vtctl", func(t *testing.T) {
   251  		uuid := testOnlineDDLStatement(t, alterTableTrivialStatement, "gh-ost", "vtctl", "ghost_col", "")
   252  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   253  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   254  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   255  	})
   256  	t.Run("successful online alter, postponed, vtgate", func(t *testing.T) {
   257  		uuid := testOnlineDDLStatement(t, alterTableTrivialStatement, "gh-ost -postpone-completion", "vtgate", "ghost_col", "")
   258  		// Should be still running!
   259  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusRunning)
   260  		// Issue a complete and wait for successful completion
   261  		onlineddl.CheckCompleteMigration(t, &vtParams, shards, uuid, true)
   262  		// This part may take a while, because we depend on vreplicatoin polling
   263  		status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
   264  		fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
   265  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   266  
   267  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   268  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   269  	})
   270  	t.Run("throttled migration", func(t *testing.T) {
   271  		uuid := testOnlineDDLStatement(t, alterTableThrottlingStatement, "gh-ost --max-load=Threads_running=1", "vtgate", "ghost_col", "")
   272  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusRunning)
   273  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, true)
   274  		status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, 20*time.Second, schema.OnlineDDLStatusFailed, schema.OnlineDDLStatusCancelled)
   275  		fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
   276  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusCancelled)
   277  	})
   278  	t.Run("failed migration", func(t *testing.T) {
   279  		uuid := testOnlineDDLStatement(t, alterTableFailedStatement, "gh-ost", "vtgate", "ghost_col", "")
   280  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusFailed)
   281  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   282  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, true)
   283  		// migration will fail again
   284  	})
   285  	t.Run("cancel all migrations: nothing to cancel", func(t *testing.T) {
   286  		// no migrations pending at this time
   287  		time.Sleep(10 * time.Second)
   288  		onlineddl.CheckCancelAllMigrations(t, &vtParams, 0)
   289  	})
   290  	t.Run("cancel all migrations: some migrations to cancel", func(t *testing.T) {
   291  		// spawn n migrations; cancel them via cancel-all
   292  		var wg sync.WaitGroup
   293  		count := 4
   294  		for i := 0; i < count; i++ {
   295  			wg.Add(1)
   296  			go func() {
   297  				defer wg.Done()
   298  				_ = testOnlineDDLStatement(t, alterTableThrottlingStatement, "gh-ost --max-load=Threads_running=1", "vtgate", "ghost_col", "")
   299  			}()
   300  		}
   301  		wg.Wait()
   302  		onlineddl.CheckCancelAllMigrations(t, &vtParams, len(shards)*count)
   303  	})
   304  	t.Run("Online DROP, vtctl", func(t *testing.T) {
   305  		uuid := testOnlineDDLStatement(t, onlineDDLDropTableStatement, "gh-ost", "vtctl", "", "")
   306  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   307  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   308  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   309  	})
   310  	t.Run("Online CREATE, vtctl", func(t *testing.T) {
   311  		uuid := testOnlineDDLStatement(t, onlineDDLCreateTableStatement, "gh-ost", "vtctl", "online_ddl_create_col", "")
   312  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   313  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   314  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   315  	})
   316  	t.Run("Online DROP TABLE IF EXISTS, vtgate", func(t *testing.T) {
   317  		uuid := testOnlineDDLStatement(t, onlineDDLDropTableIfExistsStatement, "gh-ost", "vtgate", "", "")
   318  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   319  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   320  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   321  		// this table existed
   322  		checkTables(t, schema.OnlineDDLToGCUUID(uuid), 1)
   323  	})
   324  	t.Run("Online DROP TABLE IF EXISTS for nonexistent table, vtgate", func(t *testing.T) {
   325  		uuid := testOnlineDDLStatement(t, onlineDDLDropTableIfExistsStatement, "gh-ost", "vtgate", "", "")
   326  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   327  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   328  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   329  		// this table did not exist
   330  		checkTables(t, schema.OnlineDDLToGCUUID(uuid), 0)
   331  	})
   332  	t.Run("Online DROP TABLE for nonexistent table, expect error, vtgate", func(t *testing.T) {
   333  		uuid := testOnlineDDLStatement(t, onlineDDLDropTableStatement, "gh-ost", "vtgate", "", "")
   334  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusFailed)
   335  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   336  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, true)
   337  	})
   338  	t.Run("Online CREATE no PK table, vtgate", func(t *testing.T) {
   339  		uuid := testOnlineDDLStatement(t, noPKCreateTableStatement, "gh-ost", "vtgate", "online_ddl_create_col", "")
   340  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete)
   341  		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
   342  		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
   343  	})
   344  	t.Run("Fail ALTER for no PK table, vtgate", func(t *testing.T) {
   345  		uuid := testOnlineDDLStatement(t, alterTableTrivialStatement, "gh-ost", "vtgate", "", "")
   346  		onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusFailed)
   347  
   348  		expectedMessage := "No PRIMARY nor UNIQUE key found"
   349  		rs := onlineddl.ReadMigrations(t, &vtParams, uuid)
   350  		require.NotNil(t, rs)
   351  		for _, row := range rs.Named().Rows {
   352  			message := row["message"].ToString()
   353  			// the following message is generated by gh-ost. We test that it is captured in our 'message' column:
   354  			require.Contains(t, message, expectedMessage)
   355  		}
   356  
   357  		// See that we're able to read logs after failed migration:
   358  		logs := onlineddl.ReadMigrationLogs(t, &vtParams, uuid)
   359  		assert.Equal(t, len(shards), len(logs))
   360  		for i := range logs {
   361  			require.Contains(t, logs[i], expectedMessage)
   362  		}
   363  	})
   364  }
   365  
   366  func testWithInitialSchema(t *testing.T) {
   367  	// Create 4 tables and populate them
   368  	var sqlQuery = "" //nolint
   369  	for i := 0; i < totalTableCount; i++ {
   370  		tableName := fmt.Sprintf("vt_onlineddl_test_%02d", i)
   371  		sqlQuery = fmt.Sprintf(createTable, tableName)
   372  		err := clusterInstance.VtctlclientProcess.ApplySchema(keyspaceName, sqlQuery)
   373  		require.Nil(t, err)
   374  
   375  		for _, insert := range insertStatements {
   376  			insertQuery := fmt.Sprintf(insert, tableName)
   377  			r := onlineddl.VtgateExecQuery(t, &vtParams, insertQuery, "")
   378  			require.NotNil(t, r)
   379  		}
   380  	}
   381  
   382  	// Check if 4 tables are created
   383  	checkTables(t, "", totalTableCount)
   384  }
   385  
   386  // testOnlineDDLStatement runs an online DDL, ALTER statement
   387  func testOnlineDDLStatement(t *testing.T, alterStatement string, ddlStrategy string, executeStrategy string, expectHint string, callerID string) (uuid string) {
   388  	tableName := fmt.Sprintf("vt_onlineddl_test_%02d", 3)
   389  	sqlQuery := fmt.Sprintf(alterStatement, tableName)
   390  	if executeStrategy == "vtgate" {
   391  		row := onlineddl.VtgateExecDDL(t, &vtParams, ddlStrategy, sqlQuery, "").Named().Row()
   392  		if row != nil {
   393  			uuid = row.AsString("uuid", "")
   394  		}
   395  	} else {
   396  		var err error
   397  		uuid, err = clusterInstance.VtctlclientProcess.ApplySchemaWithOutput(keyspaceName, sqlQuery, cluster.VtctlClientParams{DDLStrategy: ddlStrategy, CallerID: callerID})
   398  		assert.NoError(t, err)
   399  	}
   400  	uuid = strings.TrimSpace(uuid)
   401  	fmt.Println("# Generated UUID (for debug purposes):")
   402  	fmt.Printf("<%s>\n", uuid)
   403  
   404  	strategySetting, err := schema.ParseDDLStrategy(ddlStrategy)
   405  	assert.NoError(t, err)
   406  
   407  	if !strategySetting.Strategy.IsDirect() {
   408  		status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
   409  		fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
   410  	}
   411  
   412  	if expectHint != "" {
   413  		checkMigratedTable(t, tableName, expectHint)
   414  	}
   415  	return uuid
   416  }
   417  
   418  // checkTables checks the number of tables in the first two shards.
   419  func checkTables(t *testing.T, showTableName string, expectCount int) {
   420  	for i := range clusterInstance.Keyspaces[0].Shards {
   421  		checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[i].Vttablets[0], showTableName, expectCount)
   422  	}
   423  }
   424  
   425  // checkTablesCount checks the number of tables in the given tablet
   426  func checkTablesCount(t *testing.T, tablet *cluster.Vttablet, showTableName string, expectCount int) {
   427  	query := fmt.Sprintf(`show tables like '%%%s%%';`, showTableName)
   428  	queryResult, err := tablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   429  	require.Nil(t, err)
   430  	assert.Equal(t, expectCount, len(queryResult.Rows))
   431  }
   432  
   433  // checkMigratedTables checks the CREATE STATEMENT of a table after migration
   434  func checkMigratedTable(t *testing.T, tableName, expectColumn string) {
   435  	for i := range clusterInstance.Keyspaces[0].Shards {
   436  		createStatement := getCreateTableStatement(t, clusterInstance.Keyspaces[0].Shards[i].Vttablets[0], tableName)
   437  		assert.Contains(t, createStatement, expectColumn)
   438  	}
   439  }
   440  
   441  // getCreateTableStatement returns the CREATE TABLE statement for a given table
   442  func getCreateTableStatement(t *testing.T, tablet *cluster.Vttablet, tableName string) (statement string) {
   443  	queryResult, err := tablet.VttabletProcess.QueryTablet(fmt.Sprintf("show create table %s;", tableName), keyspaceName, true)
   444  	require.Nil(t, err)
   445  
   446  	assert.Equal(t, len(queryResult.Rows), 1)
   447  	assert.Equal(t, len(queryResult.Rows[0]), 2) // table name, create statement
   448  	statement = queryResult.Rows[0][1].ToString()
   449  	return statement
   450  }