github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/servermaster/server_test.go (about) 1 // Copyright 2022 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package servermaster 15 16 import ( 17 "context" 18 "encoding/json" 19 "fmt" 20 "io" 21 "net" 22 "net/http" 23 "strings" 24 "sync" 25 "testing" 26 "time" 27 28 "github.com/golang/mock/gomock" 29 "github.com/phayes/freeport" 30 pb "github.com/pingcap/tiflow/engine/enginepb" 31 "github.com/pingcap/tiflow/engine/model" 32 "github.com/pingcap/tiflow/engine/pkg/openapi" 33 "github.com/pingcap/tiflow/engine/pkg/p2p" 34 "github.com/pingcap/tiflow/pkg/election" 35 electionMock "github.com/pingcap/tiflow/pkg/election/mock" 36 "github.com/pingcap/tiflow/pkg/errors" 37 "github.com/pingcap/tiflow/pkg/httputil" 38 "github.com/pingcap/tiflow/pkg/logutil" 39 "github.com/stretchr/testify/require" 40 ) 41 42 func init() { 43 err := logutil.InitLogger(&logutil.Config{Level: "warn"}) 44 if err != nil { 45 panic(err) 46 } 47 } 48 49 func prepareServerEnv(t *testing.T) *Config { 50 ports, err := freeport.GetFreePorts(1) 51 require.NoError(t, err) 52 cfgTpl := ` 53 addr = "127.0.0.1:%d" 54 advertise-addr = "127.0.0.1:%d" 55 [framework-meta] 56 store-id = "root" 57 endpoints = ["127.0.0.1:%d"] 58 schema = "test0" 59 user = "root" 60 [business-meta] 61 store-id = "default" 62 endpoints = ["127.0.0.1:%d"] 63 schema = "test1" 64 ` 65 cfgStr := fmt.Sprintf(cfgTpl, ports[0], ports[0], ports[0], ports[0]) 66 cfg := GetDefaultMasterConfig() 67 err = cfg.configFromString(cfgStr) 68 require.Nil(t, err) 69 err = cfg.AdjustAndValidate() 70 require.Nil(t, err) 71 72 cfg.Addr = fmt.Sprintf("127.0.0.1:%d", ports[0]) 73 74 return cfg 75 } 76 77 func newMockElector(t *testing.T) election.Elector { 78 elector := electionMock.NewMockElector(gomock.NewController(t)) 79 elector.EXPECT().IsLeader().AnyTimes().Return(true) 80 return elector 81 } 82 83 // Disable parallel run for this case, because prometheus http handler will meet 84 // data race if parallel run is enabled 85 func TestServe(t *testing.T) { 86 cfg := prepareServerEnv(t) 87 s := &Server{ 88 cfg: cfg, 89 msgService: p2p.NewMessageRPCServiceWithRPCServer("servermaster", nil, nil), 90 leaderDegrader: newFeatureDegrader(), 91 elector: electionMock.NewMockElector(gomock.NewController(t)), 92 } 93 94 ctx, cancel := context.WithCancel(context.Background()) 95 96 var wg sync.WaitGroup 97 wg.Add(1) 98 go func() { 99 defer wg.Done() 100 _ = s.serve(ctx) 101 }() 102 103 require.Eventually(t, func() bool { 104 conn, err := net.Dial("tcp", cfg.Addr) 105 if err != nil { 106 return false 107 } 108 _ = conn.Close() 109 return true 110 }, time.Second*5, time.Millisecond*100, "wait for server to start") 111 112 apiURL := "http://" + cfg.Addr 113 testPprof(t, apiURL) 114 testPrometheusMetrics(t, apiURL) 115 116 cancel() 117 wg.Wait() 118 } 119 120 func testPprof(t *testing.T, addr string) { 121 ctx := context.Background() 122 urls := []string{ 123 "/debug/pprof/", 124 "/debug/pprof/cmdline", 125 "/debug/pprof/symbol", 126 // enable these two apis will make ut slow 127 //"/debug/pprof/profile", http.MethodGet, 128 //"/debug/pprof/trace", http.MethodGet, 129 "/debug/pprof/threadcreate", 130 "/debug/pprof/allocs", 131 "/debug/pprof/block", 132 "/debug/pprof/goroutine?debug=1", 133 "/debug/pprof/mutex?debug=1", 134 } 135 cli, err := httputil.NewClient(nil) 136 require.NoError(t, err) 137 for _, uri := range urls { 138 resp, err := cli.Get(ctx, addr+uri) 139 require.NoError(t, err) 140 require.Equal(t, http.StatusOK, resp.StatusCode) 141 _, err = io.ReadAll(resp.Body) 142 require.NoError(t, err) 143 require.NoError(t, resp.Body.Close()) 144 } 145 } 146 147 func testPrometheusMetrics(t *testing.T, addr string) { 148 ctx := context.Background() 149 cli, err := httputil.NewClient(nil) 150 require.NoError(t, err) 151 resp, err := cli.Get(ctx, addr+"/metrics") 152 require.NoError(t, err) 153 defer resp.Body.Close() 154 require.Equal(t, http.StatusOK, resp.StatusCode) 155 _, err = io.ReadAll(resp.Body) 156 require.NoError(t, err) 157 } 158 159 type mockJobManager struct { 160 JobManager 161 jobMu sync.RWMutex 162 jobs map[pb.Job_State][]*pb.Job 163 } 164 165 func (m *mockJobManager) GetJob(ctx context.Context, req *pb.GetJobRequest) (*pb.Job, error) { 166 for _, jobs := range m.jobs { 167 for _, job := range jobs { 168 if job.GetId() == req.GetId() { 169 return job, nil 170 } 171 } 172 } 173 return nil, errors.ErrJobNotFound.GenWithStackByArgs(req.GetId()) 174 } 175 176 func (m *mockJobManager) JobCount(status pb.Job_State) int { 177 m.jobMu.RLock() 178 defer m.jobMu.RUnlock() 179 return len(m.jobs[status]) 180 } 181 182 type mockExecutorManager struct { 183 ExecutorManager 184 executorMu sync.RWMutex 185 count map[model.ExecutorStatus]int 186 } 187 188 func (m *mockExecutorManager) ExecutorCount(status model.ExecutorStatus) int { 189 m.executorMu.RLock() 190 defer m.executorMu.RUnlock() 191 return m.count[status] 192 } 193 194 func TestCollectMetric(t *testing.T) { 195 cfg := prepareServerEnv(t) 196 197 s := &Server{ 198 cfg: cfg, 199 metrics: newServerMasterMetric(), 200 msgService: p2p.NewMessageRPCServiceWithRPCServer("servermaster", nil, nil), 201 leaderDegrader: newFeatureDegrader(), 202 elector: newMockElector(t), 203 } 204 ctx, cancel := context.WithCancel(context.Background()) 205 206 var wg sync.WaitGroup 207 wg.Add(1) 208 go func() { 209 defer wg.Done() 210 _ = s.serve(ctx) 211 }() 212 213 jobManager := &mockJobManager{ 214 jobs: map[pb.Job_State][]*pb.Job{ 215 pb.Job_Running: { 216 &pb.Job{ 217 Id: "job-1", 218 }, 219 &pb.Job{ 220 Id: "job-2", 221 }, 222 &pb.Job{ 223 Id: "job-3", 224 }, 225 }, 226 }, 227 } 228 executorManager := &mockExecutorManager{ 229 count: map[model.ExecutorStatus]int{ 230 model.Initing: 1, 231 model.Running: 2, 232 }, 233 } 234 s.jobManager = jobManager 235 s.executorManager = executorManager 236 237 s.collectLeaderMetric() 238 apiURL := fmt.Sprintf("http://%s", cfg.Addr) 239 testCustomedPrometheusMetrics(t, apiURL) 240 241 cancel() 242 wg.Wait() 243 } 244 245 func testCustomedPrometheusMetrics(t *testing.T, addr string) { 246 ctx := context.Background() 247 cli, err := httputil.NewClient(nil) 248 require.NoError(t, err) 249 require.Eventually(t, func() bool { 250 resp, err := cli.Get(ctx, addr+"/metrics") 251 require.NoError(t, err) 252 defer resp.Body.Close() 253 require.Equal(t, http.StatusOK, resp.StatusCode) 254 body, err := io.ReadAll(resp.Body) 255 require.NoError(t, err) 256 metric := string(body) 257 return strings.Contains(metric, "tiflow_server_master_job_num") && 258 strings.Contains(metric, "tiflow_server_master_executor_num") 259 }, time.Second, time.Millisecond*20) 260 } 261 262 func TestHTTPErrorHandler(t *testing.T) { 263 cfg := prepareServerEnv(t) 264 265 s := &Server{ 266 cfg: cfg, 267 msgService: p2p.NewMessageRPCServiceWithRPCServer("servermaster", nil, nil), 268 jobManager: &mockJobManager{ 269 jobs: map[pb.Job_State][]*pb.Job{ 270 pb.Job_Running: { 271 &pb.Job{ 272 Id: "job-1", 273 }, 274 }, 275 }, 276 }, 277 leaderDegrader: newFeatureDegrader(), 278 elector: newMockElector(t), 279 forwardChecker: newForwardChecker(newMockElector(t)), 280 } 281 s.leaderDegrader.updateMasterWorkerManager(true) 282 283 ctx, cancel := context.WithCancel(context.Background()) 284 defer cancel() 285 286 var wg sync.WaitGroup 287 wg.Add(1) 288 go func() { 289 defer wg.Done() 290 _ = s.serve(ctx) 291 }() 292 293 require.Eventually(t, func() bool { 294 conn, err := net.Dial("tcp", cfg.Addr) 295 if err != nil { 296 return false 297 } 298 require.NoError(t, conn.Close()) 299 return true 300 }, time.Second*5, time.Millisecond*100, "wait for server start") 301 302 cli, err := httputil.NewClient(nil) 303 require.NoError(t, err) 304 305 resp, err := cli.Get(ctx, fmt.Sprintf("http://%s/api/v1/jobs/job-1", cfg.Addr)) 306 require.NoError(t, err) 307 require.NoError(t, resp.Body.Close()) 308 require.Equal(t, http.StatusOK, resp.StatusCode) 309 310 resp, err = cli.Get(ctx, fmt.Sprintf("http://%s/api/v1/jobs/job-2", cfg.Addr)) 311 require.NoError(t, err) 312 require.Equal(t, http.StatusNotFound, resp.StatusCode) 313 body, err := io.ReadAll(resp.Body) 314 require.NoError(t, err) 315 err = resp.Body.Close() 316 require.NoError(t, err) 317 318 var httpErr openapi.HTTPError 319 err = json.Unmarshal(body, &httpErr) 320 require.NoError(t, err) 321 require.Equal(t, string(errors.ErrJobNotFound.RFCCode()), httpErr.Code) 322 323 cancel() 324 wg.Wait() 325 }