vitess.io/vitess@v0.16.2/go/test/endtoend/tabletmanager/tablegc/tablegc_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  package tablegc
    17  
    18  import (
    19  	"context"
    20  	"flag"
    21  	"os"
    22  	"testing"
    23  	"time"
    24  
    25  	"vitess.io/vitess/go/mysql"
    26  	"vitess.io/vitess/go/vt/schema"
    27  	"vitess.io/vitess/go/vt/sqlparser"
    28  
    29  	"vitess.io/vitess/go/test/endtoend/cluster"
    30  	"vitess.io/vitess/go/test/endtoend/onlineddl"
    31  
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  var (
    37  	clusterInstance *cluster.LocalProcessCluster
    38  	primaryTablet   cluster.Vttablet
    39  	hostname        = "localhost"
    40  	keyspaceName    = "ks"
    41  	cell            = "zone1"
    42  	fastDropTable   bool
    43  	sqlCreateTable  = `
    44  		create table if not exists t1(
    45  			id bigint not null auto_increment,
    46  			value varchar(32),
    47  			primary key(id)
    48  		) Engine=InnoDB;
    49  	`
    50  	sqlCreateView = `
    51  		create or replace view v1 as select * from t1;
    52  	`
    53  	sqlSchema = sqlCreateTable + sqlCreateView
    54  
    55  	vSchema = `
    56  	{
    57      "sharded": true,
    58      "vindexes": {
    59        "hash": {
    60          "type": "hash"
    61        }
    62      },
    63      "tables": {
    64        "t1": {
    65          "column_vindexes": [
    66            {
    67              "column": "id",
    68              "name": "hash"
    69            }
    70          ]
    71        }
    72      }
    73  	}`
    74  
    75  	tableTransitionExpiration = 10 * time.Second
    76  	gcCheckInterval           = 2 * time.Second
    77  	gcPurgeCheckInterval      = 2 * time.Second
    78  	waitForTransitionTimeout  = 30 * time.Second
    79  )
    80  
    81  func TestMain(m *testing.M) {
    82  	defer cluster.PanicHandler(nil)
    83  	flag.Parse()
    84  
    85  	exitCode := func() int {
    86  		clusterInstance = cluster.NewCluster(cell, hostname)
    87  		defer clusterInstance.Teardown()
    88  
    89  		// Start topo server
    90  		err := clusterInstance.StartTopo()
    91  		if err != nil {
    92  			return 1
    93  		}
    94  
    95  		// Set extra tablet args for lock timeout
    96  		clusterInstance.VtTabletExtraArgs = []string{
    97  			"--lock_tables_timeout", "5s",
    98  			"--watch_replication_stream",
    99  			"--enable_replication_reporter",
   100  			"--heartbeat_enable",
   101  			"--heartbeat_interval", "250ms",
   102  			"--gc_check_interval", gcCheckInterval.String(),
   103  			"--gc_purge_check_interval", gcPurgeCheckInterval.String(),
   104  			"--table_gc_lifecycle", "hold,purge,evac,drop",
   105  		}
   106  
   107  		// Start keyspace
   108  		keyspace := &cluster.Keyspace{
   109  			Name:      keyspaceName,
   110  			SchemaSQL: sqlSchema,
   111  			VSchema:   vSchema,
   112  		}
   113  
   114  		if err = clusterInstance.StartUnshardedKeyspace(*keyspace, 1, false); err != nil {
   115  			return 1
   116  		}
   117  
   118  		// Collect table paths and ports
   119  		tablets := clusterInstance.Keyspaces[0].Shards[0].Vttablets
   120  		for _, tablet := range tablets {
   121  			if tablet.Type == "primary" {
   122  				primaryTablet = *tablet
   123  			}
   124  		}
   125  
   126  		return m.Run()
   127  	}()
   128  	os.Exit(exitCode)
   129  }
   130  
   131  func checkTableRows(t *testing.T, tableName string, expect int64) {
   132  	require.NotEmpty(t, tableName)
   133  	query := `select count(*) as c from %a`
   134  	parsed := sqlparser.BuildParsedQuery(query, tableName)
   135  	rs, err := primaryTablet.VttabletProcess.QueryTablet(parsed.Query, keyspaceName, true)
   136  	require.NoError(t, err)
   137  	count := rs.Named().Row().AsInt64("c", 0)
   138  	assert.Equal(t, expect, count)
   139  }
   140  
   141  func populateTable(t *testing.T) {
   142  	_, err := primaryTablet.VttabletProcess.QueryTablet(sqlSchema, keyspaceName, true)
   143  	require.NoError(t, err)
   144  	_, err = primaryTablet.VttabletProcess.QueryTablet("delete from t1", keyspaceName, true)
   145  	require.NoError(t, err)
   146  	_, err = primaryTablet.VttabletProcess.QueryTablet("insert into t1 (id, value) values (null, md5(rand()))", keyspaceName, true)
   147  	require.NoError(t, err)
   148  	for i := 0; i < 10; i++ {
   149  		_, err = primaryTablet.VttabletProcess.QueryTablet("insert into t1 (id, value) select null, md5(rand()) from t1", keyspaceName, true)
   150  		require.NoError(t, err)
   151  	}
   152  	checkTableRows(t, "t1", 1024)
   153  	{
   154  		exists, _, err := tableExists("t1")
   155  		require.NoError(t, err)
   156  		require.True(t, exists)
   157  	}
   158  }
   159  
   160  // tableExists sees that a given table exists in MySQL
   161  func tableExists(tableExpr string) (exists bool, tableName string, err error) {
   162  	query := `select table_name as table_name from information_schema.tables where table_schema=database() and table_name like '%a'`
   163  	parsed := sqlparser.BuildParsedQuery(query, tableExpr)
   164  	rs, err := primaryTablet.VttabletProcess.QueryTablet(parsed.Query, keyspaceName, true)
   165  	if err != nil {
   166  		return false, "", err
   167  	}
   168  	row := rs.Named().Row()
   169  	if row == nil {
   170  		return false, "", nil
   171  	}
   172  	return true, row.AsString("table_name", ""), nil
   173  }
   174  
   175  func validateTableDoesNotExist(t *testing.T, tableExpr string) {
   176  	ctx, cancel := context.WithTimeout(context.Background(), waitForTransitionTimeout)
   177  	defer cancel()
   178  
   179  	ticker := time.NewTicker(time.Second)
   180  	var foundTableName string
   181  	var exists bool
   182  	var err error
   183  	for {
   184  		select {
   185  		case <-ticker.C:
   186  			exists, foundTableName, err = tableExists(tableExpr)
   187  			require.NoError(t, err)
   188  			if !exists {
   189  				return
   190  			}
   191  		case <-ctx.Done():
   192  			assert.NoError(t, ctx.Err(), "validateTableDoesNotExist timed out, table %v still exists (%v)", tableExpr, foundTableName)
   193  			return
   194  		}
   195  	}
   196  }
   197  
   198  func validateTableExists(t *testing.T, tableExpr string) {
   199  	ctx, cancel := context.WithTimeout(context.Background(), waitForTransitionTimeout)
   200  	defer cancel()
   201  
   202  	ticker := time.NewTicker(time.Second)
   203  	var exists bool
   204  	var err error
   205  	for {
   206  		select {
   207  		case <-ticker.C:
   208  			exists, _, err = tableExists(tableExpr)
   209  			require.NoError(t, err)
   210  			if exists {
   211  				return
   212  			}
   213  		case <-ctx.Done():
   214  			assert.NoError(t, ctx.Err(), "validateTableExists timed out, table %v still does not exist", tableExpr)
   215  			return
   216  		}
   217  	}
   218  }
   219  
   220  func validateAnyState(t *testing.T, expectNumRows int64, states ...schema.TableGCState) {
   221  	for _, state := range states {
   222  		expectTableToExist := true
   223  		searchExpr := ""
   224  		switch state {
   225  		case schema.HoldTableGCState:
   226  			searchExpr = `\_vt\_HOLD\_%`
   227  		case schema.PurgeTableGCState:
   228  			searchExpr = `\_vt\_PURGE\_%`
   229  		case schema.EvacTableGCState:
   230  			searchExpr = `\_vt\_EVAC\_%`
   231  		case schema.DropTableGCState:
   232  			searchExpr = `\_vt\_DROP\_%`
   233  		case schema.TableDroppedGCState:
   234  			searchExpr = `\_vt\_%`
   235  			expectTableToExist = false
   236  		default:
   237  			t.Log("Unknown state")
   238  			t.Fail()
   239  		}
   240  		exists, tableName, err := tableExists(searchExpr)
   241  		require.NoError(t, err)
   242  
   243  		if exists {
   244  			if expectNumRows >= 0 {
   245  				checkTableRows(t, tableName, expectNumRows)
   246  			}
   247  			// Now that the table is validated, we can drop it
   248  			dropTable(t, tableName)
   249  		}
   250  		if exists == expectTableToExist {
   251  			// condition met
   252  			return
   253  		}
   254  	}
   255  	assert.Fail(t, "could not match any of the states: %v", states)
   256  }
   257  
   258  // dropTable drops a table
   259  func dropTable(t *testing.T, tableName string) {
   260  	query := `drop table if exists %a`
   261  	parsed := sqlparser.BuildParsedQuery(query, tableName)
   262  	_, err := primaryTablet.VttabletProcess.QueryTablet(parsed.Query, keyspaceName, true)
   263  	require.NoError(t, err)
   264  }
   265  
   266  func TestCapability(t *testing.T) {
   267  	mysqlVersion := onlineddl.GetMySQLVersion(t, clusterInstance.Keyspaces[0].Shards[0].PrimaryTablet())
   268  	require.NotEmpty(t, mysqlVersion)
   269  
   270  	_, capableOf, _ := mysql.GetFlavor(mysqlVersion, nil)
   271  	require.NotNil(t, capableOf)
   272  	var err error
   273  	fastDropTable, err = capableOf(mysql.FastDropTableFlavorCapability)
   274  	require.NoError(t, err)
   275  }
   276  
   277  func TestPopulateTable(t *testing.T) {
   278  	populateTable(t)
   279  	validateTableExists(t, "t1")
   280  	validateTableDoesNotExist(t, "no_such_table")
   281  }
   282  
   283  func TestHold(t *testing.T) {
   284  	populateTable(t)
   285  	query, tableName, err := schema.GenerateRenameStatement("t1", schema.HoldTableGCState, time.Now().UTC().Add(tableTransitionExpiration))
   286  	assert.NoError(t, err)
   287  
   288  	_, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   289  	assert.NoError(t, err)
   290  
   291  	validateTableDoesNotExist(t, "t1")
   292  	validateTableExists(t, tableName)
   293  
   294  	time.Sleep(tableTransitionExpiration / 2)
   295  	{
   296  		// Table was created with +10s timestamp, so it should still exist
   297  		validateTableExists(t, tableName)
   298  
   299  		checkTableRows(t, tableName, 1024)
   300  	}
   301  
   302  	time.Sleep(tableTransitionExpiration)
   303  	// We're now both beyond table's timestamp as well as a tableGC interval
   304  	validateTableDoesNotExist(t, tableName)
   305  	if fastDropTable {
   306  		validateAnyState(t, -1, schema.DropTableGCState, schema.TableDroppedGCState)
   307  	} else {
   308  		validateAnyState(t, -1, schema.PurgeTableGCState, schema.EvacTableGCState, schema.DropTableGCState, schema.TableDroppedGCState)
   309  	}
   310  }
   311  
   312  func TestEvac(t *testing.T) {
   313  	populateTable(t)
   314  	query, tableName, err := schema.GenerateRenameStatement("t1", schema.EvacTableGCState, time.Now().UTC().Add(tableTransitionExpiration))
   315  	assert.NoError(t, err)
   316  
   317  	_, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   318  	assert.NoError(t, err)
   319  
   320  	validateTableDoesNotExist(t, "t1")
   321  
   322  	time.Sleep(tableTransitionExpiration / 2)
   323  	{
   324  		// Table was created with +10s timestamp, so it should still exist
   325  		if fastDropTable {
   326  			// EVAC state is skipped in mysql 8.0.23 and beyond
   327  			validateTableDoesNotExist(t, tableName)
   328  		} else {
   329  			validateTableExists(t, tableName)
   330  			checkTableRows(t, tableName, 1024)
   331  		}
   332  	}
   333  
   334  	time.Sleep(tableTransitionExpiration)
   335  	// We're now both beyond table's timestamp as well as a tableGC interval
   336  	validateTableDoesNotExist(t, tableName)
   337  	// Table should be renamed as _vt_DROP_... and then dropped!
   338  	validateAnyState(t, 0, schema.DropTableGCState, schema.TableDroppedGCState)
   339  }
   340  
   341  func TestDrop(t *testing.T) {
   342  	populateTable(t)
   343  	query, tableName, err := schema.GenerateRenameStatement("t1", schema.DropTableGCState, time.Now().UTC().Add(tableTransitionExpiration))
   344  	assert.NoError(t, err)
   345  
   346  	_, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   347  	assert.NoError(t, err)
   348  
   349  	validateTableDoesNotExist(t, "t1")
   350  
   351  	time.Sleep(tableTransitionExpiration)
   352  	time.Sleep(2 * gcCheckInterval)
   353  	// We're now both beyond table's timestamp as well as a tableGC interval
   354  	validateTableDoesNotExist(t, tableName)
   355  }
   356  
   357  func TestPurge(t *testing.T) {
   358  	populateTable(t)
   359  	query, tableName, err := schema.GenerateRenameStatement("t1", schema.PurgeTableGCState, time.Now().UTC().Add(tableTransitionExpiration))
   360  	require.NoError(t, err)
   361  
   362  	_, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   363  	require.NoError(t, err)
   364  
   365  	validateTableDoesNotExist(t, "t1")
   366  	if !fastDropTable {
   367  		validateTableExists(t, tableName)
   368  		checkTableRows(t, tableName, 1024)
   369  	}
   370  	time.Sleep(5 * gcPurgeCheckInterval) // wwait for table to be purged
   371  	time.Sleep(2 * gcCheckInterval)      // wait for GC state transition
   372  	if fastDropTable {
   373  		validateAnyState(t, 0, schema.DropTableGCState, schema.TableDroppedGCState)
   374  	} else {
   375  		validateAnyState(t, 0, schema.EvacTableGCState, schema.DropTableGCState, schema.TableDroppedGCState)
   376  	}
   377  }
   378  
   379  func TestPurgeView(t *testing.T) {
   380  	populateTable(t)
   381  	query, tableName, err := schema.GenerateRenameStatement("v1", schema.PurgeTableGCState, time.Now().UTC().Add(tableTransitionExpiration))
   382  	require.NoError(t, err)
   383  
   384  	_, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   385  	require.NoError(t, err)
   386  
   387  	// table untouched
   388  	validateTableExists(t, "t1")
   389  	if !fastDropTable {
   390  		validateTableExists(t, tableName)
   391  	}
   392  	validateTableDoesNotExist(t, "v1")
   393  
   394  	time.Sleep(tableTransitionExpiration / 2)
   395  	{
   396  		// View was created with +10s timestamp, so it should still exist
   397  		if fastDropTable {
   398  			// PURGE is skipped in mysql 8.0.23
   399  			validateTableDoesNotExist(t, tableName)
   400  		} else {
   401  			validateTableExists(t, tableName)
   402  			// We're really reading the view here:
   403  			checkTableRows(t, tableName, 1024)
   404  		}
   405  	}
   406  
   407  	time.Sleep(2 * gcPurgeCheckInterval) // wwait for table to be purged
   408  	time.Sleep(2 * gcCheckInterval)      // wait for GC state transition
   409  
   410  	// We're now both beyond view's timestamp as well as a tableGC interval
   411  	validateTableDoesNotExist(t, tableName)
   412  	// table still untouched
   413  	validateTableExists(t, "t1")
   414  	validateAnyState(t, 1024, schema.EvacTableGCState, schema.DropTableGCState, schema.TableDroppedGCState)
   415  }