Skip to content

internal/locate: probe follower once when the cached leader keeps rejecting leader reads with ServerIsBusy(0) - #2041

Merged
ti-chi-bot[bot] merged 4 commits into
tikv:masterfrom
ekexium:busy-leader-probe
Aug 5, 2026
Merged

internal/locate: probe follower once when the cached leader keeps rejecting leader reads with ServerIsBusy(0)#2041
ti-chi-bot[bot] merged 4 commits into
tikv:masterfrom
ekexium:busy-leader-probe

Conversation

@ekexium

@ekexium ekexium commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Close #2028

Problem

When a TiKV store becomes half-dead (e.g. unified read pool wedged, tikv/tikv#18491),
leader reads are rejected at the read-pool entrance with ServerIsBusy and
EstimatedWaitMs == 0, before the request reaches the raft layer. Even after PD has
moved the leader away, the store keeps answering gRPC with ServerIsBusy(0) instead of
NotLeader. The client only marks the store slow and backs off, retrying the same
cached leader indefinitely — a production incident lasted ~25 minutes and ended only
when the store became fully unreachable.

Fix

"Sleep first, probe once":

  • The 1st ServerIsBusy(0) on the cached leader behaves exactly as today (mark slow +
    backoff + retry leader) — transient busyness costs nothing.
  • On the 2nd ServerIsBusy(0) from the same cached leader within one selector,
    mark the leader replica with a new dedicated suspectNotLeaderFlag (at most once
    per selector; the count restarts only when the cached leader changes).
  • The next attempt skips the flagged leader, so the existing mixed strategy picks a
    follower with unchanged request semantics (req.ReplicaRead stays false). A follower
    can only reject a leader read with NotLeader + leader hint — never serve it — so
    there is no stale-read risk.
  • The hint drives the existing onNotLeaderupdateLeader path, healing the shared
    region cache for all subsequent requests. onUpdateLeader clears the flag, so a
    misjudgment (the store is still the leader) costs exactly one rejected RPC and the
    request reverts to today's behavior.
  • If the followers yield no hint (election in progress, or followers also
    busy/exhausted), the selector restores the cached leader when the mixed strategy
    finds no candidate, instead of invalidating the region: it falls back to the plain
    backoff-and-retry loop, keeping essentially today's retry shape in an all-busy
    meltdown and adding no PD traffic. Region invalidation / PD reload still happen only
    through the pre-existing paths (e.g. after the leader exhausts its attempts).

No read/write semantics change, no extra PD traffic on the busy path, no new state
machines/timers/configs. Bounded extra cost: the probe fires at most once per
selector — when it succeeds (or is a misjudgment) it costs one rejected RPC; in the
worst case (all followers also busy) each follower is contacted once before the leader
is restored. Applies to reads and writes alike — a write rejected by a follower gets
the same NotLeader treatment.

What it does NOT cover

The window where the wedged store is still the leader (before PD eviction): the hint
points back to the same store and behavior degrades to status quo; that window is
covered by PD slow-store eviction. Complementary to the server-side fix
tikv/tikv#19932 (return NotLeader at the rejection gate): that one heals old clients
on new TiKV, this one heals old TiKV on new clients.

The forwarding (proxy) path is out of scope: EnableForwarding defaults to false, and
this change is designed and tested for the default selection path only — its
interaction with ReplicaSelectLeaderWithProxyStrategy is not covered.

A compound failure — wedged old leader AND all followers also rejecting with
ServerIsBusy(0) AND PD has already moved the leader — is intentionally not cured
eagerly: the probe cannot obtain a hint, and invalidating/reloading would tax PD in
the (much more common) case where the cache is actually correct. The request falls
back to today's behavior; the cure arrives once any follower can answer again.

Tests

TestReplicaSelectorLeaderBusyProbe: no probe on a single busy; probe on the 2nd busy
with leader-read semantics kept; cache healed via hint and a new selector goes
straight to the new leader; misjudgment restores the leader with no further probing;
no-hint goes through the region-scheduling backoff path; stale/follower/leaderOnly
reads never probe; a single-replica region (no probe target) restores the leader
without invalidating; the busy count restarts when the cached leader changes.

Two existing access-path tests were updated: their expectations encoded the old
behavior this PR removes (leader hammered until exhausted before trying a follower).
The all-busy meltdown case now ends with the leader restored — twelve accesses ending
in success with the region cache intact, essentially the same retry shape as before
this PR, with only one early fruitless probe of each follower.

Summary by CodeRabbit

  • Bug Fixes
    • Improved request routing when a cached leader repeatedly reports that it is busy.
    • Temporarily probes an available follower and preserves leader-read behavior where appropriate.
    • Automatically recovers from stale or incorrect leader information.
    • Prevents unnecessary probing for stale reads, follower-only requests, leader-only requests, and single-replica configurations.
    • Improves handling of transient busy responses while avoiding false leader suspicions.

…ecting leader reads with ServerIsBusy(0)

ref tikv#2028

When a TiKV store's unified read pool is wedged, it rejects leader reads
with ServerIsBusy(EstimatedWaitMs=0) at the pool entrance, so the request
never reaches the raft layer and no NotLeader error is returned even if PD
has already moved the leader away. The replica selector then retries the
cached leader forever and hammers the half-dead store.

After 2 consecutive such rejections on the cached leader within one
selector, mark the leader replica with a new suspectNotLeaderFlag so that
the next attempt skips it in the leader strategy and probes a follower via
the mixed strategy with the leader-read semantics of the request
unchanged. The follower replies NotLeader with the real leader hint,
which heals the shared region cache through the existing
onNotLeader/updateLeader path. The probe fires at most once per selector;
if the store is still the leader, the hint points back to it,
onUpdateLeader clears the flag, and the only cost is one rejected RPC.

Signed-off-by: Ziqian Qin <eke@fastmail.com>
@ti-chi-bot ti-chi-bot Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 4, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@ti-chi-bot ti-chi-bot Bot added the dco-signoff: yes Indicates the PR's author has signed the dco. label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 746dc7a3-3754-4e97-b79f-58196cebe2f2

📥 Commits

Reviewing files that changed from the base of the PR and between 7d67e89 and 1d95702.

📒 Files selected for processing (2)
  • internal/locate/replica_selector.go
  • internal/locate/replica_selector_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/locate/replica_selector.go

📝 Walkthrough

Walkthrough

The replica selector tracks repeated zero-wait ServerIsBusy responses from a cached leader, probes a follower, and handles leader hints. Confirmed leader updates clear both leadership-loss flags. Tests cover probing, exclusions, cache healing, fallback behavior, and flag state.

Changes

Leader busy probe

Layer / File(s) Summary
Leader suspicion and follower selection
internal/locate/region_request.go, internal/locate/replica_selector.go
The selector counts consecutive ServerIsBusy(0) responses per cached leader. After two responses, it marks the leader as suspect and probes a follower once. It restores or retries the leader when probing is unavailable or ineffective. Confirmed leader updates clear the suspect flag.
Busy probe behavior coverage
internal/locate/replica_selector_test.go
Tests cover threshold handling, follower probing, cache healing, false positives, missing hints, leader changes, excluded request types, fallback behavior, updated access paths, and flag ordering.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReplicaSelector
  participant CachedLeader
  participant Follower
  participant RegionCache
  ReplicaSelector->>CachedLeader: Send leader read
  CachedLeader-->>ReplicaSelector: Return ServerIsBusy(0)
  ReplicaSelector->>CachedLeader: Send second leader read
  CachedLeader-->>ReplicaSelector: Return ServerIsBusy(0)
  ReplicaSelector->>Follower: Send follower probe
  Follower-->>ReplicaSelector: Return NotLeader hint
  ReplicaSelector->>RegionCache: Update leader information
Loading

Suggested reviewers: andremouche, wfxr

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: probing a follower after repeated zero-wait ServerIsBusy responses from the cached leader.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ti-chi-bot ti-chi-bot Bot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 4, 2026
@ekexium
ekexium marked this pull request as ready for review August 4, 2026 12:48
Copilot AI lite review requested due to automatic review settings August 4, 2026 12:48
@ti-chi-bot ti-chi-bot Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/locate/replica_selector.go (1)

250-259: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Exclude suspect-leader routing in the proxy strategy.

ReplicaSelectLeaderWithProxyStrategy.next returns the cached leader as target when the leader’s store is reachable, and it can also return the cached leader with any reachable follower as proxy. The subsequent caller then falls through to ReplicaSelectLeaderStrategy only if the proxy strategy does not return both a target and a proxy, so suspectNotLeaderFlag is ignored in the forwarding path. Apply the same suspect-leader check before returning leader, proxy so the probe still occurs when forwarding is enabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/locate/replica_selector.go` around lines 250 - 259, The proxy
forwarding path must also exclude leaders marked with suspectNotLeaderFlag. In
ReplicaSelectLeaderWithProxyStrategy.next, apply the same
isLeaderCandidate(leader) and !leader.hasFlag(suspectNotLeaderFlag) validation
before returning the cached leader with a reachable follower as proxy, allowing
fallback to ReplicaSelectLeaderStrategy for probing.
🧹 Nitpick comments (1)
internal/locate/replica_selector.go (1)

594-612: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the wording with the counter semantics, and name the threshold.

leaderBusyCount is never reset. A non-busy response between two ServerIsBusy(0) responses does not clear it, so the counter is cumulative per selector, not consecutive. The comment here, the field comment at lines 43-47, and the test comments all state "consecutive". Fix the wording, or reset the counter when the cached leader returns anything other than ServerIsBusy(0). Also extract the literal 2 into a named constant.

♻️ Proposed wording and constant change
-			// half-dead store indefinitely. After 2 consecutive such rejections on the cached
+			// half-dead store indefinitely. After maxLeaderBusyBeforeProbe such rejections on the cached
 			// leader, mark it suspect-not-leader so that the next attempt probes a follower
 			// with the leader read (req.ReplicaRead is kept unchanged). The follower replies
 			// NotLeader with the real leader hint, which heals the shared region cache via
 			// onNotLeader/updateLeader. Probe at most once per selector; if the store is
 			// still the leader, the hint points back to it, onUpdateLeader clears the flag,
 			// and the only cost is one rejected RPC.
 			if s.replicaReadType == kv.ReplicaReadLeader && !s.isStaleRead && !s.option.leaderOnly &&
 				s.target != nil && s.target.peer.Id == s.region.GetLeaderPeerID() && !s.leaderBusyProbed {
 				s.leaderBusyCount++
-				if s.leaderBusyCount >= 2 {
+				if s.leaderBusyCount >= maxLeaderBusyBeforeProbe {
 					s.target.addFlag(suspectNotLeaderFlag)
 					s.leaderBusyProbed = true
 				}
 			}

Declare the constant near the selector types:

// maxLeaderBusyBeforeProbe is the number of ServerIsBusy(0) rejections on the cached
// leader that triggers a single follower probe per selector.
const maxLeaderBusyBeforeProbe = 2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/locate/replica_selector.go` around lines 594 - 612, Update the
leader-busy handling around leaderBusyCount to match its cumulative semantics by
removing “consecutive” wording from the associated comments and tests, and
replace the literal threshold 2 with a named maxLeaderBusyBeforeProbe constant
declared near the selector types. Keep the existing probe behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/locate/replica_selector.go`:
- Around line 250-259: The proxy forwarding path must also exclude leaders
marked with suspectNotLeaderFlag. In ReplicaSelectLeaderWithProxyStrategy.next,
apply the same isLeaderCandidate(leader) and
!leader.hasFlag(suspectNotLeaderFlag) validation before returning the cached
leader with a reachable follower as proxy, allowing fallback to
ReplicaSelectLeaderStrategy for probing.

---

Nitpick comments:
In `@internal/locate/replica_selector.go`:
- Around line 594-612: Update the leader-busy handling around leaderBusyCount to
match its cumulative semantics by removing “consecutive” wording from the
associated comments and tests, and replace the literal threshold 2 with a named
maxLeaderBusyBeforeProbe constant declared near the selector types. Keep the
existing probe behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 193f308f-365e-45d6-9754-fd7a42646564

📥 Commits

Reviewing files that changed from the base of the PR and between 341d469 and 7a82b22.

📒 Files selected for processing (3)
  • internal/locate/region_request.go
  • internal/locate/replica_selector.go
  • internal/locate/replica_selector_test.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a targeted workaround in the TiKV region replica selector to avoid repeatedly retrying a cached leader that keeps rejecting leader reads with ServerIsBusy where EstimatedWaitMs == 0, by probing a follower once to obtain a NotLeader hint and heal the shared region cache (#2028).

Changes:

  • Add per-selector tracking to detect repeated ServerIsBusy(0) on the cached leader and trigger a one-time follower probe via a new suspectNotLeaderFlag.
  • Update leader-selection logic to skip a leader replica flagged as suspect so the mixed strategy can choose a follower for the probe.
  • Extend and adjust tests to cover the new probe behavior and update expectations for access-path behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
internal/locate/replica_selector.go Adds per-selector busy tracking and skips suspect leader to trigger follower probing after repeated ServerIsBusy(0).
internal/locate/replica_selector_test.go Updates existing access-path expectations and adds a dedicated test covering the new busy-probe behavior.
internal/locate/region_request.go Adds suspectNotLeaderFlag and clears it when leader is updated via a NotLeader hint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +605 to +609
if s.replicaReadType == kv.ReplicaReadLeader && !s.isStaleRead && !s.option.leaderOnly &&
s.target != nil && s.target.peer.Id == s.region.GetLeaderPeerID() && !s.leaderBusyProbed {
s.leaderBusyCount++
if s.leaderBusyCount >= 2 {
s.target.addFlag(suspectNotLeaderFlag)
@dillon-zheng

Copy link
Copy Markdown

/retest

1 similar comment
@dillon-zheng

Copy link
Copy Markdown

/retest

…der when no probe target, tie busy count to the cached leader

Address review on tikv#2041, ref tikv#2028.

- When the mixed strategy finds no candidate while the leader is skipped
  only due to suspectNotLeaderFlag (single-replica region, or all
  followers unreachable/stale/exhausted, or the probe turns out
  fruitless), restore the leader and fall back to the plain backoff-retry
  behavior instead of invalidating the region and reloading it from PD
  for nothing.
- Tie leaderBusyCount to the new leaderBusyPeerID: whenever the cached
  leader changes (e.g. switched by a NotLeader hint), restart the count
  so the new leader won't be marked after inheriting the old leader's
  count.

Signed-off-by: Ziqian Qin <eke@fastmail.com>
The trigger is the 2nd ServerIsBusy(0) from the same cached leader within
one selector, not necessarily on consecutive attempts; the count restarts
only when the cached leader changes. Comment-only change.

Signed-off-by: Ziqian Qin <eke@fastmail.com>
Comment thread internal/locate/replica_selector.go Outdated
s.leaderBusyCount = 0
}
s.leaderBusyCount++
if s.leaderBusyCount >= 2 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the impact for case the leader is busy for real? Backoff would be skipped for some times?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't be skipped. The sequence would be leader: busy -> leader: busy -> follower: not leader(leader is the original one) -> leader... The test case at line 2585 covered such situation

@cfzjywxk
cfzjywxk requested a review from wfxr August 5, 2026 02:46
@ti-chi-bot ti-chi-bot Bot added needs-1-more-lgtm Indicates a PR needs 1 more LGTM. approved labels Aug 5, 2026
Comment thread internal/locate/replica_selector.go Outdated
…hreshold

Address review on tikv#2041, ref tikv#2028.

- Hoist the GetLeaderPeerID read out of the probe trigger condition so it
  is read once and shared by the condition and the count-reset logic,
  closing a TOCTOU window against concurrent cached-leader changes
  (double-read review comment by zyguan).
- Name the probe trigger threshold as leaderBusyProbeThreshold instead of
  a literal 2.
- Add contract tests: the busy count is cumulative per cached leader
  (busy(0) interleaved with other errors still counts), and when both
  probed followers reply NotLeader without a hint, the leader is restored
  with the region kept valid instead of being eagerly invalidated.

Signed-off-by: Ziqian Qin <eke@fastmail.com>
} else {
// Mark the server is busy (the next incoming READs could be redirected to expected followers.)
ctx.Store.healthStatus.markAlreadySlow()
// Workaround for tikv/client-go#2028: if the store's read pool is wedged, leader

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServerIsBusy.estimated_wait_ms = 0 seems must not be interpreted as “the store is healthy” or “expected wait time is zero.” in this case, could we add more comments for ServerIsBusy.estimated_wait_ms = 0

@AndreMouche AndreMouche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ti-chi-bot ti-chi-bot Bot added the lgtm label Aug 5, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: AndreMouche, cfzjywxk

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [AndreMouche,cfzjywxk]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot removed the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Aug 5, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

[LGTM Timeline notifier]

Timeline:

  • 2026-08-05 02:58:49.531717116 +0000 UTC m=+2583315.567812162: ☑️ agreed by cfzjywxk.
  • 2026-08-05 17:44:09.292937624 +0000 UTC m=+2636435.329032680: ☑️ agreed by AndreMouche.

@ti-chi-bot
ti-chi-bot Bot merged commit aff3e66 into tikv:master Aug 5, 2026
13 checks passed
@ekexium

ekexium commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

/cherry-pick tidb-8.5

@ti-chi-bot

Copy link
Copy Markdown
Member

@ekexium: new pull request created to branch tidb-8.5: #2044.

Details

In response to this:

/cherry-pick tidb-8.5

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the ti-community-infra/tichi repository.

ti-chi-bot Bot pushed a commit that referenced this pull request Aug 6, 2026
…ecting leader reads with ServerIsBusy(0) (#2041) (#2044)

close #2028, fix tikv/tikv#19932\n\nSigned-off-by: Ziqian Qin <eke@fastmail.com>\n\nCo-authored-by: Ziqian Qin <eke@fastmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved dco-signoff: yes Indicates the PR's author has signed the dco. lgtm size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

region-cache: leader-read keeps retrying same store on ServerIsBusy without EstimatedWaitMs (stale leader cache)

7 participants