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 }