vitess.io/vitess@v0.16.2/go/vt/vtgate/endtoend/vstream_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 endtoend
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"sort"
    24  	"sync"
    25  	"testing"
    26  
    27  	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
    28  
    29  	"github.com/stretchr/testify/require"
    30  	"google.golang.org/protobuf/proto"
    31  
    32  	"vitess.io/vitess/go/mysql"
    33  	"vitess.io/vitess/go/sqltypes"
    34  	"vitess.io/vitess/go/vt/log"
    35  	binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
    36  	"vitess.io/vitess/go/vt/proto/query"
    37  	querypb "vitess.io/vitess/go/vt/proto/query"
    38  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    39  	"vitess.io/vitess/go/vt/vtgate/vtgateconn"
    40  )
    41  
    42  func initialize(ctx context.Context, t *testing.T) (*vtgateconn.VTGateConn, *mysql.Conn, *mysql.Conn, func()) {
    43  	gconn, err := vtgateconn.Dial(ctx, grpcAddress)
    44  	if err != nil {
    45  		t.Fatal(err)
    46  	}
    47  	conn, err := mysql.Connect(ctx, &vtParams)
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  	mconn, err := mysql.Connect(ctx, &mysqlParams)
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	close := func() {
    56  		gconn.Close()
    57  		conn.Close()
    58  		mconn.Close()
    59  	}
    60  	return gconn, conn, mconn, close
    61  }
    62  func TestVStream(t *testing.T) {
    63  	ctx, cancel := context.WithCancel(context.Background())
    64  	defer cancel()
    65  
    66  	gconn, conn, mconn, closeConnections := initialize(ctx, t)
    67  	defer closeConnections()
    68  
    69  	mpos, err := mconn.PrimaryPosition()
    70  	if err != nil {
    71  		t.Fatal(err)
    72  	}
    73  	vgtid := &binlogdatapb.VGtid{
    74  		ShardGtids: []*binlogdatapb.ShardGtid{{
    75  			Keyspace: "ks",
    76  			Shard:    "-80",
    77  			Gtid:     fmt.Sprintf("%s/%s", mpos.GTIDSet.Flavor(), mpos),
    78  		}},
    79  	}
    80  	filter := &binlogdatapb.Filter{
    81  		Rules: []*binlogdatapb.Rule{{
    82  			Match: "/.*/",
    83  		}},
    84  	}
    85  	flags := &vtgatepb.VStreamFlags{}
    86  	reader, err := gconn.VStream(ctx, topodatapb.TabletType_PRIMARY, vgtid, filter, flags)
    87  	if err != nil {
    88  		t.Fatal(err)
    89  	}
    90  
    91  	_, err = conn.ExecuteFetch("insert into vstream_test(id,val) values(1,1), (4,4)", 1, false)
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  	// We expect two events because the insert goes to two shards (-80 and 80-),
    96  	// and both of them are in the same mysql server.
    97  	// The row that goes to 80- will have events.
    98  	// The other will be an empty transaction.
    99  	// In a real world scenario where every mysql instance hosts only one
   100  	// keyspace/shard, we should expect only a single event.
   101  	// The events could come in any order as the scatter insert runs in parallel.
   102  	emptyEventSkipped := false
   103  	for i := 0; i < 2; i++ {
   104  		events, err := reader.Recv()
   105  		if err != nil {
   106  			t.Fatal(err)
   107  		}
   108  		fmt.Printf("events: %v\n", events)
   109  		// An empty transaction has three events: begin, gtid and commit.
   110  		if len(events) == 3 && !emptyEventSkipped {
   111  			emptyEventSkipped = true
   112  			continue
   113  		}
   114  		if len(events) != 5 {
   115  			t.Errorf("Unexpected event length: %v", events)
   116  			continue
   117  		}
   118  		wantFields := &binlogdatapb.FieldEvent{
   119  			TableName: "ks.vstream_test",
   120  			Keyspace:  "ks",
   121  			Shard:     "-80",
   122  			Fields: []*querypb.Field{{
   123  				Name: "id",
   124  				Type: querypb.Type_INT64,
   125  			}, {
   126  				Name: "val",
   127  				Type: querypb.Type_INT64,
   128  			}},
   129  		}
   130  
   131  		gotFields := events[1].FieldEvent
   132  		filteredFields := &binlogdatapb.FieldEvent{
   133  			TableName: gotFields.TableName,
   134  			Keyspace:  gotFields.Keyspace,
   135  			Shard:     gotFields.Shard,
   136  			Fields:    []*querypb.Field{},
   137  		}
   138  		for _, field := range gotFields.Fields {
   139  			filteredFields.Fields = append(filteredFields.Fields, &querypb.Field{
   140  				Name: field.Name,
   141  				Type: field.Type,
   142  			})
   143  		}
   144  		if !proto.Equal(filteredFields, wantFields) {
   145  			t.Errorf("FieldEvent:\n%v, want\n%v", filteredFields, wantFields)
   146  		}
   147  		wantRows := &binlogdatapb.RowEvent{
   148  			TableName: "ks.vstream_test",
   149  			Keyspace:  "ks",
   150  			Shard:     "-80",
   151  			RowChanges: []*binlogdatapb.RowChange{{
   152  				After: &query.Row{
   153  					Lengths: []int64{1, 1},
   154  					Values:  []byte("11"),
   155  				},
   156  			}},
   157  		}
   158  		gotRows := events[2].RowEvent
   159  		if !proto.Equal(gotRows, wantRows) {
   160  			t.Errorf("RowEvent:\n%v, want\n%v", gotRows, wantRows)
   161  		}
   162  	}
   163  	cancel()
   164  }
   165  
   166  func TestVStreamCopyBasic(t *testing.T) {
   167  	ctx, cancel := context.WithCancel(context.Background())
   168  	defer cancel()
   169  	gconn, conn, mconn, closeConnections := initialize(ctx, t)
   170  	defer closeConnections()
   171  
   172  	_, err := conn.ExecuteFetch("insert into t1_copy_basic(id1,id2) values(1,1), (2,2), (3,3), (4,4), (5,5), (6,6), (7,7), (8,8)", 1, false)
   173  	if err != nil {
   174  		t.Fatal(err)
   175  	}
   176  
   177  	lastPK := sqltypes.Result{
   178  		Fields: []*query.Field{{Name: "id1", Type: query.Type_INT32}},
   179  		Rows:   [][]sqltypes.Value{{sqltypes.NewInt32(4)}},
   180  	}
   181  	qr := sqltypes.ResultToProto3(&lastPK)
   182  	tablePKs := []*binlogdatapb.TableLastPK{{
   183  		TableName: "t1_copy_basic",
   184  		Lastpk:    qr,
   185  	}}
   186  	var shardGtids []*binlogdatapb.ShardGtid
   187  	var vgtid = &binlogdatapb.VGtid{}
   188  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   189  		Keyspace: "ks",
   190  		Shard:    "-80",
   191  		Gtid:     "",
   192  		TablePKs: tablePKs,
   193  	})
   194  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   195  		Keyspace: "ks",
   196  		Shard:    "80-",
   197  		Gtid:     "",
   198  		TablePKs: tablePKs,
   199  	})
   200  	vgtid.ShardGtids = shardGtids
   201  	filter := &binlogdatapb.Filter{
   202  		Rules: []*binlogdatapb.Rule{{
   203  			Match:  "t1_copy_basic",
   204  			Filter: "select * from t1_copy_basic",
   205  		}},
   206  	}
   207  	flags := &vtgatepb.VStreamFlags{}
   208  	reader, err := gconn.VStream(ctx, topodatapb.TabletType_PRIMARY, vgtid, filter, flags)
   209  	_, _ = conn, mconn
   210  	if err != nil {
   211  		t.Fatal(err)
   212  	}
   213  	numExpectedEvents := 2 /* num shards */ *(7 /* begin/field/vgtid:pos/2 rowevents avg/vgitd: lastpk/commit) */ +3 /* begin/vgtid/commit for completed table */ +1 /* copy operation completed */) + 1 /* fully copy operation completed */
   214  	expectedCompletedEvents := []string{
   215  		`type:COPY_COMPLETED keyspace:"ks" shard:"-80"`,
   216  		`type:COPY_COMPLETED keyspace:"ks" shard:"80-"`,
   217  		`type:COPY_COMPLETED`,
   218  	}
   219  	require.NotNil(t, reader)
   220  	var evs []*binlogdatapb.VEvent
   221  	var completedEvs []*binlogdatapb.VEvent
   222  	for {
   223  		e, err := reader.Recv()
   224  		switch err {
   225  		case nil:
   226  			evs = append(evs, e...)
   227  
   228  			for _, ev := range e {
   229  				if ev.Type == binlogdatapb.VEventType_COPY_COMPLETED {
   230  					completedEvs = append(completedEvs, ev)
   231  				}
   232  			}
   233  
   234  			printEvents(evs) // for debugging ci failures
   235  
   236  			if len(evs) == numExpectedEvents {
   237  				// The arrival order of COPY_COMPLETED events with keyspace/shard is not constant.
   238  				// On the other hand, the last event should always be a fully COPY_COMPLETED event.
   239  				// That's why the sort.Slice doesn't have to handle the last element in completedEvs.
   240  				sort.Slice(completedEvs[:len(completedEvs)-1], func(i, j int) bool {
   241  					return completedEvs[i].GetShard() < completedEvs[j].GetShard()
   242  				})
   243  				for i, ev := range completedEvs {
   244  					require.Regexp(t, expectedCompletedEvents[i], ev.String())
   245  				}
   246  				t.Logf("TestVStreamCopyBasic was successful")
   247  				return
   248  			} else if numExpectedEvents < len(evs) {
   249  				t.Fatalf("len(events)=%v are not expected\n", len(evs))
   250  			}
   251  		case io.EOF:
   252  			log.Infof("stream ended\n")
   253  			cancel()
   254  		default:
   255  			log.Errorf("Returned err %v", err)
   256  			t.Fatalf("remote error: %v\n", err)
   257  		}
   258  	}
   259  }
   260  
   261  func TestVStreamCopyResume(t *testing.T) {
   262  	ctx, cancel := context.WithCancel(context.Background())
   263  	defer cancel()
   264  	gconn, conn, mconn, closeConnections := initialize(ctx, t)
   265  	defer closeConnections()
   266  
   267  	_, err := conn.ExecuteFetch("insert into t1_copy_resume(id1,id2) values(1,1), (2,2), (3,3), (4,4), (5,5), (6,6), (7,7), (8,8)", 1, false)
   268  	if err != nil {
   269  		t.Fatal(err)
   270  	}
   271  
   272  	// Any subsequent GTIDs will be part of the stream
   273  	mpos, err := mconn.PrimaryPosition()
   274  	require.NoError(t, err)
   275  
   276  	// lastPK is id1=4, meaning we should only copy rows for id1 IN(5,6,7,8,9)
   277  	lastPK := sqltypes.Result{
   278  		Fields: []*query.Field{{Name: "id1", Type: query.Type_INT64}},
   279  		Rows:   [][]sqltypes.Value{{sqltypes.NewInt64(4)}},
   280  	}
   281  	tableLastPK := []*binlogdatapb.TableLastPK{{
   282  		TableName: "t1_copy_resume",
   283  		Lastpk:    sqltypes.ResultToProto3(&lastPK),
   284  	}}
   285  
   286  	catchupQueries := []string{
   287  		"insert into t1_copy_resume(id1,id2) values(9,9)", // this row will show up twice: once in catchup and copy
   288  		"update t1_copy_resume set id2 = 10 where id1 = 1",
   289  		"insert into t1(id1, id2) values(100,100)",
   290  		"delete from t1_copy_resume where id1 = 1",
   291  		"update t1_copy_resume set id2 = 90 where id1 = 9",
   292  	}
   293  	for _, query := range catchupQueries {
   294  		_, err = conn.ExecuteFetch(query, 1, false)
   295  		require.NoError(t, err)
   296  	}
   297  
   298  	var shardGtids []*binlogdatapb.ShardGtid
   299  	var vgtid = &binlogdatapb.VGtid{}
   300  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   301  		Keyspace: "ks",
   302  		Shard:    "-80",
   303  		Gtid:     fmt.Sprintf("%s/%s", mpos.GTIDSet.Flavor(), mpos),
   304  		TablePKs: tableLastPK,
   305  	})
   306  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   307  		Keyspace: "ks",
   308  		Shard:    "80-",
   309  		Gtid:     fmt.Sprintf("%s/%s", mpos.GTIDSet.Flavor(), mpos),
   310  		TablePKs: tableLastPK,
   311  	})
   312  	vgtid.ShardGtids = shardGtids
   313  	filter := &binlogdatapb.Filter{
   314  		Rules: []*binlogdatapb.Rule{{
   315  			Match:  "t1_copy_resume",
   316  			Filter: "select * from t1_copy_resume",
   317  		}},
   318  	}
   319  	flags := &vtgatepb.VStreamFlags{}
   320  	reader, err := gconn.VStream(ctx, topodatapb.TabletType_PRIMARY, vgtid, filter, flags)
   321  	if err != nil {
   322  		t.Fatal(err)
   323  	}
   324  	require.NotNil(t, reader)
   325  
   326  	expectedRowCopyEvents := 5                       // id1 and id2 IN(5,6,7,8,9)
   327  	expectedCatchupEvents := len(catchupQueries) - 1 // insert into t1 should never reach
   328  	rowCopyEvents, replCatchupEvents := 0, 0
   329  	expectedEvents := []string{
   330  		`type:ROW timestamp:[0-9]+ row_event:{table_name:"ks.t1_copy_resume" row_changes:{before:{lengths:1 lengths:1 values:"11"} after:{lengths:1 lengths:2 values:"110"}} keyspace:"ks" shard:"-80"} current_time:[0-9]+ keyspace:"ks" shard:"-80"`,
   331  		`type:ROW timestamp:[0-9]+ row_event:{table_name:"ks.t1_copy_resume" row_changes:{before:{lengths:1 lengths:2 values:"110"}} keyspace:"ks" shard:"-80"} current_time:[0-9]+ keyspace:"ks" shard:"-80"`,
   332  		`type:ROW row_event:{table_name:"ks.t1_copy_resume" row_changes:{after:{lengths:1 lengths:1 values:"55"}} keyspace:"ks" shard:"-80"} keyspace:"ks" shard:"-80"`,
   333  		`type:ROW row_event:{table_name:"ks.t1_copy_resume" row_changes:{after:{lengths:1 lengths:1 values:"66"}} keyspace:"ks" shard:"80-"} keyspace:"ks" shard:"80-"`,
   334  		`type:ROW row_event:{table_name:"ks.t1_copy_resume" row_changes:{after:{lengths:1 lengths:1 values:"77"}} keyspace:"ks" shard:"80-"} keyspace:"ks" shard:"80-"`,
   335  		`type:ROW row_event:{table_name:"ks.t1_copy_resume" row_changes:{after:{lengths:1 lengths:1 values:"88"}} keyspace:"ks" shard:"80-"} keyspace:"ks" shard:"80-"`,
   336  		`type:ROW timestamp:[0-9]+ row_event:{table_name:"ks.t1_copy_resume" row_changes:{after:{lengths:1 lengths:1 values:"99"}} keyspace:"ks" shard:"-80"} current_time:[0-9]+ keyspace:"ks" shard:"-80"`,
   337  		`type:ROW row_event:{table_name:"ks.t1_copy_resume" row_changes:{after:{lengths:1 lengths:2 values:"990"}} keyspace:"ks" shard:"-80"} keyspace:"ks" shard:"-80"`,
   338  		`type:ROW timestamp:[0-9]+ row_event:{table_name:"ks.t1_copy_resume" row_changes:{before:{lengths:1 lengths:1 values:"99"} after:{lengths:1 lengths:2 values:"990"}} keyspace:"ks" shard:"-80"} current_time:[0-9]+ keyspace:"ks" shard:"-80"`,
   339  	}
   340  	var evs []*binlogdatapb.VEvent
   341  	for {
   342  		e, err := reader.Recv()
   343  		switch err {
   344  		case nil:
   345  			for _, ev := range e {
   346  				if ev.Type == binlogdatapb.VEventType_ROW {
   347  					evs = append(evs, ev)
   348  					if ev.Timestamp == 0 {
   349  						rowCopyEvents++
   350  					} else {
   351  						replCatchupEvents++
   352  					}
   353  					printEvents(evs) // for debugging ci failures
   354  				}
   355  			}
   356  			if expectedCatchupEvents == replCatchupEvents && expectedRowCopyEvents == rowCopyEvents {
   357  				sort.Sort(VEventSorter(evs))
   358  				for i, ev := range evs {
   359  					require.Regexp(t, expectedEvents[i], ev.String())
   360  				}
   361  				t.Logf("TestVStreamCopyResume was successful")
   362  				return
   363  			}
   364  		case io.EOF:
   365  			log.Infof("stream ended\n")
   366  			cancel()
   367  		default:
   368  			log.Errorf("Returned err %v", err)
   369  			t.Fatalf("remote error: %v\n", err)
   370  		}
   371  	}
   372  }
   373  
   374  func TestVStreamCurrent(t *testing.T) {
   375  	ctx, cancel := context.WithCancel(context.Background())
   376  	defer cancel()
   377  	gconn, conn, mconn, closeConnections := initialize(ctx, t)
   378  	defer closeConnections()
   379  
   380  	var shardGtids []*binlogdatapb.ShardGtid
   381  	var vgtid = &binlogdatapb.VGtid{}
   382  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   383  		Keyspace: "ks",
   384  		Shard:    "-80",
   385  		Gtid:     "current",
   386  	})
   387  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   388  		Keyspace: "ks",
   389  		Shard:    "80-",
   390  		Gtid:     "current",
   391  	})
   392  	vgtid.ShardGtids = shardGtids
   393  	filter := &binlogdatapb.Filter{
   394  		Rules: []*binlogdatapb.Rule{{
   395  			Match:  "t1",
   396  			Filter: "select * from t1",
   397  		}},
   398  	}
   399  	flags := &vtgatepb.VStreamFlags{}
   400  	reader, err := gconn.VStream(ctx, topodatapb.TabletType_PRIMARY, vgtid, filter, flags)
   401  	_, _ = conn, mconn
   402  	if err != nil {
   403  		t.Fatal(err)
   404  	}
   405  	numExpectedEvents := 4 // vgtid+other per shard for "current"
   406  	require.NotNil(t, reader)
   407  	var evs []*binlogdatapb.VEvent
   408  	for {
   409  		e, err := reader.Recv()
   410  		switch err {
   411  		case nil:
   412  			evs = append(evs, e...)
   413  			printEvents(evs) // for debugging ci failures
   414  			if len(evs) == numExpectedEvents {
   415  				t.Logf("TestVStreamCurrent was successful")
   416  				return
   417  			}
   418  		case io.EOF:
   419  			log.Infof("stream ended\n")
   420  			cancel()
   421  		default:
   422  			log.Errorf("Returned err %v", err)
   423  			t.Fatalf("remote error: %v\n", err)
   424  		}
   425  	}
   426  }
   427  
   428  func TestVStreamSharded(t *testing.T) {
   429  	ctx, cancel := context.WithCancel(context.Background())
   430  	defer cancel()
   431  	gconn, conn, mconn, closeConnections := initialize(ctx, t)
   432  	defer closeConnections()
   433  
   434  	var shardGtids []*binlogdatapb.ShardGtid
   435  	var vgtid = &binlogdatapb.VGtid{}
   436  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   437  		Keyspace: "ks",
   438  		Shard:    "-80",
   439  		Gtid:     "",
   440  	})
   441  	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
   442  		Keyspace: "ks",
   443  		Shard:    "80-",
   444  		Gtid:     "",
   445  	})
   446  	vgtid.ShardGtids = shardGtids
   447  	filter := &binlogdatapb.Filter{
   448  		Rules: []*binlogdatapb.Rule{{
   449  			Match:  "t1_sharded",
   450  			Filter: "select * from t1_sharded",
   451  		}},
   452  	}
   453  	_, err := conn.ExecuteFetch("insert into t1_sharded(id1,id2) values(1,1), (4,4)", 1, false)
   454  	if err != nil {
   455  		t.Fatal(err)
   456  	}
   457  	flags := &vtgatepb.VStreamFlags{}
   458  	reader, err := gconn.VStream(ctx, topodatapb.TabletType_PRIMARY, vgtid, filter, flags)
   459  	_, _ = conn, mconn
   460  	if err != nil {
   461  		t.Fatal(err)
   462  	}
   463  	numExpectedEvents := 4
   464  	require.NotNil(t, reader)
   465  	var evs []*binlogdatapb.VEvent
   466  
   467  	type expectedEvent struct {
   468  		ev       string
   469  		received bool
   470  	}
   471  	expectedEvents := []*expectedEvent{
   472  		{`type:FIELD field_event:{table_name:"ks.t1_sharded" fields:{name:"id1" type:INT64 table:"t1_sharded" org_table:"t1_sharded" database:"vt_ks_-80" org_name:"id1" column_length:20 charset:63 flags:53251} fields:{name:"id2" type:INT64 table:"t1_sharded" org_table:"t1_sharded" database:"vt_ks_-80" org_name:"id2" column_length:20 charset:63 flags:32768} keyspace:"ks" shard:"-80"}`, false},
   473  		{`type:ROW row_event:{table_name:"ks.t1_sharded" row_changes:{after:{lengths:1 lengths:1 values:"11"}} keyspace:"ks" shard:"-80"}`, false},
   474  		{`type:FIELD field_event:{table_name:"ks.t1_sharded" fields:{name:"id1" type:INT64 table:"t1_sharded" org_table:"t1_sharded" database:"vt_ks_80-" org_name:"id1" column_length:20 charset:63 flags:53251} fields:{name:"id2" type:INT64 table:"t1_sharded" org_table:"t1_sharded" database:"vt_ks_80-" org_name:"id2" column_length:20 charset:63 flags:32768} keyspace:"ks" shard:"80-"}`, false},
   475  		{`type:ROW row_event:{table_name:"ks.t1_sharded" row_changes:{after:{lengths:1 lengths:1 values:"44"}} keyspace:"ks" shard:"80-"}`, false},
   476  	}
   477  	for {
   478  		events, err := reader.Recv()
   479  		switch err {
   480  		case nil:
   481  			for _, event := range events {
   482  				// check for Keyspace/Shard values first and then reset them so that we don't have to add them to each expected event proto string
   483  				require.Equal(t, "ks", event.Keyspace, "event has an incorrect keyspace attribute: %s", event.Keyspace)
   484  				require.True(t, event.Shard == "-80" || event.Shard == "80-", "event has an incorrect shard attribute: %s", event.Shard)
   485  				event.Keyspace = ""
   486  				event.Shard = ""
   487  				switch event.Type {
   488  				case binlogdatapb.VEventType_ROW, binlogdatapb.VEventType_FIELD:
   489  					evs = append(evs, event)
   490  				default:
   491  				}
   492  			}
   493  			printEvents(evs) // for debugging ci failures
   494  			if len(evs) == numExpectedEvents {
   495  				// events from the two shards -80 and 80- can come in any order, hence this logic
   496  				for _, ev := range evs {
   497  					s := fmt.Sprintf("%v", ev)
   498  					for _, expectedEv := range expectedEvents {
   499  						if expectedEv.ev == s {
   500  							expectedEv.received = true
   501  							break
   502  						}
   503  					}
   504  				}
   505  				for _, expectedEv := range expectedEvents {
   506  					require.Truef(t, expectedEv.received, "event %s not received", expectedEv.ev)
   507  				}
   508  
   509  				t.Logf("TestVStreamCurrent was successful")
   510  				return
   511  			}
   512  		case io.EOF:
   513  			log.Infof("stream ended\n")
   514  			cancel()
   515  		default:
   516  			log.Errorf("Returned err %v", err)
   517  			t.Fatalf("remote error: %v\n", err)
   518  		}
   519  	}
   520  
   521  }
   522  
   523  var printMu sync.Mutex
   524  
   525  func printEvents(evs []*binlogdatapb.VEvent) {
   526  	printMu.Lock()
   527  	defer printMu.Unlock()
   528  	if len(evs) == 0 {
   529  		return
   530  	}
   531  	s := "\n===START===" + "\n"
   532  	for i, ev := range evs {
   533  		s += fmt.Sprintf("Event %d; %v\n", i, ev)
   534  	}
   535  	s += "===END===" + "\n"
   536  	log.Infof("%s", s)
   537  }
   538  
   539  // Sort the VEvents by the first row change's after value bytes primarily, with
   540  // secondary ordering by timestamp (ASC). Note that row copy events do not have
   541  // a timestamp and the value will be 0.
   542  type VEventSorter []*binlogdatapb.VEvent
   543  
   544  func (v VEventSorter) Len() int {
   545  	return len(v)
   546  }
   547  func (v VEventSorter) Swap(i, j int) {
   548  	v[i], v[j] = v[j], v[i]
   549  }
   550  func (v VEventSorter) Less(i, j int) bool {
   551  	valsI := v[i].GetRowEvent().RowChanges[0].After
   552  	if valsI == nil {
   553  		valsI = v[i].GetRowEvent().RowChanges[0].Before
   554  	}
   555  	valsJ := v[j].GetRowEvent().RowChanges[0].After
   556  	if valsJ == nil {
   557  		valsJ = v[j].GetRowEvent().RowChanges[0].Before
   558  	}
   559  	valI := string(valsI.Values)
   560  	valJ := string(valsJ.Values)
   561  	if valI == valJ {
   562  		return v[i].Timestamp < v[j].Timestamp
   563  	}
   564  	return valI < valJ
   565  }