github.com/pingcap/br@v5.3.0-alpha.0.20220125034240-ec59c7b6ce30+incompatible/pkg/mock/mock_cluster.go (about) 1 // Copyright 2020 PingCAP, Inc. Licensed under Apache-2.0. 2 3 package mock 4 5 import ( 6 "database/sql" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/pprof" 11 "net/url" 12 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/go-sql-driver/mysql" 18 "github.com/pingcap/errors" 19 "github.com/pingcap/log" 20 "github.com/pingcap/tidb/config" 21 "github.com/pingcap/tidb/domain" 22 "github.com/pingcap/tidb/kv" 23 "github.com/pingcap/tidb/server" 24 "github.com/pingcap/tidb/session" 25 "github.com/pingcap/tidb/store/mockstore" 26 "github.com/tikv/client-go/v2/testutils" 27 "github.com/tikv/client-go/v2/tikv" 28 pd "github.com/tikv/pd/client" 29 "github.com/tikv/pd/pkg/tempurl" 30 "go.uber.org/zap" 31 ) 32 33 var pprofOnce sync.Once 34 35 // Cluster is mock tidb cluster, includes tikv and pd. 36 type Cluster struct { 37 *server.Server 38 testutils.Cluster 39 kv.Storage 40 *server.TiDBDriver 41 *domain.Domain 42 DSN string 43 PDClient pd.Client 44 } 45 46 // NewCluster create a new mock cluster. 47 func NewCluster() (*Cluster, error) { 48 pprofOnce.Do(func() { 49 go func() { 50 // Make sure pprof is registered. 51 _ = pprof.Handler 52 addr := "0.0.0.0:12235" 53 log.Info("start pprof", zap.String("addr", addr)) 54 if e := http.ListenAndServe(addr, nil); e != nil { 55 log.Warn("fail to start pprof", zap.String("addr", addr), zap.Error(e)) 56 } 57 }() 58 }) 59 60 var mockCluster testutils.Cluster 61 storage, err := mockstore.NewMockStore( 62 mockstore.WithClusterInspector(func(c testutils.Cluster) { 63 mockstore.BootstrapWithSingleStore(c) 64 mockCluster = c 65 }), 66 ) 67 if err != nil { 68 return nil, errors.Trace(err) 69 } 70 session.SetSchemaLease(0) 71 session.DisableStats4Test() 72 dom, err := session.BootstrapSession(storage) 73 if err != nil { 74 return nil, errors.Trace(err) 75 } 76 return &Cluster{ 77 Storage: storage, 78 Cluster: mockCluster, 79 Domain: dom, 80 PDClient: storage.(tikv.Storage).GetRegionCache().PDClient(), 81 }, nil 82 } 83 84 // Start runs a mock cluster. 85 func (mock *Cluster) Start() error { 86 statusURL, err := url.Parse(tempurl.Alloc()) 87 if err != nil { 88 return errors.Trace(err) 89 } 90 statusPort, err := strconv.ParseInt(statusURL.Port(), 10, 32) 91 if err != nil { 92 return errors.Trace(err) 93 } 94 95 addrURL, err := url.Parse(tempurl.Alloc()) 96 if err != nil { 97 return errors.Trace(err) 98 } 99 addrPort, err := strconv.ParseInt(addrURL.Port(), 10, 32) 100 if err != nil { 101 return errors.Trace(err) 102 } 103 _ = addrPort 104 105 mock.TiDBDriver = server.NewTiDBDriver(mock.Storage) 106 cfg := config.NewConfig() 107 cfg.Port = uint(addrPort) 108 cfg.Store = "tikv" 109 cfg.Status.StatusPort = uint(statusPort) 110 cfg.Status.ReportStatus = true 111 112 svr, err := server.NewServer(cfg, mock.TiDBDriver) 113 if err != nil { 114 return errors.Trace(err) 115 } 116 mock.Server = svr 117 go func() { 118 if err1 := svr.Run(); err != nil { 119 panic(err1) 120 } 121 }() 122 mock.DSN = waitUntilServerOnline(addrURL.Host, cfg.Status.StatusPort) 123 return nil 124 } 125 126 // Stop stops a mock cluster. 127 func (mock *Cluster) Stop() { 128 if mock.Domain != nil { 129 mock.Domain.Close() 130 } 131 if mock.Storage != nil { 132 mock.Storage.Close() 133 } 134 if mock.Server != nil { 135 mock.Server.Close() 136 } 137 } 138 139 type configOverrider func(*mysql.Config) 140 141 const retryTime = 100 142 143 var defaultDSNConfig = mysql.Config{ 144 User: "root", 145 Net: "tcp", 146 Addr: "127.0.0.1:4001", 147 } 148 149 // getDSN generates a DSN string for MySQL connection. 150 func getDSN(overriders ...configOverrider) string { 151 cfg := defaultDSNConfig 152 for _, overrider := range overriders { 153 if overrider != nil { 154 overrider(&cfg) 155 } 156 } 157 return cfg.FormatDSN() 158 } 159 160 func waitUntilServerOnline(addr string, statusPort uint) string { 161 // connect server 162 retry := 0 163 dsn := getDSN(func(cfg *mysql.Config) { 164 cfg.Addr = addr 165 }) 166 for ; retry < retryTime; retry++ { 167 time.Sleep(time.Millisecond * 10) 168 db, err := sql.Open("mysql", dsn) 169 if err == nil { 170 db.Close() 171 break 172 } 173 } 174 if retry == retryTime { 175 log.Panic("failed to connect DB in every 10 ms", zap.Int("retryTime", retryTime)) 176 } 177 // connect http status 178 statusURL := fmt.Sprintf("http://127.0.0.1:%d/status", statusPort) 179 for retry = 0; retry < retryTime; retry++ { 180 resp, err := http.Get(statusURL) // nolint:noctx 181 if err == nil { 182 // Ignore errors. 183 _, _ = io.ReadAll(resp.Body) 184 _ = resp.Body.Close() 185 break 186 } 187 time.Sleep(time.Millisecond * 10) 188 } 189 if retry == retryTime { 190 log.Panic("failed to connect HTTP status in every 10 ms", 191 zap.Int("retryTime", retryTime)) 192 } 193 return strings.SplitAfter(dsn, "/")[0] 194 }