Skip to content

server: warn when the advertised status endpoint cannot identify the same TiDB#69880

Open
OliverS929 wants to merge 3 commits into
pingcap:masterfrom
OliverS929:issue-69688-endpoint-self-check
Open

server: warn when the advertised status endpoint cannot identify the same TiDB#69880
OliverS929 wants to merge 3 commits into
pingcap:masterfrom
OliverS929:issue-69688-endpoint-self-check

Conversation

@OliverS929

@OliverS929 OliverS929 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #69688

Problem Summary:

TiDB publishes its advertised status endpoint as runtime metadata, but it does not verify whether the endpoint reaches the same TiDB process. A misconfigured endpoint can identify another TiDB instance and surface later as downstream errors without a direct diagnostic.

What changed and how does it work?

After the status and SQL listeners are initialized, TiDB schedules a one-shot background request to the advertised /info endpoint and compares the returned ddl_id with the local TiDB identity.

  • A matching identity remains silent.
  • An unreachable, invalid, or different-instance endpoint emits one structured warning without blocking startup or SQL service.
  • The dedicated HTTP client bypasses environment proxies, rejects redirects, preserves cluster TLS settings, limits the response size, and uses a bounded request timeout.
  • The request is canceled when the corresponding Server.Run invocation exits.

A direct self-roundtrip can be unavailable in some intentional VIP, NAT, Kubernetes Service, or load-balancer topologies. Such a topology can produce a warning; the check remains diagnostic only and does not reject startup.

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.

Manual test:

  1. Started a TiDB whose advertised endpoint reached itself. SQL succeeded and no warning was emitted.
  2. Started two TiDB instances where one advertised the other instance's status endpoint. Exactly one identity-mismatch warning was emitted with the expected local and remote identities, while SQL remained available. The same topology emitted no feature warning with the baseline binary.
  3. Tested non-responsive and malformed endpoints. Each emitted one classified warning without delaying or terminating SQL service.
  4. Tested cluster TLS with trusted and untrusted endpoints. The trusted endpoint remained silent, while certificate verification failure emitted one warning without plaintext or insecure-TLS fallback.
  5. Tested a known false-positive case with a network-isolated proxy: the advertised endpoint returned the correct TiDB identity to an external client but rejected TiDB's own request. TiDB emitted one warning while SQL remained available.

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 logs a warning during startup when its advertised status endpoint cannot be verified as reaching the same TiDB instance. The check does not block startup or SQL service.

Summary by CodeRabbit

  • New Features

    • Added an automatic background verification for the server’s advertised status endpoint, including HTTPS handling, identity checks against the returned /info, and strict validation of expected responses.
    • Handles redirects, timeouts, maximum response sizes, and preserves TLS settings; proxy usage is bypassed during the check.
    • Failure results in a structured warning and stops promptly during shutdown.
  • Tests

    • Added a comprehensive test suite covering URL formatting (IPv4/IPv6), validation failures, response/error cases, lifecycle/cancellation, TLS/proxy behavior, and request failure handling.
  • Documentation

    • Updated the TiDB server test case map with coverage for the advertised status endpoint self-check.

@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. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Jul 16, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 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 disksing 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

@coderabbitai

coderabbitai Bot commented Jul 16, 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: 0869cf1b-fad0-4e72-94ce-d1adf8f2f9a8

📥 Commits

Reviewing files that changed from the base of the PR and between 7dce4d3 and 1d65ac7.

📒 Files selected for processing (3)
  • pkg/server/BUILD.bazel
  • pkg/server/http_status.go
  • pkg/server/http_status_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/server/http_status.go
  • pkg/server/http_status_test.go

📝 Walkthrough

Walkthrough

Changes

TiDB now asynchronously checks whether its advertised status endpoint returns the same local DDL identity, handling HTTP, JSON, TLS, proxy, timeout, cancellation, and lifecycle cases. Server startup wires the checker, with comprehensive tests and build/test-map registration.

Advertised status endpoint verification

Layer / File(s) Summary
Check contract and endpoint input
pkg/server/http_status.go, pkg/server/http_status_test.go
Defines check inputs and results, validates configuration, builds IPv4/IPv6 /info URLs, and configures a direct HTTP client.
Request verification and failure reporting
pkg/server/http_status.go, pkg/server/http_status_test.go
Checks response status, bounded JSON body, remote ddl_id, request failures, redirects, TLS, proxy bypass, warnings, cancellation, and idle connection cleanup.
Server startup wiring and test registration
pkg/server/server.go, pkg/server/BUILD.bazel, .agents/skills/tidb-test-guidelines/references/server-case-map.md
Starts the cancellable checker during Server.Run and registers its implementation and tests in build metadata and the server test map.

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

