diff --git a/dm/_utils/terror_gen/errors_release.txt b/dm/_utils/terror_gen/errors_release.txt index 2697655c99c..1d732811c0b 100644 --- a/dm/_utils/terror_gen/errors_release.txt +++ b/dm/_utils/terror_gen/errors_release.txt @@ -212,7 +212,7 @@ ErrRelayBinlogNameNotValid,[code=30004:class=relay-unit:scope=internal:level=hig ErrRelayNoCurrentUUID,[code=30005:class=relay-unit:scope=internal:level=high], "Message: no current UUID set" ErrRelayFlushLocalMeta,[code=30006:class=relay-unit:scope=internal:level=high], "Message: flush local meta" ErrRelayUpdateIndexFile,[code=30007:class=relay-unit:scope=internal:level=high], "Message: update UUID index file %s" -ErrRelayLogDirpathEmpty,[code=30008:class=relay-unit:scope=internal:level=high], "Message: dirpath is empty, Workaround: Please check the `relay-dir` config in source config file." +ErrRelayLogDirpathEmpty,[code=30008:class=relay-unit:scope=internal:level=high], "Message: dirpath is empty, Workaround: Please check the `relay-dir` config in source config file or dm-worker config file." ErrRelayReaderNotStateNew,[code=30009:class=relay-unit:scope=internal:level=high], "Message: stage %s, expect %s, already started" ErrRelayReaderStateCannotClose,[code=30010:class=relay-unit:scope=internal:level=high], "Message: stage %s, expect %s, can not close" ErrRelayReaderNeedStart,[code=30011:class=relay-unit:scope=internal:level=high], "Message: stage %s, expect %s" @@ -396,6 +396,7 @@ ErrMasterFailToImportFromV10x,[code=38053:class=dm-master:scope=internal:level=h ErrMasterInconsistentOptimisticDDLsAndInfo,[code=38054:class=dm-master:scope=internal:level=high], "Message: inconsistent count of optimistic ddls and table infos, ddls: %d, table info: %d" ErrMasterOptimisticTableInfoBeforeNotExist,[code=38055:class=dm-master:scope=internal:level=high], "Message: table-info-before not exist in optimistic ddls: %v" ErrMasterOptimisticDownstreamMetaNotFound,[code=38056:class=dm-master:scope=internal:level=high], "Message: downstream database config and meta for task %s not found" +ErrMasterInvalidClusterID,[code=38057:class=dm-master:scope=internal:level=high], "Message: invalid cluster id: %v" ErrWorkerParseFlagSet,[code=40001:class=dm-worker:scope=internal:level=medium], "Message: parse dm-worker config flag set" ErrWorkerInvalidFlag,[code=40002:class=dm-worker:scope=internal:level=medium], "Message: '%s' is an invalid flag" ErrWorkerDecodeConfigFromFile,[code=40003:class=dm-worker:scope=internal:level=medium], "Message: toml decode file, Workaround: Please check the configuration file has correct TOML format." diff --git a/dm/dm/common/common.go b/dm/dm/common/common.go index a447c8ee373..ab4cecfd67a 100644 --- a/dm/dm/common/common.go +++ b/dm/dm/common/common.go @@ -26,6 +26,9 @@ var ( useOfClosedErrMsg = "use of closed network connection" // ClusterVersionKey is used to store the version of the cluster. ClusterVersionKey = "/dm-cluster/version" + // ClusterIDKey is used to store the cluster id of the whole dm cluster. Cluster id is the unique identification of dm cluster + // After leader of dm master bootstraped, the leader will get the id from etcd or generate fresh one, and backfill to etcd. + ClusterIDKey = "/dm-cluster/id" // WorkerRegisterKeyAdapter is used to encode and decode register key. // k/v: Encode(worker-name) -> the information of the DM-worker node. WorkerRegisterKeyAdapter KeyAdapter = keyHexEncoderDecoder("/dm-worker/r/") diff --git a/dm/dm/master/election.go b/dm/dm/master/election.go index d6bd3989c42..8d9e59ebf40 100644 --- a/dm/dm/master/election.go +++ b/dm/dm/master/election.go @@ -188,6 +188,12 @@ func (s *Server) startLeaderComponent(ctx context.Context) bool { return false } + err = s.initClusterID(ctx) + if err != nil { + log.L().Error("init cluster id failed", zap.Error(err)) + return false + } + failpoint.Inject("FailToStartLeader", func(val failpoint.Value) { masterStrings := val.(string) if strings.Contains(masterStrings, s.cfg.Name) { diff --git a/dm/dm/master/election_test.go b/dm/dm/master/election_test.go index 11a6c62aa8e..a831726accd 100644 --- a/dm/dm/master/election_test.go +++ b/dm/dm/master/election_test.go @@ -76,6 +76,8 @@ func (t *testElectionSuite) TestFailToStartLeader(c *check.C) { _, leaderID, _, err := s2.election.LeaderInfo(ctx) c.Assert(err, check.IsNil) c.Assert(leaderID, check.Equals, cfg1.Name) + c.Assert(s1.ClusterID(), check.Greater, uint64(0)) + c.Assert(s2.ClusterID(), check.Equals, uint64(0)) // fail to start scheduler/pessimism/optimism c.Assert(failpoint.Enable("github.com/pingcap/tiflow/dm/dm/master/FailToStartLeader", `return("dm-master-2")`), check.IsNil) @@ -89,6 +91,7 @@ func (t *testElectionSuite) TestFailToStartLeader(c *check.C) { _, leaderID, _, err = s2.election.LeaderInfo(ctx) c.Assert(err, check.IsNil) c.Assert(leaderID, check.Equals, cfg1.Name) + clusterID := s1.ClusterID() //nolint:errcheck failpoint.Disable("github.com/pingcap/tiflow/dm/dm/master/FailToStartLeader") @@ -99,6 +102,7 @@ func (t *testElectionSuite) TestFailToStartLeader(c *check.C) { _, leaderID, _, err = s2.election.LeaderInfo(ctx) c.Assert(err, check.IsNil) c.Assert(leaderID, check.Equals, cfg2.Name) + c.Assert(clusterID, check.Equals, s2.ClusterID()) cancel() } diff --git a/dm/dm/master/openapi.go b/dm/dm/master/openapi.go index 41ce4e81165..ada0dee4700 100644 --- a/dm/dm/master/openapi.go +++ b/dm/dm/master/openapi.go @@ -901,6 +901,13 @@ func (s *Server) DMAPUpdateTaskTemplate(c *gin.Context, taskName string) { c.IndentedJSON(http.StatusOK, task) } +// DMAPIGetClusterInfo return cluster id of dm cluster. +func (s *Server) DMAPIGetClusterInfo(c *gin.Context) { + r := &openapi.GetClusterInfoResponse{} + r.ClusterId = s.ClusterID() + c.IndentedJSON(http.StatusOK, r) +} + func terrorHTTPErrorHandler() gin.HandlerFunc { return func(c *gin.Context) { c.Next() diff --git a/dm/dm/master/openapi_test.go b/dm/dm/master/openapi_test.go index a57f8b35521..3be3e748246 100644 --- a/dm/dm/master/openapi_test.go +++ b/dm/dm/master/openapi_test.go @@ -632,6 +632,15 @@ func (t *openAPISuite) TestClusterAPI(c *check.C) { c.Assert(resultMasters.Data[0].Leader, check.IsTrue) c.Assert(resultMasters.Data[0].Alive, check.IsTrue) + // check cluster id + clusterIDURL := baseURL + "info" + resp := testutil.NewRequest().Get(clusterIDURL).GoWithHTTPHandler(t.testT, s1.openapiHandles) + c.Assert(resp.Code(), check.Equals, http.StatusOK) + var clusterIDResp openapi.GetClusterInfoResponse + err = resp.UnmarshalBodyToObject(&clusterIDResp) + c.Assert(err, check.IsNil) + c.Assert(clusterIDResp.ClusterId, check.Greater, uint64(0)) + // offline master-2 with retry // operate etcd cluster may met `etcdserver: unhealthy cluster`, add some retry for i := 0; i < 20; i++ { diff --git a/dm/dm/master/server.go b/dm/dm/master/server.go index 48691c15c8e..310a6836d7a 100644 --- a/dm/dm/master/server.go +++ b/dm/dm/master/server.go @@ -15,7 +15,9 @@ package master import ( "context" + "encoding/binary" "fmt" + "math/rand" "net" "net/http" "reflect" @@ -121,6 +123,8 @@ type Server struct { closed atomic.Bool openapiHandles *gin.Engine // injected in `InitOpenAPIHandles` + + clusterID atomic.Uint64 } // NewServer creates a new Server. @@ -405,6 +409,45 @@ func subtaskCfgPointersToInstances(stCfgPointers ...*config.SubTaskConfig) []con return stCfgs } +func (s *Server) initClusterID(ctx context.Context) error { + log.L().Info("init cluster id begin") + ctx1, cancel := context.WithTimeout(ctx, etcdutil.DefaultRequestTimeout) + defer cancel() + + resp, err := s.etcdClient.Get(ctx1, dmcommon.ClusterIDKey) + if err != nil { + return err + } + + // New cluster, generate a cluster id and backfill it to etcd + if len(resp.Kvs) == 0 { + ts := uint64(time.Now().Unix()) + clusterID := (ts << 32) + uint64(rand.Uint32()) + clusterIDBytes := make([]byte, 8) + binary.BigEndian.PutUint64(clusterIDBytes, clusterID) + _, err = s.etcdClient.Put(ctx1, dmcommon.ClusterIDKey, string(clusterIDBytes)) + if err != nil { + return err + } + s.clusterID.Store(clusterID) + log.L().Info("generate and init cluster id success", zap.Uint64("cluster_id", s.clusterID.Load())) + return nil + } + + if len(resp.Kvs[0].Value) != 8 { + return terror.ErrMasterInvalidClusterID.Generate(resp.Kvs[0].Value) + } + + s.clusterID.Store(binary.BigEndian.Uint64(resp.Kvs[0].Value)) + log.L().Info("init cluster id success", zap.Uint64("cluster_id", s.clusterID.Load())) + return nil +} + +// ClusterID return correct cluster id when as leader. +func (s *Server) ClusterID() uint64 { + return s.clusterID.Load() +} + // StartTask implements MasterServer.StartTask. func (s *Server) StartTask(ctx context.Context, req *pb.StartTaskRequest) (*pb.StartTaskResponse, error) { var ( diff --git a/dm/errors.toml b/dm/errors.toml index 96bab495cf8..beee203ee50 100644 --- a/dm/errors.toml +++ b/dm/errors.toml @@ -1285,7 +1285,7 @@ tags = ["internal", "high"] [error.DM-relay-unit-30008] message = "dirpath is empty" description = "" -workaround = "Please check the `relay-dir` config in source config file." +workaround = "Please check the `relay-dir` config in source config file or dm-worker config file." tags = ["internal", "high"] [error.DM-relay-unit-30009] @@ -2386,6 +2386,12 @@ description = "" workaround = "" tags = ["internal", "high"] +[error.DM-dm-master-38057] +message = "invalid cluster id: %v" +description = "" +workaround = "" +tags = ["internal", "high"] + [error.DM-dm-worker-40001] message = "parse dm-worker config flag set" description = "" diff --git a/dm/openapi/gen.client.go b/dm/openapi/gen.client.go index 3f1a57a84c5..f8f9c45317c 100644 --- a/dm/openapi/gen.client.go +++ b/dm/openapi/gen.client.go @@ -90,6 +90,9 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // DMAPIGetClusterInfo request + DMAPIGetClusterInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // DMAPIGetClusterMasterList request DMAPIGetClusterMasterList(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -213,6 +216,18 @@ type ClientInterface interface { DMAPIGetTaskStatus(ctx context.Context, taskName string, params *DMAPIGetTaskStatusParams, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) DMAPIGetClusterInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDMAPIGetClusterInfoRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DMAPIGetClusterMasterList(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDMAPIGetClusterMasterListRequest(c.Server) if err != nil { @@ -741,6 +756,33 @@ func (c *Client) DMAPIGetTaskStatus(ctx context.Context, taskName string, params return c.Client.Do(req) } +// NewDMAPIGetClusterInfoRequest generates requests for DMAPIGetClusterInfo +func NewDMAPIGetClusterInfoRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/cluster/info") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewDMAPIGetClusterMasterListRequest generates requests for DMAPIGetClusterMasterList func NewDMAPIGetClusterMasterListRequest(server string) (*http.Request, error) { var err error @@ -2174,6 +2216,9 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // DMAPIGetClusterInfo request + DMAPIGetClusterInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DMAPIGetClusterInfoResponse, error) + // DMAPIGetClusterMasterList request DMAPIGetClusterMasterListWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DMAPIGetClusterMasterListResponse, error) @@ -2297,6 +2342,28 @@ type ClientWithResponsesInterface interface { DMAPIGetTaskStatusWithResponse(ctx context.Context, taskName string, params *DMAPIGetTaskStatusParams, reqEditors ...RequestEditorFn) (*DMAPIGetTaskStatusResponse, error) } +type DMAPIGetClusterInfoResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GetClusterInfoResponse +} + +// Status returns HTTPResponse.Status +func (r DMAPIGetClusterInfoResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DMAPIGetClusterInfoResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DMAPIGetClusterMasterListResponse struct { Body []byte HTTPResponse *http.Response @@ -3061,6 +3128,15 @@ func (r DMAPIGetTaskStatusResponse) StatusCode() int { return 0 } +// DMAPIGetClusterInfoWithResponse request returning *DMAPIGetClusterInfoResponse +func (c *ClientWithResponses) DMAPIGetClusterInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DMAPIGetClusterInfoResponse, error) { + rsp, err := c.DMAPIGetClusterInfo(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseDMAPIGetClusterInfoResponse(rsp) +} + // DMAPIGetClusterMasterListWithResponse request returning *DMAPIGetClusterMasterListResponse func (c *ClientWithResponses) DMAPIGetClusterMasterListWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DMAPIGetClusterMasterListResponse, error) { rsp, err := c.DMAPIGetClusterMasterList(ctx, reqEditors...) @@ -3447,6 +3523,31 @@ func (c *ClientWithResponses) DMAPIGetTaskStatusWithResponse(ctx context.Context return ParseDMAPIGetTaskStatusResponse(rsp) } +// ParseDMAPIGetClusterInfoResponse parses an HTTP response from a DMAPIGetClusterInfoWithResponse call +func ParseDMAPIGetClusterInfoResponse(rsp *http.Response) (*DMAPIGetClusterInfoResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DMAPIGetClusterInfoResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GetClusterInfoResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + } + + return response, nil +} + // ParseDMAPIGetClusterMasterListResponse parses an HTTP response from a DMAPIGetClusterMasterListWithResponse call func ParseDMAPIGetClusterMasterListResponse(rsp *http.Response) (*DMAPIGetClusterMasterListResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) diff --git a/dm/openapi/gen.server.go b/dm/openapi/gen.server.go index 434f551728e..2ec86b20155 100644 --- a/dm/openapi/gen.server.go +++ b/dm/openapi/gen.server.go @@ -20,6 +20,9 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // get cluster info such as cluster id + // (GET /api/v1/cluster/info) + DMAPIGetClusterInfo(c *gin.Context) // get cluster master node list // (GET /api/v1/cluster/masters) DMAPIGetClusterMasterList(c *gin.Context) @@ -132,6 +135,15 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) +// DMAPIGetClusterInfo operation middleware +func (siw *ServerInterfaceWrapper) DMAPIGetClusterInfo(c *gin.Context) { + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + } + + siw.Handler.DMAPIGetClusterInfo(c) +} + // DMAPIGetClusterMasterList operation middleware func (siw *ServerInterfaceWrapper) DMAPIGetClusterMasterList(c *gin.Context) { for _, middleware := range siw.HandlerMiddlewares { @@ -895,6 +907,8 @@ func RegisterHandlersWithOptions(router *gin.Engine, si ServerInterface, options HandlerMiddlewares: options.Middlewares, } + router.GET(options.BaseURL+"/api/v1/cluster/info", wrapper.DMAPIGetClusterInfo) + router.GET(options.BaseURL+"/api/v1/cluster/masters", wrapper.DMAPIGetClusterMasterList) router.DELETE(options.BaseURL+"/api/v1/cluster/masters/:master-name", wrapper.DMAPIOfflineMasterNode) @@ -969,91 +983,92 @@ func RegisterHandlersWithOptions(router *gin.Engine, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AA==", + "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", + "1pEAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/dm/openapi/gen.types.go b/dm/openapi/gen.types.go index 6913035c0bf..ca478469ac9 100644 --- a/dm/openapi/gen.types.go +++ b/dm/openapi/gen.types.go @@ -93,6 +93,12 @@ type ErrorWithMessage struct { ErrorMsg string `json:"error_msg"` } +// GetClusterInfoResponse defines model for GetClusterInfoResponse. +type GetClusterInfoResponse struct { + // cluster id + ClusterId uint64 `json:"cluster_id"` +} + // GetClusterMasterListResponse defines model for GetClusterMasterListResponse. type GetClusterMasterListResponse struct { Data []ClusterMaster `json:"data"` diff --git a/dm/openapi/spec/dm.yaml b/dm/openapi/spec/dm.yaml index b1bfa185e96..7db4ce256f2 100644 --- a/dm/openapi/spec/dm.yaml +++ b/dm/openapi/spec/dm.yaml @@ -959,6 +959,19 @@ paths: "application/json": schema: $ref: "#/components/schemas/ErrorWithMessage" + /api/v1/cluster/info: + get: + tags: + - cluster + summary: "get cluster info such as cluster id" + operationId: "DMAPIGetClusterInfo" + responses: + "200": + description: "success" + content: + "application/json": + schema: + $ref: "#/components/schemas/GetClusterInfoResponse" components: schemas: @@ -1833,3 +1846,12 @@ components: required: - "success_task_list" - "failed_task_list" + GetClusterInfoResponse: + type: object + properties: + cluster_id: + type: integer + format: uint64 + description: "cluster id" + required: + - "cluster_id" diff --git a/dm/pkg/retry/strategy.go b/dm/pkg/retry/strategy.go index bbaceb65fb6..74a64849d39 100644 --- a/dm/pkg/retry/strategy.go +++ b/dm/pkg/retry/strategy.go @@ -17,6 +17,9 @@ import ( "time" tcontext "github.com/pingcap/tiflow/dm/pkg/context" + "github.com/pingcap/tiflow/dm/pkg/log" + + "go.uber.org/zap" ) // backoffStrategy represents enum of retry wait interval. @@ -77,6 +80,7 @@ func (*FiniteRetryStrategy) Apply(ctx *tcontext.Context, params Params, duration = time.Duration(i+1) * params.FirstRetryDuration default: } + log.L().Warn("retry stratey takes effect", zap.Error(err), zap.Int("retry_times", i), zap.Int("retry_count", params.RetryCount)) select { case <-ctx.Context().Done(): diff --git a/dm/pkg/terror/error_list.go b/dm/pkg/terror/error_list.go index 6c95627d941..04490f95306 100644 --- a/dm/pkg/terror/error_list.go +++ b/dm/pkg/terror/error_list.go @@ -497,6 +497,7 @@ const ( codeMasterInconsistentOptimistDDLsAndInfo codeMasterOptimisticTableInfobeforeNotExist codeMasterOptimisticDownstreamMetaNotFound + codeMasterInvalidClusterID ) // DM-worker error code. @@ -947,7 +948,7 @@ var ( ErrRelayNoCurrentUUID = New(codeRelayNoCurrentUUID, ClassRelayUnit, ScopeInternal, LevelHigh, "no current UUID set", "") ErrRelayFlushLocalMeta = New(codeRelayFlushLocalMeta, ClassRelayUnit, ScopeInternal, LevelHigh, "flush local meta", "") ErrRelayUpdateIndexFile = New(codeRelayUpdateIndexFile, ClassRelayUnit, ScopeInternal, LevelHigh, "update UUID index file %s", "") - ErrRelayLogDirpathEmpty = New(codeRelayLogDirpathEmpty, ClassRelayUnit, ScopeInternal, LevelHigh, "dirpath is empty", "Please check the `relay-dir` config in source config file.") + ErrRelayLogDirpathEmpty = New(codeRelayLogDirpathEmpty, ClassRelayUnit, ScopeInternal, LevelHigh, "dirpath is empty", "Please check the `relay-dir` config in source config file or dm-worker config file.") ErrRelayReaderNotStateNew = New(codeRelayReaderNotStateNew, ClassRelayUnit, ScopeInternal, LevelHigh, "stage %s, expect %s, already started", "") ErrRelayReaderStateCannotClose = New(codeRelayReaderStateCannotClose, ClassRelayUnit, ScopeInternal, LevelHigh, "stage %s, expect %s, can not close", "") ErrRelayReaderNeedStart = New(codeRelayReaderNeedStart, ClassRelayUnit, ScopeInternal, LevelHigh, "stage %s, expect %s", "") @@ -1145,6 +1146,7 @@ var ( ErrMasterInconsistentOptimisticDDLsAndInfo = New(codeMasterInconsistentOptimistDDLsAndInfo, ClassDMMaster, ScopeInternal, LevelHigh, "inconsistent count of optimistic ddls and table infos, ddls: %d, table info: %d", "") ErrMasterOptimisticTableInfoBeforeNotExist = New(codeMasterOptimisticTableInfobeforeNotExist, ClassDMMaster, ScopeInternal, LevelHigh, "table-info-before not exist in optimistic ddls: %v", "") ErrMasterOptimisticDownstreamMetaNotFound = New(codeMasterOptimisticDownstreamMetaNotFound, ClassDMMaster, ScopeInternal, LevelHigh, "downstream database config and meta for task %s not found", "") + ErrMasterInvalidClusterID = New(codeMasterInvalidClusterID, ClassDMMaster, ScopeInternal, LevelHigh, "invalid cluster id: %v", "") // DM-worker error. ErrWorkerParseFlagSet = New(codeWorkerParseFlagSet, ClassDMWorker, ScopeInternal, LevelMedium, "parse dm-worker config flag set", "")