Skip to content

client: reduce membership refreshes during transport outages - #11037

Open
rleungx wants to merge 7 commits into
tikv:masterfrom
rleungx:codex/member-refresh-controller
Open

client: reduce membership refreshes during transport outages#11037
rleungx wants to merge 7 commits into
tikv:masterfrom
rleungx:codex/member-refresh-controller

Conversation

@rleungx

@rleungx rleungx commented Jul 21, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: ref #11020

During a sustained PD transport outage, asynchronous membership-check triggers can keep checkMembershipCh populated. Each trigger starts another one-second GetMembers retry batch, causing continuous RPC bursts and repetitive per-URL logs.

What is changed and how does it work?

client: suppress repeated membership refreshes during transport outages
  • Preserve the existing complete fast retry batch before changing behavior.
  • Enter degraded mode only after every current member URL has an availability failure and every corresponding gRPC connection is observed in IDLE, CONNECTING, or TRANSIENT_FAILURE.
  • While degraded:
    • coalesce asynchronous membership-check requests without issuing GetMembers;
    • inspect local connection state every 100 ms and call Connect() for IDLE connections;
    • resume the existing retry batch when a connection becomes READY, is missing, is shut down, or the member URL set changes;
    • perform one real membership safety sweep on the existing one-minute tick.
  • Keep synchronous CheckMemberChanged calls immediate and unsuppressed.
  • Track availability-failure episodes per service-discovery client and URL: log the first detailed error, aggregate suppressed failures into a one-minute summary, and log recovery for the URL that actually recovers.

For this PR, an availability failure means an explicit gRPC dial failure or an RPC failure classified as Unavailable or DeadlineExceeded (including a local context deadline). An availability failure alone neither identifies a transport outage nor enables request suppression; every corresponding connection must independently satisfy the connectivity gate above. Response-header errors, cluster-ID mismatches, and other application-level or uncertain failures retain the existing retry behavior.

Scope relative to #11020

The issue initially proposed sampling only the per-URL log while leaving retry
frequency unchanged. This PR intentionally extends that proposal: it suppresses
only asynchronous membership refresh batches after both a complete retry batch
and every current gRPC connection independently confirms a transport outage.
Synchronous refreshes remain immediate, local connection-state changes resume the
existing retry batch, and the existing one-minute tick performs a real safety
sweep. This stricter gating is the compatibility boundary for request suppression
in this PR.

Compatibility and correctness

  • No public API or configuration changes.
  • updateMember remains a compatibility wrapper and returns the existing error unchanged.
  • Callback and synchronous refresh behavior are unchanged.
  • Router and TSO service-discovery implementations are unchanged.
  • No healthy-mode ticker, background goroutine, dependency, or Prometheus metric is added.

End-to-end validation

  • Setup: three PD v8.5.5 instances in TiUP Playground; TSO every 100 ms;
    asynchronous member check every 20 ms; all PD processes stopped gracefully.
  • A/B commits: exact base 39b6220; behavioral PR head 3b3e570 (222a48c is
    naming-only).
  • Comparison window: first 12 seconds after the first URL-level member failure.
Measurement Exact base PR head Reduction
[pd] failed to update member 11 1 90.9%
[pd] cannot update member from this url 426 11 97.4%
  • Full base outage run (37.2 s): 36 batch warnings, 1,323 per-URL logs.
  • Full PR outage run (58.6 s): 2 batch warnings, 11 per-URL logs; the safety
    sweep reported 28 failed attempts and 25 suppressed errors.
  • Recovery observation (62.5 s): 625/625 TSO requests succeeded; synchronous
    member check passed 1/1.

Check List

Tests

  • Unit test
    • make -C client gotest GOTEST_ARGS='./servicediscovery -count=1'
    • make -C client gotest GOTEST_ARGS='./servicediscovery -race -count=1'
    • make -C client static
    • Membership update and controller regression tests repeated 10 times

The regression coverage includes degraded-mode eligibility, connection-state transitions, URL replacement, error compatibility, log suppression and recovery, tracker concurrency and allocation behavior, and the end-to-end membership loop.

Benchmarks on darwin/amd64:

BenchmarkMemberRefreshControllerCanRemainDegraded          11.02-11.14 ns/op    0 B/op    0 allocs/op
BenchmarkMemberAvailabilityFailureTrackerSuppression       16.55-16.63 ns/op    0 B/op    0 allocs/op

Release note

Reduce repeated PD membership refresh requests and logs during sustained transport outages.