Sequence Diagram(s)

sequenceDiagram
  participant TiDBServer
  participant StatusHTTPClient
  participant AdvertisedInfoEndpoint
  participant WarningLogger
  TiDBServer->>StatusHTTPClient: GET `/info`
  StatusHTTPClient->>AdvertisedInfoEndpoint: request advertised endpoint
  AdvertisedInfoEndpoint-->>StatusHTTPClient: HTTP status and `ddl_id`
  StatusHTTPClient-->>TiDBServer: verification result
  TiDBServer->>WarningLogger: log mismatch or request failure
Loading

Suggested reviewers: bb7133, yangkeao

Poem

A rabbit checks the /info trail,
Through TLS and timeouts, swift as hail.
IDs match—or warnings hop,
Cancellations make it stop.
Tests bloom bright beside the build.

🚥 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
Title check ✅ Passed The title is concise and accurately summarizes the main change: warning on mismatched advertised status endpoints.
Description check ✅ Passed The description matches the template with issue number, problem summary, change details, tests, side effects, and release note.
Linked Issues check ✅ Passed The changes implement the linked issue’s warning-only self-check for advertised status endpoint mismatches without blocking startup.
Out of Scope Changes check ✅ Passed The changes are focused on the advertised status endpoint check, related tests, build wiring, and a small test-map doc update.
✨ 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.

@OliverS929
OliverS929 requested review from YangKeao and bb7133 July 16, 2026 04:30
@ingress-bot

Copy link
Copy Markdown

🔍 Starting code review for this PR...

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 35.02825% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.9901%. Comparing base (531e40c) to head (1d65ac7).

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #69880        +/-   ##
================================================
- Coverage   76.3207%   73.9901%   -2.3306%     
================================================
  Files          2041       2056        +15     
  Lines        559975     578447     +18472     
================================================
+ Hits         427377     427994       +617     
- Misses       131697     150105     +18408     
+ Partials        901        348       -553     
Flag Coverage Δ
integration 40.7357% <35.0282%> (+1.0305%) ⬆️

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.

@OliverS929

Copy link
Copy Markdown
Contributor Author

/test check-dev2

@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: 10
  • Inline comments: 9
  • Summary-only findings (no inline anchor): 1
Findings (highest risk first)

🟡 [Minor] (8)

  1. Warning message/action text is wrong for a local client-construction failure (pkg/server/http_status.go:302, pkg/server/http_status.go:81)
  2. newAdvertisedStatusEndpointCheckInput takes two adjacent same-typed string params with no compiler-checked ordering (pkg/server/http_status.go:132, pkg/server/http_status.go:295)
  3. No comment documents that the self-check silently no-ops when advertise-address is unset (the default) (pkg/server/http_status.go:139, pkg/server/http_status.go:285, pkg/config/config.go:1105)
  4. No intent comment for silently stripping proxy config and blocking redirects (pkg/server/http_status.go:161)
  5. invalid-response reason conflates a body-read transport failure with malformed response content (pkg/server/http_status.go:209)
  6. Self-check response schema duplicates the ddl_id JSON contract instead of reusing serverinfo.StaticInfo (pkg/server/http_status.go:224, pkg/domain/serverinfo/info.go:56)
  7. Unreachable self-endpoint is reported as a topology-mismatch warning, producing false alarms in common deployments (pkg/server/http_status.go:277, pkg/server/http_status.go:263)
  8. Self-check identity contract (ddl.GetID vs /info ddl_id) has no end-to-end regression coverage (pkg/server/http_status.go:296, pkg/server/http_status.go:225, pkg/server/http_status_test.go)

ℹ️ [Info] (2)

  1. Self-check feature adds a whole new concern into an already multi-purpose file (pkg/server/http_status.go:76)
  2. New background-check context bypasses the existing Server-scoped cancellation convention (pkg/server/server.go:528, pkg/server/server.go:452, pkg/server/server.go:701)

Unanchored findings

ℹ️ [Info] (1)

  1. Self-check feature adds a whole new concern into an already multi-purpose file
    • Request: Consider moving this self-check logic (and its test file) into a dedicated file, e.g. pkg/server/advertised_status_check.go, to keep http_status.go scoped to status-server bootstrapping and improve discoverability.

