vitess.io/vitess@v0.16.2/go/test/endtoend/onlineddl/vtgate_util.go (about)

     1  /*
     2  Copyright 2021 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 onlineddl
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"math"
    24  	"net/http"
    25  	"os"
    26  	"testing"
    27  	"time"
    28  
    29  	"vitess.io/vitess/go/mysql"
    30  	"vitess.io/vitess/go/sqltypes"
    31  	"vitess.io/vitess/go/vt/log"
    32  	"vitess.io/vitess/go/vt/schema"
    33  	"vitess.io/vitess/go/vt/sqlparser"
    34  
    35  	"vitess.io/vitess/go/test/endtoend/cluster"
    36  
    37  	"github.com/buger/jsonparser"
    38  	"github.com/stretchr/testify/assert"
    39  	"github.com/stretchr/testify/require"
    40  )
    41  
    42  // VtgateExecQuery runs a query on VTGate using given query params
    43  func VtgateExecQuery(t *testing.T, vtParams *mysql.ConnParams, query string, expectError string) *sqltypes.Result {
    44  	t.Helper()
    45  
    46  	ctx := context.Background()
    47  	conn, err := mysql.Connect(ctx, vtParams)
    48  	require.Nil(t, err)
    49  	defer conn.Close()
    50  
    51  	qr, err := conn.ExecuteFetch(query, math.MaxInt64, true)
    52  	if expectError == "" {
    53  		require.NoError(t, err)
    54  	} else {
    55  		require.Error(t, err, "error should not be nil")
    56  		assert.Contains(t, err.Error(), expectError, "Unexpected error")
    57  	}
    58  	return qr
    59  }
    60  
    61  // VtgateExecDDL executes a DDL query with given strategy
    62  func VtgateExecDDL(t *testing.T, vtParams *mysql.ConnParams, ddlStrategy string, query string, expectError string) *sqltypes.Result {
    63  	t.Helper()
    64  
    65  	ctx := context.Background()
    66  	conn, err := mysql.Connect(ctx, vtParams)
    67  	require.Nil(t, err)
    68  	defer conn.Close()
    69  
    70  	setSession := fmt.Sprintf("set @@ddl_strategy='%s'", ddlStrategy)
    71  	_, err = conn.ExecuteFetch(setSession, 1000, true)
    72  	assert.NoError(t, err)
    73  
    74  	qr, err := conn.ExecuteFetch(query, 1000, true)
    75  	if expectError == "" {
    76  		require.NoError(t, err)
    77  	} else {
    78  		require.Error(t, err, "error should not be nil")
    79  		assert.Contains(t, err.Error(), expectError, "Unexpected error")
    80  	}
    81  	return qr
    82  }
    83  
    84  // CheckRetryMigration attempts to retry a migration, and expects success/failure by counting affected rows
    85  func CheckRetryMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectRetryPossible bool) {
    86  	query, err := sqlparser.ParseAndBind("alter vitess_migration %a retry",
    87  		sqltypes.StringBindVariable(uuid),
    88  	)
    89  	require.NoError(t, err)
    90  	r := VtgateExecQuery(t, vtParams, query, "")
    91  
    92  	if expectRetryPossible {
    93  		assert.Equal(t, len(shards), int(r.RowsAffected))
    94  	} else {
    95  		assert.Equal(t, int(0), int(r.RowsAffected))
    96  	}
    97  }
    98  
    99  // CheckRetryPartialMigration attempts to retry a migration where a subset of shards failed
   100  func CheckRetryPartialMigration(t *testing.T, vtParams *mysql.ConnParams, uuid string, expectAtLeastRowsAffected uint64) {
   101  	query, err := sqlparser.ParseAndBind("alter vitess_migration %a retry",
   102  		sqltypes.StringBindVariable(uuid),
   103  	)
   104  	require.NoError(t, err)
   105  	r := VtgateExecQuery(t, vtParams, query, "")
   106  
   107  	assert.GreaterOrEqual(t, expectAtLeastRowsAffected, r.RowsAffected)
   108  }
   109  
   110  // CheckCancelMigration attempts to cancel a migration, and expects success/failure by counting affected rows
   111  func CheckCancelMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectCancelPossible bool) {
   112  	query, err := sqlparser.ParseAndBind("alter vitess_migration %a cancel",
   113  		sqltypes.StringBindVariable(uuid),
   114  	)
   115  	require.NoError(t, err)
   116  	r := VtgateExecQuery(t, vtParams, query, "")
   117  
   118  	if expectCancelPossible {
   119  		assert.Equal(t, len(shards), int(r.RowsAffected))
   120  	} else {
   121  		assert.Equal(t, int(0), int(r.RowsAffected))
   122  	}
   123  }
   124  
   125  // CheckCleanupMigration attempts to cleanup a migration, and expects success by counting affected rows
   126  func CheckCleanupMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string) {
   127  	query, err := sqlparser.ParseAndBind("alter vitess_migration %a cleanup",
   128  		sqltypes.StringBindVariable(uuid),
   129  	)
   130  	require.NoError(t, err)
   131  	r := VtgateExecQuery(t, vtParams, query, "")
   132  
   133  	assert.Equal(t, len(shards), int(r.RowsAffected))
   134  }
   135  
   136  // CheckCompleteMigration attempts to complete a migration, and expects success by counting affected rows
   137  func CheckCompleteMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectCompletePossible bool) {
   138  	query, err := sqlparser.ParseAndBind("alter vitess_migration %a complete",
   139  		sqltypes.StringBindVariable(uuid),
   140  	)
   141  	require.NoError(t, err)
   142  	r := VtgateExecQuery(t, vtParams, query, "")
   143  
   144  	if expectCompletePossible {
   145  		assert.Equal(t, len(shards), int(r.RowsAffected))
   146  	} else {
   147  		assert.Equal(t, int(0), int(r.RowsAffected))
   148  	}
   149  }
   150  
   151  // CheckLaunchMigration attempts to launch a migration, and expects success by counting affected rows
   152  func CheckLaunchMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, launchShards string, expectLaunchPossible bool) {
   153  	query, err := sqlparser.ParseAndBind("alter vitess_migration %a launch vitess_shards %a",
   154  		sqltypes.StringBindVariable(uuid),
   155  		sqltypes.StringBindVariable(launchShards),
   156  	)
   157  	require.NoError(t, err)
   158  	r := VtgateExecQuery(t, vtParams, query, "")
   159  
   160  	if expectLaunchPossible {
   161  		assert.Equal(t, len(shards), int(r.RowsAffected))
   162  	} else {
   163  		assert.Equal(t, int(0), int(r.RowsAffected))
   164  	}
   165  }
   166  
   167  // CheckCompleteAllMigrations completes all pending migrations and expect number of affected rows
   168  // A negative value for expectCount indicates "don't care, no need to check"
   169  func CheckCompleteAllMigrations(t *testing.T, vtParams *mysql.ConnParams, expectCount int) {
   170  	completeQuery := "alter vitess_migration complete all"
   171  	r := VtgateExecQuery(t, vtParams, completeQuery, "")
   172  
   173  	if expectCount >= 0 {
   174  		assert.Equal(t, expectCount, int(r.RowsAffected))
   175  	}
   176  }
   177  
   178  // CheckCancelAllMigrations cancels all pending migrations and expect number of affected rows
   179  // A negative value for expectCount indicates "don't care, no need to check"
   180  func CheckCancelAllMigrations(t *testing.T, vtParams *mysql.ConnParams, expectCount int) {
   181  	cancelQuery := "alter vitess_migration cancel all"
   182  	r := VtgateExecQuery(t, vtParams, cancelQuery, "")
   183  
   184  	if expectCount >= 0 {
   185  		assert.Equal(t, expectCount, int(r.RowsAffected))
   186  	}
   187  }
   188  
   189  // CheckLaunchAllMigrations launches all queued posponed migrations and expect number of affected rows
   190  // A negative value for expectCount indicates "don't care, no need to check"
   191  func CheckLaunchAllMigrations(t *testing.T, vtParams *mysql.ConnParams, expectCount int) {
   192  	completeQuery := "alter vitess_migration launch all"
   193  	r := VtgateExecQuery(t, vtParams, completeQuery, "")
   194  
   195  	if expectCount >= 0 {
   196  		assert.Equal(t, expectCount, int(r.RowsAffected))
   197  	}
   198  }
   199  
   200  // CheckMigrationStatus verifies that the migration indicated by given UUID has the given expected status
   201  func CheckMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectStatuses ...schema.OnlineDDLStatus) {
   202  	query, err := sqlparser.ParseAndBind("show vitess_migrations like %a",
   203  		sqltypes.StringBindVariable(uuid),
   204  	)
   205  	require.NoError(t, err)
   206  
   207  	r := VtgateExecQuery(t, vtParams, query, "")
   208  	fmt.Printf("# output for `%s`:\n", query)
   209  	PrintQueryResult(os.Stdout, r)
   210  
   211  	count := 0
   212  	for _, row := range r.Named().Rows {
   213  		if row["migration_uuid"].ToString() != uuid {
   214  			continue
   215  		}
   216  		for _, expectStatus := range expectStatuses {
   217  			if row["migration_status"].ToString() == string(expectStatus) {
   218  				count++
   219  				break
   220  			}
   221  		}
   222  	}
   223  	assert.Equal(t, len(shards), count)
   224  }
   225  
   226  // WaitForMigrationStatus waits for a migration to reach either provided statuses (returns immediately), or eventually time out
   227  func WaitForMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, timeout time.Duration, expectStatuses ...schema.OnlineDDLStatus) schema.OnlineDDLStatus {
   228  	shardNames := map[string]bool{}
   229  	for _, shard := range shards {
   230  		shardNames[shard.Name] = true
   231  	}
   232  	query, err := sqlparser.ParseAndBind("show vitess_migrations like %a",
   233  		sqltypes.StringBindVariable(uuid),
   234  	)
   235  	require.NoError(t, err)
   236  
   237  	statusesMap := map[string]bool{}
   238  	for _, status := range expectStatuses {
   239  		statusesMap[string(status)] = true
   240  	}
   241  	startTime := time.Now()
   242  	lastKnownStatus := ""
   243  	for time.Since(startTime) < timeout {
   244  		countMatchedShards := 0
   245  		r := VtgateExecQuery(t, vtParams, query, "")
   246  		for _, row := range r.Named().Rows {
   247  			shardName := row["shard"].ToString()
   248  			if !shardNames[shardName] {
   249  				// irrelevant shard
   250  				continue
   251  			}
   252  			lastKnownStatus = row["migration_status"].ToString()
   253  			if row["migration_uuid"].ToString() == uuid && statusesMap[lastKnownStatus] {
   254  				countMatchedShards++
   255  			}
   256  		}
   257  		if countMatchedShards == len(shards) {
   258  			return schema.OnlineDDLStatus(lastKnownStatus)
   259  		}
   260  		time.Sleep(1 * time.Second)
   261  	}
   262  	return schema.OnlineDDLStatus(lastKnownStatus)
   263  }
   264  
   265  // CheckMigrationArtifacts verifies given migration exists, and checks if it has artifacts
   266  func CheckMigrationArtifacts(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectArtifacts bool) {
   267  	r := ReadMigrations(t, vtParams, uuid)
   268  
   269  	assert.Equal(t, len(shards), len(r.Named().Rows))
   270  	for _, row := range r.Named().Rows {
   271  		hasArtifacts := (row["artifacts"].ToString() != "")
   272  		assert.Equal(t, expectArtifacts, hasArtifacts)
   273  	}
   274  }
   275  
   276  // ReadMigrations reads migration entries
   277  func ReadMigrations(t *testing.T, vtParams *mysql.ConnParams, like string) *sqltypes.Result {
   278  	query, err := sqlparser.ParseAndBind("show vitess_migrations like %a",
   279  		sqltypes.StringBindVariable(like),
   280  	)
   281  	require.NoError(t, err)
   282  
   283  	return VtgateExecQuery(t, vtParams, query, "")
   284  }
   285  
   286  // ReadMigrationLogs reads migration logs for a given migration, on all shards
   287  func ReadMigrationLogs(t *testing.T, vtParams *mysql.ConnParams, uuid string) (logs []string) {
   288  	query, err := sqlparser.ParseAndBind("show vitess_migration %a logs",
   289  		sqltypes.StringBindVariable(uuid),
   290  	)
   291  	require.NoError(t, err)
   292  
   293  	r := VtgateExecQuery(t, vtParams, query, "")
   294  	for _, row := range r.Named().Rows {
   295  		migrationLog := row["migration_log"].ToString()
   296  		logs = append(logs, migrationLog)
   297  	}
   298  	return logs
   299  }
   300  
   301  // ThrottleAllMigrations fully throttles online-ddl apps
   302  func ThrottleAllMigrations(t *testing.T, vtParams *mysql.ConnParams) {
   303  	query := "alter vitess_migration throttle all expire '24h' ratio 1"
   304  	_ = VtgateExecQuery(t, vtParams, query, "")
   305  }
   306  
   307  // UnthrottleAllMigrations cancels migration throttling
   308  func UnthrottleAllMigrations(t *testing.T, vtParams *mysql.ConnParams) {
   309  	query := "alter vitess_migration unthrottle all"
   310  	_ = VtgateExecQuery(t, vtParams, query, "")
   311  }
   312  
   313  // CheckThrottledApps checks for existence or non-existence of an app in the throttled apps list
   314  func CheckThrottledApps(t *testing.T, vtParams *mysql.ConnParams, appName string, expectFind bool) {
   315  	query := "show vitess_throttled_apps"
   316  	r := VtgateExecQuery(t, vtParams, query, "")
   317  
   318  	found := false
   319  	for _, row := range r.Named().Rows {
   320  		if row.AsString("app", "") == appName {
   321  			found = true
   322  		}
   323  	}
   324  	assert.Equal(t, expectFind, found, "check app %v in throttled apps: %v", appName, found)
   325  }
   326  
   327  // WaitForThrottledTimestamp waits for a migration to have a non-empty last_throttled_timestamp
   328  func WaitForThrottledTimestamp(t *testing.T, vtParams *mysql.ConnParams, uuid string, timeout time.Duration) (
   329  	row sqltypes.RowNamedValues,
   330  	startedTimestamp string,
   331  	lastThrottledTimestamp string,
   332  ) {
   333  	startTime := time.Now()
   334  	for time.Since(startTime) < timeout {
   335  		rs := ReadMigrations(t, vtParams, uuid)
   336  		require.NotNil(t, rs)
   337  		for _, row = range rs.Named().Rows {
   338  			startedTimestamp = row.AsString("started_timestamp", "")
   339  			require.NotEmpty(t, startedTimestamp)
   340  			lastThrottledTimestamp = row.AsString("last_throttled_timestamp", "")
   341  			if lastThrottledTimestamp != "" {
   342  				// good. This is what we've been waiting for.
   343  				return row, startedTimestamp, lastThrottledTimestamp
   344  			}
   345  		}
   346  		time.Sleep(1 * time.Second)
   347  	}
   348  	t.Error("timeout waiting for last_throttled_timestamp to have nonempty value")
   349  	return
   350  }
   351  
   352  // WaitForThrottlerStatusEnabled waits for a tablet to report its throttler status as enabled.
   353  func WaitForThrottlerStatusEnabled(t *testing.T, tablet *cluster.Vttablet, timeout time.Duration) {
   354  	jsonPath := "IsEnabled"
   355  	url := fmt.Sprintf("http://localhost:%d/throttler/status", tablet.HTTPPort)
   356  
   357  	ctx, cancel := context.WithTimeout(context.Background(), throttlerConfigTimeout)
   358  	defer cancel()
   359  
   360  	ticker := time.NewTicker(time.Second)
   361  	defer ticker.Stop()
   362  
   363  	for {
   364  		body := getHTTPBody(url)
   365  		val, err := jsonparser.GetBoolean([]byte(body), jsonPath)
   366  		require.NoError(t, err)
   367  		if val {
   368  			return
   369  		}
   370  		select {
   371  		case <-ctx.Done():
   372  			t.Error("timeout waiting for tablet's throttler status to be enabled")
   373  			return
   374  		case <-ticker.C:
   375  		}
   376  	}
   377  }
   378  
   379  func getHTTPBody(url string) string {
   380  	resp, err := http.Get(url)
   381  	if err != nil {
   382  		log.Infof("http Get returns %+v", err)
   383  		return ""
   384  	}
   385  	if resp.StatusCode != 200 {
   386  		log.Infof("http Get returns status %d", resp.StatusCode)
   387  		return ""
   388  	}
   389  	respByte, _ := io.ReadAll(resp.Body)
   390  	defer resp.Body.Close()
   391  	body := string(respByte)
   392  	return body
   393  }
   394  
   395  // ValidateSequentialMigrationIDs validates that schem_migrations.id column, which is an AUTO_INCREMENT, does
   396  // not have gaps
   397  func ValidateSequentialMigrationIDs(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard) {
   398  	r := VtgateExecQuery(t, vtParams, "show vitess_migrations", "")
   399  	shardMin := map[string]uint64{}
   400  	shardMax := map[string]uint64{}
   401  	shardCount := map[string]uint64{}
   402  
   403  	for _, row := range r.Named().Rows {
   404  		id := row.AsUint64("id", 0)
   405  		require.NotZero(t, id)
   406  
   407  		shard := row.AsString("shard", "")
   408  		require.NotEmpty(t, shard)
   409  
   410  		if _, ok := shardMin[shard]; !ok {
   411  			shardMin[shard] = id
   412  			shardMax[shard] = id
   413  		}
   414  		if id < shardMin[shard] {
   415  			shardMin[shard] = id
   416  		}
   417  		if id > shardMax[shard] {
   418  			shardMax[shard] = id
   419  		}
   420  		shardCount[shard]++
   421  	}
   422  	require.NotEmpty(t, shards)
   423  	assert.Equal(t, len(shards), len(shardMin))
   424  	assert.Equal(t, len(shards), len(shardMax))
   425  	assert.Equal(t, len(shards), len(shardCount))
   426  	for shard, count := range shardCount {
   427  		assert.NotZero(t, count)
   428  		assert.Equalf(t, count, shardMax[shard]-shardMin[shard]+1, "mismatch: shared=%v, count=%v, min=%v, max=%v", shard, count, shardMin[shard], shardMax[shard])
   429  	}
   430  }