Skip to content

autoid: fail fast after repeated not leader responses from an unchanged leader#69961

Open
OliverS929 wants to merge 2 commits into
pingcap:masterfrom
OliverS929:issue-69690-autoid-not-leader-fast-fail
Open

autoid: fail fast after repeated not leader responses from an unchanged leader#69961
OliverS929 wants to merge 2 commits into
pingcap:masterfrom
OliverS929:issue-69690-autoid-not-leader-fast-fail

Conversation

@OliverS929

@OliverS929 OliverS929 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #69690

Problem Summary:

When an AutoID client repeatedly reaches the same leader endpoint but receives not leader, the existing retry path can keep resetting the connection and rediscovering the same winning candidate. Requests that need AutoIDs can remain blocked without an actionable terminal error.

What changed and how does it work?

Each AutoID allocation or rebase request now tracks consecutive exact gRPC not leader responses from the same leader snapshot. The snapshot includes the endpoint, election key, and winning candidate's create revision.

  • Only codes.Unknown with the exact message not leader contributes to the threshold.
  • The production threshold requires at least 10 consecutive responses spanning at least 15 seconds.
  • Other RPC errors break the consecutive sequence and retain the existing retry behavior.
  • Context cancellation remains higher priority than fast-fail handling.
  • At the threshold boundary, the client resets its cached connection and reads the current winning candidate again. If the endpoint or campaign generation changed, it retries the new snapshot instead of returning the stale diagnosis.
  • If the snapshot remains unchanged, the request returns an actionable error containing the operation, leader endpoint, response count, elapsed duration, keyspace, database, and table context.

An operation that observes not leader emits at most one start record and one completion record. The terminal warning is itself the completion record for a fast-failed request. Normal requests add no new feature logs.

This change does not modify election state, revoke leases, remove candidates, or automatically select another leader.

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Please refer to Release Notes Language Style Guide to write a quality release note.

TiDB now returns an actionable error instead of retrying indefinitely when an AutoID request repeatedly receives `not leader` from an unchanged leader endpoint.

Summary by CodeRabbit

  • Bug Fixes
    • Improved AutoID allocation and rebasing reliability with structured “not leader” detection and controlled retry/backoff.
    • Added fast-fail behavior after repeated “not leader” responses while still respecting context cancellation timing.
    • Improved leader snapshot caching and connection refresh/reset behavior when leadership or service endpoints change.
  • Tests
    • Expanded AutoID integration tests to cover “not leader” classification, retry lifecycle (alloc/rebase), cache correctness, recovery scenarios, and concurrent retry accounting.

@ti-chi-bot ti-chi-bot Bot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Jul 21, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign wjhuang2016 for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

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

Details Needs approval from an approver in each of these files:

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 added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4689223e-b519-40e3-9dd3-ecf333f9fb6c

📥 Commits

Reviewing files that changed from the base of the PR and between eb7af8f and 3b09d53.

📒 Files selected for processing (1)
  • pkg/meta/autoid/autoid_service_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/meta/autoid/autoid_service_test.go

📝 Walkthrough

Walkthrough

AutoID client discovery now tracks leader snapshots and uses structured not-leader retry handling with bounded failure behavior, cancellation checks, connection resets, and request logging. New integration tests cover leader changes, retries, cancellation, concurrency, and recovery.

Changes

AutoID not-leader handling

Layer / File(s) Summary
Leader snapshots and retry state
pkg/meta/autoid/autoid_service.go
Leader discovery returns leader metadata, while structured not-leader classification, retry state, request logging, and synchronized connection resets are implemented.
Alloc and rebase retry integration
pkg/meta/autoid/autoid_service.go
Allocation and rebase requests use leader-aware retries, backoff, cancellation checks, terminal errors, and completion logging.
Retry scenarios and build support
pkg/meta/autoid/autoid_service_test.go, pkg/meta/autoid/BUILD.bazel, .agents/skills/tidb-test-guidelines/references/meta-case-map.md
Scripted AutoID and etcd helpers support tests for retries, leader changes, cancellation, concurrency, and recovery; Bazel dependencies, sharding, and test mapping are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant AutoIDClient
  participant ClientDiscover
  participant Etcd
  participant AutoIDService
  Request->>AutoIDClient: Alloc or rebase
  AutoIDClient->>ClientDiscover: discover client and leader
  ClientDiscover->>Etcd: read leader metadata
  Etcd-->>ClientDiscover: leader snapshot
  ClientDiscover-->>AutoIDClient: gRPC client and snapshot
  AutoIDClient->>AutoIDService: issue request
  AutoIDService-->>AutoIDClient: success or not-leader response
  AutoIDClient->>ClientDiscover: reset and retry
  AutoIDClient-->>Request: result or terminal error