Summary by CodeRabbit

  • New Features

    • Enhanced service discovery member refresh with stricter degraded mode when all member URL requests have availability failures and connectivity is non-ready.
    • Adds proactive recovery during degraded operation by inspecting known connection states.
    • Introduces per-URL availability-failure episode tracking with structured suppression/recovery logging.
  • Bug Fixes

    • Preserves existing membership update error behavior while improving availability-failure classification and retention logic across retries.
  • Tests

    • Expanded unit/integration coverage with in-memory gRPC server, plus concurrency and allocation-focused tests/benchmarks for degraded-mode, failure episodes, and snapshot reuse.

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-linked-issue dco-signoff: yes Indicates the PR's author has signed the dco. do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. labels Jul 21, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

[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 yisaer 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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Member refresh gains transport-failure classification, per-URL episode tracking, degraded-mode connection inspection, scheduled-refresh suppression, recovery logging, and service-client integration. Tests cover error contracts, state transitions, concurrency, logging, allocation behavior, and benchmarks.

Changes

Member refresh failure control

Layer / File(s) Summary
Failure classification and degraded-mode decisions
client/servicediscovery/member_refresh_controller.go, client/servicediscovery/member_refresh_controller_test.go
Transport failures are classified, and degraded-mode entry and retention depend on exact failed URLs and inactive observed connection states.
Failure episodes and summaries
client/servicediscovery/member_refresh_controller.go, client/servicediscovery/member_refresh_controller_test.go
Per-URL attempts, suppression, recovery, cleanup, sorted summaries, logging, concurrency, and performance behavior are implemented and tested.
Member refresh loop integration
client/servicediscovery/service_discovery.go, client/servicediscovery/service_discovery_test.go
The update loop polls connection states, classifies fetch failures, suppresses scheduled retries, logs recovery and summaries, updates clients, and preserves the existing error contract.
Controller and tracker test coverage
client/servicediscovery/member_refresh_controller_test.go
Controller transitions, tracker behavior, logging, concurrency, allocation checks, and suppression benchmarks are covered.

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

Possibly related issues

Suggested labels: contribution

Suggested reviewers: lhy1024

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: reducing membership refreshes during transport outages.
Description check ✅ Passed The description covers the problem, behavior changes, validation, checklist items, and release note, and includes the required Issue Number line.
✨ 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.

Signed-off-by: Ryan Leung <rleungx@gmail.com>
@rleungx
rleungx force-pushed the codex/member-refresh-controller branch from b630e07 to 989aee5 Compare July 22, 2026 02:40
rleungx added 2 commits July 27, 2026 17:30
Signed-off-by: Ryan Leung <rleungx@gmail.com>
Signed-off-by: Ryan Leung <rleungx@gmail.com>

@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)
client/servicediscovery/service_discovery_test.go (1)

474-477: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

require.NoError is called from the bufconn server goroutines in both new tests. require calls t.FailNow, which must run on the test goroutine, and a late failure after cleanup panics with "Log in goroutine after test has completed".

  • client/servicediscovery/service_discovery_test.go#L474-L477: replace require.NoError(t, server.Serve(listener)) with a t.Errorf (ignoring grpc.ErrServerStopped).
  • client/servicediscovery/service_discovery_test.go#L555-L558: apply the same replacement in TestUpdateMemberClearsUnobservedFailureEpisodesAfterRecovery.
🤖 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 `@client/servicediscovery/service_discovery_test.go` around lines 474 - 477,
Replace the goroutine-side require.NoError calls around server.Serve in
client/servicediscovery/service_discovery_test.go:474-477 and
client/servicediscovery/service_discovery_test.go:555-558 with non-fatal
t.Errorf handling, ignoring the expected grpc.ErrServerStopped result so
failures are reported safely from the test goroutines.
🧹 Nitpick comments (2)
client/servicediscovery/service_discovery.go (1)

566-631: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the degraded-branch ticker read nil-safe. case <-connectionStateTicker.C panics if the invariant "degraded ⇒ ticker started" is ever broken by a future edit (e.g. a new enterDegraded call site without startConnectionStateTicker). Selecting on a channel variable is free and self-protecting, since a nil channel blocks forever.

♻️ Suggested hardening
 	var connectionStateTicker *time.Ticker
+	var connectionStateCh <-chan time.Time
 	stopConnectionStateTicker := func() {
 		if connectionStateTicker != nil {
 			connectionStateTicker.Stop()
 			connectionStateTicker = nil
+			connectionStateCh = nil
 		}
 	}
 	defer stopConnectionStateTicker()
 	startConnectionStateTicker := func() {
 		if connectionStateTicker == nil {
 			connectionStateTicker = time.NewTicker(memberConnectionStateCheckInterval)
+			connectionStateCh = connectionStateTicker.C
 		}
 	}
