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  }