Loading

Possibly related PRs

Suggested labels: type/bug

Suggested reviewers: bb7133, tiancaiamao, yangkeao

Poem

I’m a rabbit watching leaders hop,
While retries count and errors stop.
Snapshots turn, connections renew,
Canceled paths know what to do.
Tests chase every service trail—
And bounded hops prevail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main AutoID fail-fast behavior and matches the changeset.
Description check ✅ Passed The description follows the template with issue number, problem summary, implementation details, tests, side effects, and release note.
Linked Issues check ✅ Passed The PR implements a bounded fail-fast path for repeated not-leader responses from the same leader and returns actionable context, matching #69690.
Out of Scope Changes check ✅ Passed The BUILD, docs, and test changes support the AutoID retry work, and no unrelated scope creep is evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 13.93443% with 210 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.9286%. Comparing base (b4d328f) to head (3b09d53).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #69961        +/-   ##
================================================
- Coverage   76.3192%   73.9286%   -2.3906%     
================================================
  Files          2041       2058        +17     
  Lines        559918     579169     +19251     
================================================
+ Hits         427325     428172       +847     
- Misses       131692     150632     +18940     
+ Partials        901        365       -536     
Flag Coverage Δ
integration 40.7253% <13.9344%> (+1.0200%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 60.4471% <ø> (ø)
parser ∅ <ø> (∅)
br 47.4060% <ø> (-15.3154%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ingress-bot

Copy link
Copy Markdown

🔍 Starting code review for this PR...

@ingress-bot ingress-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.

This review was generated by AI and should be verified by a human reviewer.
Manual follow-up is recommended before merge.

Summary

  • Total findings: 11
  • Inline comments: 6
  • Summary-only findings (no inline anchor): 5
Findings (highest risk first)

🟡 [Minor] (10)

  1. CAS invariant comment for version dropped during refactor (pkg/meta/autoid/autoid_service.go:65, pkg/meta/autoid/autoid_service.go:517)
  2. Exact-match wire contract in isAutoIDNotLeaderError has no comment explaining why it must be exact (pkg/meta/autoid/autoid_service.go:122)
  3. Not-leader log line's field set is hand-duplicated across three methods (pkg/meta/autoid/autoid_service.go:163, pkg/meta/autoid/autoid_service.go:193, pkg/meta/autoid/autoid_service.go:213)
  4. Not-leader retry orchestration is duplicated between Alloc and rebase (pkg/meta/autoid/autoid_service.go:433, pkg/meta/autoid/autoid_service.go:621)
  5. handleNotLeaderError's tri-state return contract is undocumented despite two duplicated call sites (pkg/meta/autoid/autoid_service.go:261, pkg/meta/autoid/autoid_service.go:442, pkg/meta/autoid/autoid_service.go:630)
  6. autoIDLeaderSnapshot.equal compares three fields with no comment on why address alone is insufficient (pkg/meta/autoid/autoid_service.go:82, pkg/meta/autoid/autoid_service.go:88)
  7. Current leader address is tracked in two independently-updated copies (pkg/meta/autoid/autoid_service.go:175, pkg/meta/autoid/autoid_service.go:220, pkg/meta/autoid/autoid_service.go:288)
  8. Exported ClientDiscover.GetClient is now dead code after the retry rewrite (pkg/meta/autoid/autoid_service.go:317, pkg/meta/autoid/autoid_service.go:322)
  9. Production time-based fast-fail window (minDuration=15s) is never exercised through the real Alloc/rebase path (pkg/meta/autoid/autoid_service_test.go:151, pkg/meta/autoid/autoid_service.go:278, pkg/meta/autoid/autoid_service.go:78)
  10. Test cleanup uses a fixed sleep to race the background gRPC-close goroutine (pkg/meta/autoid/autoid_service_test.go:711)

ℹ️ [Info] (1)

  1. New fast-fail changes autoid Alloc/Rebase from retry-until-deadline to erroring after ~15s, with no release note or rollout gate (pkg/meta/autoid/autoid_service.go:247, pkg/meta/autoid/autoid_service.go:78)

Unanchored findings

🟡 [Minor] (5)

  1. CAS invariant comment for version dropped during refactor
    • Request: Restore a short comment on the version field (or on resetConn/resetConnLocked) explaining that it is bumped on every reset so concurrent callers holding a stale version don't trigger redundant resets.
  2. Exact-match wire contract in isAutoIDNotLeaderError has no comment explaining why it must be exact
    • Request: Add a short comment above isAutoIDNotLeaderError stating that the exact code+message pairing mirrors a specific server-side error literal, and note where that literal is produced server-side (or link to it) so the two sides are kept in sync.
  3. Not-leader log line's field set is hand-duplicated across three methods
    • Request: Extract a helper (e.g. (s *autoIDRequestLogState) baseFields() []zap.Field) that returns the shared prefix, and have all three log call sites append only their unique fields on top of it.
  4. Not-leader retry orchestration is duplicated between Alloc and rebase
    • Request: Factor the shared decision-and-retry orchestration (the part after handleNotLeaderError returns) into one local helper both call sites invoke, e.g. a closure that performs the backoff/goto-retry-equivalent loop and returns only the terminal error, so Alloc and rebase each just map that single error into their own return signature.
  5. handleNotLeaderError's tri-state return contract is undocumented despite two duplicated call sites
    • Request: Add a doc comment on handleNotLeaderError stating what each return combination means (e.g. handled=false means not a not-leader error; terminalErr != nil means the caller must return it; retryImmediately vs backoff-then-retry), so the duplicated call sites stay consistent.

createRevision int64
}

func (s autoIDLeaderSnapshot) equal(other autoIDLeaderSnapshot) bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [Minor] autoIDLeaderSnapshot.equal compares three fields with no comment on why address alone is insufficient

Impact
equal drives notLeaderRetryState.observe's decision to reset vs. continue the not-leader streak, which in turn decides whether a request fast-fails after defaultNotLeaderMinConsecutive responses or gets a fresh count.
The method deliberately compares electionKey and createRevision in addition to address so that a new leadership campaign that happens to reuse the same address is treated as a different leader (per the "same address with a new campaign gets a fresh attempt" test case), but nothing in the code states that rationale.
A maintainer who later "simplifies" this to an address-only comparison (a natural-looking cleanup) would silently make the fast-fail threshold count across unrelated leader terms, changing when clients give up.

Scope

  • pkg/meta/autoid/autoid_service.go:82autoIDLeaderSnapshot
  • pkg/meta/autoid/autoid_service.go:88autoIDLeaderSnapshot.equal

Evidence
equal (autoid_service.go:88-92) compares address, electionKey, and createRevision; autoid_service_test.go:797-812 ("leader generation changes restart the sequence") is the only place documenting that an election-key or create-revision change, even with the same address, must restart the not-leader count.

Change request
Add a comment on autoIDLeaderSnapshot or equal explaining that electionKey/createRevision are included so a re-elected leader reusing the same address is not conflated with the prior stale leader's streak.

zap.Uint32("keyspace-id", s.keyspaceID),
zap.Int64("db-id", s.dbID),
zap.Int64("table-id", s.tableID),
zap.String("leader-address", state.leader.address),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [Minor] Current leader address is tracked in two independently-updated copies

Impact
autoIDRequestLogState.leaderAddress and notLeaderRetryState.leader.address both represent "the leader endpoint this request is currently dealing with", but they are written by separate code paths: observeNotLeader/setLeader update s.leaderAddress, while notLeaderRetryState.observe/reset update state.leader independently.
fastFail reads from state.leader.address (line 220) instead of the already-populated s.leaderAddress that complete and observeNotLeader use, so the two fields only agree today because handleNotLeaderError happens to call requestLog.setLeader(currentLeader) immediately before the state.leader.equal(currentLeader) check that gates the fastFail call.

Scope

  • pkg/meta/autoid/autoid_service.go:175autoIDRequestLogState.setLeader
  • pkg/meta/autoid/autoid_service.go:220autoIDRequestLogState.fastFail
  • pkg/meta/autoid/autoid_service.go:288singlePointAlloc.handleNotLeaderError

Evidence
In handleNotLeaderError, requestLog.setLeader(currentLeader) (line 288) is the only reason s.leaderAddress matches state.leader.address by the time requestLog.fastFail(*state, ...) runs on line 297; fastFail itself never reads s.leaderAddress.

Change request
Have fastFail read s.leaderAddress like the other two log methods, or drop the redundant leaderAddress field on autoIDRequestLogState and source it from notLeaderRetryState everywhere, so there is a single owner for the current leader address instead of two copies kept aligned only by call order.

@@ -84,18 +315,28 @@ func GetAutoIDServiceLeaderEtcdPath(keyspaceID uint32) string {

// GetClient gets the AutoIDAllocClient.
func (d *ClientDiscover) GetClient(ctx context.Context, keyspaceID uint32) (autoid.AutoIDAllocClient, uint64, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [Minor] Exported ClientDiscover.GetClient is now dead code after the retry rewrite

Impact
Alloc and rebase used to be GetClient's only callers; this PR switches both of them to call the new unexported getClientWithLeader directly so they can also see the leader snapshot.
That leaves the exported GetClient wrapper with zero callers anywhere in the repository, so it now exists only as an unused, narrower view (it silently drops the leader address) of the real implementation.

Scope

  • pkg/meta/autoid/autoid_service.go:317GetClient
  • pkg/meta/autoid/autoid_service.go:322getClientWithLeader

Evidence
A repo-wide search for calls to ClientDiscover.GetClient (.GetClient(ctx) matches only the method's own declaration at line 317; Alloc (line 412) and rebase (line 604) both call getClientWithLeader instead.

Change request
Remove the now-unused GetClient wrapper (or unexport/inline it) unless there is a concrete external caller that still needs the narrower 3-tuple signature; if one exists, note it in a comment so the wrapper isn't mistaken for dead code later.

keyspaceID: uint32(tikv.NullspaceID),
notLeaderPolicy: notLeaderRetryPolicy{
minConsecutive: 3,
minDuration: 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [Minor] Production time-based fast-fail window (minDuration=15s) is never exercised through the real Alloc/rebase path

Impact
Every end-to-end not-leader test overrides the policy to minDuration: 0, so the fast-fail is validated purely on the consecutive-count gate.
The dominant production behavior (keep retrying until roughly 15s elapse, then return the terminal error) is only covered by the isolated observe unit test with injected time, so a regression that mis-wires or drops the duration component in the real request path would still pass all the integration tests.

Scope

  • pkg/meta/autoid/autoid_service_test.go:151newNotLeaderTestAllocator
  • pkg/meta/autoid/autoid_service.go:278handleNotLeaderError
  • pkg/meta/autoid/autoid_service.go:78defaultNotLeaderMinDuration

Evidence
newNotLeaderTestAllocator hardcodes notLeaderPolicy{minConsecutive: 3, minDuration: 0}, so state.observe's now.Sub(firstSeen) >= policy.minDuration is trivially true in handleNotLeaderError. With the real default (defaultNotLeaderMinConsecutive=10, defaultNotLeaderMinDuration=15s) the 15s duration is the binding constraint, yet no test drives Alloc/rebase with a non-zero minDuration, and handleNotLeaderError reads time.Now() directly with no clock seam.

Change request
Add a focused test that drives Alloc/rebase with a small but non-zero minDuration (e.g. tens of milliseconds) and asserts the request keeps retrying until that window elapses before fast-failing, so the real-path duration gate is regression-covered rather than only the pure observe helper.

defer cancel()
err := runAutoIDTestOperation(ctx, operation, allocator)
require.EqualError(t, err, "service rejected request")
require.Equal(t, int64(2), autoIDOperationCallCount(operation, service))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [Minor] Test cleanup uses a fixed sleep to race the background gRPC-close goroutine

Impact
The cleanup relies on wall-clock timing to let the 200ms background connection-close goroutine finish before the package goleak check runs.
Under a loaded CI shard, scheduling latency can push that goroutine past the 250ms window, producing an intermittent leaked-goroutine failure that is unrelated to the code under test.

Scope

  • pkg/meta/autoid/autoid_service_test.go:711newNotLeaderTestAllocator

Evidence
newNotLeaderTestAllocator cleanup does allocator.ResetConn(nil) then time.Sleep(250 * time.Millisecond), banking on the afterReset goroutine that sleeps 200 * time.Millisecond before grpcConn.Close(). This is the sleep-based-synchronization anti-pattern the determinism checklist calls out.

Change request
Replace the fixed sleep with a deterministic wait (e.g. poll an observable closed condition via require.Eventually, or make the close synchronous in test paths), or explicitly document that this leans on goleak's own retry budget rather than the 250ms constant.

) error {
elapsed := now.Sub(state.firstSeen)
// TODO: Consider enriching terminal errors with the remote TiDB identity from /info.
return ErrAutoincReadFailed.GenWithStack(fmt.Sprintf(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ [Info] New fast-fail changes autoid Alloc/Rebase from retry-until-deadline to erroring after ~15s, with no release note or rollout gate

On a fully upgraded cluster, an autoid endpoint that keeps answering "not leader" now makes Alloc/Rebase return ErrAutoincReadFailed after roughly 15s instead of retrying until the caller's context deadline, so DML that previously blocked-and-retried can now surface an error.
The behavior is not behind any flag or config and the diff carries no release note or runbook entry describing the new terminal error, so operators mid-rollout have no documented signal or opt-out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fail fast when autoid clients repeatedly receive not leader from the same leader endpoint

2 participants