vitess.io/vitess@v0.16.2/go/vt/wrangler/testlib/fake_tablet.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  /*
    18  Package testlib contains utility methods to include in unit tests to
    19  deal with topology common tasks, like fake tablets and action loops.
    20  */
    21  package testlib
    22  
    23  import (
    24  	"context"
    25  	"net"
    26  	"net/http"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/stretchr/testify/require"
    31  	"google.golang.org/grpc"
    32  
    33  	"vitess.io/vitess/go/mysql/fakesqldb"
    34  	"vitess.io/vitess/go/netutil"
    35  	"vitess.io/vitess/go/vt/binlog/binlogplayer"
    36  	"vitess.io/vitess/go/vt/dbconfigs"
    37  	"vitess.io/vitess/go/vt/mysqlctl/fakemysqldaemon"
    38  	"vitess.io/vitess/go/vt/topo"
    39  	"vitess.io/vitess/go/vt/topo/topoproto"
    40  	"vitess.io/vitess/go/vt/vttablet/grpctmserver"
    41  	"vitess.io/vitess/go/vt/vttablet/tabletconntest"
    42  	"vitess.io/vitess/go/vt/vttablet/tabletmanager"
    43  	"vitess.io/vitess/go/vt/vttablet/tabletmanager/vreplication"
    44  	"vitess.io/vitess/go/vt/vttablet/tabletservermock"
    45  	"vitess.io/vitess/go/vt/vttablet/tmclient"
    46  	"vitess.io/vitess/go/vt/vttablet/tmclienttest"
    47  	"vitess.io/vitess/go/vt/wrangler"
    48  
    49  	querypb "vitess.io/vitess/go/vt/proto/query"
    50  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    51  
    52  	// import the gRPC client implementation for tablet manager
    53  	_ "vitess.io/vitess/go/vt/vttablet/grpctmclient"
    54  
    55  	// import the gRPC client implementation for query service
    56  	_ "vitess.io/vitess/go/vt/vttablet/grpctabletconn"
    57  )
    58  
    59  // This file contains utility methods for unit tests.
    60  // We allow the creation of fake tablets, and running their event loop based
    61  // on a FakeMysqlDaemon.
    62  
    63  // FakeTablet keeps track of a fake tablet in memory. It has:
    64  // - a Tablet record (used for creating the tablet, kept for user's information)
    65  // - a FakeMysqlDaemon (used by the fake event loop)
    66  // - a 'done' channel (used to terminate the fake event loop)
    67  type FakeTablet struct {
    68  	// Tablet and FakeMysqlDaemon are populated at NewFakeTablet time.
    69  	// We also create the RPCServer, so users can register more services
    70  	// before calling StartActionLoop().
    71  	Tablet          *topodatapb.Tablet
    72  	FakeMysqlDaemon *fakemysqldaemon.FakeMysqlDaemon
    73  	RPCServer       *grpc.Server
    74  
    75  	// The following fields are created when we start the event loop for
    76  	// the tablet, and closed / cleared when we stop it.
    77  	// The Listener is used by the gRPC server.
    78  	TM       *tabletmanager.TabletManager
    79  	Listener net.Listener
    80  
    81  	// These optional fields are used if the tablet also needs to
    82  	// listen on the 'vt' port.
    83  	StartHTTPServer bool
    84  	HTTPListener    net.Listener
    85  	HTTPServer      *http.Server
    86  }
    87  
    88  // TabletOption is an interface for changing tablet parameters.
    89  // It's a way to pass multiple parameters to NewFakeTablet without
    90  // making it too cumbersome.
    91  type TabletOption func(tablet *topodatapb.Tablet)
    92  
    93  // TabletKeyspaceShard is the option to set the tablet keyspace and shard
    94  func TabletKeyspaceShard(t *testing.T, keyspace, shard string) TabletOption {
    95  	return func(tablet *topodatapb.Tablet) {
    96  		tablet.Keyspace = keyspace
    97  		shard, kr, err := topo.ValidateShardName(shard)
    98  		if err != nil {
    99  			t.Fatalf("cannot ValidateShardName value %v", shard)
   100  		}
   101  		tablet.Shard = shard
   102  		tablet.KeyRange = kr
   103  	}
   104  }
   105  
   106  // ForceInitTablet is the tablet option to set the 'force' flag during InitTablet
   107  func ForceInitTablet() TabletOption {
   108  	return func(tablet *topodatapb.Tablet) {
   109  		// set the force_init field into the portmap as a hack
   110  		tablet.PortMap["force_init"] = 1
   111  	}
   112  }
   113  
   114  // StartHTTPServer is the tablet option to start the HTTP server when
   115  // starting a tablet.
   116  func StartHTTPServer() TabletOption {
   117  	return func(tablet *topodatapb.Tablet) {
   118  		// set the start_http_server field into the portmap as a hack
   119  		tablet.PortMap["start_http_server"] = 1
   120  	}
   121  }
   122  
   123  // NewFakeTablet creates the test tablet in the topology.  'uid'
   124  // has to be between 0 and 99. All the tablet info will be derived
   125  // from that. Look at the implementation if you need values.
   126  // Use TabletOption implementations if you need to change values at creation.
   127  // 'db' can be nil if the test doesn't use a database at all.
   128  func NewFakeTablet(t *testing.T, wr *wrangler.Wrangler, cell string, uid uint32, tabletType topodatapb.TabletType, db *fakesqldb.DB, options ...TabletOption) *FakeTablet {
   129  	t.Helper()
   130  
   131  	if uid > 99 {
   132  		t.Fatalf("uid has to be between 0 and 99: %v", uid)
   133  	}
   134  	mysqlPort := int32(3300 + uid)
   135  	hostname, err := netutil.FullyQualifiedHostname()
   136  	require.NoError(t, err)
   137  	tablet := &topodatapb.Tablet{
   138  		Alias:         &topodatapb.TabletAlias{Cell: cell, Uid: uid},
   139  		Hostname:      hostname,
   140  		MysqlHostname: hostname,
   141  		PortMap: map[string]int32{
   142  			"vt":   int32(8100 + uid),
   143  			"grpc": int32(8200 + uid),
   144  		},
   145  		Keyspace: "test_keyspace",
   146  		Shard:    "0",
   147  		Type:     tabletType,
   148  	}
   149  	tablet.MysqlPort = mysqlPort
   150  	for _, option := range options {
   151  		option(tablet)
   152  	}
   153  	_, startHTTPServer := tablet.PortMap["start_http_server"]
   154  	delete(tablet.PortMap, "start_http_server")
   155  	_, force := tablet.PortMap["force_init"]
   156  	delete(tablet.PortMap, "force_init")
   157  	if err := wr.TopoServer().InitTablet(context.Background(), tablet, force, true /* createShardAndKeyspace */, false /* allowUpdate */); err != nil {
   158  		t.Fatalf("cannot create tablet %v: %v", uid, err)
   159  	}
   160  
   161  	// create a FakeMysqlDaemon with the right information by default
   162  	fakeMysqlDaemon := fakemysqldaemon.NewFakeMysqlDaemon(db)
   163  	fakeMysqlDaemon.MysqlPort.Set(mysqlPort)
   164  
   165  	return &FakeTablet{
   166  		Tablet:          tablet,
   167  		FakeMysqlDaemon: fakeMysqlDaemon,
   168  		RPCServer:       grpc.NewServer(),
   169  		StartHTTPServer: startHTTPServer,
   170  	}
   171  }
   172  
   173  // StartActionLoop will start the action loop for a fake tablet,
   174  // using ft.FakeMysqlDaemon as the backing mysqld.
   175  func (ft *FakeTablet) StartActionLoop(t *testing.T, wr *wrangler.Wrangler) {
   176  	t.Helper()
   177  	if ft.TM != nil {
   178  		t.Fatalf("TM for %v is already running", ft.Tablet.Alias)
   179  	}
   180  
   181  	// Listen on a random port for gRPC.
   182  	var err error
   183  	ft.Listener, err = net.Listen("tcp", ":0")
   184  	if err != nil {
   185  		t.Fatalf("Cannot listen: %v", err)
   186  	}
   187  	gRPCPort := int32(ft.Listener.Addr().(*net.TCPAddr).Port)
   188  
   189  	// If needed, listen on a random port for HTTP.
   190  	vtPort := ft.Tablet.PortMap["vt"]
   191  	if ft.StartHTTPServer {
   192  		ft.HTTPListener, err = net.Listen("tcp", ":0")
   193  		if err != nil {
   194  			t.Fatalf("Cannot listen on http port: %v", err)
   195  		}
   196  		handler := http.NewServeMux()
   197  		ft.HTTPServer = &http.Server{
   198  			Handler: handler,
   199  		}
   200  		go ft.HTTPServer.Serve(ft.HTTPListener)
   201  		vtPort = int32(ft.HTTPListener.Addr().(*net.TCPAddr).Port)
   202  	}
   203  	ft.Tablet.PortMap["vt"] = vtPort
   204  	ft.Tablet.PortMap["grpc"] = gRPCPort
   205  
   206  	// Create a test tm on that port, and re-read the record
   207  	// (it has new ports and IP).
   208  	ft.TM = &tabletmanager.TabletManager{
   209  		BatchCtx:            context.Background(),
   210  		TopoServer:          wr.TopoServer(),
   211  		MysqlDaemon:         ft.FakeMysqlDaemon,
   212  		DBConfigs:           &dbconfigs.DBConfigs{},
   213  		QueryServiceControl: tabletservermock.NewController(),
   214  		VREngine:            vreplication.NewTestEngine(wr.TopoServer(), ft.Tablet.Alias.Cell, ft.FakeMysqlDaemon, binlogplayer.NewFakeDBClient, binlogplayer.NewFakeDBClient, topoproto.TabletDbName(ft.Tablet), nil),
   215  	}
   216  	if err := ft.TM.Start(ft.Tablet, 0); err != nil {
   217  		t.Fatalf("Error in tablet - %v, err - %v", topoproto.TabletAliasString(ft.Tablet.Alias), err.Error())
   218  	}
   219  	ft.Tablet = ft.TM.Tablet()
   220  
   221  	// Register the gRPC server, and starts listening.
   222  	grpctmserver.RegisterForTest(ft.RPCServer, ft.TM)
   223  	go ft.RPCServer.Serve(ft.Listener)
   224  
   225  	// And wait for it to serve, so we don't start using it before it's
   226  	// ready.
   227  	timeout := 5 * time.Second
   228  	step := 10 * time.Millisecond
   229  	c := tmclient.NewTabletManagerClient()
   230  	for timeout >= 0 {
   231  		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
   232  		err := c.Ping(ctx, ft.TM.Tablet())
   233  		cancel()
   234  		if err == nil {
   235  			break
   236  		}
   237  		time.Sleep(step)
   238  		timeout -= step
   239  	}
   240  	if timeout < 0 {
   241  		panic("StartActionLoop failed.")
   242  	}
   243  }
   244  
   245  // StopActionLoop will stop the Action Loop for the given FakeTablet
   246  func (ft *FakeTablet) StopActionLoop(t *testing.T) {
   247  	if ft.TM == nil {
   248  		t.Fatalf("TM for %v is not running", ft.Tablet.Alias)
   249  	}
   250  	if ft.StartHTTPServer {
   251  		ft.HTTPListener.Close()
   252  	}
   253  	ft.Listener.Close()
   254  	ft.TM.Stop()
   255  	ft.TM = nil
   256  	ft.Listener = nil
   257  	ft.HTTPListener = nil
   258  }
   259  
   260  // Target returns the keyspace/shard/type info of this tablet as Target.
   261  func (ft *FakeTablet) Target() *querypb.Target {
   262  	return &querypb.Target{
   263  		Keyspace:   ft.Tablet.Keyspace,
   264  		Shard:      ft.Tablet.Shard,
   265  		TabletType: ft.Tablet.Type,
   266  	}
   267  }
   268  
   269  func init() {
   270  	// enforce we will use the right protocol (gRPC) in all unit tests
   271  	tabletconntest.SetProtocol("go.vt.wrangler.testlib", "grpc")
   272  	tmclienttest.SetProtocol("go.vt.wrangler.testlib", "grpc")
   273  }