Repository: pingcap/pd
Pull request #1984 (2019-12-05 03:52:12)

*: use operator.Builder to create operators
<!--
Thank you for working on PD! Please read PD's [CONTRIBUTING](https://github.com/pingcap/pd/blob/master/CONTRIBUTING.md) document **BEFORE** filing this PR.
-->

### What problem does this PR solve? <!--add the issue link with summary if it exists-->
Close https://github.com/pingcap/pd/issues/1959

### What is changed and how it works?
- Create*Operator switch to use builder
- fix tests

### Check List <!--REMOVE the items that are not applicable-->

Tests <!-- At least one of them must be included. -->
 - Unit test

Related changes
 - Need to be included in the release notes


codecov-io: # [Codecov](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=h1) Report
> Merging [#1984](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=desc) into [master](https://codecov.io/gh/pingcap/pd/commit/47533aac629eee6591f4bf283ad8c0a5f0eb62f9?src=pr&el=desc) will **increase** coverage by `0.28%`.
> The diff coverage is `75.49%`.

[![Impacted file tree graph](https://codecov.io/gh/pingcap/pd/pull/1984/graphs/tree.svg?width=650&token=J1yRL471OI&height=150&src=pr)](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1984      +/-   ##
==========================================
+ Coverage   77.16%   77.44%   +0.28%     
==========================================
  Files         177      177              
  Lines       18132    17997     -135     
==========================================
- Hits        13991    13938      -53     
+ Misses       3044     2986      -58     
+ Partials     1097     1073      -24
```


| [Impacted Files](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [server/schedulers/shuffle\_leader.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvc2h1ZmZsZV9sZWFkZXIuZ28=) | `88.23% <100%> (+6.98%)` | :arrow_up: |
| [pkg/testutil/operator\_check.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-cGtnL3Rlc3R1dGlsL29wZXJhdG9yX2NoZWNrLmdv) | `100% <100%> (ø)` | :arrow_up: |
| [tests/pdctl/helper.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-dGVzdHMvcGRjdGwvaGVscGVyLmdv) | `100% <100%> (ø)` | :arrow_up: |
| [pkg/mock/mockcluster/mockcluster.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-cGtnL21vY2svbW9ja2NsdXN0ZXIvbW9ja2NsdXN0ZXIuZ28=) | `90.47% <100%> (ø)` | :arrow_up: |
| [server/schedulers/grant\_leader.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvZ3JhbnRfbGVhZGVyLmdv) | `73.71% <100%> (+0.45%)` | :arrow_up: |
| [server/schedule/checker\_controller.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlL2NoZWNrZXJfY29udHJvbGxlci5nbw==) | `91.3% <100%> (ø)` | :arrow_up: |
| [server/schedulers/balance\_leader.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvYmFsYW5jZV9sZWFkZXIuZ28=) | `90.71% <25%> (-1.99%)` | :arrow_down: |
| [server/schedulers/hot\_region.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvaG90X3JlZ2lvbi5nbw==) | `87.01% <25%> (-1.73%)` | :arrow_down: |
| [server/schedulers/label.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvbGFiZWwuZ28=) | `74.24% <25%> (-3.54%)` | :arrow_down: |
| [server/schedulers/adjacent\_region.go](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvYWRqYWNlbnRfcmVnaW9uLmdv) | `75.4% <25%> (-1.23%)` | :arrow_down: |
| ... and [27 more](https://codecov.io/gh/pingcap/pd/pull/1984/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=footer). Last update [47533aa...58641e9](https://codecov.io/gh/pingcap/pd/pull/1984?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

disksing: /merge
disksing: /merge
sre-bot: /run-all-tests

Patch:
diff --git a/pkg/mock/mockcluster/mockcluster.go b/pkg/mock/mockcluster/mockcluster.go
index f47cf90d0..348421e8d 100644
--- a/pkg/mock/mockcluster/mockcluster.go
+++ b/pkg/mock/mockcluster/mockcluster.go
@@ -473,11 +473,11 @@ func (mc *Cluster) PutRegionStores(id uint64, stores ...uint64) {
 
 // PutStoreWithLabels mocks method.
 func (mc *Cluster) PutStoreWithLabels(id uint64, labelPairs ...string) {
-	var labels []*metapb.StoreLabel
+	labels := make(map[string]string)
 	for i := 0; i < len(labelPairs); i += 2 {
-		labels = append(labels, &metapb.StoreLabel{Key: labelPairs[i], Value: labelPairs[i+1]})
+		labels[labelPairs[i]] = labelPairs[i+1]
 	}
-	mc.PutStore(core.NewStoreInfo(&metapb.Store{Id: id, Labels: labels}))
+	mc.AddLabelsStore(id, 0, labels)
 }
 
 // MockRegionInfo returns a mock region
diff --git a/server/handler.go b/server/handler.go
index 8669517c1..0f139480a 100644
--- a/server/handler.go
+++ b/server/handler.go
@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/pingcap/errcode"
+	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/kvproto/pkg/pdpb"
 	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/cluster"
@@ -454,7 +455,11 @@ func (h *Handler) AddTransferLeaderOperator(regionID uint64, storeID uint64) err
 		return errors.Errorf("region has no voter in store %v", storeID)
 	}
 
-	op := operator.CreateTransferLeaderOperator("admin-transfer-leader", region, region.GetLeader().GetStoreId(), newLeader.GetStoreId(), operator.OpAdmin)
+	op, err := operator.CreateTransferLeaderOperator("admin-transfer-leader", c, region, region.GetLeader().GetStoreId(), newLeader.GetStoreId(), operator.OpAdmin)
+	if err != nil {
+		log.Debug("fail to create transfer leader operator", zap.Error(err))
+		return err
+	}
 	if ok := c.GetOperatorController().AddOperator(op); !ok {
 		return errors.WithStack(ErrAddOperator)
 	}
@@ -488,8 +493,14 @@ func (h *Handler) AddTransferRegionOperator(regionID uint64, storeIDs map[uint64
 		}
 	}
 
-	op, err := operator.CreateMoveRegionOperator("admin-move-region", c, region, operator.OpAdmin, storeIDs)
+	peers := make(map[uint64]*metapb.Peer)
+	for id := range storeIDs {
+		peers[id] = &metapb.Peer{StoreId: id}
+	}
+
+	op, err := operator.CreateMoveRegionOperator("admin-move-region", c, region, operator.OpAdmin, peers)
 	if err != nil {
+		log.Debug("fail to create move region operator", zap.Error(err))
 		return err
 	}
 	if ok := c.GetOperatorController().AddOperator(op); !ok {
@@ -530,6 +541,7 @@ func (h *Handler) AddTransferPeerOperator(regionID uint64, fromStoreID, toStoreI
 
 	op, err := operator.CreateMovePeerOperator("admin-move-peer", c, region, operator.OpAdmin, fromStoreID, newPeer)
 	if err != nil {
+		log.Debug("fail to create move peer operator", zap.Error(err))
 		return err
 	}
 	if ok := c.GetOperatorController().AddOperator(op); !ok {
@@ -577,7 +589,11 @@ func (h *Handler) AddAddPeerOperator(regionID uint64, toStoreID uint64) error {
 		return err
 	}
 
-	op := operator.CreateAddPeerOperator("admin-add-peer", region, newPeer, operator.OpAdmin)
+	op, err := operator.CreateAddPeerOperator("admin-add-peer", c, region, newPeer, operator.OpAdmin)
+	if err != nil {
+		log.Debug("fail to create add peer operator", zap.Error(err))
+		return err
+	}
 	if ok := c.GetOperatorController().AddOperator(op); !ok {
 		return errors.WithStack(ErrAddOperator)
 	}
@@ -597,7 +613,11 @@ func (h *Handler) AddAddLearnerOperator(regionID uint64, toStoreID uint64) error
 	}
 	newPeer.IsLearner = true
 
-	op := operator.CreateAddPeerOperator("admin-add-learner", region, newPeer, operator.OpAdmin)
+	op, err := operator.CreateAddPeerOperator("admin-add-learner", c, region, newPeer, operator.OpAdmin)
+	if err != nil {
+		log.Debug("fail to create add learner operator", zap.Error(err))
+		return err
+	}
 	if ok := c.GetOperatorController().AddOperator(op); !ok {
 		return errors.WithStack(ErrAddOperator)
 	}
@@ -622,6 +642,7 @@ func (h *Handler) AddRemovePeerOperator(regionID uint64, fromStoreID uint64) err
 
 	op, err := operator.CreateRemovePeerOperator("admin-remove-peer", c, operator.OpAdmin, region, fromStoreID)
 	if err != nil {
+		log.Debug("fail to create move peer operator", zap.Error(err))
 		return err
 	}
 	if ok := c.GetOperatorController().AddOperator(op); !ok {
@@ -663,6 +684,7 @@ func (h *Handler) AddMergeRegionOperator(regionID uint64, targetID uint64) error
 
 	ops, err := operator.CreateMergeRegionOperator("admin-merge-region", c, region, target, operator.OpAdmin)
 	if err != nil {
+		log.Debug("fail to create merge region operator", zap.Error(err))
 		return err
 	}
 	if ok := c.GetOperatorController().AddOperator(ops...); !ok {
diff --git a/server/schedule/checker/learner_checker.go b/server/schedule/checker/learner_checker.go
index 87da2b4d9..316ff0980 100644
--- a/server/schedule/checker/learner_checker.go
+++ b/server/schedule/checker/learner_checker.go
@@ -14,15 +14,24 @@
 package checker
 
 import (
+	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
+	"go.uber.org/zap"
 )
 
 // LearnerChecker ensures region has a learner will be promoted.
-type LearnerChecker struct{}
+type LearnerChecker struct {
+	cluster opt.Cluster
+}
 
 // NewLearnerChecker creates a learner checker.
-func NewLearnerChecker() *LearnerChecker { return &LearnerChecker{} }
+func NewLearnerChecker(cluster opt.Cluster) *LearnerChecker {
+	return &LearnerChecker{
+		cluster: cluster,
+	}
+}
 
 // Check verifies a region's role, creating an Operator if need.
 func (l *LearnerChecker) Check(region *core.RegionInfo) *operator.Operator {
@@ -30,7 +39,11 @@ func (l *LearnerChecker) Check(region *core.RegionInfo) *operator.Operator {
 		if region.GetPendingLearner(p.GetId()) != nil {
 			continue
 		}
-		op := operator.CreatePromoteLearnerOperator("promote-learner", region, p)
+		op, err := operator.CreatePromoteLearnerOperator("promote-learner", l.cluster, region, p)
+		if err != nil {
+			log.Debug("fail to create promote learner operator", zap.Error(err))
+			return nil
+		}
 		return op
 	}
 	return nil
diff --git a/server/schedule/checker/replica_checker.go b/server/schedule/checker/replica_checker.go
index abc98c876..7967a14bf 100644
--- a/server/schedule/checker/replica_checker.go
+++ b/server/schedule/checker/replica_checker.go
@@ -28,7 +28,6 @@ import (
 
 const (
 	replicaCheckerName = "replica-checker"
-	minReplicaCount    = 1
 )
 
 const (
@@ -89,7 +88,12 @@ func (r *ReplicaChecker) Check(region *core.RegionInfo) *operator.Operator {
 			return nil
 		}
 		checkerCounter.WithLabelValues("replica_checker", "new-operator").Inc()
-		return operator.CreateAddPeerOperator("make-up-replica", region, newPeer, operator.OpReplica)
+		op, err := operator.CreateAddPeerOperator("make-up-replica", r.cluster, region, newPeer, operator.OpReplica)
+		if err != nil {
+			log.Debug("create make-up-replica operator fail", zap.Error(err))
+			return nil
+		}
+		return op
 	}
 
 	// when add learner peer, the number of peer will exceed max replicas for a while,
@@ -279,12 +283,7 @@ func (r *ReplicaChecker) fixPeer(region *core.RegionInfo, peer *metapb.Peer, sta
 	}
 
 	replace := fmt.Sprintf("replace-%s-replica", status)
-	var op *operator.Operator
-	if status == offlineStatus && r.cluster.GetMaxReplicas() > minReplicaCount {
-		op, err = operator.CreateOfflinePeerOperator(replace, r.cluster, region, operator.OpReplica, peer.GetStoreId(), newPeer)
-	} else {
-		op, err = operator.CreateMovePeerOperator(replace, r.cluster, region, operator.OpReplica, peer.GetStoreId(), newPeer)
-	}
+	op, err := operator.CreateMovePeerOperator(replace, r.cluster, region, operator.OpReplica, peer.GetStoreId(), newPeer)
 	if err != nil {
 		reason := fmt.Sprintf("%s-fail", replace)
 		checkerCounter.WithLabelValues("replica_checker", reason).Inc()
diff --git a/server/schedule/checker_controller.go b/server/schedule/checker_controller.go
index bdd275b97..77d0d179d 100644
--- a/server/schedule/checker_controller.go
+++ b/server/schedule/checker_controller.go
@@ -37,7 +37,7 @@ func NewCheckerController(ctx context.Context, cluster opt.Cluster, opController
 	return &CheckerController{
 		cluster:        cluster,
 		opController:   opController,
-		learnerChecker: checker.NewLearnerChecker(),
+		learnerChecker: checker.NewLearnerChecker(cluster),
 		replicaChecker: checker.NewReplicaChecker(cluster),
 		mergeChecker:   checker.NewMergeChecker(ctx, cluster),
 	}
diff --git a/server/schedule/operator/create_operator.go b/server/schedule/operator/create_operator.go
index 4b1618c96..58db0b7d6 100644
--- a/server/schedule/operator/create_operator.go
+++ b/server/schedule/operator/create_operator.go
@@ -14,235 +14,64 @@
 package operator
 
 import (
-	"errors"
 	"fmt"
 	"math/rand"
-	"sort"
 
 	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/kvproto/pkg/pdpb"
-	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/core"
-	"github.com/pingcap/pd/server/schedule/opt"
-	"go.uber.org/zap"
 )
 
 // CreateAddPeerOperator creates an operator that adds a new peer.
-func CreateAddPeerOperator(desc string, region *core.RegionInfo, peer *metapb.Peer, kind OpKind) *Operator {
-	steps := CreateAddPeerSteps(peer)
-	brief := fmt.Sprintf("add peer: store %v", peer.StoreId)
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), kind|OpRegion, steps...)
+func CreateAddPeerOperator(desc string, cluster Cluster, region *core.RegionInfo, peer *metapb.Peer, kind OpKind) (*Operator, error) {
+	return NewBuilder(desc, cluster, region).
+		AddPeer(peer).
+		Build(kind)
 }
 
 // CreatePromoteLearnerOperator creates an operator that promotes a learner.
-func CreatePromoteLearnerOperator(desc string, region *core.RegionInfo, peer *metapb.Peer) *Operator {
-	step := PromoteLearner{
-		ToStore: peer.GetStoreId(),
-		PeerID:  peer.GetId(),
-	}
-	brief := fmt.Sprintf("promote learner: store %v", peer.GetStoreId())
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), OpRegion, step)
+func CreatePromoteLearnerOperator(desc string, cluster Cluster, region *core.RegionInfo, peer *metapb.Peer) (*Operator, error) {
+	return NewBuilder(desc, cluster, region).
+		PromoteLearner(peer.GetStoreId()).
+		Build(0)
 }
 
 // CreateRemovePeerOperator creates an operator that removes a peer from region.
 func CreateRemovePeerOperator(desc string, cluster Cluster, kind OpKind, region *core.RegionInfo, storeID uint64) (*Operator, error) {
-	removeKind, steps, err := removePeerSteps(cluster, region, storeID, getRegionFollowerIDs(region))
-	if err != nil {
-		return nil, err
-	}
-	brief := fmt.Sprintf("rm peer: store %v", storeID)
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), removeKind|kind, steps...), nil
-}
-
-// CreateAddPeerSteps creates an OpStep list that add a new peer.
-func CreateAddPeerSteps(newPeer *metapb.Peer) []OpStep {
-	if newPeer.IsLearner {
-		return []OpStep{AddLearner{ToStore: newPeer.StoreId, PeerID: newPeer.Id}}
-	}
-	return []OpStep{
-		AddLearner{ToStore: newPeer.StoreId, PeerID: newPeer.Id},
-		PromoteLearner{ToStore: newPeer.StoreId, PeerID: newPeer.Id},
-	}
-}
-
-// CreateAddLightPeerSteps creates an OpStep list that add a new peer without considering the influence.
-func CreateAddLightPeerSteps(newStore uint64, peerID uint64) []OpStep {
-	st := []OpStep{
-		AddLightLearner{ToStore: newStore, PeerID: peerID},
-		PromoteLearner{ToStore: newStore, PeerID: peerID},
-	}
-	return st
+	return NewBuilder(desc, cluster, region).
+		RemovePeer(storeID).
+		Build(kind)
 }
 
 // CreateTransferLeaderOperator creates an operator that transfers the leader from a source store to a target store.
-func CreateTransferLeaderOperator(desc string, region *core.RegionInfo, sourceStoreID uint64, targetStoreID uint64, kind OpKind) *Operator {
-	step := TransferLeader{FromStore: sourceStoreID, ToStore: targetStoreID}
-	brief := fmt.Sprintf("transfer leader: store %v to %v", sourceStoreID, targetStoreID)
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), kind|OpLeader, step)
+func CreateTransferLeaderOperator(desc string, cluster Cluster, region *core.RegionInfo, sourceStoreID uint64, targetStoreID uint64, kind OpKind) (*Operator, error) {
+	return NewBuilder(desc, cluster, region).
+		SetLeader(targetStoreID).
+		Build(kind)
 }
 
 // CreateMoveRegionOperator creates an operator that moves a region to specified stores.
-func CreateMoveRegionOperator(desc string, cluster Cluster, region *core.RegionInfo, kind OpKind, storeIDs map[uint64]struct{}) (*Operator, error) {
-	mvkind, steps, err := moveRegionSteps(cluster, region, storeIDs)
-	if err != nil {
-		return nil, err
-	}
-	kind |= mvkind
-	brief := fmt.Sprintf("mv region: stores %v to %v", u64Set(region.GetStoreIds()), u64Set(storeIDs))
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), kind, steps...), nil
-}
-
-// moveRegionSteps returns steps to move a region to specific stores.
-//
-// The first store in the slice will not have RejectLeader label.
-// If all of the stores have RejectLeader label, it returns an error.
-func moveRegionSteps(cluster Cluster, region *core.RegionInfo, stores map[uint64]struct{}) (OpKind, []OpStep, error) {
-	storeIDs := make([]uint64, 0, len(stores))
-	for id := range stores {
-		storeIDs = append(storeIDs, id)
-	}
-
-	i, _ := findNoLabelProperty(cluster, opt.RejectLeader, storeIDs)
-	if i < 0 {
-		return 0, nil, errors.New("all of the stores have RejectLeader label")
-	}
-
-	storeIDs[0], storeIDs[i] = storeIDs[i], storeIDs[0]
-	return orderedMoveRegionSteps(cluster, region, storeIDs)
-}
-
-// orderedMoveRegionSteps returns steps to move peers of a region to specific stores in order.
-//
-// If the current leader is not in storeIDs, it will be transferred to a follower which
-// do not need to move if there is one, otherwise the first suitable new added follower.
-// New peers will be added in the same order in storeIDs.
-// NOTE: orderedMoveRegionSteps does NOT check duplicate stores.
-func orderedMoveRegionSteps(cluster Cluster, region *core.RegionInfo, storeIDs []uint64) (OpKind, []OpStep, error) {
-	var kind OpKind
-
-	oldStores := make([]uint64, 0, len(storeIDs))
-	newStores := make([]uint64, 0, len(storeIDs))
-
-	sourceStores := region.GetStoreIds()
-	targetStores := make(map[uint64]struct{}, len(storeIDs))
-
-	// Add missing peers.
-	var addPeerSteps [][]OpStep
-	for _, id := range storeIDs {
-		targetStores[id] = struct{}{}
-		if _, ok := sourceStores[id]; ok {
-			oldStores = append(oldStores, id)
-			continue
-		}
-		newStores = append(newStores, id)
-		peer, err := cluster.AllocPeer(id)
-		if err != nil {
-			log.Debug("peer alloc failed", zap.Error(err))
-			return kind, nil, err
-		}
-		addPeerSteps = append(addPeerSteps, CreateAddPeerSteps(peer))
-		kind |= OpRegion
-	}
-
-	// Transferring leader to a new added follower may be refused by TiKV.
-	// Ref: https://github.com/tikv/tikv/issues/3819
-	// So, the new leader should be a follower that do not need to move if there is one,
-	// otherwise the first suitable new added follower.
-	orderedStoreIDs := append(oldStores, newStores...)
-
-	// Remove redundant peers.
-	var rmPeerSteps [][]OpStep
-	// Transfer leader as late as possible to prevent transferring to a new added follower.
-	var mvLeaderSteps []OpStep
-	for _, peer := range region.GetPeers() {
-		id := peer.GetStoreId()
-		if _, ok := targetStores[id]; ok {
-			continue
-		}
-		if region.GetLeader().GetStoreId() == id {
-			tlkind, tlsteps, err := transferLeaderToSuitableSteps(cluster, id, orderedStoreIDs)
-			if err != nil {
-				log.Debug("move region to stores failed", zap.Uint64("region-id", region.GetID()), zap.Uint64s("store-ids", orderedStoreIDs), zap.Error(err))
-				return kind, nil, err
-			}
-			mvLeaderSteps = append(tlsteps, RemovePeer{FromStore: id})
-			kind |= tlkind
-		} else {
-			rmPeerSteps = append(rmPeerSteps, []OpStep{RemovePeer{FromStore: id}})
-		}
-		kind |= OpRegion
-	}
-
-	// Interleaving makes the operator add and remove peers one by one, so that there won't have
-	// too many additional peers if the operator fails in the half.
-	hint := len(addPeerSteps)*2 + len(rmPeerSteps) + len(mvLeaderSteps)
-	steps := interleaveStepGroups(addPeerSteps, rmPeerSteps, hint)
-
-	steps = append(steps, mvLeaderSteps...)
-
-	return kind, steps, nil
-}
-
-// interleaveStepGroups interleaves two slice of step groups. For example:
-//
-//  a = [[opA1, opA2], [opA3], [opA4, opA5, opA6]]
-//  b = [[opB1], [opB2], [opB3, opB4], [opB5, opB6]]
-//  c = interleaveStepGroups(a, b, 0)
-//  c == [opA1, opA2, opB1, opA3, opB2, opA4, opA5, opA6, opB3, opB4, opB5, opB6]
-//
-// sizeHint is a hint for the capacity of returned slice.
-func interleaveStepGroups(a, b [][]OpStep, sizeHint int) []OpStep {
-	steps := make([]OpStep, 0, sizeHint)
-	i, j := 0, 0
-	for ; i < len(a) && j < len(b); i, j = i+1, j+1 {
-		steps = append(steps, a[i]...)
-		steps = append(steps, b[j]...)
-	}
-	for ; i < len(a); i++ {
-		steps = append(steps, a[i]...)
-	}
-	for ; j < len(b); j++ {
-		steps = append(steps, b[j]...)
-	}
-	return steps
+func CreateMoveRegionOperator(desc string, cluster Cluster, region *core.RegionInfo, kind OpKind, peers map[uint64]*metapb.Peer) (*Operator, error) {
+	return NewBuilder(desc, cluster, region).
+		SetPeers(peers).
+		Build(kind)
 }
 
 // CreateMovePeerOperator creates an operator that replaces an old peer with a new peer.
 func CreateMovePeerOperator(desc string, cluster Cluster, region *core.RegionInfo, kind OpKind, oldStore uint64, peer *metapb.Peer) (*Operator, error) {
-	removeKind, steps, err := removePeerSteps(cluster, region, oldStore, append(getRegionFollowerIDs(region), peer.StoreId))
-	if err != nil {
-		return nil, err
-	}
-	st := CreateAddPeerSteps(peer)
-	steps = append(st, steps...)
-	brief := fmt.Sprintf("mv peer: store %v to %v", oldStore, peer.StoreId)
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), removeKind|kind|OpRegion, steps...), nil
-}
-
-// CreateOfflinePeerOperator creates an operator that replaces an old peer with a new peer when offline a store.
-func CreateOfflinePeerOperator(desc string, cluster Cluster, region *core.RegionInfo, kind OpKind, oldStore uint64, peer *metapb.Peer) (*Operator, error) {
-	k, steps, err := transferLeaderStep(cluster, region, oldStore, getRegionFollowerIDs(region))
-	if err != nil {
-		return nil, err
-	}
-	kind |= k
-	st := CreateAddPeerSteps(peer)
-	steps = append(steps, st...)
-	steps = append(steps, RemovePeer{FromStore: oldStore})
-	brief := fmt.Sprintf("mv peer: store %v to %v", oldStore, peer.StoreId)
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), kind|OpRegion, steps...), nil
+	return NewBuilder(desc, cluster, region).
+		RemovePeer(oldStore).
+		AddPeer(peer).
+		Build(kind)
 }
 
 // CreateMoveLeaderOperator creates an operator that replaces an old leader with a new leader.
 func CreateMoveLeaderOperator(desc string, cluster Cluster, region *core.RegionInfo, kind OpKind, oldStore uint64, peer *metapb.Peer) (*Operator, error) {
-	removeKind, steps, err := removePeerSteps(cluster, region, oldStore, []uint64{peer.StoreId})
-	if err != nil {
-		return nil, err
-	}
-	st := CreateAddPeerSteps(peer)
-	steps = append(st, steps...)
-	brief := fmt.Sprintf("mv leader: store %v to %v", oldStore, peer.StoreId)
-	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), removeKind|kind|OpLeader|OpRegion, steps...), nil
+	return NewBuilder(desc, cluster, region).
+		RemovePeer(oldStore).
+		AddPeer(peer).
+		SetLeader(peer.GetStoreId()).
+		Build(kind)
 }
 
 // CreateSplitRegionOperator creates an operator that splits a region.
@@ -257,67 +86,26 @@ func CreateSplitRegionOperator(desc string, region *core.RegionInfo, kind OpKind
 	return NewOperator(desc, brief, region.GetID(), region.GetRegionEpoch(), kind, step)
 }
 
-func getRegionFollowerIDs(region *core.RegionInfo) []uint64 {
-	var ids []uint64
-	for id := range region.GetFollowers() {
-		ids = append(ids, id)
-	}
-	return ids
-}
-
-// removePeerSteps returns the steps to safely remove a peer. It prevents removing leader by transfer its leadership first.
-func removePeerSteps(cluster Cluster, region *core.RegionInfo, storeID uint64, followerIDs []uint64) (kind OpKind, steps []OpStep, err error) {
-	kind, steps, err = transferLeaderStep(cluster, region, storeID, followerIDs)
-	if err != nil {
-		return
-	}
-
-	steps = append(steps, RemovePeer{FromStore: storeID})
-	kind |= OpRegion
-	return
-}
-
-func transferLeaderStep(cluster Cluster, region *core.RegionInfo, storeID uint64, followerIDs []uint64) (kind OpKind, steps []OpStep, err error) {
-	if region.GetLeader() != nil && region.GetLeader().GetStoreId() == storeID {
-		kind, steps, err = transferLeaderToSuitableSteps(cluster, storeID, followerIDs)
-		if err != nil {
-			log.Debug("failed to create transfer leader step", zap.Uint64("region-id", region.GetID()), zap.Error(err))
-			return
-		}
-	}
-	return
-}
-
-// findNoLabelProperty finds the first store without given label property.
-func findNoLabelProperty(cluster Cluster, prop string, storeIDs []uint64) (int, uint64) {
-	for i, id := range storeIDs {
-		store := cluster.GetStore(id)
-		if store != nil {
-			if !cluster.CheckLabelProperty(prop, store.GetLabels()) {
-				return i, id
+// CreateMergeRegionOperator creates an operator that merge two region into one.
+func CreateMergeRegionOperator(desc string, cluster Cluster, source *core.RegionInfo, target *core.RegionInfo, kind OpKind) ([]*Operator, error) {
+	var steps []OpStep
+	if !isRegionMatch(source, target) {
+		peers := make(map[uint64]*metapb.Peer)
+		for _, p := range target.GetPeers() {
+			peers[p.GetStoreId()] = &metapb.Peer{
+				StoreId:   p.GetStoreId(),
+				IsLearner: p.GetIsLearner(),
 			}
-		} else {
-			log.Debug("nil store", zap.Uint64("store-id", id))
 		}
-	}
-	return -1, 0
-}
-
-// transferLeaderToSuitableSteps returns the first suitable store to become region leader.
-// Returns an error if there is no suitable store.
-func transferLeaderToSuitableSteps(cluster Cluster, leaderID uint64, storeIDs []uint64) (OpKind, []OpStep, error) {
-	_, id := findNoLabelProperty(cluster, opt.RejectLeader, storeIDs)
-	if id != 0 {
-		return OpLeader, []OpStep{TransferLeader{FromStore: leaderID, ToStore: id}}, nil
-	}
-	return 0, nil, errors.New("no suitable store to become region leader")
-}
+		matchOp, err := NewBuilder("", cluster, source).
+			SetPeers(peers).
+			Build(kind)
+		if err != nil {
+			return nil, err
+		}
 
-// CreateMergeRegionOperator creates an operator that merge two region into one.
-func CreateMergeRegionOperator(desc string, cluster Cluster, source *core.RegionInfo, target *core.RegionInfo, kind OpKind) ([]*Operator, error) {
-	kinds, steps, err := matchPeerSteps(cluster, source, target)
-	if err != nil {
-		return nil, err
+		steps = append(steps, matchOp.steps...)
+		kind = matchOp.Kind()
 	}
 
 	steps = append(steps, MergeRegion{
@@ -327,8 +115,8 @@ func CreateMergeRegionOperator(desc string, cluster Cluster, source *core.Region
 	})
 
 	brief := fmt.Sprintf("merge: region %v to %v", source.GetID(), target.GetID())
-	op1 := NewOperator(desc, brief, source.GetID(), source.GetRegionEpoch(), kinds|kind|OpMerge, steps...)
-	op2 := NewOperator(desc, brief, target.GetID(), target.GetRegionEpoch(), kinds|kind|OpMerge, MergeRegion{
+	op1 := NewOperator(desc, brief, source.GetID(), source.GetRegionEpoch(), kind|OpMerge, steps...)
+	op2 := NewOperator(desc, brief, target.GetID(), target.GetRegionEpoch(), kind|OpMerge, MergeRegion{
 		FromRegion: source.GetMeta(),
 		ToRegion:   target.GetMeta(),
 		IsPassive:  true,
@@ -337,112 +125,35 @@ func CreateMergeRegionOperator(desc string, cluster Cluster, source *core.Region
 	return []*Operator{op1, op2}, nil
 }
 
-// matchPeerSteps returns the steps to match the location of peer stores of source region with target's.
-func matchPeerSteps(cluster Cluster, source *core.RegionInfo, target *core.RegionInfo) (OpKind, []OpStep, error) {
-	var kind OpKind
-
-	sourcePeers := source.GetPeers()
-	targetPeers := target.GetPeers()
-
-	// make sure the peer count is same
-	if len(sourcePeers) != len(targetPeers) {
-		return kind, nil, errors.New("mismatch count of peer")
+func isRegionMatch(a, b *core.RegionInfo) bool {
+	if len(a.GetPeers()) != len(b.GetPeers()) {
+		return false
 	}
-
-	targetLeader := target.GetLeader().GetStoreId()
-	if targetLeader == 0 {
-		return kind, nil, errors.New("target does not have a leader")
-	}
-
-	// The target leader store must not have RejectLeader.
-	targetStores := make([]uint64, 1, len(targetPeers))
-	targetStores[0] = targetLeader
-
-	for _, peer := range targetPeers {
-		id := peer.GetStoreId()
-		if id != targetLeader {
-			targetStores = append(targetStores, id)
+	for _, pa := range a.GetPeers() {
+		pb := b.GetStorePeer(pa.GetStoreId())
+		if pb == nil || pb.GetIsLearner() != pa.GetIsLearner() {
+			return false
 		}
 	}
-
-	return orderedMoveRegionSteps(cluster, source, targetStores)
+	return true
 }
 
 // CreateScatterRegionOperator creates an operator that scatters the specified region.
-func CreateScatterRegionOperator(desc string, cluster Cluster, origin *core.RegionInfo, replacedPeers, targetPeers []*metapb.Peer) *Operator {
-	// Randomly pick a leader
-	i := rand.Intn(len(targetPeers))
-	targetLeaderPeer := targetPeers[i]
-	originLeaderStoreID := origin.GetLeader().GetStoreId()
-
-	originStoreIDs := origin.GetStoreIds()
-	steps := make([]OpStep, 0, len(targetPeers)*3+1)
-	// deferSteps will append to the end of the steps
-	deferSteps := make([]OpStep, 0, 5)
-	var kind OpKind
-	sameLeader := targetLeaderPeer.GetStoreId() == originLeaderStoreID
-	// No need to do anything
-	if sameLeader {
-		isSame := true
-		for _, peer := range targetPeers {
-			if _, ok := originStoreIDs[peer.GetStoreId()]; !ok {
-				isSame = false
-				break
-			}
-		}
-		if isSame {
-			return nil
-		}
-	}
-
-	// Creates the first step
-	if _, ok := originStoreIDs[targetLeaderPeer.GetStoreId()]; !ok {
-		st := CreateAddLightPeerSteps(targetLeaderPeer.GetStoreId(), targetLeaderPeer.GetId())
-		steps = append(steps, st...)
-		// Do not transfer leader to the newly added peer
-		// Ref: https://github.com/tikv/tikv/issues/3819
-		deferSteps = append(deferSteps, TransferLeader{FromStore: originLeaderStoreID, ToStore: targetLeaderPeer.GetStoreId()})
-		deferSteps = append(deferSteps, RemovePeer{FromStore: replacedPeers[i].GetStoreId()})
-		kind |= OpLeader
-		kind |= OpRegion
-	} else {
-		if !sameLeader {
-			steps = append(steps, TransferLeader{FromStore: originLeaderStoreID, ToStore: targetLeaderPeer.GetStoreId()})
-			kind |= OpLeader
-		}
-	}
-
-	// For the other steps
-	for j, peer := range targetPeers {
-		if peer.GetId() == targetLeaderPeer.GetId() {
-			continue
-		}
-		if _, ok := originStoreIDs[peer.GetStoreId()]; ok {
-			continue
-		}
-		if replacedPeers[j].GetStoreId() == originLeaderStoreID {
-			st := CreateAddLightPeerSteps(peer.GetStoreId(), peer.GetId())
-			st = append(st, RemovePeer{FromStore: replacedPeers[j].GetStoreId()})
-			deferSteps = append(deferSteps, st...)
-			kind |= OpRegion | OpLeader
-			continue
-		}
-		st := CreateAddLightPeerSteps(peer.GetStoreId(), peer.GetId())
-		steps = append(steps, st...)
-		steps = append(steps, RemovePeer{FromStore: replacedPeers[j].GetStoreId()})
-		kind |= OpRegion
+func CreateScatterRegionOperator(desc string, cluster Cluster, origin *core.RegionInfo, targetPeers map[uint64]*metapb.Peer) (*Operator, error) {
+	// randomly pick a leader.
+	var ids []uint64
+	for id := range targetPeers {
+		ids = append(ids, id)
 	}
-
-	steps = append(steps, deferSteps...)
-
-	targetStores := make([]uint64, len(targetPeers))
-	for i := range targetPeers {
-		targetStores[i] = targetPeers[i].GetStoreId()
+	var leader uint64
+	if len(ids) > 0 {
+		leader = ids[rand.Intn(len(ids))]
 	}
-	sort.Sort(u64Slice(targetStores))
-	brief := fmt.Sprintf("scatter region: stores %v to %v", u64Set(origin.GetStoreIds()), targetStores)
-	op := NewOperator(desc, brief, origin.GetID(), origin.GetRegionEpoch(), kind, steps...)
-	return op
+	return NewBuilder(desc, cluster, origin).
+		SetPeers(targetPeers).
+		SetLeader(leader).
+		SetLightWeight().
+		Build(0)
 }
 
 type u64Slice []uint64
@@ -458,14 +169,3 @@ func (s u64Slice) Swap(i, j int) {
 func (s u64Slice) Less(i, j int) bool {
 	return s[i] < s[j]
 }
-
-type u64Set map[uint64]struct{}
-
-func (s u64Set) String() string {
-	v := make([]uint64, 0, len(s))
-	for x := range s {
-		v = append(v, x)
-	}
-	sort.Sort(u64Slice(v))
-	return fmt.Sprintf("%v", v)
-}
diff --git a/server/schedule/region_scatterer.go b/server/schedule/region_scatterer.go
index c9f67e25e..90a84c69b 100644
--- a/server/schedule/region_scatterer.go
+++ b/server/schedule/region_scatterer.go
@@ -102,10 +102,7 @@ func (r *RegionScatterer) Scatter(region *core.RegionInfo) (*operator.Operator,
 
 func (r *RegionScatterer) scatterRegion(region *core.RegionInfo) *operator.Operator {
 	stores := r.collectAvailableStores(region)
-	var (
-		targetPeers   []*metapb.Peer
-		replacedPeers []*metapb.Peer
-	)
+	targetPeers := make(map[uint64]*metapb.Peer)
 	for _, peer := range region.GetPeers() {
 		if len(stores) == 0 {
 			// Reset selected stores if we have no available stores.
@@ -115,26 +112,25 @@ func (r *RegionScatterer) scatterRegion(region *core.RegionInfo) *operator.Opera
 
 		if r.selected.put(peer.GetStoreId()) {
 			delete(stores, peer.GetStoreId())
-			targetPeers = append(targetPeers, peer)
-			replacedPeers = append(replacedPeers, peer)
+			targetPeers[peer.GetStoreId()] = peer
 			continue
 		}
 		newPeer := r.selectPeerToReplace(stores, region, peer)
 		if newPeer == nil {
-			targetPeers = append(targetPeers, peer)
-			replacedPeers = append(replacedPeers, peer)
+			targetPeers[peer.GetStoreId()] = peer
 			continue
 		}
 		// Remove it from stores and mark it as selected.
 		delete(stores, newPeer.GetStoreId())
 		r.selected.put(newPeer.GetStoreId())
-		targetPeers = append(targetPeers, newPeer)
-		replacedPeers = append(replacedPeers, peer)
+		targetPeers[newPeer.GetStoreId()] = newPeer
 	}
-	op := operator.CreateScatterRegionOperator("scatter-region", r.cluster, region, replacedPeers, targetPeers)
-	if op != nil {
-		op.SetPriorityLevel(core.HighPriority)
+	op, err := operator.CreateScatterRegionOperator("scatter-region", r.cluster, region, targetPeers)
+	if err != nil {
+		log.Debug("fail to create scatter region operator", zap.Error(err))
+		return nil
 	}
+	op.SetPriorityLevel(core.HighPriority)
 	return op
 }
 
@@ -165,6 +161,7 @@ func (r *RegionScatterer) selectPeerToReplace(stores map[uint64]*core.StoreInfo,
 	if err != nil {
 		return nil
 	}
+	newPeer.IsLearner = oldPeer.GetIsLearner()
 	return newPeer
 }
 
diff --git a/server/schedulers/adjacent_region.go b/server/schedulers/adjacent_region.go
index 448032f3b..3ba3016e6 100644
--- a/server/schedulers/adjacent_region.go
+++ b/server/schedulers/adjacent_region.go
@@ -295,7 +295,11 @@ func (l *balanceAdjacentRegionScheduler) disperseLeader(cluster opt.Cluster, bef
 	if target == nil {
 		return nil
 	}
-	op := operator.CreateTransferLeaderOperator("balance-adjacent-leader", before, before.GetLeader().GetStoreId(), target.GetID(), operator.OpAdjacent)
+	op, err := operator.CreateTransferLeaderOperator("balance-adjacent-leader", cluster, before, before.GetLeader().GetStoreId(), target.GetID(), operator.OpAdjacent)
+	if err != nil {
+		log.Debug("fail to create transfer leader operator", zap.Error(err))
+		return nil
+	}
 	op.SetPriorityLevel(core.LowPriority)
 	schedulerCounter.WithLabelValues(l.GetName(), "adjacent-leader").Inc()
 	return op
diff --git a/server/schedulers/balance_leader.go b/server/schedulers/balance_leader.go
index 8d1984248..23d558142 100644
--- a/server/schedulers/balance_leader.go
+++ b/server/schedulers/balance_leader.go
@@ -256,6 +256,10 @@ func (l *balanceLeaderScheduler) createOperator(cluster opt.Cluster, region *cor
 	l.counter.WithLabelValues("move-leader", source.GetAddress()+"-out", sourceLabel).Inc()
 	l.counter.WithLabelValues("move-leader", target.GetAddress()+"-in", targetLabel).Inc()
 	balanceDirectionCounter.WithLabelValues(l.GetName(), sourceLabel, targetLabel).Inc()
-	op := operator.CreateTransferLeaderOperator(BalanceLeaderType, region, region.GetLeader().GetStoreId(), targetID, operator.OpBalance)
+	op, err := operator.CreateTransferLeaderOperator(BalanceLeaderType, cluster, region, region.GetLeader().GetStoreId(), targetID, operator.OpBalance)
+	if err != nil {
+		log.Debug("fail to create balance leader operator", zap.Error(err))
+		return nil
+	}
 	return []*operator.Operator{op}
 }
diff --git a/server/schedulers/evict_leader.go b/server/schedulers/evict_leader.go
index 3f611173a..147cfdfd0 100644
--- a/server/schedulers/evict_leader.go
+++ b/server/schedulers/evict_leader.go
@@ -19,6 +19,7 @@ import (
 	"sync"
 
 	"github.com/gorilla/mux"
+	"github.com/pingcap/log"
 	"github.com/pingcap/pd/pkg/apiutil"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule"
@@ -28,6 +29,7 @@ import (
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/pkg/errors"
 	"github.com/unrolled/render"
+	"go.uber.org/zap"
 )
 
 const (
@@ -232,7 +234,11 @@ func (s *evictLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Operato
 			continue
 		}
 		schedulerCounter.WithLabelValues(s.GetName(), "new-operator").Inc()
-		op := operator.CreateTransferLeaderOperator(EvictLeaderType, region, region.GetLeader().GetStoreId(), target.GetID(), operator.OpLeader)
+		op, err := operator.CreateTransferLeaderOperator(EvictLeaderType, cluster, region, region.GetLeader().GetStoreId(), target.GetID(), operator.OpLeader)
+		if err != nil {
+			log.Debug("fail to create evict leader operator", zap.Error(err))
+			continue
+		}
 		op.SetPriorityLevel(core.HighPriority)
 		ops = append(ops, op)
 	}
diff --git a/server/schedulers/grant_leader.go b/server/schedulers/grant_leader.go
index 62086d3df..8e85b0ab4 100644
--- a/server/schedulers/grant_leader.go
+++ b/server/schedulers/grant_leader.go
@@ -19,6 +19,7 @@ import (
 	"sync"
 
 	"github.com/gorilla/mux"
+	"github.com/pingcap/log"
 	"github.com/pingcap/pd/pkg/apiutil"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule"
@@ -26,6 +27,7 @@ import (
 	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pkg/errors"
 	"github.com/unrolled/render"
+	"go.uber.org/zap"
 )
 
 const (
@@ -218,7 +220,11 @@ func (s *grantLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Operato
 		}
 
 		schedulerCounter.WithLabelValues(s.GetName(), "new-operator").Inc()
-		op := operator.CreateTransferLeaderOperator(GrantLeaderType, region, region.GetLeader().GetStoreId(), id, operator.OpLeader)
+		op, err := operator.CreateTransferLeaderOperator(GrantLeaderType, cluster, region, region.GetLeader().GetStoreId(), id, operator.OpLeader)
+		if err != nil {
+			log.Debug("fail to create grant leader operator", zap.Error(err))
+			continue
+		}
 		op.SetPriorityLevel(core.HighPriority)
 		ops = append(ops, op)
 	}
diff --git a/server/schedulers/hot_region.go b/server/schedulers/hot_region.go
index c2b55f439..0c82b4b8b 100644
--- a/server/schedulers/hot_region.go
+++ b/server/schedulers/hot_region.go
@@ -232,7 +232,11 @@ func (h *balanceHotRegionsScheduler) balanceHotReadRegions(cluster opt.Cluster)
 		schedulerCounter.WithLabelValues(h.GetName(), "move-leader").Inc()
 		srcStore := srcRegion.GetLeader().GetStoreId()
 		dstStore := newLeader.GetStoreId()
-		op := operator.CreateTransferLeaderOperator("transfer-hot-read-leader", srcRegion, srcStore, dstStore, operator.OpHotRegion)
+		op, err := operator.CreateTransferLeaderOperator("transfer-hot-read-leader", cluster, srcRegion, srcStore, dstStore, operator.OpHotRegion)
+		if err != nil {
+			log.Debug("fail to create transfer hot read leader operator", zap.Error(err))
+			return nil
+		}
 		op.SetPriorityLevel(core.HighPriority)
 		h.readPendings[newPendingInfluence(op, srcStore, dstStore, infl)] = struct{}{}
 		return []*operator.Operator{op}
@@ -283,7 +287,11 @@ func (h *balanceHotRegionsScheduler) balanceHotWriteRegions(cluster opt.Cluster)
 				schedulerCounter.WithLabelValues(h.GetName(), "move-leader").Inc()
 				srcStore := srcRegion.GetLeader().GetStoreId()
 				dstStore := newLeader.GetStoreId()
-				op := operator.CreateTransferLeaderOperator("transfer-hot-write-leader", srcRegion, srcStore, dstStore, operator.OpHotRegion)
+				op, err := operator.CreateTransferLeaderOperator("transfer-hot-write-leader", cluster, srcRegion, srcStore, dstStore, operator.OpHotRegion)
+				if err != nil {
+					log.Debug("fail to create transfer hot write leader operator", zap.Error(err))
+					return nil
+				}
 				op.SetPriorityLevel(core.HighPriority)
 				// transfer leader do not influence the byte rate
 				infl.ByteRate = 0
diff --git a/server/schedulers/label.go b/server/schedulers/label.go
index 0f759164d..401b31252 100644
--- a/server/schedulers/label.go
+++ b/server/schedulers/label.go
@@ -133,7 +133,11 @@ func (s *labelScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 			}
 
 			schedulerCounter.WithLabelValues(s.GetName(), "new-operator").Inc()
-			op := operator.CreateTransferLeaderOperator("label-reject-leader", region, id, target.GetID(), operator.OpLeader)
+			op, err := operator.CreateTransferLeaderOperator("label-reject-leader", cluster, region, id, target.GetID(), operator.OpLeader)
+			if err != nil {
+				log.Debug("fail to create transfer label reject leader operator", zap.Error(err))
+				return nil
+			}
 			return []*operator.Operator{op}
 		}
 	}
diff --git a/server/schedulers/random_merge.go b/server/schedulers/random_merge.go
index 8d5aebd46..b5e4f2c41 100644
--- a/server/schedulers/random_merge.go
+++ b/server/schedulers/random_merge.go
@@ -16,6 +16,7 @@ package schedulers
 import (
 	"math/rand"
 
+	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
@@ -23,6 +24,7 @@ import (
 	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/pkg/errors"
+	"go.uber.org/zap"
 )
 
 const (
@@ -124,6 +126,7 @@ func (s *randomMergeScheduler) Schedule(cluster opt.Cluster) []*operator.Operato
 
 	ops, err := operator.CreateMergeRegionOperator(RandomMergeType, cluster, region, target, operator.OpAdmin)
 	if err != nil {
+		log.Debug("fail to create merge region operator", zap.Error(err))
 		return nil
 	}
 	schedulerCounter.WithLabelValues(s.GetName(), "new-operator").Inc()
diff --git a/server/schedulers/shuffle_hot_region.go b/server/schedulers/shuffle_hot_region.go
index da68dab2d..736472da6 100644
--- a/server/schedulers/shuffle_hot_region.go
+++ b/server/schedulers/shuffle_hot_region.go
@@ -179,6 +179,7 @@ func (s *shuffleHotRegionScheduler) randomSchedule(cluster opt.Cluster, storeSta
 		}
 		op, err := operator.CreateMoveLeaderOperator("random-move-hot-leader", cluster, srcRegion, operator.OpRegion|operator.OpLeader, srcStoreID, destPeer)
 		if err != nil {
+			log.Debug("fail to create move leader operator", zap.Error(err))
 			return nil
 		}
 		schedulerCounter.WithLabelValues(s.GetName(), "create-operator").Inc()
diff --git a/server/schedulers/shuffle_leader.go b/server/schedulers/shuffle_leader.go
index c4210e6b9..0913779d4 100644
--- a/server/schedulers/shuffle_leader.go
+++ b/server/schedulers/shuffle_leader.go
@@ -14,6 +14,7 @@
 package schedulers
 
 import (
+	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
@@ -21,6 +22,7 @@ import (
 	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/pkg/errors"
+	"go.uber.org/zap"
 )
 
 const (
@@ -114,7 +116,11 @@ func (s *shuffleLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Opera
 		return nil
 	}
 	schedulerCounter.WithLabelValues(s.GetName(), "new-operator").Inc()
-	op := operator.CreateTransferLeaderOperator(ShuffleLeaderType, region, region.GetLeader().GetId(), targetStore.GetID(), operator.OpAdmin)
+	op, err := operator.CreateTransferLeaderOperator(ShuffleLeaderType, cluster, region, region.GetLeader().GetId(), targetStore.GetID(), operator.OpAdmin)
+	if err != nil {
+		log.Debug("fail to create shuffle leader operator", zap.Error(err))
+		return nil
+	}
 	op.SetPriorityLevel(core.HighPriority)
 	return []*operator.Operator{op}
 }


Test patch:
diff --git a/client/client_test.go b/client/client_test.go
index fdac0f1ef..6a8fd8f5f 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -112,6 +112,7 @@ func (s *testClientSuite) SetUpSuite(c *C) {
 	c.Assert(cluster, NotNil)
 	for _, store := range stores {
 		s.srv.PutStore(context.Background(), &pdpb.PutStoreRequest{Header: newHeader(s.srv), Store: store})
+		s.srv.StoreHeartbeat(context.Background(), &pdpb.StoreHeartbeatRequest{Header: newHeader(s.srv), Stats: &pdpb.StoreStats{StoreId: store.GetId()}})
 	}
 }
 
diff --git a/pkg/testutil/operator_check.go b/pkg/testutil/operator_check.go
index 9e2a581fb..31e43adf0 100644
--- a/pkg/testutil/operator_check.go
+++ b/pkg/testutil/operator_check.go
@@ -57,21 +57,27 @@ func CheckTransferLeaderFrom(c *check.C, op *operator.Operator, kind operator.Op
 	c.Assert(op.Kind()&kind, check.Equals, kind)
 }
 
+func trimTransferLeaders(op *operator.Operator) (steps []operator.OpStep, lastLeader uint64) {
+	for i := 0; i < op.Len(); i++ {
+		step := op.Step(i)
+		if s, ok := step.(operator.TransferLeader); ok {
+			lastLeader = s.ToStore
+		} else {
+			steps = append(steps, step)
+		}
+	}
+	return
+}
+
 // CheckTransferPeer checks if the operator is to transfer peer between the specified source and target stores.
 func CheckTransferPeer(c *check.C, op *operator.Operator, kind operator.OpKind, sourceID, targetID uint64) {
 	c.Assert(op, check.NotNil)
-	if op.Len() == 3 {
-		c.Assert(op.Step(0).(operator.AddLearner).ToStore, check.Equals, targetID)
-		c.Assert(op.Step(1), check.FitsTypeOf, operator.PromoteLearner{})
-		c.Assert(op.Step(2).(operator.RemovePeer).FromStore, check.Equals, sourceID)
-	} else {
-		c.Assert(op.Len(), check.Equals, 4)
-		c.Assert(op.Step(0).(operator.AddLearner).ToStore, check.Equals, targetID)
-		c.Assert(op.Step(1), check.FitsTypeOf, operator.PromoteLearner{})
-		c.Assert(op.Step(2).(operator.TransferLeader).FromStore, check.Equals, sourceID)
-		c.Assert(op.Step(3).(operator.RemovePeer).FromStore, check.Equals, sourceID)
-		kind |= operator.OpLeader
-	}
+
+	steps, _ := trimTransferLeaders(op)
+	c.Assert(steps, check.HasLen, 3)
+	c.Assert(steps[0].(operator.AddLearner).ToStore, check.Equals, targetID)
+	c.Assert(steps[1], check.FitsTypeOf, operator.PromoteLearner{})
+	c.Assert(steps[2].(operator.RemovePeer).FromStore, check.Equals, sourceID)
 	kind |= operator.OpRegion
 	c.Assert(op.Kind()&kind, check.Equals, kind)
 }
@@ -81,8 +87,16 @@ func CheckTransferPeer(c *check.C, op *operator.Operator, kind operator.OpKind,
 // transfers the leader out of source store.
 func CheckTransferPeerWithLeaderTransfer(c *check.C, op *operator.Operator, kind operator.OpKind, sourceID, targetID uint64) {
 	c.Assert(op, check.NotNil)
-	c.Assert(op.Len(), check.Equals, 4)
-	CheckTransferPeer(c, op, kind, sourceID, targetID)
+
+	steps, lastLeader := trimTransferLeaders(op)
+	c.Assert(steps, check.HasLen, 3)
+	c.Assert(steps[0].(operator.AddLearner).ToStore, check.Equals, targetID)
+	c.Assert(steps[1], check.FitsTypeOf, operator.PromoteLearner{})
+	c.Assert(steps[2].(operator.RemovePeer).FromStore, check.Equals, sourceID)
+	c.Assert(lastLeader, check.Not(check.Equals), uint64(0))
+	c.Assert(lastLeader, check.Not(check.Equals), sourceID)
+	kind |= operator.OpRegion
+	c.Assert(op.Kind()&kind, check.Equals, kind)
 }
 
 // CheckTransferPeerWithLeaderTransferFrom checks if the operator is to transfer
@@ -90,11 +104,13 @@ func CheckTransferPeerWithLeaderTransfer(c *check.C, op *operator.Operator, kind
 // the store.
 func CheckTransferPeerWithLeaderTransferFrom(c *check.C, op *operator.Operator, kind operator.OpKind, sourceID uint64) {
 	c.Assert(op, check.NotNil)
-	c.Assert(op.Len(), check.Equals, 4)
-	c.Assert(op.Step(0), check.FitsTypeOf, operator.AddLearner{})
-	c.Assert(op.Step(1), check.FitsTypeOf, operator.PromoteLearner{})
-	c.Assert(op.Step(2), check.FitsTypeOf, operator.TransferLeader{})
-	c.Assert(op.Step(3).(operator.RemovePeer).FromStore, check.Equals, sourceID)
+
+	steps, lastLeader := trimTransferLeaders(op)
+	c.Assert(steps[0], check.FitsTypeOf, operator.AddLearner{})
+	c.Assert(steps[1], check.FitsTypeOf, operator.PromoteLearner{})
+	c.Assert(steps[2].(operator.RemovePeer).FromStore, check.Equals, sourceID)
+	c.Assert(lastLeader, check.Not(check.Equals), uint64(0))
+	c.Assert(lastLeader, check.Not(check.Equals), sourceID)
 	kind |= (operator.OpRegion | operator.OpLeader)
 	c.Assert(op.Kind()&kind, check.Equals, kind)
 }
diff --git a/server/api/operator_test.go b/server/api/operator_test.go
index 4a26f5118..a3b95b0ca 100644
--- a/server/api/operator_test.go
+++ b/server/api/operator_test.go
@@ -147,6 +147,11 @@ func mustPutStore(c *C, svr *server.Server, id uint64, state metapb.StoreState,
 		},
 	})
 	c.Assert(err, IsNil)
+	_, err = svr.StoreHeartbeat(context.Background(), &pdpb.StoreHeartbeatRequest{
+		Header: &pdpb.RequestHeader{ClusterId: svr.ClusterID()},
+		Stats:  &pdpb.StoreStats{StoreId: id},
+	})
+	c.Assert(err, IsNil)
 }
 
 func mustRegionHeartbeat(c *C, svr *server.Server, region *core.RegionInfo) {
diff --git a/server/heartbeat_stream_test.go b/server/heartbeat_stream_test.go
index c1f9fba25..e336b30b3 100644
--- a/server/heartbeat_stream_test.go
+++ b/server/heartbeat_stream_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/pingcap/log"
 	"github.com/pingcap/pd/pkg/testutil"
 	"github.com/pingcap/pd/pkg/typeutil"
+	"github.com/pingcap/pd/server/core"
 	"go.uber.org/zap"
 )
 
@@ -60,6 +61,8 @@ func (s *testHeartbeatStreamSuite) TestActivity(c *C) {
 	}
 	_, err = s.grpcPDClient.PutStore(context.Background(), req)
 	c.Assert(err, IsNil)
+	err = s.svr.GetRaftCluster().HandleRegionHeartbeat(core.NewRegionInfo(s.region, s.region.GetPeers()[0]))
+	c.Assert(err, IsNil)
 	err = newHandler(s.svr).AddAddPeerOperator(s.region.GetId(), 2)
 	c.Assert(err, IsNil)
 
diff --git a/server/schedule/checker/merge_checker_test.go b/server/schedule/checker/merge_checker_test.go
index 8229d0f31..7bc8b0ff3 100644
--- a/server/schedule/checker/merge_checker_test.go
+++ b/server/schedule/checker/merge_checker_test.go
@@ -178,7 +178,21 @@ func (s *testMergeCheckerSuite) checkSteps(c *C, op *operator.Operator, steps []
 	c.Assert(steps, NotNil)
 	c.Assert(op.Len(), Equals, len(steps))
 	for i := range steps {
-		c.Assert(op.Step(i), DeepEquals, steps[i])
+		switch op.Step(i).(type) {
+		case operator.AddLearner:
+			c.Assert(op.Step(i).(operator.AddLearner).ToStore, Equals, steps[i].(operator.AddLearner).ToStore)
+		case operator.PromoteLearner:
+			c.Assert(op.Step(i).(operator.PromoteLearner).ToStore, Equals, steps[i].(operator.PromoteLearner).ToStore)
+		case operator.TransferLeader:
+			c.Assert(op.Step(i).(operator.TransferLeader).FromStore, Equals, steps[i].(operator.TransferLeader).FromStore)
+			c.Assert(op.Step(i).(operator.TransferLeader).ToStore, Equals, steps[i].(operator.TransferLeader).ToStore)
+		case operator.RemovePeer:
+			c.Assert(op.Step(i).(operator.RemovePeer).FromStore, Equals, steps[i].(operator.RemovePeer).FromStore)
+		case operator.MergeRegion:
+			c.Assert(op.Step(i).(operator.MergeRegion).IsPassive, Equals, steps[i].(operator.MergeRegion).IsPassive)
+		default:
+			c.Fatal("unknown operator step type")
+		}
 	}
 }
 
@@ -187,17 +201,13 @@ func (s *testMergeCheckerSuite) TestMatchPeers(c *C) {
 	ops := s.mc.Check(s.regions[2])
 	c.Assert(ops, NotNil)
 	s.checkSteps(c, ops[0], []operator.OpStep{
-		operator.AddLearner{ToStore: 4, PeerID: 1},
-		operator.PromoteLearner{ToStore: 4, PeerID: 1},
-
+		operator.AddLearner{ToStore: 1},
+		operator.PromoteLearner{ToStore: 1},
 		operator.RemovePeer{FromStore: 2},
-
-		operator.AddLearner{ToStore: 1, PeerID: 2},
-		operator.PromoteLearner{ToStore: 1, PeerID: 2},
-
+		operator.AddLearner{ToStore: 4},
+		operator.PromoteLearner{ToStore: 4},
 		operator.TransferLeader{FromStore: 6, ToStore: 5},
 		operator.RemovePeer{FromStore: 6},
-
 		operator.MergeRegion{
 			FromRegion: s.regions[2].GetMeta(),
 			ToRegion:   s.regions[1].GetMeta(),
@@ -225,8 +235,8 @@ func (s *testMergeCheckerSuite) TestMatchPeers(c *C) {
 	s.cluster.PutRegion(s.regions[2])
 	ops = s.mc.Check(s.regions[2])
 	s.checkSteps(c, ops[0], []operator.OpStep{
-		operator.AddLearner{ToStore: 4, PeerID: 3},
-		operator.PromoteLearner{ToStore: 4, PeerID: 3},
+		operator.AddLearner{ToStore: 4},
+		operator.PromoteLearner{ToStore: 4},
 		operator.RemovePeer{FromStore: 6},
 		operator.MergeRegion{
 			FromRegion: s.regions[2].GetMeta(),
@@ -274,22 +284,16 @@ func (s *testMergeCheckerSuite) TestMatchPeers(c *C) {
 	s.cluster.PutRegion(s.regions[2])
 	ops = s.mc.Check(s.regions[2])
 	s.checkSteps(c, ops[0], []operator.OpStep{
-		operator.AddLearner{ToStore: 4, PeerID: 4},
-		operator.PromoteLearner{ToStore: 4, PeerID: 4},
-
+		operator.AddLearner{ToStore: 1},
+		operator.PromoteLearner{ToStore: 1},
 		operator.RemovePeer{FromStore: 3},
-
-		operator.AddLearner{ToStore: 1, PeerID: 5},
-		operator.PromoteLearner{ToStore: 1, PeerID: 5},
-
+		operator.AddLearner{ToStore: 4},
+		operator.PromoteLearner{ToStore: 4},
 		operator.RemovePeer{FromStore: 6},
-
-		operator.AddLearner{ToStore: 5, PeerID: 6},
-		operator.PromoteLearner{ToStore: 5, PeerID: 6},
-
-		operator.TransferLeader{FromStore: 2, ToStore: 4},
+		operator.AddLearner{ToStore: 5},
+		operator.PromoteLearner{ToStore: 5},
+		operator.TransferLeader{FromStore: 2, ToStore: 1},
 		operator.RemovePeer{FromStore: 2},
-
 		operator.MergeRegion{
 			FromRegion: s.regions[2].GetMeta(),
 			ToRegion:   s.regions[1].GetMeta(),
@@ -316,19 +320,16 @@ func (s *testMergeCheckerSuite) TestMatchPeers(c *C) {
 	s.cluster.PutRegion(s.regions[1])
 	ops = s.mc.Check(s.regions[2])
 	s.checkSteps(c, ops[0], []operator.OpStep{
-		operator.AddLearner{ToStore: 1, PeerID: 7},
-		operator.PromoteLearner{ToStore: 1, PeerID: 7},
-
+		operator.AddLearner{ToStore: 1},
+		operator.PromoteLearner{ToStore: 1},
 		operator.RemovePeer{FromStore: 3},
 
-		operator.AddLearner{ToStore: 7, PeerID: 8},
-		operator.PromoteLearner{ToStore: 7, PeerID: 8},
-
+		operator.AddLearner{ToStore: 7},
+		operator.PromoteLearner{ToStore: 7},
 		operator.RemovePeer{FromStore: 6},
 
-		operator.AddLearner{ToStore: 8, PeerID: 9},
-		operator.PromoteLearner{ToStore: 8, PeerID: 9},
-
+		operator.AddLearner{ToStore: 8},
+		operator.PromoteLearner{ToStore: 8},
 		operator.TransferLeader{FromStore: 2, ToStore: 1},
 		operator.RemovePeer{FromStore: 2},
 
@@ -366,17 +367,13 @@ func (s *testMergeCheckerSuite) TestMatchPeers(c *C) {
 	s.cluster.PutRegion(s.regions[1])
 	ops = s.mc.Check(s.regions[2])
 	s.checkSteps(c, ops[0], []operator.OpStep{
-		operator.AddLearner{ToStore: 1, PeerID: 10},
-		operator.PromoteLearner{ToStore: 1, PeerID: 10},
-
+		operator.AddLearner{ToStore: 1},
+		operator.PromoteLearner{ToStore: 1},
 		operator.RemovePeer{FromStore: 3},
-
-		operator.AddLearner{ToStore: 8, PeerID: 11},
-		operator.PromoteLearner{ToStore: 8, PeerID: 11},
-
+		operator.AddLearner{ToStore: 8},
+		operator.PromoteLearner{ToStore: 8},
 		operator.TransferLeader{FromStore: 2, ToStore: 1},
 		operator.RemovePeer{FromStore: 2},
-
 		operator.MergeRegion{
 			FromRegion: s.regions[2].GetMeta(),
 			ToRegion:   s.regions[1].GetMeta(),
diff --git a/server/schedule/operator/operator_test.go b/server/schedule/operator/operator_test.go
index 93dcb3621..9ee151ebb 100644
--- a/server/schedule/operator/operator_test.go
+++ b/server/schedule/operator/operator_test.go
@@ -75,39 +75,6 @@ func (s *testOperatorSuite) newTestRegion(regionID uint64, leaderPeer uint64, pe
 	return regionInfo
 }
 
-func genAddPeers(store uint64, groups [][]uint64) [][]OpStep {
-	ret := make([][]OpStep, len(groups))
-	for i, grp := range groups {
-		steps := make([]OpStep, len(grp))
-		for j, id := range grp {
-			steps[j] = AddPeer{ToStore: store, PeerID: id}
-		}
-		ret[i] = steps
-	}
-	return ret
-}
-
-func (s *testOperatorSuite) TestInterleaveStepGroups(c *C) {
-	a := genAddPeers(1, [][]uint64{{1, 2}, {3}, {4, 5, 6}})
-	b := genAddPeers(1, [][]uint64{{11}, {12}, {13, 14}, {15, 16}})
-	ans := genAddPeers(1, [][]uint64{{1, 2, 11, 3, 12, 4, 5, 6, 13, 14, 15, 16}})
-	res := interleaveStepGroups(a, b, 12)
-	c.Assert(res, DeepEquals, ans[0])
-}
-
-func (s *testOperatorSuite) TestFindNoLabelProperty(c *C) {
-	stores := []uint64{8, 7, 3, 4, 7, 3, 1, 5, 6}
-	i, id := findNoLabelProperty(s.cluster, opt.RejectLeader, stores)
-	c.Assert(i, Equals, 2)
-	c.Assert(id, Equals, uint64(3))
-	i, id = findNoLabelProperty(s.cluster, opt.RejectLeader, stores[2:])
-	c.Assert(i, Equals, 0)
-	c.Assert(id, Equals, uint64(3))
-	i, id = findNoLabelProperty(s.cluster, opt.RejectLeader, stores[:2])
-	c.Assert(i, Less, 0)
-	c.Assert(id, Equals, uint64(0))
-}
-
 func (s *testOperatorSuite) TestOperatorStep(c *C) {
 	region := s.newTestRegion(1, 1, [2]uint64{1, 1}, [2]uint64{2, 2})
 	c.Assert(TransferLeader{FromStore: 1, ToStore: 2}.IsFinish(region), IsFalse)
diff --git a/server/schedulers/balance_test.go b/server/schedulers/balance_test.go
index 51076f9b3..7e4e4bc76 100644
--- a/server/schedulers/balance_test.go
+++ b/server/schedulers/balance_test.go
@@ -815,7 +815,7 @@ func (s *testReplicaCheckerSuite) TestBasic(c *C) {
 	c.Assert(rc.Check(region), IsNil)
 	opt.EnableRemoveExtraReplica = true
 
-	region = region.Clone(core.WithRemoveStorePeer(1))
+	region = region.Clone(core.WithRemoveStorePeer(1), core.WithLeader(region.GetStorePeer(3)))
 
 	// Peer in store 2 is down, remove it.
 	tc.SetStoreDown(2)
@@ -966,7 +966,7 @@ func (s *testReplicaCheckerSuite) TestDistinctScore(c *C) {
 	peer6, _ := tc.AllocPeer(6)
 	region = region.Clone(core.WithAddPeer(peer6))
 	testutil.CheckRemovePeer(c, rc.Check(region), 1)
-	region = region.Clone(core.WithRemoveStorePeer(1))
+	region = region.Clone(core.WithRemoveStorePeer(1), core.WithLeader(region.GetStorePeer(2)))
 	c.Assert(rc.Check(region), IsNil)
 
 	// Store 8 has the same zone and different rack with store 7.
diff --git a/server/schedulers/scheduler_test.go b/server/schedulers/scheduler_test.go
index 5456c1aad..cb9237429 100644
--- a/server/schedulers/scheduler_test.go
+++ b/server/schedulers/scheduler_test.go
@@ -389,7 +389,7 @@ func (s *testShuffleHotRegionSchedulerSuite) TestBalance(c *C) {
 		}
 	}
 	c.Assert(op, NotNil)
-	c.Assert(op[0].Step(1).(operator.PromoteLearner).ToStore, Equals, op[0].Step(2).(operator.TransferLeader).ToStore)
+	c.Assert(op[0].Step(1).(operator.PromoteLearner).ToStore, Equals, op[0].Step(op[0].Len()-1).(operator.TransferLeader).ToStore)
 	c.Assert(op[0].Step(1).(operator.PromoteLearner).ToStore, Not(Equals), 6)
 }
 
diff --git a/tests/pdctl/helper.go b/tests/pdctl/helper.go
index 735907649..6caec6b54 100644
--- a/tests/pdctl/helper.go
+++ b/tests/pdctl/helper.go
@@ -116,6 +116,11 @@ func MustPutStore(c *check.C, svr *server.Server, id uint64, state metapb.StoreS
 		},
 	})
 	c.Assert(err, check.IsNil)
+	_, err = svr.StoreHeartbeat(context.Background(), &pdpb.StoreHeartbeatRequest{
+		Header: &pdpb.RequestHeader{ClusterId: svr.ClusterID()},
+		Stats:  &pdpb.StoreStats{StoreId: id},
+	})
+	c.Assert(err, check.IsNil)
 }
 
 // MustPutRegion is used for test purpose.