github.com/pingcap/ticdc@v0.0.0-20220526033649-485a10ef2652/cdc/http_handler.go (about)

     1  // Copyright 2020 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 cdc
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	"io/ioutil"
    20  	"net/http"
    21  	"strconv"
    22  	"time"
    23  
    24  	"github.com/pingcap/errors"
    25  	"github.com/pingcap/log"
    26  	"github.com/pingcap/ticdc/cdc/model"
    27  	"github.com/pingcap/ticdc/cdc/owner"
    28  	"github.com/pingcap/ticdc/pkg/config"
    29  	cerror "github.com/pingcap/ticdc/pkg/errors"
    30  	"github.com/pingcap/ticdc/pkg/logutil"
    31  	"github.com/pingcap/tidb/store/tikv/oracle"
    32  	"go.etcd.io/etcd/clientv3/concurrency"
    33  	"go.uber.org/zap"
    34  )
    35  
    36  const (
    37  	// APIOpVarAdminJob is the key of admin job in HTTP API
    38  	APIOpVarAdminJob = "admin-job"
    39  	// APIOpVarChangefeedID is the key of changefeed ID in HTTP API
    40  	APIOpVarChangefeedID = "cf-id"
    41  	// APIOpVarTargetCaptureID is the key of to-capture ID in HTTP API
    42  	APIOpVarTargetCaptureID = "target-cp-id"
    43  	// APIOpVarTableID is the key of table ID in HTTP API
    44  	APIOpVarTableID = "table-id"
    45  	// APIOpForceRemoveChangefeed is used when remove a changefeed
    46  	APIOpForceRemoveChangefeed = "force-remove"
    47  )
    48  
    49  type commonResp struct {
    50  	Status  bool   `json:"status"`
    51  	Message string `json:"message"`
    52  }
    53  
    54  // ChangefeedResp holds the most common usage information for a changefeed
    55  type ChangefeedResp struct {
    56  	FeedState    string              `json:"state"`
    57  	TSO          uint64              `json:"tso"`
    58  	Checkpoint   string              `json:"checkpoint"`
    59  	RunningError *model.RunningError `json:"error"`
    60  }
    61  
    62  func handleOwnerResp(w http.ResponseWriter, err error) {
    63  	if err != nil {
    64  		if errors.Cause(err) == concurrency.ErrElectionNotLeader {
    65  			writeError(w, http.StatusBadRequest, err)
    66  			return
    67  		}
    68  		writeInternalServerError(w, err)
    69  		return
    70  	}
    71  	writeData(w, commonResp{Status: true})
    72  }
    73  
    74  func (s *Server) handleResignOwner(w http.ResponseWriter, req *http.Request) {
    75  	if req.Method != http.MethodPost {
    76  		writeError(w, http.StatusBadRequest, cerror.ErrSupportPostOnly.GenWithStackByArgs())
    77  		return
    78  	}
    79  	if config.NewReplicaImpl {
    80  		if s.captureV2 == nil {
    81  			// for test only
    82  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
    83  			return
    84  		}
    85  		err := s.captureV2.OperateOwnerUnderLock(func(owner *owner.Owner) error {
    86  			owner.AsyncStop()
    87  			return nil
    88  		})
    89  		handleOwnerResp(w, err)
    90  		return
    91  	}
    92  	s.ownerLock.RLock()
    93  	if s.owner == nil {
    94  		handleOwnerResp(w, concurrency.ErrElectionNotLeader)
    95  		s.ownerLock.RUnlock()
    96  		return
    97  	}
    98  	// Resign is a complex process that needs to be synchronized because
    99  	// it happens in two separate goroutines
   100  	//
   101  	// Imagine that we have goroutines A and B
   102  	// A1. Notify the owner to exit
   103  	// B1. The owner exits gracefully
   104  	// A2. Delete the leader key until the owner has exited
   105  	// B2. Restart to campaign
   106  	//
   107  	// A2 must occur between B1 and B2, so we register the Resign process
   108  	// as the stepDown function which is called when the owner exited.
   109  	s.owner.Close(req.Context(), func(ctx context.Context) error {
   110  		return s.capture.Resign(ctx)
   111  	})
   112  	s.ownerLock.RUnlock()
   113  	s.setOwner(nil)
   114  	handleOwnerResp(w, nil)
   115  }
   116  
   117  func (s *Server) handleChangefeedAdmin(w http.ResponseWriter, req *http.Request) {
   118  	if req.Method != http.MethodPost {
   119  		writeError(w, http.StatusBadRequest, cerror.ErrSupportPostOnly.GenWithStackByArgs())
   120  		return
   121  	}
   122  	if !config.NewReplicaImpl {
   123  		s.ownerLock.RLock()
   124  		defer s.ownerLock.RUnlock()
   125  		if s.owner == nil {
   126  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   127  			return
   128  		}
   129  	} else {
   130  		if s.captureV2 == nil {
   131  			// for test only
   132  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   133  			return
   134  		}
   135  	}
   136  
   137  	err := req.ParseForm()
   138  	if err != nil {
   139  		writeInternalServerError(w, err)
   140  		return
   141  	}
   142  	typeStr := req.Form.Get(APIOpVarAdminJob)
   143  	typ, err := strconv.ParseInt(typeStr, 10, 64)
   144  	if err != nil {
   145  		writeError(w, http.StatusBadRequest, cerror.ErrAPIInvalidParam.GenWithStack("invalid admin job type: %s", typeStr))
   146  		return
   147  	}
   148  	opts := &model.AdminJobOption{}
   149  	if forceRemoveStr := req.Form.Get(APIOpForceRemoveChangefeed); forceRemoveStr != "" {
   150  		forceRemoveOpt, err := strconv.ParseBool(forceRemoveStr)
   151  		if err != nil {
   152  			writeError(w, http.StatusBadRequest,
   153  				cerror.ErrAPIInvalidParam.GenWithStack("invalid force remove option: %s", forceRemoveStr))
   154  			return
   155  		}
   156  		opts.ForceRemove = forceRemoveOpt
   157  	}
   158  	job := model.AdminJob{
   159  		CfID: req.Form.Get(APIOpVarChangefeedID),
   160  		Type: model.AdminJobType(typ),
   161  		Opts: opts,
   162  	}
   163  	if config.NewReplicaImpl {
   164  		err = s.captureV2.OperateOwnerUnderLock(func(owner *owner.Owner) error {
   165  			owner.EnqueueJob(job)
   166  			return nil
   167  		})
   168  	} else {
   169  		err = s.owner.EnqueueJob(job)
   170  	}
   171  	handleOwnerResp(w, err)
   172  }
   173  
   174  func (s *Server) handleRebalanceTrigger(w http.ResponseWriter, req *http.Request) {
   175  	if req.Method != http.MethodPost {
   176  		writeError(w, http.StatusBadRequest, cerror.ErrSupportPostOnly.GenWithStackByArgs())
   177  		return
   178  	}
   179  	if !config.NewReplicaImpl {
   180  		s.ownerLock.RLock()
   181  		defer s.ownerLock.RUnlock()
   182  		if s.owner == nil {
   183  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   184  			return
   185  		}
   186  	} else {
   187  		if s.captureV2 == nil {
   188  			// for test only
   189  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   190  			return
   191  		}
   192  	}
   193  
   194  	err := req.ParseForm()
   195  	if err != nil {
   196  		writeInternalServerError(w, err)
   197  		return
   198  	}
   199  	changefeedID := req.Form.Get(APIOpVarChangefeedID)
   200  	if err := model.ValidateChangefeedID(changefeedID); err != nil {
   201  		writeError(w, http.StatusBadRequest,
   202  			cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed id: %s", changefeedID))
   203  		return
   204  	}
   205  	if config.NewReplicaImpl {
   206  		err = s.captureV2.OperateOwnerUnderLock(func(owner *owner.Owner) error {
   207  			owner.TriggerRebalance(changefeedID)
   208  			return nil
   209  		})
   210  	} else {
   211  		s.owner.TriggerRebalance(changefeedID)
   212  	}
   213  	handleOwnerResp(w, err)
   214  }
   215  
   216  func (s *Server) handleMoveTable(w http.ResponseWriter, req *http.Request) {
   217  	if req.Method != http.MethodPost {
   218  		writeError(w, http.StatusBadRequest, cerror.ErrSupportPostOnly.GenWithStackByArgs())
   219  		return
   220  	}
   221  	if !config.NewReplicaImpl {
   222  		s.ownerLock.RLock()
   223  		defer s.ownerLock.RUnlock()
   224  		if s.owner == nil {
   225  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   226  			return
   227  		}
   228  	} else {
   229  		if s.captureV2 == nil {
   230  			// for test only
   231  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   232  			return
   233  		}
   234  	}
   235  
   236  	err := req.ParseForm()
   237  	if err != nil {
   238  		writeInternalServerError(w, cerror.WrapError(cerror.ErrInternalServerError, err))
   239  		return
   240  	}
   241  	changefeedID := req.Form.Get(APIOpVarChangefeedID)
   242  	if err := model.ValidateChangefeedID(changefeedID); err != nil {
   243  		writeError(w, http.StatusBadRequest,
   244  			cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed id: %s", changefeedID))
   245  		return
   246  	}
   247  	to := req.Form.Get(APIOpVarTargetCaptureID)
   248  	if err := model.ValidateChangefeedID(to); err != nil {
   249  		writeError(w, http.StatusBadRequest,
   250  			cerror.ErrAPIInvalidParam.GenWithStack("invalid target capture id: %s", to))
   251  		return
   252  	}
   253  	tableIDStr := req.Form.Get(APIOpVarTableID)
   254  	tableID, err := strconv.ParseInt(tableIDStr, 10, 64)
   255  	if err != nil {
   256  		writeError(w, http.StatusBadRequest,
   257  			cerror.ErrAPIInvalidParam.GenWithStack("invalid tableID: %s", tableIDStr))
   258  		return
   259  	}
   260  	if config.NewReplicaImpl {
   261  		err = s.captureV2.OperateOwnerUnderLock(func(owner *owner.Owner) error {
   262  			owner.ManualSchedule(changefeedID, to, tableID)
   263  			return nil
   264  		})
   265  	} else {
   266  		s.owner.ManualSchedule(changefeedID, to, tableID)
   267  	}
   268  	handleOwnerResp(w, err)
   269  }
   270  
   271  func (s *Server) handleChangefeedQuery(w http.ResponseWriter, req *http.Request) {
   272  	if req.Method != http.MethodPost {
   273  		writeError(w, http.StatusBadRequest, cerror.ErrSupportPostOnly.GenWithStackByArgs())
   274  		return
   275  	}
   276  
   277  	if !config.NewReplicaImpl {
   278  		s.ownerLock.RLock()
   279  		defer s.ownerLock.RUnlock()
   280  		if s.owner == nil {
   281  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   282  			return
   283  		}
   284  	} else {
   285  		if s.captureV2 == nil {
   286  			// for test only
   287  			handleOwnerResp(w, concurrency.ErrElectionNotLeader)
   288  			return
   289  		}
   290  	}
   291  
   292  	err := req.ParseForm()
   293  	if err != nil {
   294  		writeInternalServerError(w, err)
   295  		return
   296  	}
   297  	changefeedID := req.Form.Get(APIOpVarChangefeedID)
   298  	if err := model.ValidateChangefeedID(changefeedID); err != nil {
   299  		writeError(w, http.StatusBadRequest,
   300  			cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed id: %s", changefeedID))
   301  		return
   302  	}
   303  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   304  	defer cancel()
   305  	cfInfo, err := s.etcdClient.GetChangeFeedInfo(ctx, changefeedID)
   306  	if err != nil && cerror.ErrChangeFeedNotExists.NotEqual(err) {
   307  		writeError(w, http.StatusBadRequest,
   308  			cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed id: %s", changefeedID))
   309  		return
   310  	}
   311  	cfStatus, _, err := s.etcdClient.GetChangeFeedStatus(ctx, changefeedID)
   312  	if err != nil && cerror.ErrChangeFeedNotExists.NotEqual(err) {
   313  		writeError(w, http.StatusBadRequest, err)
   314  		return
   315  	}
   316  
   317  	resp := &ChangefeedResp{}
   318  	if cfInfo != nil {
   319  		resp.FeedState = string(cfInfo.State)
   320  		resp.RunningError = cfInfo.Error
   321  	}
   322  	if cfStatus != nil {
   323  		resp.TSO = cfStatus.CheckpointTs
   324  		tm := oracle.GetTimeFromTS(cfStatus.CheckpointTs)
   325  		resp.Checkpoint = tm.Format("2006-01-02 15:04:05.000")
   326  	}
   327  	if !config.NewReplicaImpl && cfStatus != nil {
   328  		switch cfStatus.AdminJobType {
   329  		case model.AdminNone, model.AdminResume:
   330  			if cfInfo != nil && cfInfo.Error != nil {
   331  				resp.FeedState = string(model.StateFailed)
   332  			}
   333  		case model.AdminStop:
   334  			resp.FeedState = string(model.StateStopped)
   335  		case model.AdminRemove:
   336  			resp.FeedState = string(model.StateRemoved)
   337  		case model.AdminFinish:
   338  			resp.FeedState = string(model.StateFinished)
   339  		}
   340  	}
   341  	writeData(w, resp)
   342  }
   343  
   344  func handleAdminLogLevel(w http.ResponseWriter, r *http.Request) {
   345  	var level string
   346  	data, err := ioutil.ReadAll(r.Body)
   347  	r.Body.Close()
   348  	if err != nil {
   349  		writeInternalServerError(w, err)
   350  		return
   351  	}
   352  	err = json.Unmarshal(data, &level)
   353  	if err != nil {
   354  		writeError(w, http.StatusBadRequest,
   355  			cerror.ErrAPIInvalidParam.GenWithStack("invalid log level: %s", err))
   356  		return
   357  	}
   358  
   359  	err = logutil.SetLogLevel(level)
   360  	if err != nil {
   361  		writeError(w, http.StatusBadRequest,
   362  			cerror.ErrAPIInvalidParam.GenWithStack("fail to change log level: %s", err))
   363  		return
   364  	}
   365  	log.Warn("log level changed", zap.String("level", level))
   366  
   367  	writeData(w, struct{}{})
   368  }