Comment thread pkg/server/http_status.go Outdated
client, err := newAdvertisedStatusEndpointHTTPClient(util.InternalHTTPClient(), advertisedStatusEndpointCheckTimeout)
if err != nil {
logAdvertisedStatusEndpointCheckWarning(input, advertisedStatusEndpointCheckResult{
reason: advertisedStatusEndpointRequestFailed,

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] Warning message/action text is wrong for a local client-construction failure

Impact
When newAdvertisedStatusEndpointHTTPClient fails (for example the internal HTTP client's Transport is not a *http.Transport), the failure is reported with reason: advertisedStatusEndpointRequestFailed, which shares the same fixed advertisedStatusEndpointWarningMessage ("advertised status endpoint does not identify this TiDB instance") and advertisedStatusEndpointWarningSuggestedAction ("check that advertise-address and status-port route directly to this TiDB instance and that no TiDB exists outside the intended topology") used for real network/identity failures.
This is a local client-construction error, not evidence of a misrouted or duplicated TiDB instance, so an operator paged by this warning would be steered toward checking cluster topology instead of the actual root cause (a broken internal HTTP client).

Scope

  • pkg/server/http_status.go:302startAdvertisedStatusEndpointCheck
  • pkg/server/http_status.go:81advertisedStatusEndpointWarningMessage

Evidence
At pkg/server/http_status.go:302-308, a newAdvertisedStatusEndpointHTTPClient error is logged via logAdvertisedStatusEndpointCheckWarning with reason: advertisedStatusEndpointRequestFailed, reusing the same generic message/action pair that is meant for the case where the remote /info endpoint answered but did not identify this instance.
The same reused text also applies to advertisedStatusEndpointRequestFailed results coming from genuine network failures like the TLS-verification-failure case covered in TestAdvertisedStatusEndpointCheckRequestFailures, where "check topology" is equally off-target advice for a cert problem.

Change request
Give the client-construction failure its own distinct log message/reason (e.g. "failed to build HTTP client for advertised status endpoint self-check") instead of reusing advertisedStatusEndpointRequestFailed/advertisedStatusEndpointWarningSuggestedAction, or make the suggested action conditional on reason so it only points at topology duplication for missing-identity/identity-mismatch/unexpected-status.

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.

Updated in 1d65ac7.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ The client-construction path now logs a dedicated client-setup-failed reason with its own remediation text, so a broken internal transport no longer sends operators chasing a topology problem.

Comment thread pkg/server/http_status.go
func newAdvertisedStatusEndpointCheckInput(
reportStatus bool,
statusListener net.Listener,
advertiseAddress string,

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] newAdvertisedStatusEndpointCheckInput takes two adjacent same-typed string params with no compiler-checked ordering

Impact
advertiseAddress and localID sit next to each other as plain string parameters (positions 3 and 4) in newAdvertisedStatusEndpointCheckInput, and the same ordering repeats at the production call site and in every test call.
Swapping them compiles cleanly since both are string, and would feed the DDL ID into the host-building path (buildAdvertisedStatusEndpointURL) while comparing the real advertise address against the remote identity, silently misconfiguring the check for the next person who edits this call.

Scope

  • pkg/server/http_status.go:132newAdvertisedStatusEndpointCheckInput
  • pkg/server/http_status.go:295

Evidence
Signature is newAdvertisedStatusEndpointCheckInput(reportStatus bool, statusListener net.Listener, advertiseAddress string, localID string, scheme string); nothing distinguishes advertiseAddress from localID at the type level, so an argument-order mistake is not caught until the self-check silently misbehaves at runtime.

Change request
Group the semantically distinct string fields into a small named struct (or at least give advertiseAddress/localID distinct wrapper types) so a future reorder fails to compile instead of silently producing a wrong check.

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.

I don't think another input struct or wrapper types are necessary for this private helper with one production call site, so I kept the current signature.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Fair enough — for a private helper with a single production call site, keeping the flat signature rather than introducing a wrapper struct is a reasonable trade-off. Leaving as-is.

Comment thread pkg/server/http_status.go
localID string,
scheme string,
) (advertisedStatusEndpointCheckInput, bool) {
if !reportStatus || statusListener == nil || advertiseAddress == "" || localID == "" {

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] No comment documents that the self-check silently no-ops when advertise-address is unset (the default)

Impact
newAdvertisedStatusEndpointCheckInput returns ok=false with zero logging whenever advertiseAddress == "", which is the documented default value of AdvertiseAddress.
Operators who expect this new self-check to catch a duplicated or misrouted TiDB instance get silently no protection at all in the common case where advertise-address was never configured, and nothing in the code states this limitation.

Scope

  • pkg/server/http_status.go:139newAdvertisedStatusEndpointCheckInput
  • pkg/server/http_status.go:285startAdvertisedStatusEndpointCheck
  • pkg/config/config.go:1105

Evidence
pkg/server/http_status.go:139 folds !reportStatus, a missing listener, empty advertiseAddress, and empty localID into a single silent false return with no log line, and pkg/config/config.go:1105 sets AdvertiseAddress: "" as the default, so a stock deployment never runs this check.
The sibling GetStatusServerAddr (pkg/server/server.go:213-224) treats the same empty-AdvertiseAddress condition the same way but at least returns an explicit bool the caller inspects, whereas here the caller (startAdvertisedStatusEndpointCheck) just silently returns.

Change request
Add a short comment above the guard in newAdvertisedStatusEndpointCheckInput stating that an empty advertiseAddress intentionally disables the check (mirroring GetStatusServerAddr's behavior), and consider a one-time debug/info log noting the check was skipped so operators are not left assuming it is active by default.

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.

Normal tidb-server startup fills an empty advertise address before constructing the server. The remaining empty cases are direct library/test configurations and intentionally skip the check, so I did not add a startup log.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Makes sense: normal tidb-server startup fills advertise-address before the server is constructed, so the empty-value skip only touches direct library/test configs. No startup log needed.

Comment thread pkg/server/http_status.go
}
}
directTransport := baseTransport.Clone()
directTransport.Proxy = nil

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] No intent comment for silently stripping proxy config and blocking redirects

Impact
newAdvertisedStatusEndpointHTTPClient clones the shared util.InternalHTTPClient() transport but sets Proxy = nil and forces CheckRedirect to stop-at-first-hop, diverging from the base client's normal proxy-aware, redirect-following behavior with no comment explaining why.
A future maintainer reusing or "simplifying" this helper (it is not tied to the endpoint-check name) could drop either behavior without realizing both are load-bearing for the self-check's correctness: proxy bypass ensures the request reaches the advertised host directly, and blocking redirects prevents a misrouted request from silently reporting success via a different backend.

Scope

  • pkg/server/http_status.go:161newAdvertisedStatusEndpointHTTPClient

Evidence
directTransport.Proxy = nil (line 177) and the CheckRedirect override returning http.ErrUseLastResponse (lines 181-183) have no accompanying comment, even though util.InternalHTTPClient() itself is proxy-aware (http.ProxyFromEnvironment via http.DefaultTransport), so the divergence is a deliberate, non-obvious design choice rather than an accident.

Change request
Add a short comment above the directTransport.Proxy = nil / CheckRedirect block explaining that the self-check must bypass any configured proxy and must not follow redirects, since either would let a misrouted request appear to succeed.

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.

Updated in 1d65ac7.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ A comment now sits above the proxy/redirect overrides spelling out that either one could let a different endpoint pass the identity check, so the intent is preserved for future readers.

Comment thread pkg/server/http_status.go Outdated
if err != nil {
result.reason = advertisedStatusEndpointInvalidResponse
var networkError net.Error
if ctx.Err() != nil || (stderrors.As(err, &networkError) && networkError.Timeout()) {

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] invalid-response reason conflates a body-read transport failure with malformed response content

Impact
The logged reason field is meant to tell an operator whether the advertised status endpoint answered with bad content (invalid-response) or the request itself failed (request-failed), but any non-timeout io.ReadAll error while reading the body — for example a connection reset mid-response — is left labeled invalid-response even though no content was actually received to judge as malformed.
Someone triaging via the reason field would be steered toward a response-format problem when the real cause is a dropped connection.

Scope

  • pkg/server/http_status.go:209checkAdvertisedStatusEndpoint

Evidence
At pkg/server/http_status.go:209-217, body, err := io.ReadAll(...) on error first sets result.reason = advertisedStatusEndpointInvalidResponse, then only re-classifies to advertisedStatusEndpointRequestFailed when ctx.Err() != nil or the error satisfies net.Error.Timeout().
A mid-stream connection reset (e.g. io.ErrUnexpectedEOF or a wrapped ECONNRESET) is neither a context error nor a Timeout() error, so it falls through and keeps the invalid-response label, unlike the oversized-body and malformed-JSON checks a few lines below that correctly reserve invalid-response for cases where a full body was actually obtained.

Change request
Treat any io.ReadAll error on the body as request-failed by default (the transport did not deliver a complete response), reserving invalid-response for the oversized-body and JSON-unmarshal checks that already run only after a full body is successfully read.

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.

Updated in 1d65ac7.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Body-read errors are now classified as request-failed, reserving invalid-response for the oversized-body and JSON-unmarshal checks that only run once a full body is in hand — a mid-stream reset lands in the right bucket.

Comment thread pkg/server/http_status.go Outdated
result.err = errors.Errorf("response body exceeds %d-byte limit", advertisedStatusEndpointResponseBodyLimit)
return result
}
var responseInfo struct {

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] Self-check response schema duplicates the ddl_id JSON contract instead of reusing serverinfo.StaticInfo

Impact
The /info endpoint already serializes ddl_id through serverinfo.StaticInfo.ID (embedded in tikvhandler.ServerInfo), but checkAdvertisedStatusEndpoint re-declares an independent anonymous struct with its own json:"ddl_id" tag to parse the same response.
A future rename or restructuring of the source-of-truth field has no compiler or reference link to this second definition, so it can drift silently and make the self-check permanently report missing-identity without anyone noticing.

Scope

  • pkg/server/http_status.go:224checkAdvertisedStatusEndpoint
  • pkg/domain/serverinfo/info.go:56StaticInfo

Evidence
checkAdvertisedStatusEndpoint unmarshals into var responseInfo struct { DDLID string \json:"ddl_id"` }, duplicating the tag already owned by serverinfo.StaticInfo.ID (json:"ddl_id"), which is what ServerInfoHandler.ServeHTTPactually serves at/info`.

Change request
Unmarshal into (or extract just the ID field from) the existing serverinfo.StaticInfo/ServerInfo type instead of a local anonymous struct, so a future field rename only needs to change one place.

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.

Updated in 1d65ac7.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Resolved — the self-check unmarshals into serverinfo.StaticInfo directly, so the ddl_id contract has a single source of truth and can no longer drift silently.

Comment thread pkg/server/http_status.go
defer client.CloseIdleConnections()
result := checker(ctx, client, input.endpoint, input.localID)
// Cancellation means this Server.Run invocation is ending, not that the endpoint failed verification.
if ctx.Err() != nil || result.reason == "" {

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] Unreachable self-endpoint is reported as a topology-mismatch warning, producing false alarms in common deployments

Impact
A transient or structural failure to reach the node's own advertised endpoint (reason request-failed) is logged with the same alarming warning and "check that advertise-address and status-port route directly to this TiDB instance" action as a confirmed wrong-instance mismatch.
In topologies where a node cannot reach its own advertised address (Kubernetes hairpin NAT disabled, advertise-address set to a Service/VIP not routable from the pod itself), this fires on every startup even though the topology is correct, pushing operators to chase a nonexistent misconfiguration.

Scope

  • pkg/server/http_status.go:277scheduleAdvertisedStatusEndpointCheck
  • pkg/server/http_status.go:263logAdvertisedStatusEndpointCheckWarning

Evidence
The reporting gate only filters ctx.Err() != nil || result.reason == "", so a request-failed result flows to reporter and logAdvertisedStatusEndpointCheckWarning emits advertisedStatusEndpointWarningMessage ("does not identify this TiDB instance") plus the topology-remediation action. "Could not verify" (advertisedStatusEndpointRequestFailed) is not distinguished from "verified wrong" (advertisedStatusEndpointIdentityMismatch) at the message/severity level, and self-address unreachability is a real recurring condition, not a transient edge case.

Change request
Treat request-failed as a distinct, softer outcome from identity-mismatch: either log it at a lower level with a "could not verify advertised endpoint" message (not the definitive mismatch warning + topology action), or suppress the topology-remediation wording when the endpoint was simply unreachable, so healthy clusters that cannot hairpin to their own advertised address do not emit misconfiguration alarms.

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.

Kept this at warning level, but changed the message to “failed to verify” and made the action reason-specific in 1d65ac7. An unreachable endpoint is now reported as unverified rather than a confirmed topology mismatch.

Comment thread pkg/server/http_status.go
s.cfg.Status.ReportStatus,
s.statusListener,
s.cfg.AdvertiseAddress,
ddl.GetID(),

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] Self-check identity contract (ddl.GetID vs /info ddl_id) has no end-to-end regression coverage

Impact
The whole feature depends on the invariant that the local ddl.GetID() equals the ddl_id returned by the live /info handler, but no test exercises that coupling or the real startAdvertisedStatusEndpointCheck wiring.
Every test hardcodes both sides to "local-id" and injects a fake /info body, so a future change to the /info identity field (or to which id is passed as expectedID) would silently make production emit a false "does not identify this TiDB instance" warning on every startup while all tests still pass.

Scope

  • pkg/server/http_status.go:296startAdvertisedStatusEndpointCheck
  • pkg/server/http_status.go:225checkAdvertisedStatusEndpoint
  • pkg/server/http_status_test.go

Evidence
Production sources expectedID from s.dom.DDL().GetID(), while the /info handler serializes infosync.GetServerInfo().ID as ddl_id (both currently derive from do.ddl.GetID()). Tests only call the decomposed helpers (checkAdvertisedStatusEndpoint, scheduleAdvertisedStatusEndpointCheck) with fabricated ddl_id bodies; startAdvertisedStatusEndpointCheck and the real /info mapping are never driven, so the GetID()-equals-ddl_id invariant is unverified.

Change request
Add one integration-style test that stands up the real status server and drives startAdvertisedStatusEndpointCheck (or an equivalent seam) so a genuine identity match suppresses the warning and a real mismatch triggers exactly one warning, pinning the ddl.GetID()/info ddl_id contract against silent drift.

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.

The live /info identity invariant is already covered by TestServerInfo, which asserts d.GetID() == info.ID. The focused tests and two-process manual runs cover this check without adding another heavy server fixture.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ TestServerInfo already pins d.GetID() == info.ID, so the identity mapping the check relies on is guarded; combined with the focused helper tests and two-process manual runs, standing up a full server fixture just for this isn't worth the weight. Makes sense to leave as-is.

Comment thread pkg/server/server.go
terror.RegisterFinish()
go s.startNetworkListener(s.listener, false, errChan)
go s.startNetworkListener(s.socket, true, errChan)
advertisedStatusEndpointCheckCtx, cancelAdvertisedStatusEndpointCheck := context.WithCancel(context.Background())

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 background-check context bypasses the existing Server-scoped cancellation convention

The package already has an established convention for a startup-spawned background task: store the context.CancelFunc on the Server struct (s.authTokenCancelFunc) and cancel it centrally in closeListener() alongside the other listener/service teardown.
startAdvertisedStatusEndpointCheck's context instead lives as a local variable in Run(), canceled only via defer when Run() itself returns, so a maintainer extending Close()/closeListener() has no single place to see all background-task lifecycles tied to the server.

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.

The auth-token worker is long-running and joined by the Server wait group; this check is one-shot and scoped to one Run invocation. The local cancel is therefore the lifecycle owner, so I did not add a shared Server field.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Right — unlike the auth-token worker, which is long-running and joined via the Server wait group, this check is one-shot and lives entirely within a single Run invocation. Scoping its cancel to that local context keeps lifecycle ownership where it belongs, so a shared Server field would be overkill here.

@OliverS929

Copy link
Copy Markdown
Contributor Author

/test pull-integration-realcluster-test-next-gen

Comment thread pkg/server/http_status.go
Comment on lines +79 to +87
advertisedStatusEndpointCheckTimeout = 5 * time.Second
advertisedStatusEndpointResponseBodyLimit = 1 << 20
advertisedStatusEndpointWarningMessage = "failed to verify advertised status endpoint identity"
)

type advertisedStatusEndpointCheckReason string

const (
advertisedStatusEndpointClientSetupFailed advertisedStatusEndpointCheckReason = "client-setup-failed"

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.

let's put all those advertised status endpoint checking related logic into a new pkg under pkg/server

Comment thread pkg/server/http_status.go
ddl.GetID(),
util.InternalHTTPSchema(),
)
if !ok {

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.

IMHO it's better to return (advertisedStatusEndpointCheckInput, err) for newAdvertisedStatusEndpointCheckInput so that the errors will not be silently lost here.

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.

Warn when a TiDB advertised status endpoint does not reach the same instance

4 participants