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 }