vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/health_streamer_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 tabletserver
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"sort"
    23  	"sync"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  	"google.golang.org/protobuf/proto"
    29  
    30  	"vitess.io/vitess/go/sync2"
    31  
    32  	"vitess.io/vitess/go/mysql"
    33  	"vitess.io/vitess/go/mysql/fakesqldb"
    34  	"vitess.io/vitess/go/sqltypes"
    35  	querypb "vitess.io/vitess/go/vt/proto/query"
    36  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    37  	"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"
    38  )
    39  
    40  func TestHealthStreamerClosed(t *testing.T) {
    41  	db := fakesqldb.New(t)
    42  	defer db.Close()
    43  	config := newConfig(db)
    44  	env := tabletenv.NewEnv(config, "ReplTrackerTest")
    45  	alias := &topodatapb.TabletAlias{
    46  		Cell: "cell",
    47  		Uid:  1,
    48  	}
    49  	blpFunc = testBlpFunc
    50  	hs := newHealthStreamer(env, alias)
    51  	err := hs.Stream(context.Background(), func(shr *querypb.StreamHealthResponse) error {
    52  		return nil
    53  	})
    54  	assert.Contains(t, err.Error(), "tabletserver is shutdown")
    55  }
    56  
    57  func newConfig(db *fakesqldb.DB) *tabletenv.TabletConfig {
    58  	cfg := tabletenv.NewDefaultConfig()
    59  	cfg.DB = newDBConfigs(db)
    60  	return cfg
    61  }
    62  
    63  func TestHealthStreamerBroadcast(t *testing.T) {
    64  	db := fakesqldb.New(t)
    65  	defer db.Close()
    66  	config := newConfig(db)
    67  	config.SignalWhenSchemaChange = false
    68  
    69  	env := tabletenv.NewEnv(config, "ReplTrackerTest")
    70  	alias := &topodatapb.TabletAlias{
    71  		Cell: "cell",
    72  		Uid:  1,
    73  	}
    74  	blpFunc = testBlpFunc
    75  	hs := newHealthStreamer(env, alias)
    76  	hs.InitDBConfig(&querypb.Target{TabletType: topodatapb.TabletType_PRIMARY}, config.DB.DbaWithDB())
    77  	hs.Open()
    78  	defer hs.Close()
    79  	target := &querypb.Target{}
    80  	hs.InitDBConfig(target, db.ConnParams())
    81  
    82  	ch, cancel := testStream(hs)
    83  	defer cancel()
    84  
    85  	shr := <-ch
    86  	want := &querypb.StreamHealthResponse{
    87  		Target:      &querypb.Target{},
    88  		TabletAlias: alias,
    89  		RealtimeStats: &querypb.RealtimeStats{
    90  			HealthError: "tabletserver uninitialized",
    91  		},
    92  	}
    93  	assert.Truef(t, proto.Equal(want, shr), "want: %v, got: %v", want, shr)
    94  
    95  	hs.ChangeState(topodatapb.TabletType_REPLICA, time.Time{}, 0, nil, false)
    96  	shr = <-ch
    97  	want = &querypb.StreamHealthResponse{
    98  		Target: &querypb.Target{
    99  			TabletType: topodatapb.TabletType_REPLICA,
   100  		},
   101  		TabletAlias: alias,
   102  		RealtimeStats: &querypb.RealtimeStats{
   103  			FilteredReplicationLagSeconds: 1,
   104  			BinlogPlayersCount:            2,
   105  		},
   106  	}
   107  	assert.Truef(t, proto.Equal(want, shr), "want: %v, got: %v", want, shr)
   108  
   109  	// Test primary and timestamp.
   110  	now := time.Now()
   111  	hs.ChangeState(topodatapb.TabletType_PRIMARY, now, 0, nil, true)
   112  	shr = <-ch
   113  	want = &querypb.StreamHealthResponse{
   114  		Target: &querypb.Target{
   115  			TabletType: topodatapb.TabletType_PRIMARY,
   116  		},
   117  		TabletAlias:                         alias,
   118  		Serving:                             true,
   119  		TabletExternallyReparentedTimestamp: now.Unix(),
   120  		RealtimeStats: &querypb.RealtimeStats{
   121  			FilteredReplicationLagSeconds: 1,
   122  			BinlogPlayersCount:            2,
   123  		},
   124  	}
   125  	assert.Truef(t, proto.Equal(want, shr), "want: %v, got: %v", want, shr)
   126  
   127  	// Test non-serving, and 0 timestamp for non-primary.
   128  	hs.ChangeState(topodatapb.TabletType_REPLICA, now, 1*time.Second, nil, false)
   129  	shr = <-ch
   130  	want = &querypb.StreamHealthResponse{
   131  		Target: &querypb.Target{
   132  			TabletType: topodatapb.TabletType_REPLICA,
   133  		},
   134  		TabletAlias: alias,
   135  		RealtimeStats: &querypb.RealtimeStats{
   136  			ReplicationLagSeconds:         1,
   137  			FilteredReplicationLagSeconds: 1,
   138  			BinlogPlayersCount:            2,
   139  		},
   140  	}
   141  	assert.Truef(t, proto.Equal(want, shr), "want: %v, got: %v", want, shr)
   142  
   143  	// Test Health error.
   144  	hs.ChangeState(topodatapb.TabletType_REPLICA, now, 0, errors.New("repl err"), false)
   145  	shr = <-ch
   146  	want = &querypb.StreamHealthResponse{
   147  		Target: &querypb.Target{
   148  			TabletType: topodatapb.TabletType_REPLICA,
   149  		},
   150  		TabletAlias: alias,
   151  		RealtimeStats: &querypb.RealtimeStats{
   152  			HealthError:                   "repl err",
   153  			FilteredReplicationLagSeconds: 1,
   154  			BinlogPlayersCount:            2,
   155  		},
   156  	}
   157  	assert.Truef(t, proto.Equal(want, shr), "want: %v, got: %v", want, shr)
   158  }
   159  
   160  func TestReloadSchema(t *testing.T) {
   161  	db := fakesqldb.New(t)
   162  	defer db.Close()
   163  	config := newConfig(db)
   164  	config.SignalSchemaChangeReloadIntervalSeconds.Set(100 * time.Millisecond)
   165  	config.SignalWhenSchemaChange = true
   166  
   167  	env := tabletenv.NewEnv(config, "ReplTrackerTest")
   168  	alias := &topodatapb.TabletAlias{
   169  		Cell: "cell",
   170  		Uid:  1,
   171  	}
   172  	blpFunc = testBlpFunc
   173  	hs := newHealthStreamer(env, alias)
   174  
   175  	target := &querypb.Target{TabletType: topodatapb.TabletType_PRIMARY}
   176  	configs := config.DB
   177  
   178  	db.AddQueryPattern(mysql.ClearSchemaCopy+".*", &sqltypes.Result{})
   179  	db.AddQueryPattern(mysql.InsertIntoSchemaCopy+".*", &sqltypes.Result{})
   180  	db.AddQuery("begin", &sqltypes.Result{})
   181  	db.AddQuery("commit", &sqltypes.Result{})
   182  	db.AddQuery("rollback", &sqltypes.Result{})
   183  	db.AddQuery(mysql.DetectSchemaChange, sqltypes.MakeTestResult(
   184  		sqltypes.MakeTestFields(
   185  			"table_name",
   186  			"varchar",
   187  		),
   188  		"product",
   189  		"users",
   190  	))
   191  	db.AddQuery(mysql.SelectAllViews, &sqltypes.Result{})
   192  
   193  	hs.InitDBConfig(target, configs.DbaWithDB())
   194  	hs.Open()
   195  	defer hs.Close()
   196  	var wg sync.WaitGroup
   197  	wg.Add(1)
   198  	go func() {
   199  		hs.Stream(ctx, func(response *querypb.StreamHealthResponse) error {
   200  			if response.RealtimeStats.TableSchemaChanged != nil {
   201  				assert.Equal(t, []string{"product", "users"}, response.RealtimeStats.TableSchemaChanged)
   202  				wg.Done()
   203  			}
   204  			return nil
   205  		})
   206  	}()
   207  
   208  	c := make(chan struct{})
   209  	go func() {
   210  		defer close(c)
   211  		wg.Wait()
   212  	}()
   213  	select {
   214  	case <-c:
   215  	case <-time.After(1 * time.Second):
   216  		t.Errorf("timed out")
   217  	}
   218  }
   219  
   220  func TestDoesNotReloadSchema(t *testing.T) {
   221  	db := fakesqldb.New(t)
   222  	defer db.Close()
   223  	config := newConfig(db)
   224  	config.SignalSchemaChangeReloadIntervalSeconds.Set(100 * time.Millisecond)
   225  	config.SignalWhenSchemaChange = false
   226  
   227  	env := tabletenv.NewEnv(config, "ReplTrackerTest")
   228  	alias := &topodatapb.TabletAlias{
   229  		Cell: "cell",
   230  		Uid:  1,
   231  	}
   232  	blpFunc = testBlpFunc
   233  	hs := newHealthStreamer(env, alias)
   234  
   235  	target := &querypb.Target{TabletType: topodatapb.TabletType_PRIMARY}
   236  	configs := config.DB
   237  
   238  	hs.InitDBConfig(target, configs.DbaWithDB())
   239  	hs.Open()
   240  	defer hs.Close()
   241  	var wg sync.WaitGroup
   242  	wg.Add(1)
   243  	go func() {
   244  		hs.Stream(ctx, func(response *querypb.StreamHealthResponse) error {
   245  			if response.RealtimeStats.TableSchemaChanged != nil {
   246  				wg.Done()
   247  			}
   248  			return nil
   249  		})
   250  	}()
   251  
   252  	c := make(chan struct{})
   253  	go func() {
   254  		defer close(c)
   255  		wg.Wait()
   256  	}()
   257  
   258  	timeout := false
   259  
   260  	// here we will wait for a second, to make sure that we are not signaling a changed schema.
   261  	select {
   262  	case <-c:
   263  	case <-time.After(1 * time.Second):
   264  		timeout = true
   265  	}
   266  
   267  	assert.True(t, timeout, "should have timed out")
   268  }
   269  
   270  func TestInitialReloadSchema(t *testing.T) {
   271  	db := fakesqldb.New(t)
   272  	defer db.Close()
   273  	config := newConfig(db)
   274  	// Setting the signal schema change reload interval to one minute
   275  	// that way we can test the initial reload trigger.
   276  	config.SignalSchemaChangeReloadIntervalSeconds.Set(1 * time.Minute)
   277  	config.SignalWhenSchemaChange = true
   278  
   279  	env := tabletenv.NewEnv(config, "ReplTrackerTest")
   280  	alias := &topodatapb.TabletAlias{
   281  		Cell: "cell",
   282  		Uid:  1,
   283  	}
   284  	blpFunc = testBlpFunc
   285  	hs := newHealthStreamer(env, alias)
   286  
   287  	target := &querypb.Target{TabletType: topodatapb.TabletType_PRIMARY}
   288  	configs := config.DB
   289  
   290  	db.AddQueryPattern(mysql.ClearSchemaCopy+".*", &sqltypes.Result{})
   291  	db.AddQueryPattern(mysql.InsertIntoSchemaCopy+".*", &sqltypes.Result{})
   292  	db.AddQuery("begin", &sqltypes.Result{})
   293  	db.AddQuery("commit", &sqltypes.Result{})
   294  	db.AddQuery("rollback", &sqltypes.Result{})
   295  	db.AddQuery(mysql.DetectSchemaChange, sqltypes.MakeTestResult(
   296  		sqltypes.MakeTestFields(
   297  			"table_name",
   298  			"varchar",
   299  		),
   300  		"product",
   301  		"users",
   302  	))
   303  	db.AddQuery(mysql.SelectAllViews, &sqltypes.Result{})
   304  
   305  	hs.InitDBConfig(target, configs.DbaWithDB())
   306  	hs.Open()
   307  	defer hs.Close()
   308  	var wg sync.WaitGroup
   309  	wg.Add(1)
   310  	go func() {
   311  		hs.Stream(ctx, func(response *querypb.StreamHealthResponse) error {
   312  			if response.RealtimeStats.TableSchemaChanged != nil {
   313  				assert.Equal(t, []string{"product", "users"}, response.RealtimeStats.TableSchemaChanged)
   314  				wg.Done()
   315  			}
   316  			return nil
   317  		})
   318  	}()
   319  
   320  	c := make(chan struct{})
   321  	go func() {
   322  		defer close(c)
   323  		wg.Wait()
   324  	}()
   325  	select {
   326  	case <-c:
   327  	case <-time.After(1 * time.Second):
   328  		// should not timeout despite SignalSchemaChangeReloadIntervalSeconds being set to 1 minute
   329  		t.Errorf("timed out")
   330  	}
   331  }
   332  
   333  // TestReloadView tests that the health streamer tracks view changes correctly
   334  func TestReloadView(t *testing.T) {
   335  	db := fakesqldb.New(t)
   336  	defer db.Close()
   337  	config := newConfig(db)
   338  	config.SignalSchemaChangeReloadIntervalSeconds.Set(100 * time.Millisecond)
   339  	config.EnableViews = true
   340  
   341  	env := tabletenv.NewEnv(config, "TestReloadView")
   342  	alias := &topodatapb.TabletAlias{Cell: "cell", Uid: 1}
   343  	hs := newHealthStreamer(env, alias)
   344  
   345  	target := &querypb.Target{TabletType: topodatapb.TabletType_PRIMARY}
   346  	configs := config.DB
   347  
   348  	db.AddQuery(mysql.DetectSchemaChangeOnlyBaseTable, &sqltypes.Result{})
   349  	db.AddQuery(mysql.SelectAllViews, &sqltypes.Result{})
   350  
   351  	hs.InitDBConfig(target, configs.DbaWithDB())
   352  	hs.Open()
   353  	defer hs.Close()
   354  
   355  	tcases := []struct {
   356  		res *sqltypes.Result
   357  		exp []string
   358  	}{{
   359  		// view_a and view_b added.
   360  		res: sqltypes.MakeTestResult(sqltypes.MakeTestFields("table_name|updated_at", "varchar|timestamp"),
   361  			"view_a|2023-01-12 14:23:33", "view_b|2023-01-12 15:23:33"),
   362  		exp: []string{"view_a", "view_b"},
   363  	}, {
   364  		// view_b modified
   365  		res: sqltypes.MakeTestResult(sqltypes.MakeTestFields("table_name|updated_at", "varchar|timestamp"),
   366  			"view_a|2023-01-12 14:23:33", "view_b|2023-01-12 18:23:33"),
   367  		exp: []string{"view_b"},
   368  	}, {
   369  		// view_a modified, view_b deleted and view_c added.
   370  		res: sqltypes.MakeTestResult(sqltypes.MakeTestFields("table_name|updated_at", "varchar|timestamp"),
   371  			"view_a|2023-01-12 16:23:33", "view_c|2023-01-12 18:23:33"),
   372  		exp: []string{"view_a", "view_b", "view_c"},
   373  	}}
   374  
   375  	// setting first test case result.
   376  	db.AddQuery(mysql.SelectAllViews, tcases[0].res)
   377  
   378  	var tcCount sync2.AtomicInt32
   379  	ch := make(chan struct{})
   380  
   381  	go func() {
   382  		hs.Stream(ctx, func(response *querypb.StreamHealthResponse) error {
   383  			if response.RealtimeStats.ViewSchemaChanged != nil {
   384  				sort.Strings(response.RealtimeStats.ViewSchemaChanged)
   385  				assert.Equal(t, tcases[tcCount.Get()].exp, response.RealtimeStats.ViewSchemaChanged)
   386  				tcCount.Add(1)
   387  				ch <- struct{}{}
   388  			}
   389  			return nil
   390  		})
   391  	}()
   392  
   393  	for {
   394  		select {
   395  		case <-ch:
   396  			if tcCount.Get() == int32(len(tcases)) {
   397  				return
   398  			}
   399  			db.AddQuery(mysql.SelectAllViews, tcases[tcCount.Get()].res)
   400  		case <-time.After(1000 * time.Second):
   401  			t.Fatalf("timed out")
   402  		}
   403  	}
   404  
   405  }
   406  
   407  func testStream(hs *healthStreamer) (<-chan *querypb.StreamHealthResponse, context.CancelFunc) {
   408  	ctx, cancel := context.WithCancel(context.Background())
   409  	ch := make(chan *querypb.StreamHealthResponse)
   410  	go func() {
   411  		_ = hs.Stream(ctx, func(shr *querypb.StreamHealthResponse) error {
   412  			ch <- shr
   413  			return nil
   414  		})
   415  	}()
   416  	return ch, cancel
   417  }
   418  
   419  func testBlpFunc() (int64, int32) {
   420  	return 1, 2
   421  }