@@
-			case <-connectionStateTicker.C:
+			case <-connectionStateCh:
🤖 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 `@client/servicediscovery/service_discovery.go` around lines 566 - 631, Make
the degraded-branch ticker select nil-safe by reading the ticker channel through
a separate channel variable that is nil when connectionStateTicker is nil,
rather than dereferencing connectionStateTicker.C directly in the select.
Preserve the existing ticker behavior when startConnectionStateTicker has
initialized it.
client/servicediscovery/member_refresh_controller_test.go (1)

104-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Branching on the test-case name is brittle. Prefer an explicit field (e.g. currentURLs []string) in the table so renaming a case can't silently change coverage.

♻️ Suggested change
 	testCases := []struct {
 		name        string
 		connections []memberConnection
+		currentURLs []string
 		wait        bool
 	}{
@@
 		{
 			name:        "url replacement restores normal behavior",
 			connections: observedMemberConnections(connectivity.TransientFailure, connectivity.TransientFailure),
+			currentURLs: []string{"url-0", "replacement-url"},
 		},
@@
 			currentURLs := initialURLs
-			if testCase.name == "url replacement restores normal behavior" {
-				currentURLs = []string{"url-0", "replacement-url"}
+			if testCase.currentURLs != nil {
+				currentURLs = testCase.currentURLs
 			}
🤖 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 `@client/servicediscovery/member_refresh_controller_test.go` around lines 104 -
149, Replace the name-based condition in the test loop with an explicit
currentURLs field on each test case. Set that field only for the URL replacement
scenario, use it when calling shouldWait, and otherwise default to initialURLs
so renaming test cases cannot alter coverage.
🤖 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 `@client/servicediscovery/service_discovery_test.go`:
- Around line 474-477: Replace the goroutine-side require.NoError calls around
server.Serve in client/servicediscovery/service_discovery_test.go:474-477 and
client/servicediscovery/service_discovery_test.go:555-558 with non-fatal
t.Errorf handling, ignoring the expected grpc.ErrServerStopped result so
failures are reported safely from the test goroutines.

---

Nitpick comments:
In `@client/servicediscovery/member_refresh_controller_test.go`:
- Around line 104-149: Replace the name-based condition in the test loop with an
explicit currentURLs field on each test case. Set that field only for the URL
replacement scenario, use it when calling shouldWait, and otherwise default to
initialURLs so renaming test cases cannot alter coverage.

In `@client/servicediscovery/service_discovery.go`:
- Around line 566-631: Make the degraded-branch ticker select nil-safe by
reading the ticker channel through a separate channel variable that is nil when
connectionStateTicker is nil, rather than dereferencing connectionStateTicker.C
directly in the select. Preserve the existing ticker behavior when
startConnectionStateTicker has initialized it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc343168-a22b-4296-a48e-5ac2129e4e59

📥 Commits

Reviewing files that changed from the base of the PR and between 989aee5 and fa0a551.

📒 Files selected for processing (4)
  • client/servicediscovery/member_refresh_controller.go
  • client/servicediscovery/member_refresh_controller_test.go
  • client/servicediscovery/service_discovery.go
  • client/servicediscovery/service_discovery_test.go

@rleungx

rleungx commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

/retest

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.19149% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.29%. Comparing base (39b6220) to head (9501fdd).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11037      +/-   ##
==========================================
+ Coverage   79.25%   79.29%   +0.03%     
==========================================
  Files         541      541              
  Lines       76037    76227     +190     
==========================================
+ Hits        60262    60443     +181     
- Misses      11534    11535       +1     
- Partials     4241     4249       +8     
Flag Coverage Δ
unittests 79.29% <93.19%> (+0.03%) ⬆️

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Signed-off-by: Ryan Leung <rleungx@gmail.com>
@rleungx rleungx added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Jul 28, 2026
@ti-chi-bot ti-chi-bot Bot removed do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. do-not-merge/needs-linked-issue labels Jul 28, 2026
@rleungx

rleungx commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

/retest

rleungx added 3 commits July 28, 2026 16:34
Signed-off-by: Ryan Leung <rleungx@gmail.com>
Signed-off-by: Ryan Leung <rleungx@gmail.com>
Signed-off-by: Ryan Leung <rleungx@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. 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.

1 participant