vitess.io/vitess@v0.16.2/go/vt/wrangler/vexec_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 wrangler
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/stretchr/testify/require"
    29  
    30  	"vitess.io/vitess/go/sqltypes"
    31  	"vitess.io/vitess/go/vt/logutil"
    32  )
    33  
    34  func TestVExec(t *testing.T) {
    35  	ctx := context.Background()
    36  	workflow := "wrWorkflow"
    37  	keyspace := "target"
    38  	query := "update _vt.vreplication set state = 'Running'"
    39  	env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, time.Now().Unix())
    40  	defer env.close()
    41  	var logger = logutil.NewMemoryLogger()
    42  	wr := New(logger, env.topoServ, env.tmc)
    43  
    44  	vx := newVExec(ctx, workflow, keyspace, query, wr)
    45  	err := vx.getPrimaries()
    46  	require.Nil(t, err)
    47  	primaries := vx.primaries
    48  	require.NotNil(t, primaries)
    49  	require.Equal(t, len(primaries), 2)
    50  	var shards []string
    51  	for _, primary := range primaries {
    52  		shards = append(shards, primary.Shard)
    53  	}
    54  	sort.Strings(shards)
    55  	require.Equal(t, fmt.Sprintf("%v", shards), "[-80 80-]")
    56  
    57  	plan, err := vx.parseAndPlan(ctx)
    58  	require.NoError(t, err)
    59  	require.NotNil(t, plan)
    60  
    61  	addWheres := func(query string) string {
    62  		if strings.Contains(query, " where ") {
    63  			query += " and "
    64  		} else {
    65  			query += " where "
    66  		}
    67  		query += fmt.Sprintf("db_name = %s and workflow = %s", encodeString("vt_"+keyspace), encodeString(workflow))
    68  		return query
    69  	}
    70  	want := addWheres(query)
    71  	require.Equal(t, want, plan.parsedQuery.Query)
    72  
    73  	vx.plannedQuery = plan.parsedQuery.Query
    74  	vx.exec()
    75  
    76  	res, err := wr.getStreams(ctx, workflow, keyspace)
    77  	require.NoError(t, err)
    78  	require.Less(t, res.MaxVReplicationLag, int64(3 /*seconds*/)) // lag should be very small
    79  
    80  	type TestCase struct {
    81  		name        string
    82  		query       string
    83  		result      *sqltypes.Result
    84  		errorString string
    85  	}
    86  
    87  	var result *sqltypes.Result
    88  	var testCases []*TestCase
    89  	result = sqltypes.MakeTestResult(sqltypes.MakeTestFields(
    90  		"id|source|message|cell|tablet_types|workflow_type|workflow_sub_type|defer_secondary_keys",
    91  		"int64|varchar|varchar|varchar|varchar|int64|int64|int64"),
    92  		"1|keyspace:\"source\" shard:\"0\" filter:{rules:{match:\"t1\"}}||||0|0|0",
    93  	)
    94  	testCases = append(testCases, &TestCase{
    95  		name:   "select",
    96  		query:  "select id, source, message, cell, tablet_types, workflow_type, workflow_sub_type, defer_secondary_keys from _vt.vreplication",
    97  		result: result,
    98  	})
    99  	result = &sqltypes.Result{
   100  		RowsAffected: 1,
   101  		Rows:         [][]sqltypes.Value{},
   102  	}
   103  	testCases = append(testCases, &TestCase{
   104  		name:   "delete",
   105  		query:  "delete from _vt.vreplication where message != ''",
   106  		result: result,
   107  	})
   108  	result = &sqltypes.Result{
   109  		RowsAffected: 1,
   110  		Rows:         [][]sqltypes.Value{},
   111  	}
   112  	testCases = append(testCases, &TestCase{
   113  		name:   "update",
   114  		query:  "update _vt.vreplication set state='Stopped', message='for wrangler test'",
   115  		result: result,
   116  	})
   117  
   118  	errorString := "query not supported by vexec"
   119  	testCases = append(testCases, &TestCase{
   120  		name:        "insert",
   121  		query:       "insert into _vt.vreplication(state, workflow, db_name) values ('Running', 'wk1', 'ks1'), ('Stopped', 'wk1', 'ks1')",
   122  		errorString: errorString,
   123  	})
   124  
   125  	errorString = "table not supported by vexec"
   126  	testCases = append(testCases, &TestCase{
   127  		name:        "delete invalid-other-table",
   128  		query:       "delete from _vt.copy_state",
   129  		errorString: errorString,
   130  	})
   131  
   132  	for _, testCase := range testCases {
   133  		t.Run(testCase.query, func(t *testing.T) {
   134  			results, err := wr.VExec(ctx, workflow, keyspace, testCase.query, false)
   135  			if testCase.errorString == "" {
   136  				require.NoError(t, err)
   137  				for _, result := range results {
   138  					if !testCase.result.Equal(result) {
   139  						t.Errorf("mismatched result:\nwant: %v\ngot:  %v", testCase.result, result)
   140  					}
   141  				}
   142  			} else {
   143  				require.Error(t, err)
   144  				if !strings.Contains(err.Error(), testCase.errorString) {
   145  					t.Fatalf("Wrong error, want %s, got %s", testCase.errorString, err.Error())
   146  				}
   147  			}
   148  		})
   149  	}
   150  
   151  	query = "delete from _vt.vreplication"
   152  	_, err = wr.VExec(ctx, workflow, keyspace, query, true)
   153  	require.NoError(t, err)
   154  	dryRunResults := []string{
   155  		"Query: delete from _vt.vreplication where db_name = 'vt_target' and workflow = 'wrWorkflow'",
   156  		"will be run on the following streams in keyspace target for workflow wrWorkflow:\n\n",
   157  		`+----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   158  |        TABLET        | ID |          BINLOGSOURCE          |  STATE  |  DBNAME   |               CURRENT GTID               |
   159  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   160  | -80/zone1-0000000200 |  1 | keyspace:"source" shard:"0"    | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 |
   161  |                      |    | filter:{rules:{match:"t1"}}    |         |           |                                          |
   162  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   163  | 80-/zone1-0000000210 |  1 | keyspace:"source" shard:"0"    | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 |
   164  |                      |    | filter:{rules:{match:"t1"}}    |         |           |                                          |
   165  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+`,
   166  	}
   167  	require.Equal(t, strings.Join(dryRunResults, "\n")+"\n\n\n\n\n", logger.String())
   168  	logger.Clear()
   169  }
   170  
   171  func TestWorkflowStatusUpdate(t *testing.T) {
   172  	require.Equal(t, "Running", updateState("for vdiff", "Running", nil, int64(time.Now().Second())))
   173  	require.Equal(t, "Running", updateState("", "Running", nil, int64(time.Now().Second())))
   174  	require.Equal(t, "Lagging", updateState("", "Running", nil, int64(time.Now().Second())-100))
   175  	require.Equal(t, "Copying", updateState("", "Running", []copyState{{Table: "t1", LastPK: "[[INT64(10)]]"}}, int64(time.Now().Second())))
   176  	require.Equal(t, "Error", updateState("error: primary tablet not contactable", "Running", nil, 0))
   177  }
   178  
   179  func TestWorkflowListStreams(t *testing.T) {
   180  	ctx := context.Background()
   181  	workflow := "wrWorkflow"
   182  	keyspace := "target"
   183  	env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, 1234)
   184  	defer env.close()
   185  	logger := logutil.NewMemoryLogger()
   186  	wr := New(logger, env.topoServ, env.tmc)
   187  
   188  	_, err := wr.WorkflowAction(ctx, workflow, keyspace, "listall", false)
   189  	require.NoError(t, err)
   190  
   191  	_, err = wr.WorkflowAction(ctx, workflow, "badks", "show", false)
   192  	require.Errorf(t, err, "node doesn't exist: keyspaces/badks/shards")
   193  
   194  	_, err = wr.WorkflowAction(ctx, "badwf", keyspace, "show", false)
   195  	require.Errorf(t, err, "no streams found for workflow badwf in keyspace target")
   196  	logger.Clear()
   197  	_, err = wr.WorkflowAction(ctx, workflow, keyspace, "show", false)
   198  	require.NoError(t, err)
   199  	want := `{
   200  	"Workflow": "wrWorkflow",
   201  	"SourceLocation": {
   202  		"Keyspace": "source",
   203  		"Shards": [
   204  			"0"
   205  		]
   206  	},
   207  	"TargetLocation": {
   208  		"Keyspace": "target",
   209  		"Shards": [
   210  			"-80",
   211  			"80-"
   212  		]
   213  	},
   214  	"MaxVReplicationLag": 0,
   215  	"MaxVReplicationTransactionLag": 0,
   216  	"Frozen": false,
   217  	"ShardStatuses": {
   218  		"-80/zone1-0000000200": {
   219  			"PrimaryReplicationStatuses": [
   220  				{
   221  					"Shard": "-80",
   222  					"Tablet": "zone1-0000000200",
   223  					"ID": 1,
   224  					"Bls": {
   225  						"keyspace": "source",
   226  						"shard": "0",
   227  						"filter": {
   228  							"rules": [
   229  								{
   230  									"match": "t1"
   231  								}
   232  							]
   233  						}
   234  					},
   235  					"Pos": "14b68925-696a-11ea-aee7-fec597a91f5e:1-3",
   236  					"StopPos": "",
   237  					"State": "Copying",
   238  					"DBName": "vt_target",
   239  					"TransactionTimestamp": 0,
   240  					"TimeUpdated": 1234,
   241  					"TimeHeartbeat": 1234,
   242  					"TimeThrottled": 0,
   243  					"ComponentThrottled": "",
   244  					"Message": "",
   245  					"Tags": "",
   246  					"WorkflowType": "Materialize",
   247  					"WorkflowSubType": "None",
   248  					"CopyState": [
   249  						{
   250  							"Table": "t1",
   251  							"LastPK": "pk1"
   252  						}
   253  					]
   254  				}
   255  			],
   256  			"TabletControls": null,
   257  			"PrimaryIsServing": true
   258  		},
   259  		"80-/zone1-0000000210": {
   260  			"PrimaryReplicationStatuses": [
   261  				{
   262  					"Shard": "80-",
   263  					"Tablet": "zone1-0000000210",
   264  					"ID": 1,
   265  					"Bls": {
   266  						"keyspace": "source",
   267  						"shard": "0",
   268  						"filter": {
   269  							"rules": [
   270  								{
   271  									"match": "t1"
   272  								}
   273  							]
   274  						}
   275  					},
   276  					"Pos": "14b68925-696a-11ea-aee7-fec597a91f5e:1-3",
   277  					"StopPos": "",
   278  					"State": "Copying",
   279  					"DBName": "vt_target",
   280  					"TransactionTimestamp": 0,
   281  					"TimeUpdated": 1234,
   282  					"TimeHeartbeat": 1234,
   283  					"TimeThrottled": 0,
   284  					"ComponentThrottled": "",
   285  					"Message": "",
   286  					"Tags": "",
   287  					"WorkflowType": "Materialize",
   288  					"WorkflowSubType": "None",
   289  					"CopyState": [
   290  						{
   291  							"Table": "t1",
   292  							"LastPK": "pk1"
   293  						}
   294  					]
   295  				}
   296  			],
   297  			"TabletControls": null,
   298  			"PrimaryIsServing": true
   299  		}
   300  	},
   301  	"SourceTimeZone": "",
   302  	"TargetTimeZone": ""
   303  }
   304  
   305  `
   306  	got := logger.String()
   307  	// MaxVReplicationLag needs to be reset. This can't be determinable in this kind of a test because time.Now() is constantly shifting.
   308  	re := regexp.MustCompile(`"MaxVReplicationLag": \d+`)
   309  	got = re.ReplaceAllLiteralString(got, `"MaxVReplicationLag": 0`)
   310  	re = regexp.MustCompile(`"MaxVReplicationTransactionLag": \d+`)
   311  	got = re.ReplaceAllLiteralString(got, `"MaxVReplicationTransactionLag": 0`)
   312  	require.Equal(t, want, got)
   313  
   314  	results, err := wr.execWorkflowAction(ctx, workflow, keyspace, "stop", false)
   315  	require.Nil(t, err)
   316  
   317  	// convert map to list and sort it for comparison
   318  	var gotResults []string
   319  	for key, result := range results {
   320  		gotResults = append(gotResults, fmt.Sprintf("%s:%v", key.String(), result))
   321  	}
   322  	sort.Strings(gotResults)
   323  	wantResults := []string{"Tablet{zone1-0000000200}:rows_affected:1", "Tablet{zone1-0000000210}:rows_affected:1"}
   324  	sort.Strings(wantResults)
   325  	require.ElementsMatch(t, wantResults, gotResults)
   326  
   327  	logger.Clear()
   328  	results, err = wr.execWorkflowAction(ctx, workflow, keyspace, "stop", true)
   329  	require.Nil(t, err)
   330  	require.Equal(t, "map[]", fmt.Sprintf("%v", results))
   331  	dryRunResult := `Query: update _vt.vreplication set state = 'Stopped' where db_name = 'vt_target' and workflow = 'wrWorkflow'
   332  will be run on the following streams in keyspace target for workflow wrWorkflow:
   333  
   334  
   335  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   336  |        TABLET        | ID |          BINLOGSOURCE          |  STATE  |  DBNAME   |               CURRENT GTID               |
   337  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   338  | -80/zone1-0000000200 |  1 | keyspace:"source" shard:"0"    | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 |
   339  |                      |    | filter:{rules:{match:"t1"}}    |         |           |                                          |
   340  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   341  | 80-/zone1-0000000210 |  1 | keyspace:"source" shard:"0"    | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 |
   342  |                      |    | filter:{rules:{match:"t1"}}    |         |           |                                          |
   343  +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+
   344  
   345  
   346  
   347  
   348  `
   349  	require.Equal(t, dryRunResult, logger.String())
   350  }
   351  
   352  func TestWorkflowListAll(t *testing.T) {
   353  	ctx := context.Background()
   354  	keyspace := "target"
   355  	workflow := "wrWorkflow"
   356  	env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, 0)
   357  	defer env.close()
   358  	logger := logutil.NewMemoryLogger()
   359  	wr := New(logger, env.topoServ, env.tmc)
   360  
   361  	workflows, err := wr.ListAllWorkflows(ctx, keyspace, true)
   362  	require.Nil(t, err)
   363  	require.Equal(t, []string{workflow}, workflows)
   364  
   365  	workflows, err = wr.ListAllWorkflows(ctx, keyspace, false)
   366  	require.Nil(t, err)
   367  	require.Equal(t, []string{workflow, "wrWorkflow2"}, workflows)
   368  	logger.Clear()
   369  }
   370  
   371  func TestVExecValidations(t *testing.T) {
   372  	ctx := context.Background()
   373  	workflow := "wf"
   374  	keyspace := "ks"
   375  	query := ""
   376  	env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, 0)
   377  	defer env.close()
   378  
   379  	wr := New(logutil.NewConsoleLogger(), env.topoServ, env.tmc)
   380  
   381  	vx := newVExec(ctx, workflow, keyspace, query, wr)
   382  
   383  	type badQuery struct {
   384  		name        string
   385  		query       string
   386  		errorString string
   387  	}
   388  	badQueries := []badQuery{
   389  		{
   390  			name:        "invalid",
   391  			query:       "bad query",
   392  			errorString: "syntax error at position 4 near 'bad'",
   393  		},
   394  		{
   395  			name:        "incorrect table",
   396  			query:       "select * from _vt.vreplication2",
   397  			errorString: "table not supported by vexec: _vt.vreplication2",
   398  		},
   399  		{
   400  			name:        "unsupported query",
   401  			query:       "describe _vt.vreplication",
   402  			errorString: "query not supported by vexec: explain _vt.vreplication",
   403  		},
   404  	}
   405  	for _, bq := range badQueries {
   406  		t.Run(bq.name, func(t *testing.T) {
   407  			vx.query = bq.query
   408  			plan, err := vx.parseAndPlan(ctx)
   409  			require.EqualError(t, err, bq.errorString)
   410  			require.Nil(t, plan)
   411  		})
   412  	}
   413  
   414  	type action struct {
   415  		name          string
   416  		want          string
   417  		expectedError error
   418  	}
   419  	updateSQL := "update _vt.vreplication set state = %s"
   420  	actions := []action{
   421  		{
   422  			name:          "start",
   423  			want:          fmt.Sprintf(updateSQL, encodeString("Running")),
   424  			expectedError: nil,
   425  		},
   426  		{
   427  			name:          "stop",
   428  			want:          fmt.Sprintf(updateSQL, encodeString("Stopped")),
   429  			expectedError: nil,
   430  		},
   431  		{
   432  			name:          "delete",
   433  			want:          "delete from _vt.vreplication",
   434  			expectedError: nil,
   435  		},
   436  		{
   437  			name:          "other",
   438  			want:          "",
   439  			expectedError: fmt.Errorf("invalid action found: other"),
   440  		}}
   441  
   442  	for _, a := range actions {
   443  		t.Run(a.name, func(t *testing.T) {
   444  			sql, err := wr.getWorkflowActionQuery(a.name)
   445  			require.Equal(t, a.expectedError, err)
   446  			require.Equal(t, a.want, sql)
   447  		})
   448  	}
   449  }