github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/openapi_controller.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  // MVC for dm-master's openapi server
    15  // Model(data in etcd): source of truth
    16  // View(openapi_view): do some inner work such as validate, filter, prepare parameters/response and call controller to update model.
    17  // Controller(openapi_controller): call model func to update data.
    18  
    19  package master
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/pingcap/log"
    28  	"github.com/pingcap/tiflow/dm/checker"
    29  	dmcommon "github.com/pingcap/tiflow/dm/common"
    30  	"github.com/pingcap/tiflow/dm/config"
    31  	"github.com/pingcap/tiflow/dm/ctl/common"
    32  	"github.com/pingcap/tiflow/dm/master/scheduler"
    33  	"github.com/pingcap/tiflow/dm/master/workerrpc"
    34  	"github.com/pingcap/tiflow/dm/openapi"
    35  	"github.com/pingcap/tiflow/dm/pb"
    36  	"github.com/pingcap/tiflow/dm/pkg/ha"
    37  	"github.com/pingcap/tiflow/dm/pkg/terror"
    38  	clientv3 "go.etcd.io/etcd/client/v3"
    39  	"go.uber.org/zap"
    40  )
    41  
    42  // nolint:unparam
    43  func (s *Server) getClusterInfo(ctx context.Context) (*openapi.GetClusterInfoResponse, error) {
    44  	info := &openapi.GetClusterInfoResponse{}
    45  	info.ClusterId = s.ClusterID()
    46  
    47  	resp, err := s.etcdClient.Get(ctx, dmcommon.ClusterTopologyKey)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	// already set by tiup, load to info
    53  	if len(resp.Kvs) == 1 {
    54  		topo := &openapi.ClusterTopology{}
    55  		if err := json.Unmarshal(resp.Kvs[0].Value, topo); err != nil {
    56  			return nil, err
    57  		}
    58  		info.Topology = topo
    59  	}
    60  	return info, nil
    61  }
    62  
    63  func (s *Server) updateClusterInfo(ctx context.Context, topo *openapi.ClusterTopology) (*openapi.GetClusterInfoResponse, error) {
    64  	if val, err := json.Marshal(topo); err != nil {
    65  		return nil, err
    66  	} else if _, err := s.etcdClient.Put(ctx, dmcommon.ClusterTopologyKey, string(val)); err != nil {
    67  		return nil, err
    68  	}
    69  	info := &openapi.GetClusterInfoResponse{}
    70  	info.ClusterId = s.ClusterID()
    71  	info.Topology = topo
    72  	return info, nil
    73  }
    74  
    75  func (s *Server) getSourceStatusListFromWorker(ctx context.Context, sourceName string, specifiedSource bool) ([]openapi.SourceStatus, error) {
    76  	workerStatusList := s.getStatusFromWorkers(ctx, []string{sourceName}, "", specifiedSource)
    77  	sourceStatusList := make([]openapi.SourceStatus, len(workerStatusList))
    78  	for i, workerStatus := range workerStatusList {
    79  		if workerStatus == nil {
    80  			// this should not happen unless the rpc in the worker server has been modified
    81  			return nil, terror.ErrOpenAPICommonError.New("worker's query-status response is nil")
    82  		}
    83  		sourceStatus := openapi.SourceStatus{SourceName: sourceName, WorkerName: workerStatus.SourceStatus.Worker}
    84  		if !workerStatus.Result {
    85  			sourceStatus.ErrorMsg = &workerStatus.Msg
    86  			sourceStatusList[i] = sourceStatus
    87  			continue
    88  		}
    89  
    90  		if relayStatus := workerStatus.SourceStatus.GetRelayStatus(); relayStatus != nil {
    91  			sourceStatus.RelayStatus = &openapi.RelayStatus{
    92  				MasterBinlog:       relayStatus.MasterBinlog,
    93  				MasterBinlogGtid:   relayStatus.MasterBinlogGtid,
    94  				RelayBinlogGtid:    relayStatus.RelayBinlogGtid,
    95  				RelayCatchUpMaster: relayStatus.RelayCatchUpMaster,
    96  				RelayDir:           relayStatus.RelaySubDir,
    97  				Stage:              relayStatus.Stage.String(),
    98  			}
    99  		}
   100  		// add error if some error happen
   101  		if workerStatus.SourceStatus.Result != nil && len(workerStatus.SourceStatus.Result.Errors) > 0 {
   102  			var errorMsgs string
   103  			for _, err := range workerStatus.SourceStatus.Result.Errors {
   104  				errorMsgs += fmt.Sprintf("%s\n", err.Message)
   105  			}
   106  			sourceStatus.ErrorMsg = &errorMsgs
   107  		}
   108  		sourceStatusList[i] = sourceStatus
   109  	}
   110  	return sourceStatusList, nil
   111  }
   112  
   113  func (s *Server) createSource(ctx context.Context, req openapi.CreateSourceRequest) (*openapi.Source, error) {
   114  	cfg := config.OpenAPISourceToSourceCfg(req.Source)
   115  	if err := CheckAndAdjustSourceConfigFunc(ctx, cfg); err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	var err error
   120  	if req.WorkerName == nil {
   121  		err = s.scheduler.AddSourceCfg(cfg)
   122  	} else {
   123  		err = s.scheduler.AddSourceCfgWithWorker(cfg, *req.WorkerName)
   124  	}
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	// TODO: refine relay logic https://github.com/pingcap/tiflow/issues/4985
   129  	if cfg.EnableRelay {
   130  		return &req.Source, s.enableRelay(ctx, req.Source.SourceName, openapi.EnableRelayRequest{})
   131  	}
   132  	return &req.Source, nil
   133  }
   134  
   135  func (s *Server) updateSource(ctx context.Context, sourceName string, req openapi.UpdateSourceRequest) (*openapi.Source, error) {
   136  	// TODO: support dynamic updates
   137  	oldCfg := s.scheduler.GetSourceCfgByID(sourceName)
   138  	if oldCfg == nil {
   139  		return nil, terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   140  	}
   141  	newCfg := config.OpenAPISourceToSourceCfg(req.Source)
   142  
   143  	// update's request will be no password when user doesn't input password and wants to use old password.
   144  	if req.Source.Password == nil {
   145  		newCfg.From.Password = oldCfg.From.Password
   146  	}
   147  
   148  	if err := CheckAndAdjustSourceConfigFunc(ctx, newCfg); err != nil {
   149  		return nil, err
   150  	}
   151  	if err := s.scheduler.UpdateSourceCfg(newCfg); err != nil {
   152  		return nil, err
   153  	}
   154  	// when enable filed updated, we need operate task on this source
   155  	if worker := s.scheduler.GetWorkerBySource(sourceName); worker != nil && newCfg.Enable != oldCfg.Enable {
   156  		stage := pb.Stage_Running
   157  		taskNameList := s.scheduler.GetTaskNameListBySourceName(sourceName, nil)
   158  		if !newCfg.Enable {
   159  			stage = pb.Stage_Stopped
   160  		}
   161  		if err := s.scheduler.BatchOperateTaskOnWorker(ctx, worker, taskNameList, sourceName, stage, false); err != nil {
   162  			return nil, err
   163  		}
   164  	}
   165  	return &req.Source, nil
   166  }
   167  
   168  // nolint:unparam
   169  func (s *Server) deleteSource(ctx context.Context, sourceName string, force bool) error {
   170  	if force {
   171  		for _, taskName := range s.scheduler.GetTaskNameListBySourceName(sourceName, nil) {
   172  			if err := s.scheduler.RemoveSubTasks(taskName, sourceName); err != nil {
   173  				return err
   174  			}
   175  		}
   176  	}
   177  	return s.scheduler.RemoveSourceCfg(sourceName)
   178  }
   179  
   180  func (s *Server) getSource(ctx context.Context, sourceName string, req openapi.DMAPIGetSourceParams) (*openapi.Source, error) {
   181  	sourceCfg := s.scheduler.GetSourceCfgByID(sourceName)
   182  	if sourceCfg == nil {
   183  		return nil, terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   184  	}
   185  	source := config.SourceCfgToOpenAPISource(sourceCfg)
   186  	if req.WithStatus != nil && *req.WithStatus {
   187  		var statusList []openapi.SourceStatus
   188  		statusList, err := s.getSourceStatus(ctx, sourceName)
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  		source.StatusList = &statusList
   193  		// add task name list
   194  		taskNameList := openapi.TaskNameList{}
   195  		taskNameList = append(taskNameList, s.scheduler.GetTaskNameListBySourceName(sourceName, nil)...)
   196  		source.TaskNameList = &taskNameList
   197  	}
   198  	return &source, nil
   199  }
   200  
   201  func (s *Server) getSourceStatus(ctx context.Context, sourceName string) ([]openapi.SourceStatus, error) {
   202  	return s.getSourceStatusListFromWorker(ctx, sourceName, true)
   203  }
   204  
   205  func (s *Server) listSource(ctx context.Context, req openapi.DMAPIGetSourceListParams) ([]openapi.Source, error) {
   206  	sourceCfgM := s.scheduler.GetSourceCfgs()
   207  	openapiSourceList := make([]openapi.Source, 0, len(sourceCfgM))
   208  	// fill status and filter
   209  	for _, sourceCfg := range sourceCfgM {
   210  		// filter by enable_relay
   211  		// TODO(ehco),maybe worker should use sourceConfig.EnableRelay to determine whether start relay
   212  		if req.EnableRelay != nil {
   213  			relayWorkers, err := s.scheduler.GetRelayWorkers(sourceCfg.SourceID)
   214  			if err != nil {
   215  				return nil, err
   216  			}
   217  			if (*req.EnableRelay && len(relayWorkers) == 0) || (!*req.EnableRelay && len(relayWorkers) > 0) {
   218  				continue
   219  			}
   220  		}
   221  		source := config.SourceCfgToOpenAPISource(sourceCfg)
   222  		if req.WithStatus != nil && *req.WithStatus {
   223  			sourceStatusList, err := s.getSourceStatus(ctx, source.SourceName)
   224  			if err != nil {
   225  				return nil, err
   226  			}
   227  			source.StatusList = &sourceStatusList
   228  			// add task name list
   229  			taskNameList := openapi.TaskNameList{}
   230  			taskNameList = append(taskNameList, s.scheduler.GetTaskNameListBySourceName(sourceCfg.SourceID, nil)...)
   231  			source.TaskNameList = &taskNameList
   232  		}
   233  		openapiSourceList = append(openapiSourceList, source)
   234  	}
   235  	return openapiSourceList, nil
   236  }
   237  
   238  // nolint:unparam
   239  func (s *Server) enableRelay(ctx context.Context, sourceName string, req openapi.EnableRelayRequest) error {
   240  	sourceCfg := s.scheduler.GetSourceCfgByID(sourceName)
   241  	if sourceCfg == nil {
   242  		return terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   243  	}
   244  	if req.WorkerNameList == nil {
   245  		worker := s.scheduler.GetWorkerBySource(sourceName)
   246  		if worker == nil {
   247  			return terror.ErrWorkerNoStart.Generate()
   248  		}
   249  		req.WorkerNameList = &openapi.WorkerNameList{worker.BaseInfo().Name}
   250  	}
   251  
   252  	needUpdate := false
   253  	// update relay related in source cfg
   254  	if req.RelayBinlogName != nil && sourceCfg.RelayBinLogName != *req.RelayBinlogName {
   255  		sourceCfg.RelayBinLogName = *req.RelayBinlogName
   256  		needUpdate = true
   257  	}
   258  	if req.RelayBinlogGtid != nil && sourceCfg.RelayBinlogGTID != *req.RelayBinlogGtid {
   259  		sourceCfg.RelayBinlogGTID = *req.RelayBinlogGtid
   260  		needUpdate = true
   261  	}
   262  	if req.RelayDir != nil && sourceCfg.RelayDir != *req.RelayDir {
   263  		sourceCfg.RelayDir = *req.RelayDir
   264  		needUpdate = true
   265  	}
   266  	if needUpdate {
   267  		// update current source relay config before start relay
   268  		if err := s.scheduler.UpdateSourceCfg(sourceCfg); err != nil {
   269  			return err
   270  		}
   271  	}
   272  	return s.scheduler.StartRelay(sourceName, *req.WorkerNameList)
   273  }
   274  
   275  // nolint:unparam
   276  func (s *Server) disableRelay(ctx context.Context, sourceName string, req openapi.DisableRelayRequest) error {
   277  	sourceCfg := s.scheduler.GetSourceCfgByID(sourceName)
   278  	if sourceCfg == nil {
   279  		return terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   280  	}
   281  	if req.WorkerNameList == nil {
   282  		worker := s.scheduler.GetWorkerBySource(sourceName)
   283  		if worker == nil {
   284  			return terror.ErrWorkerNoStart.Generate()
   285  		}
   286  		req.WorkerNameList = &openapi.WorkerNameList{worker.BaseInfo().Name}
   287  	}
   288  	return s.scheduler.StopRelay(sourceName, *req.WorkerNameList)
   289  }
   290  
   291  func (s *Server) purgeRelay(ctx context.Context, sourceName string, req openapi.PurgeRelayRequest) error {
   292  	purgeReq := &workerrpc.Request{
   293  		Type:       workerrpc.CmdPurgeRelay,
   294  		PurgeRelay: &pb.PurgeRelayRequest{Filename: req.RelayBinlogName},
   295  	}
   296  	if req.RelayDir != nil {
   297  		purgeReq.PurgeRelay.SubDir = *req.RelayDir
   298  	}
   299  	// NOTE not all worker that enabled relay is recorded in scheduler, we need refine this later
   300  	workers, err := s.scheduler.GetRelayWorkers(sourceName)
   301  	if err != nil {
   302  		return err
   303  	}
   304  	if len(workers) == 0 {
   305  		return terror.ErrOpenAPICommonError.Generatef("relay worker for source %s not found, please `enable-relay` first", sourceName)
   306  	}
   307  	for _, w := range workers {
   308  		resp, err := w.SendRequest(ctx, purgeReq, s.cfg.RPCTimeout)
   309  		if err != nil {
   310  			return err
   311  		}
   312  		if resp.PurgeRelay.Msg != "" {
   313  			return terror.ErrOpenAPICommonError.Generate(resp.PurgeRelay.Msg)
   314  		}
   315  	}
   316  	return nil
   317  }
   318  
   319  func (s *Server) enableSource(ctx context.Context, sourceName string) error {
   320  	cfg := s.scheduler.GetSourceCfgByID(sourceName)
   321  	if cfg == nil {
   322  		return terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   323  	}
   324  	worker := s.scheduler.GetWorkerBySource(sourceName)
   325  	if worker == nil {
   326  		return terror.ErrWorkerNoStart.Generate()
   327  	}
   328  	if cfg.Enable {
   329  		return nil
   330  	}
   331  	cfg.Enable = true
   332  	if err := s.scheduler.UpdateSourceCfg(cfg); err != nil {
   333  		return err
   334  	}
   335  	taskNameList := s.scheduler.GetTaskNameListBySourceName(sourceName, nil)
   336  	return s.scheduler.BatchOperateTaskOnWorker(ctx, worker, taskNameList, sourceName, pb.Stage_Running, false)
   337  }
   338  
   339  func (s *Server) disableSource(ctx context.Context, sourceName string) error {
   340  	cfg := s.scheduler.GetSourceCfgByID(sourceName)
   341  	if cfg == nil {
   342  		return terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   343  	}
   344  	if !cfg.Enable {
   345  		return nil
   346  	}
   347  	worker := s.scheduler.GetWorkerBySource(sourceName)
   348  	if worker == nil {
   349  		// no need to stop task if the source is not running
   350  		cfg.Enable = false
   351  		return s.scheduler.UpdateSourceCfg(cfg)
   352  	}
   353  	taskNameList := s.scheduler.GetTaskNameListBySourceName(sourceName, nil)
   354  	if err := s.scheduler.BatchOperateTaskOnWorker(ctx, worker, taskNameList, sourceName, pb.Stage_Stopped, false); err != nil {
   355  		return err
   356  	}
   357  	cfg.Enable = false
   358  	return s.scheduler.UpdateSourceCfg(cfg)
   359  }
   360  
   361  func (s *Server) transferSource(ctx context.Context, sourceName, workerName string) error {
   362  	return s.scheduler.TransferSource(ctx, sourceName, workerName)
   363  }
   364  
   365  func (s *Server) checkTask(ctx context.Context, subtaskCfgList []*config.SubTaskConfig, errCnt, warnCnt int64) (string, error) {
   366  	// TODO(ehco) no api for this task now
   367  	return checker.CheckSyncConfigFunc(ctx, subtaskCfgList, errCnt, warnCnt)
   368  }
   369  
   370  func (s *Server) checkOpenAPITaskBeforeOperate(ctx context.Context, task *openapi.Task) ([]*config.SubTaskConfig, string, error) {
   371  	// prepare target db config
   372  	toDBCfg := config.GetTargetDBCfgFromOpenAPITask(task)
   373  	if err := AdjustTargetDBSessionCfg(ctx, toDBCfg); err != nil {
   374  		return nil, "", err
   375  	}
   376  	// prepare source db config source name -> source config
   377  	sourceCfgMap := make(map[string]*config.SourceConfig)
   378  	for _, cfg := range task.SourceConfig.SourceConf {
   379  		if sourceCfg := s.scheduler.GetSourceCfgByID(cfg.SourceName); sourceCfg != nil {
   380  			sourceCfgMap[cfg.SourceName] = sourceCfg
   381  		} else {
   382  			return nil, "", terror.ErrSchedulerSourceCfgNotExist.Generate(cfg.SourceName)
   383  		}
   384  	}
   385  	// generate sub task configs
   386  	subTaskConfigList, err := config.OpenAPITaskToSubTaskConfigs(task, toDBCfg, sourceCfgMap)
   387  	if err != nil {
   388  		return nil, "", err
   389  	}
   390  	stCfgsForCheck, err := s.generateSubTasksForCheck(subTaskConfigList)
   391  	if err != nil {
   392  		return nil, "", err
   393  	}
   394  	// check subtask config
   395  	msg, err := s.checkTask(ctx, stCfgsForCheck, common.DefaultErrorCnt, common.DefaultWarnCnt)
   396  	if err != nil {
   397  		return nil, "", terror.WithClass(err, terror.ClassDMMaster)
   398  	}
   399  	return subTaskConfigList, msg, nil
   400  }
   401  
   402  func (s *Server) createTask(ctx context.Context, req openapi.CreateTaskRequest) (*openapi.OperateTaskResponse, error) {
   403  	task := &req.Task
   404  	if err := task.Adjust(); err != nil {
   405  		return nil, err
   406  	}
   407  	subTaskConfigList, msg, err := s.checkOpenAPITaskBeforeOperate(ctx, task)
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  	res := &openapi.OperateTaskResponse{
   412  		Task:        *task,
   413  		CheckResult: msg,
   414  	}
   415  	return res, s.scheduler.AddSubTasks(false, pb.Stage_Stopped, subtaskCfgPointersToInstances(subTaskConfigList...)...)
   416  }
   417  
   418  func (s *Server) updateTask(ctx context.Context, req openapi.UpdateTaskRequest) (*openapi.OperateTaskResponse, error) {
   419  	task := &req.Task
   420  	if err := task.Adjust(); err != nil {
   421  		return nil, err
   422  	}
   423  	subTaskConfigList, msg, err := s.checkOpenAPITaskBeforeOperate(ctx, task)
   424  	if err != nil {
   425  		return nil, err
   426  	}
   427  	res := &openapi.OperateTaskResponse{
   428  		Task:        *task,
   429  		CheckResult: msg,
   430  	}
   431  	return res, s.scheduler.UpdateSubTasks(ctx, subtaskCfgPointersToInstances(subTaskConfigList...)...)
   432  }
   433  
   434  func (s *Server) deleteTask(ctx context.Context, taskName string, force bool) error {
   435  	// check if there is running task
   436  	var task *openapi.Task
   437  	var err error
   438  	if !force {
   439  		task, err = s.getTask(ctx, taskName, openapi.DMAPIGetTaskParams{})
   440  		if err != nil {
   441  			return err
   442  		}
   443  		for _, sourceConf := range task.SourceConfig.SourceConf {
   444  			stage := s.scheduler.GetExpectSubTaskStage(taskName, sourceConf.SourceName)
   445  			// TODO delete  openapi.TaskStagePasused when use openapi to impl dmctl
   446  			if stage.Expect != pb.Stage_Paused && stage.Expect != pb.Stage_Stopped {
   447  				return terror.ErrOpenAPICommonError.Generatef("task %s have running subtasks, please stop them or delete task with force.", taskName)
   448  			}
   449  		}
   450  	} else {
   451  		task, err = s.getTask(ctx, taskName, openapi.DMAPIGetTaskParams{})
   452  		if err != nil {
   453  			return err
   454  		}
   455  	}
   456  
   457  	// remove meta
   458  	release, err := s.scheduler.AcquireSubtaskLatch(taskName)
   459  	if err != nil {
   460  		return terror.ErrSchedulerLatchInUse.Generate("RemoveMeta", taskName)
   461  	}
   462  	defer release()
   463  
   464  	ignoreCannotConnectError := func(err error) bool {
   465  		if err == nil {
   466  			return true
   467  		}
   468  		if force && strings.Contains(err.Error(), "connect: connection refused") {
   469  			log.L().Warn("connect downstream error when fore delete task", zap.Error(err))
   470  			return true
   471  		}
   472  		return false
   473  	}
   474  
   475  	toDBCfg := config.GetTargetDBCfgFromOpenAPITask(task)
   476  	if adjustErr := AdjustTargetDBSessionCfg(ctx, toDBCfg); adjustErr != nil {
   477  		if !ignoreCannotConnectError(adjustErr) {
   478  			return adjustErr
   479  		}
   480  	}
   481  	metaSchema := *task.MetaSchema
   482  	err = s.removeMetaData(ctx, taskName, metaSchema, toDBCfg)
   483  	if err != nil {
   484  		if !ignoreCannotConnectError(err) {
   485  			return terror.Annotate(err, "while removing metadata")
   486  		}
   487  	}
   488  	release()
   489  	sourceNameList := s.getTaskSourceNameList(taskName)
   490  	// delete subtask on worker
   491  	return s.scheduler.RemoveSubTasks(taskName, sourceNameList...)
   492  }
   493  
   494  func (s *Server) getTask(ctx context.Context, taskName string, req openapi.DMAPIGetTaskParams) (*openapi.Task, error) {
   495  	subTaskConfigM := s.scheduler.GetSubTaskCfgsByTask(taskName)
   496  	if subTaskConfigM == nil {
   497  		return nil, terror.ErrSchedulerTaskNotExist.Generate(taskName)
   498  	}
   499  	subTaskConfigList := make([]*config.SubTaskConfig, 0, len(subTaskConfigM))
   500  	for sourceName := range subTaskConfigM {
   501  		subTaskConfigList = append(subTaskConfigList, subTaskConfigM[sourceName])
   502  	}
   503  	task := config.SubTaskConfigsToOpenAPITask(subTaskConfigList)
   504  	if req.WithStatus != nil && *req.WithStatus {
   505  		subTaskStatusList, err := s.getTaskStatus(ctx, task.Name, openapi.DMAPIGetTaskStatusParams{})
   506  		if err != nil {
   507  			return nil, err
   508  		}
   509  		task.StatusList = &subTaskStatusList
   510  	}
   511  	return task, nil
   512  }
   513  
   514  func (s *Server) getTaskStatus(ctx context.Context, taskName string, req openapi.DMAPIGetTaskStatusParams) ([]openapi.SubTaskStatus, error) {
   515  	if req.SourceNameList == nil || len(*req.SourceNameList) == 0 {
   516  		sourceNameList := openapi.SourceNameList(s.getTaskSourceNameList(taskName))
   517  		req.SourceNameList = &sourceNameList
   518  	}
   519  	workerStatusList := s.getStatusFromWorkers(ctx, *req.SourceNameList, taskName, true)
   520  	subTaskStatusList := make([]openapi.SubTaskStatus, 0, len(workerStatusList))
   521  
   522  	handleProcessError := func(err *pb.ProcessError) string {
   523  		errorMsg := fmt.Sprintf("[code=%d:class=%s:scope=%s:level=%s], Message: %s", err.ErrCode, err.ErrClass, err.ErrScope, err.ErrLevel, err.Message)
   524  		if err.RawCause != "" {
   525  			errorMsg = fmt.Sprintf("%s, RawCause: %s", errorMsg, err.RawCause)
   526  		}
   527  		if err.Workaround != "" {
   528  			errorMsg = fmt.Sprintf("%s, Workaround: %s", errorMsg, err.Workaround)
   529  		}
   530  		errorMsg = fmt.Sprintf("%s.", errorMsg)
   531  		return errorMsg
   532  	}
   533  
   534  	for _, workerStatus := range workerStatusList {
   535  		if workerStatus == nil || workerStatus.SourceStatus == nil {
   536  			// this should not happen unless the rpc in the worker server has been modified
   537  			return nil, terror.ErrOpenAPICommonError.New("worker's query-status response is nil")
   538  		}
   539  		sourceStatus := workerStatus.SourceStatus
   540  		openapiSubTaskStatus := openapi.SubTaskStatus{
   541  			Name:       taskName,
   542  			SourceName: sourceStatus.GetSource(),
   543  			WorkerName: sourceStatus.GetWorker(),
   544  		}
   545  		if !workerStatus.Result {
   546  			openapiSubTaskStatus.ErrorMsg = &workerStatus.Msg
   547  			subTaskStatusList = append(subTaskStatusList, openapiSubTaskStatus)
   548  			continue
   549  		}
   550  		if len(workerStatus.SubTaskStatus) == 0 {
   551  			// this should not happen unless the rpc in the worker server has been modified
   552  			return nil, terror.ErrOpenAPICommonError.New("worker's query-status response is nil")
   553  		}
   554  		subTaskStatus := workerStatus.SubTaskStatus[0]
   555  		if subTaskStatus == nil {
   556  			// this should not happen unless the rpc in the worker server has been modified
   557  			return nil, terror.ErrOpenAPICommonError.New("worker's query-status response is nil")
   558  		}
   559  		openapiSubTaskStatus.Stage = openapi.TaskStage(subTaskStatus.GetStage().String())
   560  		openapiSubTaskStatus.Unit = subTaskStatus.GetUnit().String()
   561  		openapiSubTaskStatus.UnresolvedDdlLockId = &subTaskStatus.UnresolvedDDLLockID
   562  		// add load status
   563  		if loadS := subTaskStatus.GetLoad(); loadS != nil {
   564  			openapiSubTaskStatus.LoadStatus = &openapi.LoadStatus{
   565  				FinishedBytes:  loadS.FinishedBytes,
   566  				MetaBinlog:     loadS.MetaBinlog,
   567  				MetaBinlogGtid: loadS.MetaBinlogGTID,
   568  				Progress:       loadS.Progress,
   569  				TotalBytes:     loadS.TotalBytes,
   570  			}
   571  		}
   572  		// add sync status
   573  		if syncerS := subTaskStatus.GetSync(); syncerS != nil {
   574  			openapiSubTaskStatus.SyncStatus = &openapi.SyncStatus{
   575  				BinlogType:          syncerS.GetBinlogType(),
   576  				BlockingDdls:        syncerS.GetBlockingDDLs(),
   577  				MasterBinlog:        syncerS.GetMasterBinlog(),
   578  				MasterBinlogGtid:    syncerS.GetMasterBinlogGtid(),
   579  				RecentTps:           syncerS.RecentTps,
   580  				SecondsBehindMaster: syncerS.SecondsBehindMaster,
   581  				Synced:              syncerS.Synced,
   582  				SyncerBinlog:        syncerS.SyncerBinlog,
   583  				SyncerBinlogGtid:    syncerS.SyncerBinlogGtid,
   584  				TotalEvents:         syncerS.TotalEvents,
   585  				TotalTps:            syncerS.TotalTps,
   586  			}
   587  			if unResolvedGroups := syncerS.GetUnresolvedGroups(); len(unResolvedGroups) > 0 {
   588  				openapiSubTaskStatus.SyncStatus.UnresolvedGroups = make([]openapi.ShardingGroup, len(unResolvedGroups))
   589  				for i, unResolvedGroup := range unResolvedGroups {
   590  					openapiSubTaskStatus.SyncStatus.UnresolvedGroups[i] = openapi.ShardingGroup{
   591  						DdlList:       unResolvedGroup.DDLs,
   592  						FirstLocation: unResolvedGroup.FirstLocation,
   593  						Synced:        unResolvedGroup.Synced,
   594  						Target:        unResolvedGroup.Target,
   595  						Unsynced:      unResolvedGroup.Unsynced,
   596  					}
   597  				}
   598  			}
   599  		}
   600  		// add dump status
   601  		if dumpS := subTaskStatus.GetDump(); dumpS != nil {
   602  			openapiSubTaskStatus.DumpStatus = &openapi.DumpStatus{
   603  				CompletedTables:   dumpS.CompletedTables,
   604  				EstimateTotalRows: dumpS.EstimateTotalRows,
   605  				FinishedBytes:     dumpS.FinishedBytes,
   606  				FinishedRows:      dumpS.FinishedRows,
   607  				TotalTables:       dumpS.TotalTables,
   608  			}
   609  		}
   610  		// add error if some error happens
   611  		if subTaskStatus.Result != nil && len(subTaskStatus.Result.Errors) > 0 {
   612  			var errorMsgs string
   613  			for _, err := range subTaskStatus.Result.Errors {
   614  				errorMsgs += fmt.Sprintf("%s\n", handleProcessError(err))
   615  			}
   616  			openapiSubTaskStatus.ErrorMsg = &errorMsgs
   617  		}
   618  		subTaskStatusList = append(subTaskStatusList, openapiSubTaskStatus)
   619  	}
   620  	return subTaskStatusList, nil
   621  }
   622  
   623  func (s *Server) listTask(ctx context.Context, req openapi.DMAPIGetTaskListParams) ([]openapi.Task, error) {
   624  	subTaskConfigMap := s.scheduler.GetALlSubTaskCfgs()
   625  	taskList := config.SubTaskConfigsToOpenAPITaskList(subTaskConfigMap)
   626  	taskArray := make([]openapi.Task, 0, len(taskList))
   627  
   628  	if req.Stage != nil || (req.WithStatus != nil && *req.WithStatus) {
   629  		for idx := range taskList {
   630  			subTaskStatusList, err := s.getTaskStatus(ctx, taskList[idx].Name, openapi.DMAPIGetTaskStatusParams{})
   631  			if err != nil {
   632  				return taskArray, err
   633  			}
   634  			taskList[idx].StatusList = &subTaskStatusList
   635  		}
   636  	}
   637  	for idx, task := range taskList {
   638  		filtered := false
   639  		// filter by stage
   640  		if task.StatusList != nil && req.Stage != nil {
   641  			for _, status := range *task.StatusList {
   642  				if status.Stage != *req.Stage {
   643  					filtered = true
   644  					break
   645  				}
   646  			}
   647  		}
   648  		// filter by source
   649  		if req.SourceNameList != nil {
   650  			if len(task.SourceConfig.SourceConf) != len(*req.SourceNameList) {
   651  				filtered = true
   652  			}
   653  			sourceNameMap := make(map[string]struct{})
   654  			for _, sourceName := range *req.SourceNameList {
   655  				sourceNameMap[sourceName] = struct{}{}
   656  			}
   657  			for _, sourceConf := range task.SourceConfig.SourceConf {
   658  				if _, ok := sourceNameMap[sourceConf.SourceName]; !ok {
   659  					filtered = true
   660  					break
   661  				}
   662  			}
   663  		}
   664  		if !filtered {
   665  			// remove status
   666  			if req.WithStatus == nil || (req.WithStatus != nil && !*req.WithStatus) {
   667  				taskList[idx].StatusList = nil
   668  			}
   669  			taskArray = append(taskArray, *taskList[idx])
   670  		}
   671  	}
   672  	return taskArray, nil
   673  }
   674  
   675  func (s *Server) startTask(ctx context.Context, taskName string, req openapi.StartTaskRequest) error {
   676  	// start all subtasks for this task
   677  	if req.SourceNameList == nil || len(*req.SourceNameList) == 0 {
   678  		sourceNameList := openapi.SourceNameList(s.getTaskSourceNameList(taskName))
   679  		req.SourceNameList = &sourceNameList
   680  	}
   681  	subTaskConfigM := s.scheduler.GetSubTaskCfgsByTask(taskName)
   682  	needStartSubTaskList := make([]*config.SubTaskConfig, 0, len(*req.SourceNameList))
   683  	for _, sourceName := range *req.SourceNameList {
   684  		subTaskCfg, ok := subTaskConfigM[sourceName]
   685  		if !ok {
   686  			return terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   687  		}
   688  		// start task check. incremental task need to specify meta or start time
   689  		if subTaskCfg.Meta == nil && subTaskCfg.Mode == config.ModeIncrement && req.StartTime == nil {
   690  			return terror.ErrConfigMetadataNotSet.Generate(sourceName, config.ModeIncrement)
   691  		}
   692  		cfg := s.scheduler.GetSourceCfgByID(sourceName)
   693  		if cfg == nil {
   694  			return terror.ErrSchedulerSourceCfgNotExist.Generate(sourceName)
   695  		}
   696  		if !cfg.Enable {
   697  			return terror.ErrMasterStartTask.Generate(taskName, fmt.Sprintf("source: %s is not enabled", sourceName))
   698  		}
   699  		needStartSubTaskList = append(needStartSubTaskList, subTaskCfg)
   700  	}
   701  	if len(needStartSubTaskList) == 0 {
   702  		return nil
   703  	}
   704  
   705  	var (
   706  		release scheduler.ReleaseFunc
   707  		err     error
   708  	)
   709  	// removeMeta
   710  	if req.RemoveMeta != nil && *req.RemoveMeta {
   711  		// use same latch for remove-meta and start-task
   712  		release, err = s.scheduler.AcquireSubtaskLatch(taskName)
   713  		if err != nil {
   714  			return terror.ErrSchedulerLatchInUse.Generate("RemoveMeta", taskName)
   715  		}
   716  		defer release()
   717  		metaSchema := needStartSubTaskList[0].MetaSchema
   718  		targetDB := needStartSubTaskList[0].To
   719  		err = s.removeMetaData(ctx, taskName, metaSchema, &targetDB)
   720  		if err != nil {
   721  			return terror.Annotate(err, "while removing metadata")
   722  		}
   723  	}
   724  
   725  	// handle task cli args
   726  	cliArgs, err := config.OpenAPIStartTaskReqToTaskCliArgs(req)
   727  	if err != nil {
   728  		return terror.Annotate(err, "while converting task command line arguments")
   729  	}
   730  
   731  	if err = handleCliArgs(s.etcdClient, taskName, *req.SourceNameList, cliArgs); err != nil {
   732  		return err
   733  	}
   734  	if release != nil {
   735  		release()
   736  	}
   737  
   738  	return s.scheduler.UpdateExpectSubTaskStage(pb.Stage_Running, taskName, *req.SourceNameList...)
   739  }
   740  
   741  // nolint:unparam
   742  func (s *Server) stopTask(ctx context.Context, taskName string, req openapi.StopTaskRequest) error {
   743  	// all subtasks for this task
   744  	if req.SourceNameList == nil || len(*req.SourceNameList) == 0 {
   745  		sourceNameList := openapi.SourceNameList(s.getTaskSourceNameList(taskName))
   746  		req.SourceNameList = &sourceNameList
   747  	}
   748  	// handle task cli args
   749  	cliArgs, err := config.OpenAPIStopTaskReqToTaskCliArgs(req)
   750  	if err != nil {
   751  		return terror.Annotate(err, "while converting task command line arguments")
   752  	}
   753  	if err = handleCliArgs(s.etcdClient, taskName, *req.SourceNameList, cliArgs); err != nil {
   754  		return err
   755  	}
   756  	return s.scheduler.UpdateExpectSubTaskStage(pb.Stage_Stopped, taskName, *req.SourceNameList...)
   757  }
   758  
   759  // handleCliArgs handles cli args.
   760  // it will try to delete args if cli args is nil.
   761  func handleCliArgs(cli *clientv3.Client, taskName string, sources []string, cliArgs *config.TaskCliArgs) error {
   762  	if cliArgs == nil {
   763  		err := ha.DeleteTaskCliArgs(cli, taskName, sources)
   764  		if err != nil {
   765  			return terror.Annotate(err, "while removing task command line arguments")
   766  		}
   767  	} else {
   768  		err := ha.PutTaskCliArgs(cli, taskName, sources, *cliArgs)
   769  		if err != nil {
   770  			return terror.Annotate(err, "while putting task command line arguments")
   771  		}
   772  	}
   773  	return nil
   774  }
   775  
   776  // nolint:unparam
   777  func (s *Server) convertTaskConfig(ctx context.Context, req openapi.ConverterTaskRequest) (*openapi.Task, *config.TaskConfig, error) {
   778  	if req.TaskConfigFile != nil {
   779  		taskCfg := config.NewTaskConfig()
   780  		if err := taskCfg.RawDecode(*req.TaskConfigFile); err != nil {
   781  			return nil, nil, err
   782  		}
   783  		// clear extra config in MySQLInstance, use cfg.xxConfigName instead otherwise adjust will fail
   784  		for _, cfg := range taskCfg.MySQLInstances {
   785  			cfg.Mydumper = nil
   786  			cfg.Loader = nil
   787  			cfg.Syncer = nil
   788  		}
   789  		if adjustErr := taskCfg.Adjust(); adjustErr != nil {
   790  			return nil, nil, adjustErr
   791  		}
   792  		sourceCfgMap := make(map[string]*config.SourceConfig, len(taskCfg.MySQLInstances))
   793  		for _, source := range taskCfg.MySQLInstances {
   794  			sourceCfg := s.scheduler.GetSourceCfgByID(source.SourceID)
   795  			if sourceCfg == nil {
   796  				return nil, nil, terror.ErrConfigSourceIDNotFound.Generate(source.SourceID)
   797  			}
   798  			sourceCfgMap[source.SourceID] = sourceCfg
   799  		}
   800  		task, err := config.TaskConfigToOpenAPITask(taskCfg, sourceCfgMap)
   801  		if err != nil {
   802  			return nil, nil, err
   803  		}
   804  		return task, taskCfg, nil
   805  	}
   806  	task := req.Task
   807  	if adjustErr := task.Adjust(); adjustErr != nil {
   808  		return nil, nil, adjustErr
   809  	}
   810  	sourceCfgMap := make(map[string]*config.SourceConfig, len(task.SourceConfig.SourceConf))
   811  	for _, cfg := range task.SourceConfig.SourceConf {
   812  		sourceCfg := s.scheduler.GetSourceCfgByID(cfg.SourceName)
   813  		if sourceCfg == nil {
   814  			return nil, nil, terror.ErrConfigSourceIDNotFound.Generate(cfg.SourceName)
   815  		}
   816  		sourceCfgMap[sourceCfg.SourceID] = sourceCfg
   817  	}
   818  	taskCfg, err := config.OpenAPITaskToTaskConfig(task, sourceCfgMap)
   819  	if err != nil {
   820  		return nil, nil, err
   821  	}
   822  	return task, taskCfg, nil
   823  }