Skip to content

client: reconnect when the address is reused by another service - #11326

Open
bufferflies wants to merge 4 commits into
tikv:masterfrom
bufferflies:feature/grpc-callee-id
Open

bufferflies wants to merge 4 commits into
tikv:masterfrom
bufferflies:feature/grpc-callee-id

Conversation

@bufferflies

@bufferflies bufferflies commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: ref #9894

When a stale DNS record points to a recycled Pod IP that now belongs to another kind of service (e.g., a scheduling server reusing an old PD Pod's IP), the client keeps reusing the READY gRPC connection to the wrong service. Every request fails with rpc error: code = Unimplemented desc = unknown service pdpb.PD until TiDB restarts, even after the DNS record is refreshed. See #9894 (comment).

The callee ID check added in #10600 cannot catch this case, because the request is rejected by gRPC routing before reaching any handler.

What is changed and how does it work?

Add a client interceptor that closes the connection once the remote
endpoint reports that it does not provide the service expected by the
connection, i.e., pdpb.PD, tsopb.TSO, routerpb.Router or
resource_manager.ResourceManager. Other unknown services or methods are
ignored to stay compatible with older servers and other deployment modes.

Recreate the closed connections: GetOrCreateGRPCConn drops a cached
connection in the Shutdown state, and the resource manager and router
service discovery reconnect even if the service URLs are unchanged.

Check List

Tests

  • Unit test
  • Integration test

Side effects

  • Increased code complexity

Related changes

  • Need to cherry-pick to the release branch

Release note

Fix the issue that the PD client keeps failing with `unknown service` errors after the PD address is reused by another service, such as when a stale DNS record points to a recycled Pod IP.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • gRPC clients now recover when a service address points to a server that does not provide the expected service, restoring requests when the correct service becomes available.
    • Service discovery replaces shut down connections even when service addresses or registry data have not changed.
    • Router and resource manager clients can reconnect after their connections close, helping maintain service access during discovery updates.
    • Failed streaming requests now resolve send and receive errors more reliably.
    • Connections remain open when an RPC targets an unknown method or an unrelated service.

When a stale DNS record points to a recycled Pod IP that now belongs to
another kind of service (e.g., a scheduling server), the client keeps
reusing a READY gRPC connection to the wrong service and every request
fails with "unknown service pdpb.PD" until the process restarts.

Add a client interceptor that closes the connection once the remote
endpoint reports that it does not provide the service expected by the
connection, i.e., pdpb.PD, tsopb.TSO, routerpb.Router or
resource_manager.ResourceManager. Other unknown services or methods are
ignored to stay compatible with older servers and other deployment modes.

Recreate the closed connections: GetOrCreateGRPCConn drops a cached
connection in the Shutdown state, and the resource manager and router
service discovery reconnect even if the service URLs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: bufferflies <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: yes Indicates the PR's author has signed the dco. labels Oct 9, 2026
@ti-chi-bot

ti-chi-bot Bot commented Oct 9, 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 rleungx 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 Oct 9, 2026
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c8874e17-f3c5-45f8-9c3e-da2f04d42858
Commits

Reviewing files that changed from the base of the PR and between 5866c42 and ec906d5.

Files selected for processing: 10
  • client/clients/router/client.go
  • client/clients/router/expected_service_test.go
  • client/pkg/utils/grpcutil/expected_service_test.go
  • client/pkg/utils/grpcutil/grpcutil.go
  • client/resource_manager_client.go
  • client/resource_manager_client_test.go
  • client/servicediscovery/expected_service_test.go
  • client/servicediscovery/service_discovery.go
  • client/servicediscovery/tso_service_discovery.go
  • tests/integrations/client/stale_address_test.go

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Walkthrough

The client detects matching unknown-service errors and closes the affected gRPC connections. Service discovery applies service-specific dial options and recreates shut-down connections. Router and ResourceManager stream processing resolves send errors by attempting to receive when Send returns io.EOF.

Changes: Expected-service connection recovery

Layer / File(s) Summary
Detect unknown-service responses
client/pkg/utils/grpcutil/grpcutil.go, client/pkg/utils/grpcutil/expected_service_test.go
Unary and stream interceptors close a connection when an error is Unimplemented with the exact message unknown service <service>. Cached connections in the Shutdown state are replaced. Tests cover matching and non-matching errors and cached connection replacement.
Apply options and recreate discovery connections
client/servicediscovery/expected_service.go, client/servicediscovery/service_discovery.go, client/servicediscovery/tso_service_discovery.go, client/servicediscovery/router_service_discovery.go, client/servicediscovery/resource_manager_service_discovery.go, client/servicediscovery/expected_service_test.go, tests/integrations/client/stale_address_test.go
PD, TSO, Router, and ResourceManager discovery apply the corresponding expected-service options. ResourceManager and Router discovery recreate shut-down connections when discovery data is unchanged. Tests cover connection replacement, leader switching, and client recovery after a stale address redirects dials.
Resolve rejected stream sends
client/pkg/utils/grpcutil/grpcutil.go, client/clients/router/client.go, client/clients/router/expected_service_test.go, client/resource_manager_client.go, client/resource_manager_client_test.go
When stream Send returns io.EOF, Router and ResourceManager request processing attempt Recv and use the resolved error. Tests cover rejected streams and error delivery.

Priority: Normal

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

Change: Bug fix

Suggested labels: type/cherry-pick-for-release-8.5

Suggested reviewers: rleungx, lhy1024

Merge Risk: Minimal · up to ec906

No actionable issue is established from the supplied evidence. The change is mergeable after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main client reconnection change caused by an address being reused by another service.
Description check Passed The description covers the issue, issue reference, implementation, unit and integration tests, side effects, release note, and cherry-pick requirement. The omitted code-changes checklist is non-critic…
Docstring Coverage Passed Docstring coverage is inconclusive: files from earlier reviews still need to be rechecked within the check's analysis limits. Not blocking.
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.
Full details: Description check

Explanation

The description covers the issue, issue reference, implementation, unit and integration tests, side effects, release note, and cherry-pick requirement. The omitted code-changes checklist is non-critical because the description identifies no configuration, API, or persistent-data changes.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR

@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.64865% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.94%. Comparing base (2d2c742) to head (ec906d5).

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11326      +/-   ##
==========================================
+ Coverage   84.89%   84.94%   +0.04%     
==========================================
  Files         554      555       +1     
  Lines       71688    71746      +58     
==========================================
+ Hits        60862    60943      +81     
+ Misses      10825    10802      -23     
  Partials        1        1              
Flag Coverage Δ
unittests 84.94% <98.64%> (+0.04%) ⬆️

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.

Comment thread client/pkg/utils/grpcutil/grpcutil.go Outdated
return false
}
// The message is generated by the gRPC server, see `grpc.Server.handleStream`.
return s.Message() == "unknown service "+service

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.

