Repository: tikv/client-go (go)

Issue:
Don't invalid region in accessFollower when set user-defined req timeout and meet DeadLineExceededErr
# Issue

### 1. Minimal reproduce step

In the following case, suppose the region leader is in `store1`.

```go
	ca = replicaSelectorAccessPathCase{
		reqType:   tikvrpc.CmdGet,
		readType:  kv.ReplicaReadMixed,
		staleRead: false,
		timeout:   time.Second,
		label:     nil,
		accessErr: []RegionErrorType{ServerIsBusyErr, ServerIsBusyErr, DeadLineExceededErr},
```

### 2. What did you expect to see?

```go
		expect: &accessPathResult{
			accessPath: []string{
				"{addr: store1, replica-read: true, stale-read: false}",
				"{addr: store2, replica-read: true, stale-read: false}",
				"{addr: store3, replica-read: true, stale-read: false}"},
			regionIsValid:   true,
```


### 3. What did you see instead?

```go
		expect: &accessPathResult{
			...
			regionIsValid:   false,
		},
```

The request has user-defined timeout 1s, the replica selector logic is:

1. try store1 by replica-read, but got ServerIsBusyErr.
2. retry store2, got `ServerIsBusyErr`.
3. retry store3, got `DeadLineExceededErr`, and then invalidate the region in cache.

The expected result is doesn't invalidate region in cache, and the do fast retry without user-defined timeout.


Pull request:
Don't invalid region in accessFollower when set user-defined req time out and meet DeadLineExceededErr
close #1190

Patch:
diff --git a/internal/locate/region_request.go b/internal/locate/region_request.go
index 58327f2..8cc7b0b 100644
--- a/internal/locate/region_request.go
+++ b/internal/locate/region_request.go
@@ -521,14 +521,11 @@ func (state *tryFollower) next(bo *retry.Backoffer, selector *replicaSelector) (
 
 	// If all followers are tried and fail, backoff and retry.
 	if selector.targetIdx < 0 {
-		for _, replica := range selector.replicas {
-			if replica.deadlineErrUsingConfTimeout {
-				// when meet deadline exceeded error, do fast retry without invalidate region cache.
-				return nil, nil
-			}
+		// when meet deadline exceeded error, do fast retry without invalidate region cache.
+		if !hasDeadlineExceededError(selector.replicas) {
+			metrics.TiKVReplicaSelectorFailureCounter.WithLabelValues("exhausted").Inc()
+			selector.invalidateRegion()
 		}
-		metrics.TiKVReplicaSelectorFailureCounter.WithLabelValues("exhausted").Inc()
-		selector.invalidateRegion()
 		return nil, nil
 	}
 	rpcCtx, err := selector.buildRPCContext(bo)
@@ -787,8 +784,11 @@ func (state *accessFollower) next(bo *retry.Backoffer, selector *replicaSelector
 				}
 				return nil, stateChanged{}
 			}
-			metrics.TiKVReplicaSelectorFailureCounter.WithLabelValues("exhausted").Inc()
-			selector.invalidateRegion()
+			// when meet deadline exceeded error, do fast retry without invalidate region cache.
+			if !hasDeadlineExceededError(selector.replicas) {
+				metrics.TiKVReplicaSelectorFailureCounter.WithLabelValues("exhausted").Inc()
+				selector.invalidateRegion()
+			}
 			return nil, nil
 		}
 		state.lastIdx = state.leaderIdx
@@ -849,6 +849,16 @@ func (state *accessFollower) isCandidate(idx AccessIndex, replica *replica) bool
 	return replica.store.IsStoreMatch(state.option.stores) && replica.store.IsLabelsMatch(state.option.labels)
 }
 
+func hasDeadlineExceededError(replicas []*replica) bool {
+	for _, replica := range replicas {
+		if replica.deadlineErrUsingConfTimeout {
+			// when meet deadline exceeded error, do fast retry without invalidate region cache.
+			return true
+		}
+	}
+	return false
+}
+
 // tryIdleReplica is the state where we find the leader is busy and retry the request using replica read.
 type tryIdleReplica struct {
 	stateBase


Test patch:
diff --git a/internal/locate/replica_selector_test.go b/internal/locate/replica_selector_test.go
index 6556d14..d68079c 100644
--- a/internal/locate/replica_selector_test.go
+++ b/internal/locate/replica_selector_test.go
@@ -305,6 +305,28 @@ func TestReplicaReadAccessPathByCase(t *testing.T) {
 	}
 	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: true, 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{"tikvServerBusy+2"},
+			regionIsValid:   true,
+		},
+	}
+	s.True(s.runCaseAndCompare(ca))
+
 	ca = replicaSelectorAccessPathCase{
 		reqType:   tikvrpc.CmdGet,
 		readType:  kv.ReplicaReadMixed,