package locate import ( "context" "fmt" "math/rand" "os" "runtime/pprof" "sort" "strconv" "strings" "sync/atomic" "testing" "time" "github.com/pingcap/failpoint" "github.com/pingcap/kvproto/pkg/errorpb" "github.com/pingcap/kvproto/pkg/kvrpcpb" "github.com/pingcap/kvproto/pkg/metapb" "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/tikv/client-go/v2/config" "github.com/tikv/client-go/v2/config/retry" "github.com/tikv/client-go/v2/internal/apicodec" "github.com/tikv/client-go/v2/internal/client" "github.com/tikv/client-go/v2/internal/client/mockserver" "github.com/tikv/client-go/v2/internal/mockstore/mocktikv" "github.com/tikv/client-go/v2/kv" "github.com/tikv/client-go/v2/oracle" "github.com/tikv/client-go/v2/tikvrpc" "github.com/tikv/client-go/v2/util/israce" ) type testReplicaSelectorSuite struct { suite.Suite cluster *mocktikv.Cluster storeIDs []uint64 peerIDs []uint64 regionID uint64 leaderPeer uint64 cache *RegionCache bo *retry.Backoffer mvccStore mocktikv.MVCCStore } func (s *testReplicaSelectorSuite) SetupTest(t *testing.T) { s.mvccStore = mocktikv.MustNewMVCCStore() s.cluster = mocktikv.NewCluster(s.mvccStore) s.storeIDs, s.peerIDs, s.regionID, s.leaderPeer = mocktikv.BootstrapWithMultiStores(s.cluster, 3) pdCli := &CodecPDClient{mocktikv.NewPDClient(s.cluster), apicodec.NewCodecV1(apicodec.ModeTxn)} // Disable the tick on health status. s.cache = NewRegionCache(pdCli, RegionCacheNoHealthTick) s.bo = retry.NewNoopBackoff(context.Background()) s.SetT(t) s.SetS(s) randIntn = func(n int) int { return 0 } s.NoError(failpoint.Enable("tikvclient/fastBackoffBySkipSleep", `return`)) s.NoError(failpoint.Enable("tikvclient/skipStoreCheckUntilHealth", `return`)) loc, err := s.cache.LocateKey(s.bo, []byte("key")) s.Nil(err) r := s.cache.GetCachedRegionWithRLock(loc.Region) s.NotNil(r) // The following assumptions are made in the latter tests, which should be checked in advance: s.Equal(r.GetLeaderStoreID(), uint64(1)) // region's leader in store1. s.Equal(len(r.getStore().stores), 3) // region has 3 peer(stores). for _, store := range r.getStore().stores { s.Equal(store.labels[0].Key, "id") // Each store has a label "id", and the value is the store's ID. s.Equal(store.labels[0].Value, fmt.Sprintf("%v", store.storeID)) } } func (s *testReplicaSelectorSuite) TearDownTest() { s.cache.Close() s.mvccStore.Close() randIntn = rand.Intn s.NoError(failpoint.Disable("tikvclient/fastBackoffBySkipSleep")) s.NoError(failpoint.Disable("tikvclient/skipStoreCheckUntilHealth")) } type replicaSelectorAccessPathCase struct { reqType tikvrpc.CmdType readType kv.ReplicaReadType staleRead bool timeout time.Duration busyThresholdMs uint32 label *metapb.StoreLabel accessErr []RegionErrorType accessErrInValid bool expect *accessPathResult result accessPathResult beforeRun func() // beforeRun will be called before the test case execute, if it is nil, resetStoreState will be called. afterRun func(*replicaSelector) // afterRun will be called after the test case execute, if it is nil, invalidateRegion will be called. } type accessPathResult struct { accessPath []string respErr string respRegionError *errorpb.Error backoffCnt int backoffDetail []string regionIsValid bool } func TestReplicaSelectorBasic(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() req := tikvrpc.NewReplicaReadRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{Key: []byte("a")}, kv.ReplicaReadMixed, nil, kvrpcpb.Context{}) req.EnableStaleWithMixedReplicaRead() rc := s.getRegion() s.NotNil(rc) rc.invalidate(Other) selector, err := newReplicaSelector(s.cache, rc.VerID(), req) s.Nil(err) s.Nil(selector) s.Equal("", selector.String()) selector2, err := newReplicaSelector(s.cache, rc.VerID(), req) s.Nil(err) s.Nil(selector2) s.True(selector2 == nil) s.Equal("", selector2.String()) rc = s.getRegion() selector, err = newReplicaSelector(s.cache, rc.VerID(), req) s.Nil(err) s.NotNil(selector) for _, reqSource := range []string{"leader", "follower", "follower", "unknown"} { _, err := selector.next(s.bo, req) s.Nil(err) s.Equal(reqSource, selector.replicaType()) } rc = s.getRegion() selector, err = newReplicaSelector(s.cache, rc.VerID(), req) s.Nil(err) s.NotNil(selector) ctx, err := selector.next(s.bo, req) s.Nil(err) s.NotNil(ctx) rc.invalidate(Other) ctx, err = selector.next(s.bo, req) s.Nil(err) s.Nil(ctx) } func TestReplicaSelectorCalculateScore(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() req := tikvrpc.NewReplicaReadRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{Key: []byte("a")}, kv.ReplicaReadMixed, nil, kvrpcpb.Context{}) region, err := s.cache.LocateKey(s.bo, []byte("a")) s.Nil(err) s.NotNil(region) selector, err := newReplicaSelector(s.cache, region.Region, req) s.Nil(err) for i, r := range selector.replicas { rc := s.cache.GetCachedRegionWithRLock(region.Region) s.NotNil(rc) isLeader := r.peer.Id == rc.GetLeaderPeerID() s.Equal(isLeader, AccessIndex(i) == rc.getStore().workTiKVIdx) strategy := ReplicaSelectMixedStrategy{leaderIdx: rc.getStore().workTiKVIdx} score := strategy.calculateScore(r, isLeader) s.Equal(r.store.healthStatus.IsSlow(), false) if isLeader { s.Equal(score, flagLabelMatches+flagNotSlow+flagNotAttempted) } else { s.Equal(score, flagLabelMatches+flagNormalPeer+flagNotSlow+flagNotAttempted) } r.store.healthStatus.markAlreadySlow() s.Equal(r.store.healthStatus.IsSlow(), true) score = strategy.calculateScore(r, isLeader) if isLeader { s.Equal(score, flagLabelMatches+flagNotAttempted) } else { s.Equal(score, flagLabelMatches+flagNormalPeer+flagNotAttempted) } strategy.tryLeader = true score = strategy.calculateScore(r, isLeader) s.Equal(score, flagLabelMatches+flagNormalPeer+flagNotAttempted) strategy.preferLeader = true score = strategy.calculateScore(r, isLeader) s.Equal(score, flagLabelMatches+flagNormalPeer+flagNotAttempted) strategy.learnerOnly = true strategy.tryLeader = false strategy.preferLeader = false score = strategy.calculateScore(r, isLeader) s.Equal(score, flagLabelMatches+flagNotAttempted) labels := []*metapb.StoreLabel{ { Key: "zone", Value: "us-west-1", }, } strategy.labels = labels score = strategy.calculateScore(r, isLeader) s.Equal(score, flagNotAttempted) strategy = ReplicaSelectMixedStrategy{ leaderIdx: rc.getStore().workTiKVIdx, tryLeader: true, labels: labels, } score = strategy.calculateScore(r, isLeader) if isLeader { s.Equal(score, flagPreferLeader+flagNotAttempted) } else { s.Equal(score, flagNormalPeer+flagNotAttempted) } strategy = ReplicaSelectMixedStrategy{ leaderIdx: rc.getStore().workTiKVIdx, preferLeader: true, labels: labels, } score = strategy.calculateScore(r, isLeader) s.Equal(score, flagNormalPeer+flagNotAttempted) r.store.labels = labels score = strategy.calculateScore(r, isLeader) s.Equal(score, flagLabelMatches+flagNormalPeer+flagNotAttempted) r.store.labels = nil } } func TestCanFastRetry(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() // Test for non-leader read. loc, err := s.cache.LocateKey(s.bo, []byte("key")) s.Nil(err) req := tikvrpc.NewRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{Key: []byte("key")}) req.EnableStaleWithMixedReplicaRead() selector, err := newReplicaSelector(s.cache, loc.Region, req) s.Nil(err) for i := 0; i < 3; i++ { _, err = selector.next(s.bo, req) s.Nil(err) selector.canFastRetry() s.True(selector.canFastRetry()) } // Test for leader read. req = tikvrpc.NewRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{Key: []byte("key")}) req.ReplicaReadType = kv.ReplicaReadLeader selector, err = newReplicaSelector(s.cache, loc.Region, req) s.Nil(err) for i := 0; i < 12; i++ { _, err = selector.next(s.bo, req) s.Nil(err) ok := selector.canFastRetry() if i <= 8 { s.False(ok) // can't skip since leader is available. } else { s.True(ok) } } } func TestPendingBackoff(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() loc, err := s.cache.LocateKey(s.bo, []byte("key")) s.Nil(err) req := tikvrpc.NewRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{Key: []byte("key")}) req.EnableStaleWithMixedReplicaRead() selector, err := newReplicaSelector(s.cache, loc.Region, req) s.Nil(err) bo := retry.NewNoopBackoff(context.Background()) err = selector.backoffOnRetry(nil, bo) s.Nil(err) err = selector.backoffOnRetry(&Store{storeID: 1}, bo) s.Nil(err) err = selector.backoffOnNoCandidate(bo) s.Nil(err) selector.addPendingBackoff(nil, retry.BoRegionScheduling, errors.New("err-0")) s.Equal(1, len(selector.pendingBackoffs)) selector.addPendingBackoff(&Store{storeID: 1}, retry.BoTiKVRPC, errors.New("err-1")) s.Equal(2, len(selector.pendingBackoffs)) selector.addPendingBackoff(&Store{storeID: 2}, retry.BoTiKVDiskFull, errors.New("err-2")) s.Equal(3, len(selector.pendingBackoffs)) selector.addPendingBackoff(&Store{storeID: 1}, retry.BoTiKVServerBusy, errors.New("err-3")) s.Equal(3, len(selector.pendingBackoffs)) _, ok := selector.pendingBackoffs[0] s.True(ok) err = selector.backoffOnRetry(nil, bo) s.NotNil(err) s.Equal("err-0", err.Error()) _, ok = selector.pendingBackoffs[0] s.False(ok) s.Equal(2, len(selector.pendingBackoffs)) err = selector.backoffOnRetry(&Store{storeID: 10}, bo) s.Nil(err) s.Equal(2, len(selector.pendingBackoffs)) err = selector.backoffOnNoCandidate(bo) s.NotNil(err) s.Equal("err-3", err.Error()) } func TestReplicaReadAccessPathByCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() testReplicaReadAccessPathByCase(s) } func TestReplicaReadAccessPathByCaseUsingAsyncAPI(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() failpoint.Enable("tikvclient/useSendReqAsync", `return(true)`) defer failpoint.Disable("tikvclient/useSendReqAsync") testReplicaReadAccessPathByCase(s) } func testReplicaReadAccessPathByCase(s *testReplicaSelectorSuite) { fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} // fake region error, cause by no replica is available. ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // test stale read with label. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", // try leader with leader read. }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: true}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, NotLeaderWithNewLeader2Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}"}, // retry the new leader. respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, NotLeaderWithNewLeader2Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: true}"}, // store2 has DeadLineExceededErr, so don't retry store2 even it is new leader. respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, accessErr: []RegionErrorType{DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, accessErr: []RegionErrorType{NotLeaderWithNewLeader3Err, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdPrewrite, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, // this actually has no effect on write req. since tikv_client_read_timeout is used for read req only. accessErr: []RegionErrorType{NotLeaderWithNewLeader3Err, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", // try new leader in store3, but got DeadLineExceededErr, and this store's liveness will be mock to unreachable in test case running. "{addr: store2, replica-read: false, stale-read: false}"}, // try remaining replica in store2. respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, accessErr: []RegionErrorType{NotLeaderErr, DeadLineExceededErr, NotLeaderWithNewLeader2Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // Don't invalid region in tryFollowers, since leader meets deadlineExceededErr. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, label: nil, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: true}", "{addr: store3, replica-read: false, stale-read: true}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // Don't invalid region in accessFollower, since leader meets deadlineExceededErr. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: nil, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: 0, label: &metapb.StoreLabel{Key: "id", Value: "3"}, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: true}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store2, replica-read: false, stale-read: true}", "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(2) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store3, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: nil, backoffCnt: 0, // no backoff since request success. backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store3, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(1) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Microsecond * 100, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyWithEstimatedWaitMsErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: true}", "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(3) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: true, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, staleRead: false, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: false, stale-read: false}", "{addr: store1, replica-read: true, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, staleRead: false, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store1, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(1) } func TestReplicaReadAccessPathByCase2(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() testReplicaReadAccessPathByCase2(s) } func TestReplicaReadAccessPathByCase2UsingAsyncAPI(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() failpoint.Enable("tikvclient/useSendReqAsync", `return(true)`) defer failpoint.Disable("tikvclient/useSendReqAsync") testReplicaReadAccessPathByCase2(s) } func testReplicaReadAccessPathByCase2(s *testReplicaSelectorSuite) { fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} // Following cases are found by other test, careful. ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, DiskFullErr, FlashbackNotPreparedErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "region 0 is not prepared for the flashback", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvDiskFull+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // Don't invalid region in tryFollowers, since leader meets deadlineExceededErr. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, label: nil, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderWithNewLeader3Err, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // Don't invalid region in accessFollower, since leader meets deadlineExceededErr. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: nil, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: 0, label: &metapb.StoreLabel{Key: "id", Value: "3"}, accessErr: []RegionErrorType{DataIsNotReadyErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{FlashbackInProgressErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Microsecond * 100, label: &metapb.StoreLabel{Key: "id", Value: "1"}, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", // don't retry leader(store1), since leader won't return DataIsNotReadyErr, so retry it with leader-read may got NotLeaderErr. "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdPrewrite, readType: kv.ReplicaReadLeader, staleRead: false, timeout: 0, label: nil, accessErr: []RegionErrorType{DeadLineExceededErr, NotLeaderErr, NotLeaderWithNewLeader2Err, ServerIsBusyErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 4, backoffDetail: []string{"regionScheduling+1", "tikvRPC+2", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: 0, label: nil, accessErr: []RegionErrorType{DeadLineExceededErr, RegionNotInitializedErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"regionNotInitialized+1", "tikvRPC+2"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, NotLeaderWithNewLeader2Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: true}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(2) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "3"}, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: true, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store1, replica-read: true, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(1) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, accessErr: []RegionErrorType{NotLeaderErr, DeadLineExceededErr, NotLeaderWithNewLeader2Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } func TestReplicaReadAccessPathByBasicCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() testReplicaReadAccessPathByBasicCase(s) } func TestReplicaReadAccessPathByBasicCaseUsingAsyncAPI(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() failpoint.Enable("tikvclient/useSendReqAsync", `return(true)`) defer failpoint.Disable("tikvclient/useSendReqAsync") testReplicaReadAccessPathByBasicCase(s) } func testReplicaReadAccessPathByBasicCase(s *testReplicaSelectorSuite) { retryableErrors := []RegionErrorType{ServerIsBusyErr, ServerIsBusyWithEstimatedWaitMsErr, StaleCommandErr, MaxTimestampNotSyncedErr, ProposalInMergingModeErr, ReadIndexNotReadyErr, RegionNotInitializedErr, DiskFullErr} noRetryErrors := []RegionErrorType{RegionNotFoundErr, KeyNotInRegionErr, EpochNotMatchErr, StoreNotMatchErr, RaftEntryTooLargeErr, RecoveryInProgressErr, FlashbackNotPreparedErr, IsWitnessErr, MismatchPeerIdErr, BucketVersionNotMatchErr} for _, reqType := range []tikvrpc.CmdType{tikvrpc.CmdGet, tikvrpc.CmdPrewrite} { for _, readType := range []kv.ReplicaReadType{kv.ReplicaReadLeader, kv.ReplicaReadFollower, kv.ReplicaReadMixed, kv.ReplicaReadPreferLeader} { if reqType == tikvrpc.CmdPrewrite && readType != kv.ReplicaReadLeader { // write req only support leader read. continue } for _, staleRead := range []bool{false, true} { if staleRead && readType != kv.ReplicaReadMixed { // stale read only support mixed read. continue } for _, tp := range retryableErrors { backoff := []string{} switch tp { case ServerIsBusyErr, ServerIsBusyWithEstimatedWaitMsErr: if readType == kv.ReplicaReadLeader { backoff = []string{"tikvServerBusy+1"} } case DiskFullErr: backoff = []string{"tikvDiskFull+1"} case RegionNotInitializedErr: backoff = []string{"regionNotInitialized+1"} case ReadIndexNotReadyErr, ProposalInMergingModeErr: backoff = []string{"regionScheduling+1"} case MaxTimestampNotSyncedErr: backoff = []string{"maxTsNotSynced+1"} } accessPath := []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", } switch readType { case kv.ReplicaReadLeader: accessPath = []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", } case kv.ReplicaReadFollower: if tp == ServerIsBusyErr { backoff = []string{} } accessPath = []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", } case kv.ReplicaReadMixed: if tp == ServerIsBusyErr { backoff = []string{} } if staleRead { if tp == ServerIsBusyErr || tp == ServerIsBusyWithEstimatedWaitMsErr { accessPath = []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: true}", } } else { accessPath = []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: true, stale-read: false}", } } } default: if tp == ServerIsBusyErr { backoff = []string{} } } ca := replicaSelectorAccessPathCase{ reqType: reqType, readType: readType, staleRead: staleRead, accessErr: []RegionErrorType{tp}, expect: &accessPathResult{ accessPath: accessPath, respErr: "", respRegionError: nil, backoffCnt: len(backoff), backoffDetail: backoff, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } for _, tp := range noRetryErrors { backoff := []string{} regionIsValid := false respErr := "" respRegionError := tp.GenRegionError() accessPath := []string{"{addr: store1, replica-read: false, stale-read: false}"} switch tp { case RecoveryInProgressErr: backoff = []string{"regionRecoveryInProgress+1"} case IsWitnessErr: backoff = []string{"isWitness+1"} case BucketVersionNotMatchErr: regionIsValid = true case RaftEntryTooLargeErr: respErr = RaftEntryTooLargeErr.GenRegionError().String() respRegionError = nil regionIsValid = true case FlashbackNotPreparedErr: respErr = "region 0 is not prepared for the flashback" respRegionError = nil regionIsValid = true case RegionNotFoundErr: regionIsValid = false } switch readType { case kv.ReplicaReadLeader: accessPath = []string{"{addr: store1, replica-read: false, stale-read: false}"} case kv.ReplicaReadFollower: // For RegionNotFoundErr from follower, it will retry on leader if tp == RegionNotFoundErr { accessPath = []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", } respRegionError = nil regionIsValid = true } else { accessPath = []string{"{addr: store2, replica-read: true, stale-read: false}"} } case kv.ReplicaReadMixed: if staleRead { accessPath = []string{"{addr: store1, replica-read: false, stale-read: true}"} } } ca := replicaSelectorAccessPathCase{ reqType: reqType, readType: readType, staleRead: staleRead, accessErr: []RegionErrorType{tp}, expect: &accessPathResult{ accessPath: accessPath, respErr: respErr, respRegionError: respRegionError, backoffCnt: len(backoff), backoffDetail: backoff, regionIsValid: regionIsValid, }, } s.True(s.runCaseAndCompare(ca)) } } } } } func TestReplicaReadAccessPathByLeaderCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() testReplicaReadAccessPathByLeaderCase(s) } func TestReplicaReadAccessPathByLeaderCaseUsingAsyncAPI(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() failpoint.Enable("tikvclient/useSendReqAsync", `return(true)`) defer failpoint.Disable("tikvclient/useSendReqAsync") testReplicaReadAccessPathByLeaderCase(s) } func testReplicaReadAccessPathByLeaderCase(s *testReplicaSelectorSuite) { fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} // fake region error, cause by no replica is available. ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: nil, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 2, backoffDetail: []string{"tikvServerBusy+2"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 9, backoffDetail: []string{"tikvServerBusy+9"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 2, backoffDetail: []string{"regionScheduling+2"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"regionScheduling+3"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderWithNewLeader3Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, // no backoff, fast retry. backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderWithNewLeader3Err, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr, NotLeaderWithNewLeader1Err, NotLeaderWithNewLeader1Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", // Suppose there is network partition between TiDB and store1 "{addr: store2, replica-read: false, stale-read: false}", // TODO(crazycs520): is this expected, should retry with replica-read? "{addr: store3, replica-read: false, stale-read: false}", // ditto. }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 2, backoffDetail: []string{"tikvRPC+1", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderWithNewLeader3Err, ServerIsBusyWithEstimatedWaitMsErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"regionScheduling+2", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderWithNewLeader3Err, ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 3, backoffDetail: []string{"regionScheduling+1", "tikvServerBusy+2"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // Test for switch leader. cas := []replicaSelectorAccessPathCase{ { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderWithNewLeader3Err}, beforeRun: func() { /* don't resetStoreState */ }, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: false}", // try new leader directly. "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{ServerIsBusyErr}, beforeRun: func() { /* don't resetStoreState */ }, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: false, stale-read: false}", // try new leader directly. "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: true, }, }, } s.True(s.runMultiCaseAndCompare(cas)) } func TestReplicaReadAccessPathByFollowerCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() testReplicaReadAccessPathByFollowerCase(s) } func TestReplicaReadAccessPathByFollowerCaseUsingAsyncAPI(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() failpoint.Enable("tikvclient/useSendReqAsync", `return(true)`) defer failpoint.Disable("tikvclient/useSendReqAsync") testReplicaReadAccessPathByFollowerCase(s) } func testReplicaReadAccessPathByFollowerCase(s *testReplicaSelectorSuite) { fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadFollower, accessErr: nil, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadFollower, accessErr: []RegionErrorType{ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadFollower, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadFollower, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadFollower, timeout: time.Second, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } func TestReplicaReadAccessPathByMixedAndPreferLeaderCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() testReplicaReadAccessPathByMixedAndPreferLeaderCase(s) } func TestReplicaReadAccessPathByMixedAndPreferLeaderCaseUsingAsyncAPI(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() failpoint.Enable("tikvclient/useSendReqAsync", `return(true)`) defer failpoint.Disable("tikvclient/useSendReqAsync") testReplicaReadAccessPathByMixedAndPreferLeaderCase(s) } func testReplicaReadAccessPathByMixedAndPreferLeaderCase(s *testReplicaSelectorSuite) { fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} var ca replicaSelectorAccessPathCase // since leader in store1, so ReplicaReadMixed and ReplicaReadPreferLeader will have the same access path. for _, readType := range []kv.ReplicaReadType{kv.ReplicaReadMixed, kv.ReplicaReadPreferLeader} { ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: readType, accessErr: nil, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: readType, accessErr: []RegionErrorType{ServerIsBusyWithEstimatedWaitMsErr, StaleCommandErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: readType, accessErr: []RegionErrorType{ServerIsBusyErr, RegionNotFoundErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: RegionNotFoundErr.GenRegionError(), backoffCnt: 0, backoffDetail: []string{}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: readType, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: readType, timeout: time.Second, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", // try match label first. "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(3) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: false, stale-read: false}", // try leader first. "{addr: store1, replica-read: true, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", // try match label first, since match label has higher priority. "{addr: store3, replica-read: false, stale-read: false}", // try leader. "{addr: store1, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(1) cas := []replicaSelectorAccessPathCase{ { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, staleRead: false, timeout: 0, accessErr: []RegionErrorType{ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", // store1 will be marked already slow. "{addr: store2, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, staleRead: false, timeout: 0, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, beforeRun: func() { /* don't resetStoreState */ }, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: true, stale-read: false}", // won't try leader in store1, since it is slow. "{addr: store3, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, }, } s.True(s.runMultiCaseAndCompare(cas)) cas = []replicaSelectorAccessPathCase{ { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, staleRead: false, timeout: 0, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", // store1 will be marked already slow. "{addr: store2, replica-read: true, stale-read: false}", // store2 will be marked already slow. "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadPreferLeader, staleRead: false, timeout: 0, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, beforeRun: func() { /* don't resetStoreState */ }, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: true, stale-read: false}", // won't try leader in store1, since it is slow, ditto for store2. "{addr: store1, replica-read: false, stale-read: false}", // won't retry store2, since it is slow, and it is not leader replica. }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, }, } s.True(s.runMultiCaseAndCompare(cas)) } func TestReplicaReadAccessPathByStaleReadCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: nil, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: []RegionErrorType{DataIsNotReadyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // test stale read with label. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", // try leader with leader read. }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, NotLeaderWithNewLeader3Err}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store3, replica-read: false, stale-read: false}", // try new leader. }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store2, replica-read: false, stale-read: true}", "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(2) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, accessErr: []RegionErrorType{DataIsNotReadyErr, ServerIsBusyErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store3, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) s.changeRegionLeader(1) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, NotLeaderErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", // try leader with leader read. "{addr: store2, replica-read: true, stale-read: false}", // retry store2 with replica-read. "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 2, backoffDetail: []string{"regionScheduling+1", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, NotLeaderWithNewLeader3Err, ServerIsBusyErr, NotLeaderWithNewLeader2Err, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 2, backoffDetail: []string{"tikvServerBusy+2"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, NotLeaderErr, ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 2, backoffDetail: []string{"regionScheduling+1", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DataIsNotReadyErr, RegionNotFoundErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: RegionNotFoundErr.GenRegionError(), backoffCnt: 0, backoffDetail: []string{}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: true}", "{addr: store2, replica-read: false, stale-read: true}", "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: time.Second, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) cas := []replicaSelectorAccessPathCase{ { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, timeout: 0, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{DeadLineExceededErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: true}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 2, backoffDetail: []string{"tikvRPC+2"}, regionIsValid: true, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: true, label: &metapb.StoreLabel{Key: "id", Value: "2"}, accessErr: []RegionErrorType{ServerIsBusyErr}, beforeRun: func() { /* don't resetStoreState */ }, expect: &accessPathResult{ accessPath: []string{ "{addr: store3, replica-read: false, stale-read: true}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: false, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, } s.True(s.runMultiCaseAndCompare(cas)) } func TestReplicaReadAccessPathByTryIdleReplicaCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} var ca replicaSelectorAccessPathCase // Try idle replica strategy not support write request. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdPrewrite, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 2, backoffDetail: []string{"tikvServerBusy+2"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyWithEstimatedWaitMsErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyWithEstimatedWaitMsErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvServerBusy+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderWithNewLeader3Err, ServerIsBusyWithEstimatedWaitMsErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"regionScheduling+2", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, busyThresholdMs: 10, accessErr: []RegionErrorType{NotLeaderErr, NotLeaderWithNewLeader3Err, ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false}"}, respErr: "", respRegionError: nil, backoffCnt: 3, backoffDetail: []string{"regionScheduling+1", "tikvServerBusy+2"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } func TestReplicaReadAccessPathByFlashbackInProgressCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() var ca replicaSelectorAccessPathCase fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, label: nil, accessErr: []RegionErrorType{FlashbackInProgressErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "region 0 is in flashback progress, FlashbackStartTS is 0", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, staleRead: false, timeout: time.Second, label: nil, accessErr: []RegionErrorType{DeadLineExceededErr, FlashbackInProgressErr, FlashbackInProgressErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // not compatible case. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: 0, label: nil, accessErr: []RegionErrorType{FlashbackInProgressErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "region 0 is in flashback progress, FlashbackStartTS is 0", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: false, timeout: time.Second, label: nil, accessErr: []RegionErrorType{DeadLineExceededErr, FlashbackInProgressErr, FlashbackInProgressErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: true, stale-read: false}", "{addr: store3, replica-read: true, stale-read: false}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } func TestReplicaReadAccessPathByProxyCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() // Enable forwarding. s.cache.enableForwarding = true fakeEpochNotMatch := &errorpb.Error{EpochNotMatch: &errorpb.EpochNotMatch{}} var ca replicaSelectorAccessPathCase cas := []replicaSelectorAccessPathCase{ { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false, forward_addr: store1}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"tikvRPC+1"}, regionIsValid: true, }, afterRun: func(_ *replicaSelector) { /* don't invalid region */ }, }, { reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{}, beforeRun: func() { /* don't resetStoreState */ }, expect: &accessPathResult{ accessPath: []string{ "{addr: store2, replica-read: false, stale-read: false, forward_addr: store1}", // access to known proxy direct. }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, }, } s.True(s.runMultiCaseAndCompare(cas)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr, DeadLineExceededErr, DeadLineExceededErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false, forward_addr: store1}", "{addr: store3, replica-read: false, stale-read: false, forward_addr: store1}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"tikvRPC+3"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr, NotLeaderWithNewLeader2Err, DeadLineExceededErr, ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false, forward_addr: store1}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false, forward_addr: store2}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"tikvRPC+2", "tikvServerBusy+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr, ServerIsBusyWithEstimatedWaitMsErr, ServerIsBusyWithEstimatedWaitMsErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false, forward_addr: store1}", "{addr: store3, replica-read: false, stale-read: false, forward_addr: store1}", }, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 3, backoffDetail: []string{"tikvRPC+1", "tikvServerBusy+2"}, regionIsValid: false, }, afterRun: func(selector *replicaSelector) { base := selector.baseReplicaSelector s.NotNil(base) s.True(base.replicas[0].isEpochStale()) s.True(base.replicas[0].epoch < atomic.LoadUint32(&base.replicas[0].store.epoch)) s.False(base.region.isValid()) }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdPrewrite, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 1, backoffDetail: []string{"regionScheduling+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) // TODO: maybe we can optimize the proxy strategy in future. ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdPrewrite, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{DeadLineExceededErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false, forward_addr: store1}", "{addr: store3, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 2, backoffDetail: []string{"regionScheduling+1", "tikvRPC+1"}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdPrewrite, readType: kv.ReplicaReadLeader, accessErr: []RegionErrorType{NotLeaderWithNewLeader2Err, DeadLineExceededErr, NotLeaderErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store1, replica-read: false, stale-read: false}", "{addr: store2, replica-read: false, stale-read: false}", "{addr: store3, replica-read: false, stale-read: false, forward_addr: store2}"}, respErr: "", respRegionError: fakeEpochNotMatch, backoffCnt: 2, backoffDetail: []string{"regionScheduling+1", "tikvRPC+1"}, regionIsValid: false, }, } s.True(s.runCaseAndCompare(ca)) } func TestReplicaReadAccessPathByLearnerCase(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() // Add a TiKV learner peer to the region. rc := s.getRegion() storeID := uint64(4) s.cluster.AddStore(storeID, fmt.Sprintf("store%d", storeID)) s.cluster.AddLearner(rc.meta.Id, storeID, s.cluster.AllocID()) rc.invalidate(Other) // invalid region cache to reload region. ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadLearner, accessErr: []RegionErrorType{ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ "{addr: store4, replica-read: true, stale-read: false}", "{addr: store1, replica-read: false, stale-read: false}", }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) } func TestReplicaReadAvoidSlowStore(t *testing.T) { s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() s.changeRegionLeader(3) store, exists := s.cache.stores.get(1) s.True(exists) for _, staleRead := range []bool{false, true} { for _, withLabel := range []bool{false, true} { var label *metapb.StoreLabel if withLabel { label = &metapb.StoreLabel{Key: "id", Value: "1"} } s.T().Logf("test case: stale read: %v, with label: %v, slow: false", staleRead, withLabel) ca := replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: staleRead, timeout: 0, busyThresholdMs: 0, label: label, accessErr: []RegionErrorType{}, expect: &accessPathResult{ accessPath: []string{ fmt.Sprintf("{addr: store1, replica-read: %v, stale-read: %v}", !staleRead, staleRead), }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.T().Logf("test case: stale read: %v, with label: %v, slow: true", staleRead, withLabel) expectedFirstStore := 2 if withLabel { // Leader is preferred in this case expectedFirstStore = 3 } ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: staleRead, timeout: 0, busyThresholdMs: 0, label: label, accessErr: []RegionErrorType{}, expect: &accessPathResult{ accessPath: []string{ fmt.Sprintf("{addr: store%v, replica-read: %v, stale-read: %v}", expectedFirstStore, !staleRead && expectedFirstStore != 3, staleRead), }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, beforeRun: func() { s.resetStoreState() store.healthStatus.updateTiKVServerSideSlowScore(100, time.Now()) }, } s.True(s.runCaseAndCompare(ca)) s.T().Logf("test case: stale read: %v, with label: %v, slow: false, encoutner err: true", staleRead, withLabel) var expectedSecondPath string if staleRead { // Retry leader, and fallback to leader-read mode. expectedSecondPath = "{addr: store3, replica-read: false, stale-read: false}" } else { if withLabel { // Prefer retrying leader. expectedSecondPath = "{addr: store3, replica-read: false, stale-read: false}" } else { // Retry any another replica. expectedSecondPath = "{addr: store2, replica-read: true, stale-read: false}" } } ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: staleRead, timeout: 0, busyThresholdMs: 0, label: label, accessErr: []RegionErrorType{ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ fmt.Sprintf("{addr: store1, replica-read: %v, stale-read: %v}", !staleRead, staleRead), // Retry leader. // For stale read, it fallbacks to leader read. However, replica-read doesn't do so. expectedSecondPath, }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, } s.True(s.runCaseAndCompare(ca)) s.T().Logf("test case: stale read: %v, with label: %v, slow: true, encoutner err: true", staleRead, withLabel) if expectedFirstStore == 3 { // Retry on store 2 which is a follower. // Stale-read mode falls back to replica-read mode. if staleRead { expectedSecondPath = "{addr: store2, replica-read: false, stale-read: true}" } else { expectedSecondPath = "{addr: store2, replica-read: true, stale-read: false}" } } else { // Retry in leader read mode will not use replica-read. expectedSecondPath = "{addr: store3, replica-read: false, stale-read: false}" } ca = replicaSelectorAccessPathCase{ reqType: tikvrpc.CmdGet, readType: kv.ReplicaReadMixed, staleRead: staleRead, timeout: 0, busyThresholdMs: 0, label: label, accessErr: []RegionErrorType{ServerIsBusyErr}, expect: &accessPathResult{ accessPath: []string{ fmt.Sprintf("{addr: store%v, replica-read: %v, stale-read: %v}", expectedFirstStore, !staleRead && expectedFirstStore != 3, staleRead), expectedSecondPath, }, respErr: "", respRegionError: nil, backoffCnt: 0, backoffDetail: []string{}, regionIsValid: true, }, beforeRun: func() { s.resetStoreState() store.healthStatus.updateTiKVServerSideSlowScore(100, time.Now()) }, } s.True(s.runCaseAndCompare(ca)) } } } func (s *testReplicaSelectorSuite) changeRegionLeader(storeId uint64) { loc, err := s.cache.LocateKey(s.bo, []byte("key")) s.Nil(err) rc := s.cache.GetCachedRegionWithRLock(loc.Region) for _, peer := range rc.meta.Peers { if peer.StoreId == storeId { s.cluster.ChangeLeader(rc.meta.Id, peer.Id) } } // Invalidate region cache to reload. s.cache.InvalidateCachedRegion(loc.Region) } func (s *testReplicaSelectorSuite) runCaseAndCompare(ca replicaSelectorAccessPathCase) bool { sender := ca.run(s) ca.checkResult(s, sender) return !ca.accessErrInValid } func (s *testReplicaSelectorSuite) runMultiCaseAndCompare(cas []replicaSelectorAccessPathCase) bool { valid := true for _, ca := range cas { sender := ca.run(s) ca.checkResult(s, sender) valid = valid && !ca.accessErrInValid } return valid } func (ca *replicaSelectorAccessPathCase) checkResult(s *testReplicaSelectorSuite, sender *RegionRequestSender) { if ca.expect == nil { return } msg := fmt.Sprintf("enable_forwarding: %v\n%v\nsender: %v\n", s.cache.enableForwarding, ca.Format(), sender.String()) expect := ca.expect result := ca.result s.Equal(expect.accessPath, result.accessPath, msg) s.Equal(expect.respErr, result.respErr, msg) s.Equal(expect.respRegionError, result.respRegionError, msg) s.Equal(expect.regionIsValid, result.regionIsValid, msg) s.Equal(expect.backoffCnt, result.backoffCnt, msg) s.Equal(expect.backoffDetail, result.backoffDetail, msg) } func (ca *replicaSelectorAccessPathCase) run(s *testReplicaSelectorSuite) *RegionRequestSender { access := []string{} fnClient := &fnClient{fn: func(ctx context.Context, addr string, req *tikvrpc.Request, timeout time.Duration) (response *tikvrpc.Response, err error) { idx := len(access) if req.ForwardedHost == "" { access = append(access, fmt.Sprintf("{addr: %v, replica-read: %v, stale-read: %v}", addr, req.ReplicaRead, req.StaleRead)) } else { access = append(access, fmt.Sprintf("{addr: %v, replica-read: %v, stale-read: %v, forward_addr: %v}", addr, req.ReplicaRead, req.StaleRead, req.ForwardedHost)) addr = req.ForwardedHost } if idx < len(ca.accessErr) { if !ca.accessErr[idx].Valid(addr, req) { // mark this case is invalid. just ignore this case. ca.accessErrInValid = true } else { rc := s.getRegion() s.NotNil(rc) regionErr, err := ca.genAccessErr(s.cache, rc, ca.accessErr[idx]) if regionErr != nil { switch req.Type { case tikvrpc.CmdGet: return &tikvrpc.Response{Resp: &kvrpcpb.GetResponse{ RegionError: regionErr, }}, nil case tikvrpc.CmdPrewrite: return &tikvrpc.Response{Resp: &kvrpcpb.PrewriteResponse{ RegionError: regionErr, }}, nil default: s.FailNow("unsupported reqType " + req.Type.String()) return nil, fmt.Errorf("unsupported reqType %v", req.Type) } } if err != nil { return nil, err } } } switch req.Type { case tikvrpc.CmdGet: return &tikvrpc.Response{Resp: &kvrpcpb.GetResponse{ Value: []byte("hello world"), }}, nil case tikvrpc.CmdPrewrite: return &tikvrpc.Response{Resp: &kvrpcpb.PrewriteResponse{}}, nil default: s.FailNow("unsupported reqType " + req.Type.String()) return nil, fmt.Errorf("unsupported reqType %v", req.Type) } }} sender := NewRegionRequestSender(s.cache, fnClient, oracle.NoopReadTSValidator{}) req, opts, timeout := ca.buildRequest(s) beforeRun(s, ca) rc := s.getRegion() s.NotNil(rc) bo := retry.NewBackofferWithVars(context.Background(), 40000, nil) resp, _, _, err := sender.SendReqCtx(bo, req, rc.VerID(), timeout, tikvrpc.TiKV, opts...) ca.recordResult(s, bo, sender.replicaSelector.region, access, resp, err) afterRun(ca, sender) return sender } func beforeRun(s *testReplicaSelectorSuite, ca *replicaSelectorAccessPathCase) { if ca.beforeRun != nil { ca.beforeRun() } else { s.resetStoreState() } } func afterRun(ca *replicaSelectorAccessPathCase, sender *RegionRequestSender) { if ca.afterRun != nil { ca.afterRun(sender.replicaSelector) } else { sender.replicaSelector.invalidateRegion() // invalidate region to reload for next test case. } } func (ca *replicaSelectorAccessPathCase) buildRequest(s *testReplicaSelectorSuite) (*tikvrpc.Request, []StoreSelectorOption, time.Duration) { var req *tikvrpc.Request switch ca.reqType { case tikvrpc.CmdGet: req = tikvrpc.NewRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{ Key: []byte("key"), }) case tikvrpc.CmdPrewrite: req = tikvrpc.NewRequest(tikvrpc.CmdPrewrite, &kvrpcpb.PrewriteRequest{}) default: s.FailNow("unsupported reqType " + ca.reqType.String()) } if ca.staleRead { req.EnableStaleWithMixedReplicaRead() req.ReadReplicaScope = oracle.GlobalTxnScope req.TxnScope = oracle.GlobalTxnScope } else { req.ReplicaReadType = ca.readType req.ReplicaRead = ca.readType.IsFollowerRead() } if ca.busyThresholdMs > 0 { req.BusyThresholdMs = ca.busyThresholdMs } opts := []StoreSelectorOption{} if ca.label != nil { opts = append(opts, WithMatchLabels([]*metapb.StoreLabel{ca.label})) } timeout := ca.timeout if timeout == 0 { timeout = client.ReadTimeoutShort } return req, opts, timeout } func (ca *replicaSelectorAccessPathCase) recordResult(s *testReplicaSelectorSuite, bo *retry.Backoffer, region *Region, access []string, resp *tikvrpc.Response, err error) { ca.result.accessPath = access ca.result.regionIsValid = region.isValid() msg := ca.Format() if err == nil { s.NotNil(resp, msg) regionErr, err := resp.GetRegionError() s.Nil(err, msg) ca.result.respRegionError = regionErr } else { ca.result.respErr = err.Error() } ca.result.backoffCnt = bo.GetTotalBackoffTimes() detail := make([]string, 0, len(bo.GetBackoffTimes())) for tp, cnt := range bo.GetBackoffTimes() { detail = append(detail, fmt.Sprintf("%v+%v", tp, cnt)) } sort.Strings(detail) ca.result.backoffDetail = detail } func (ca *replicaSelectorAccessPathCase) genAccessErr(regionCache *RegionCache, r *Region, accessErr RegionErrorType) (regionErr *errorpb.Error, err error) { genNotLeaderErr := func(storeID uint64) *errorpb.Error { var peerInStore *metapb.Peer for _, peer := range r.meta.Peers { if peer.StoreId == storeID { peerInStore = peer break } } return &errorpb.Error{ NotLeader: &errorpb.NotLeader{ RegionId: r.meta.Id, Leader: peerInStore, }, } } switch accessErr { case NotLeaderWithNewLeader1Err: regionErr = genNotLeaderErr(1) case NotLeaderWithNewLeader2Err: regionErr = genNotLeaderErr(2) case NotLeaderWithNewLeader3Err: regionErr = genNotLeaderErr(3) default: regionErr, err = accessErr.GenError() } if err != nil { // inject unreachable liveness. unreachable.injectConstantLiveness(regionCache.stores) } return regionErr, err } func (c *replicaSelectorAccessPathCase) Format() string { label := "" if c.label != nil { label = fmt.Sprintf("%v->%v", c.label.Key, c.label.Value) } respRegionError := "" if c.result.respRegionError != nil { respRegionError = c.result.respRegionError.String() } accessErr := make([]string, len(c.accessErr)) for i := range c.accessErr { accessErr[i] = c.accessErr[i].String() } return fmt.Sprintf("{\n"+ "\treq: %v\n"+ "\tread_type: %v\n"+ "\tstale_read: %v\n"+ "\ttimeout: %v\n"+ "\tbusy_threshold_ms: %v\n"+ "\tlabel: %v\n"+ "\taccess_err: %v\n"+ "\taccess_path: %v\n"+ "\tresp_err: %v\n"+ "\tresp_region_err: %v\n"+ "\tbackoff_cnt: %v\n"+ "\tbackoff_detail: %v\n"+ "\tregion_is_valid: %v\n}", c.reqType, c.readType, c.staleRead, c.timeout, c.busyThresholdMs, label, strings.Join(accessErr, ", "), strings.Join(c.result.accessPath, ", "), c.result.respErr, respRegionError, c.result.backoffCnt, strings.Join(c.result.backoffDetail, ", "), c.result.regionIsValid) } func (s *testReplicaSelectorSuite) resetStoreState() { // reset slow score, since serverIsBusyErr will mark the store is slow, and affect remaining test cases. reachable.injectConstantLiveness(s.cache.stores) // inject reachable liveness. rc := s.getRegion() s.NotNil(rc) for _, store := range rc.getStore().stores { store.loadStats.Store(nil) store.healthStatus.clientSideSlowScore.resetSlowScore() store.healthStatus.ResetTiKVServerSideSlowScoreForTest(1) store.healthStatus.updateSlowFlag() atomic.StoreUint32(&store.livenessState, uint32(reachable)) store.setResolveState(resolved) } regionStore := rc.getStore() for _, storeIdx := range regionStore.accessIndex[tiKVOnly] { epoch := regionStore.storeEpochs[storeIdx] storeEpoch := regionStore.stores[storeIdx].epoch if epoch != storeEpoch { rc.invalidate(EpochNotMatch) break } } } type RegionErrorType int const ( NotLeaderErr RegionErrorType = iota + 1 NotLeaderWithNewLeader1Err NotLeaderWithNewLeader2Err NotLeaderWithNewLeader3Err RegionNotFoundErr KeyNotInRegionErr EpochNotMatchErr ServerIsBusyErr ServerIsBusyWithEstimatedWaitMsErr StaleCommandErr StoreNotMatchErr RaftEntryTooLargeErr MaxTimestampNotSyncedErr ReadIndexNotReadyErr ProposalInMergingModeErr DataIsNotReadyErr RegionNotInitializedErr DiskFullErr RecoveryInProgressErr FlashbackInProgressErr FlashbackNotPreparedErr IsWitnessErr MismatchPeerIdErr BucketVersionNotMatchErr // following error type is not region error. DeadLineExceededErr RegionErrorTypeMax ) func (tp RegionErrorType) GenRegionError() *errorpb.Error { err := &errorpb.Error{} switch tp { case NotLeaderErr: err.NotLeader = &errorpb.NotLeader{} case RegionNotFoundErr: err.RegionNotFound = &errorpb.RegionNotFound{} case KeyNotInRegionErr: err.KeyNotInRegion = &errorpb.KeyNotInRegion{} case EpochNotMatchErr: err.EpochNotMatch = &errorpb.EpochNotMatch{} case ServerIsBusyErr: err.ServerIsBusy = &errorpb.ServerIsBusy{} case ServerIsBusyWithEstimatedWaitMsErr: err.ServerIsBusy = &errorpb.ServerIsBusy{EstimatedWaitMs: 10} case StaleCommandErr: err.StaleCommand = &errorpb.StaleCommand{} case StoreNotMatchErr: err.StoreNotMatch = &errorpb.StoreNotMatch{} case RaftEntryTooLargeErr: err.RaftEntryTooLarge = &errorpb.RaftEntryTooLarge{} case MaxTimestampNotSyncedErr: err.MaxTimestampNotSynced = &errorpb.MaxTimestampNotSynced{} case ReadIndexNotReadyErr: err.ReadIndexNotReady = &errorpb.ReadIndexNotReady{} case ProposalInMergingModeErr: err.ProposalInMergingMode = &errorpb.ProposalInMergingMode{} case DataIsNotReadyErr: err.DataIsNotReady = &errorpb.DataIsNotReady{} case RegionNotInitializedErr: err.RegionNotInitialized = &errorpb.RegionNotInitialized{} case DiskFullErr: err.DiskFull = &errorpb.DiskFull{} case RecoveryInProgressErr: err.RecoveryInProgress = &errorpb.RecoveryInProgress{} case FlashbackInProgressErr: err.FlashbackInProgress = &errorpb.FlashbackInProgress{} case FlashbackNotPreparedErr: err.FlashbackNotPrepared = &errorpb.FlashbackNotPrepared{} case IsWitnessErr: err.IsWitness = &errorpb.IsWitness{} case MismatchPeerIdErr: err.MismatchPeerId = &errorpb.MismatchPeerId{} case BucketVersionNotMatchErr: err.BucketVersionNotMatch = &errorpb.BucketVersionNotMatch{} default: return nil } return err } func (tp RegionErrorType) GenError() (*errorpb.Error, error) { regionErr := tp.GenRegionError() if regionErr != nil { return regionErr, nil } switch tp { case DeadLineExceededErr: return nil, context.DeadlineExceeded } return nil, nil } func (tp RegionErrorType) Valid(addr string, req *tikvrpc.Request) bool { // leader-read. if !req.StaleRead && !req.ReplicaRead { switch tp { case DataIsNotReadyErr: // DataIsNotReadyErr only return when req is a stale read. return false } } // replica-read. if !req.StaleRead && req.ReplicaRead { switch tp { case NotLeaderErr, NotLeaderWithNewLeader1Err, NotLeaderWithNewLeader2Err, NotLeaderWithNewLeader3Err: // NotLeaderErr will not return in replica read. return false case DataIsNotReadyErr: // DataIsNotReadyErr only return when req is a stale read. return false } } // stale-read. if req.StaleRead && !req.ReplicaRead { switch tp { case NotLeaderErr, NotLeaderWithNewLeader1Err, NotLeaderWithNewLeader2Err, NotLeaderWithNewLeader3Err: // NotLeaderErr will not return in stale read. return false } } // store1 can't return a not leader error with new leader in store1. if addr == "store1" && tp == NotLeaderWithNewLeader1Err { return false } // ditto. if addr == "store2" && tp == NotLeaderWithNewLeader2Err { return false } // ditto. if addr == "store3" && tp == NotLeaderWithNewLeader3Err { return false } return true } func (tp RegionErrorType) String() string { switch tp { case NotLeaderErr: return "NotLeaderErr" case NotLeaderWithNewLeader1Err: return "NotLeaderWithNewLeader1Err" case NotLeaderWithNewLeader2Err: return "NotLeaderWithNewLeader2Err" case NotLeaderWithNewLeader3Err: return "NotLeaderWithNewLeader3Err" case RegionNotFoundErr: return "RegionNotFoundErr" case KeyNotInRegionErr: return "KeyNotInRegionErr" case EpochNotMatchErr: return "EpochNotMatchErr" case ServerIsBusyErr: return "ServerIsBusyErr" case ServerIsBusyWithEstimatedWaitMsErr: return "ServerIsBusyWithEstimatedWaitMsErr" case StaleCommandErr: return "StaleCommandErr" case StoreNotMatchErr: return "StoreNotMatchErr" case RaftEntryTooLargeErr: return "RaftEntryTooLargeErr" case MaxTimestampNotSyncedErr: return "MaxTimestampNotSyncedErr" case ReadIndexNotReadyErr: return "ReadIndexNotReadyErr" case ProposalInMergingModeErr: return "ProposalInMergingModeErr" case DataIsNotReadyErr: return "DataIsNotReadyErr" case RegionNotInitializedErr: return "RegionNotInitializedErr" case DiskFullErr: return "DiskFullErr" case RecoveryInProgressErr: return "RecoveryInProgressErr" case FlashbackInProgressErr: return "FlashbackInProgressErr" case FlashbackNotPreparedErr: return "FlashbackNotPreparedErr" case IsWitnessErr: return "IsWitnessErr" case MismatchPeerIdErr: return "MismatchPeerIdErr" case BucketVersionNotMatchErr: return "BucketVersionNotMatchErr" case DeadLineExceededErr: return "DeadLineExceededErr" default: return "unknown_" + strconv.Itoa(int(tp)) } } func getAllRegionErrors(filter func(errorType RegionErrorType) bool) []RegionErrorType { errs := make([]RegionErrorType, 0, int(RegionErrorTypeMax)) for tp := NotLeaderErr; tp < RegionErrorTypeMax; tp++ { if filter != nil && filter(tp) == false { continue } errs = append(errs, tp) } return errs } func (s *testReplicaSelectorSuite) getRegion() *Region { for i := 0; i < 100; i++ { loc, err := s.cache.LocateKey(s.bo, []byte("key")) s.Nil(err) rc := s.cache.GetCachedRegionWithRLock(loc.Region) if rc == nil { time.Sleep(time.Millisecond * 10) continue } return rc } return nil } func TestTiKVClientReadTimeout(t *testing.T) { if israce.RaceEnabled { t.Skip("the test run with race will failed, so skip it") } config.UpdateGlobal(func(conf *config.Config) { // enable batch client. conf.TiKVClient.MaxBatchSize = 128 })() s := new(testReplicaSelectorSuite) s.SetupTest(t) defer s.TearDownTest() server, port := mockserver.StartMockTikvService() s.True(port > 0) server.SetMetaChecker(func(ctx context.Context) error { time.Sleep(time.Millisecond * 10) return nil }) rpcClient := client.NewRPCClient() defer func() { rpcClient.Close() server.Stop() }() accessPath := []string{} fnClient := &fnClient{fn: func(ctx context.Context, addr string, req *tikvrpc.Request, timeout time.Duration) (response *tikvrpc.Response, err error) { accessPath = append(accessPath, addr) accessPath = append(accessPath, fmt.Sprintf("{addr: %v, replica-read: %v, stale-read: %v, timeout: %v}", addr, req.ReplicaRead, req.StaleRead, req.MaxExecutionDurationMs)) return rpcClient.SendRequest(ctx, server.Addr(), req, timeout) }} rc := s.getRegion() s.NotNil(rc) req := tikvrpc.NewRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{Key: []byte("key"), Version: 1}) req.ReplicaReadType = kv.ReplicaReadLeader req.MaxExecutionDurationMs = 1 bo := retry.NewBackofferWithVars(context.Background(), 2000, nil) sender := NewRegionRequestSender(s.cache, fnClient, oracle.NoopReadTSValidator{}) resp, _, err := sender.SendReq(bo, req, rc.VerID(), time.Millisecond) s.Nil(err) s.NotNil(resp) regionErr, _ := resp.GetRegionError() s.True(retry.IsFakeRegionError(regionErr)) s.Equal(0, bo.GetTotalBackoffTimes()) s.Equal([]string{ "store1", "{addr: store1, replica-read: false, stale-read: false, timeout: 1}", "store2", "{addr: store2, replica-read: true, stale-read: false, timeout: 1}", "store3", "{addr: store3, replica-read: true, stale-read: false, timeout: 1}", }, accessPath) // clear max execution duration for retry. req.MaxExecutionDurationMs = 0 sender = NewRegionRequestSender(s.cache, fnClient, oracle.NoopReadTSValidator{}) resp, _, err = sender.SendReq(bo, req, rc.VerID(), time.Second) // use a longer timeout. s.Nil(err) s.NotNil(resp) regionErr, _ = resp.GetRegionError() s.Nil(regionErr) s.Equal(0, bo.GetTotalBackoffTimes()) s.Equal([]string{ "store1", "{addr: store1, replica-read: false, stale-read: false, timeout: 1}", "store2", "{addr: store2, replica-read: true, stale-read: false, timeout: 1}", "store3", "{addr: store3, replica-read: true, stale-read: false, timeout: 1}", "store1", "{addr: store1, replica-read: true, stale-read: false, timeout: 1000}", }, accessPath) } func TestReplicaFlag(t *testing.T) { r := &replica{} allFlags := []uint8{deadlineErrUsingConfTimeoutFlag, dataIsNotReadyFlag, notLeaderFlag, serverIsBusyFlag} for i, flag := range allFlags { if i > 0 { require.True(t, flag > allFlags[i-1]) } for j := i; j < len(allFlags); j++ { require.Equal(t, false, r.hasFlag(allFlags[j])) } r.addFlag(flag) require.Equal(t, true, r.hasFlag(flag)) } for i, flag := range allFlags { for j := i; j < len(allFlags); j++ { require.Equal(t, true, r.hasFlag(allFlags[j])) } r.deleteFlag(flag) require.Equal(t, false, r.hasFlag(flag)) } } func BenchmarkReplicaSelector(b *testing.B) { mvccStore := mocktikv.MustNewMVCCStore() cluster := mocktikv.NewCluster(mvccStore) mocktikv.BootstrapWithMultiStores(cluster, 3) pdCli := &CodecPDClient{mocktikv.NewPDClient(cluster), apicodec.NewCodecV1(apicodec.ModeTxn)} cache := NewRegionCache(pdCli) defer func() { cache.Close() mvccStore.Close() }() cnt := 0 allErrs := getAllRegionErrors(nil) fnClient := &fnClient{fn: func(ctx context.Context, addr string, req *tikvrpc.Request, timeout time.Duration) (response *tikvrpc.Response, err error) { pberr, err := allErrs[cnt%len(allErrs)].GenError() cnt++ return &tikvrpc.Response{Resp: &kvrpcpb.GetResponse{ RegionError: pberr, Value: []byte("value"), }}, err }} f, _ := os.Create("cpu.profile") pprof.StartCPUProfile(f) defer func() { pprof.StopCPUProfile() f.Close() }() b.ResetTimer() for i := 0; i < b.N; i++ { bo := retry.NewBackofferWithVars(context.Background(), 40000, nil) req := tikvrpc.NewRequest(tikvrpc.CmdGet, &kvrpcpb.GetRequest{ Key: []byte("key"), }) req.ReplicaReadType = kv.ReplicaReadMixed loc, err := cache.LocateKey(bo, []byte("key")) if err != nil { b.Fail() } sender := NewRegionRequestSender(cache, fnClient, oracle.NoopReadTSValidator{}) sender.SendReqCtx(bo, req, loc.Region, client.ReadTimeoutShort, tikvrpc.TiKV) } }