This matches an internal gRPC error-message string rather than a stable contract, so a grpc-go version bump that rewords it would silently disable the reconnect. Prefer checking the grpc.UnknownServiceError type instead, and add a test pinning the current format so a dependency bump fails CI.

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.

Confirmed, a reworded message would silently disable the reconnect. I pinned the format in 5866c4268, but kept matching the message instead of checking a type, because grpc-go doesn't expose one: there is no grpc.UnknownServiceError in v1.82.1. The server writes status.New(codes.Unimplemented, "unknown service <S>") in handleStream, so the client only gets the code and the message. The code alone isn't enough either. An unknown method of a registered service also returns Unimplemented, and treating that as a wrong endpoint would drop valid connections to older servers that lack a newer RPC.

The prefix is now unknownServiceErrorPrefix, and the new TestUnknownServiceErrorFormat checks the exact message from a real gRPC server for both unary and stream calls, and that an unknown method doesn't match. When the prefix doesn't match the server's message, the test fails and says to update unknownServiceErrorPrefix. So a grpc-go bump that changes the wording fails CI instead of quietly turning off the reconnect.

isUnknownServiceError matches the message generated by the gRPC server,
since gRPC exposes no typed error for an unregistered service. Move the
prefix into unknownServiceErrorPrefix and add TestUnknownServiceErrorFormat
to fail CI if a gRPC upgrade changes the format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: bufferflies <1045931706@qq.com>
}

// RecvMsg implements grpc.ClientStream.
func (s *expectedServiceClientStream) RecvMsg(m any) error {

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.

When a pre-opened Router QueryRegion stream is rejected for a missing service, the next Send can return io.EOF; processRequestsInner returns on that error without calling Recv, so this RecvMsg hook never observes the Unimplemented status and the underlying connection remains READY. With unchanged registry URLs, discovery skips reconnecting, and the background connection daemon can keep pre-opening rejected streams on the same cached connection after DNS recovers, causing repeated failures for sparse Router requests.

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.

Good catch, fixed in ec906d57f: processRequestsInner and the resource manager's processTokenRequests now pass a send error through the new grpcutil.ResolveStreamSendError, which calls Recv once when Send returns io.EOF and returns the stream status instead. That Recv goes through the expected-service stream wrapper, so an unknown service rejection closes the connection, and the discoveries recreate it as for the unary path. Other send errors are returned unchanged. The token path also now routes the resolved error through gRPCErrorHandler / resourceManagerErrorHandler, consistent with the Recv failure path. Added TestProcessRequestsClosesConnOnRejectedStream and TestProcessTokenRequestsClosesConnOnRejectedStream, which pre-open a stream to a server without the service and wait for Send to fail. Both fail without the fix: they get io.EOF and the connection stays open. TSO is unaffected since its recvLoop already receives the status.

@bufferflies bufferflies reopened this Oct 27, 2026
bufferflies and others added 2 commits October 27, 2026 07:56
A pre-opened Router QueryRegion stream or resource manager token stream
rejected for an unknown service only returns io.EOF on Send, the status
is reported on Recv. Receive the status on io.EOF so the expected
service check closes the connection to be recreated, and fix the
modernize lint in the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: bufferflies <1045931706@qq.com>
@ti-chi-bot

ti-chi-bot Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

@bufferflies: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-unit-test-next-gen-3 ec906d5 link true /test pull-unit-test-next-gen-3

Full PR test history. Your PR dashboard.

Details

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 kubernetes-sigs/prow repository. I understand the commands that are listed here.

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.

2 participants