Skip to content

server: make EtcdStartTimeout a no-progress timeout#11015

Open
bufferflies wants to merge 4 commits into
tikv:masterfrom
bufferflies:etcd-start-progress-timeout
Open

server: make EtcdStartTimeout a no-progress timeout#11015
bufferflies wants to merge 4 commits into
tikv:masterfrom
bufferflies:etcd-start-progress-timeout

Conversation

@bufferflies

@bufferflies bufferflies commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #11014

server.EtcdStartTimeout (default 5m) is applied as a fixed overall deadline in (*Server).startEtcd. The deadline counts down regardless of whether etcd is making progress, so a member that restarts and has to catch up a large raft log can be killed once 5 minutes elapse even though it is healthy and steadily applying entries. Under a process supervisor it then restarts and loops forever, never coming up. The timeout also cannot tell a slow-but-healthy startup apart from a genuine hang (e.g. a removed member that can never rejoin the quorum).

What is changed and how does it work?

Wait for etcd readiness using the etcd applied index as a liveness signal instead of a fixed deadline:

  • New helper waitEtcdReady polls etcd.Server.AppliedIndex() while waiting for ReadyNotify().
  • As long as the applied index advances, the member is still applying its raft log, so we keep waiting.
  • We give up only when there is no apply progress for EtcdStartTimeout, which still fails fast on a genuine hang.
  • Parent ctx cancellation is still honored for normal shutdown.
  • initMember now uses its own EtcdStartTimeout-bounded context.

AppliedIndex is used (rather than committed index) because during catch-up the committed index may already sit at the target while the state machine is still applying, which would look "stuck" while actually healthy.

Known limitation: this covers the wait after embed.StartEtcd returns. The bulk snapshot load / WAL replay happens inside embed.StartEtcd, which takes no ctx and runs before this wait, so that phase is out of scope here.

server: make EtcdStartTimeout a no-progress timeout

startEtcd previously wrapped the whole "wait until etcd is ready" in a
fixed context deadline of EtcdStartTimeout (5m). A slow-but-healthy
startup that keeps catching up its raft log would be killed once the
deadline elapsed, and on process restart it would loop forever.

Wait for readiness using the etcd applied index as a liveness signal
instead: keep waiting as long as the applied index advances, and only
give up when there is no apply progress for EtcdStartTimeout. This tells
a slow restore apart from a genuine hang (e.g. a removed member that can
never rejoin the quorum), which still fails fast. Parent ctx
cancellation is still honored for normal shutdown.

Check List

Tests

  • Unit test (TestWaitEtcdReadyProgress: ready-immediately, keeps-waiting-while-progressing, no-progress-timeout, ctx-cancellation)
  • Integration test (existing tests/server/join suite, which exercises EtcdStartTimeout and the removed-member hang path, still passes)

Side effects

  • The overall etcd startup wait is no longer bounded by a fixed wall-clock deadline; a startup that keeps making apply progress may take longer than EtcdStartTimeout to complete (intended, to avoid killing healthy slow startups).

Release note

Fix PD failing to start when the embedded etcd needs longer than the fixed start timeout to catch up its raft log; the start timeout is now a no-progress timeout based on etcd apply progress.

Summary by CodeRabbit

  • Bug Fixes
    • Improved embedded etcd startup reliability by waiting for readiness using actual progress signals (applied index advancement) rather than a single fixed timeout.
    • Startup now only times out when progress stalls, while still failing fast on startup errors or unexpected early shutdown.
    • More consistent handling when startup is canceled.
  • Tests
    • Added coverage for progress-based readiness waiting, including immediate readiness, fast failure on errors/stops, stalled progress timeouts, and context cancellation.

startEtcd previously wrapped the whole "wait until etcd is ready" in a
fixed context deadline of EtcdStartTimeout (5m). A slow-but-healthy
startup that keeps catching up its raft log would be killed once the
deadline elapsed, and on process restart it would loop forever.

Wait for readiness using the etcd applied index as a liveness signal
instead: keep waiting as long as the applied index advances, and only
give up when there is no apply progress for EtcdStartTimeout. This tells
a slow restore apart from a genuine hang (e.g. a removed member that can
never rejoin the quorum), which still fails fast. Parent ctx
cancellation is still honored for normal shutdown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <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 Jul 16, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 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/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Embedded etcd startup now treats EtcdStartTimeout as a no-progress timeout. It polls AppliedIndex() while waiting for readiness, handles startup failures and cancellation, and uses a fresh timeout context for initMember.

Changes

Embedded etcd startup

Layer / File(s) Summary
Progress-based readiness monitoring
server/server.go
Documents the no-progress timeout and adds applied-index polling that detects readiness, cancellation, asynchronous errors, premature stopping, and stalled progress.
Startup readiness and member initialization
server/server.go
Replaces the fixed-deadline readiness wait with waitEtcdReady, starts clients after readiness, then runs initMember under a new timeout context.
Readiness progress validation
server/etcd_start_test.go
Tests immediate readiness, asynchronous errors, premature stopping, continued applied-index progress, stalled readiness failure, and context cancellation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant EmbeddedEtcd
  participant initMember
  Server->>EmbeddedEtcd: waitEtcdReady(ctx, etcd)
  EmbeddedEtcd-->>Server: readiness or startup result
  Server->>initMember: initMember(initCtx, etcd)
  initMember-->>Server: initialization result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: EtcdStartTimeout becomes a no-progress timeout.
Description check ✅ Passed The PR description follows the template, includes the issue number, change summary, checklist, and release note.
Linked Issues check ✅ Passed The code matches #11014 by using AppliedIndex progress, honoring ctx cancellation, and failing on stalls and startup errors.
Out of Scope Changes check ✅ Passed The added tests and startup helpers are directly related to the timeout semantics change; no unrelated code 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.

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@server/server.go`:
- Around line 118-121: Update the GoDoc comment associated with the exported
identifier near startEtcd/waitEtcdReady so its first words are the exact
exported identifier name, while preserving the existing explanation of the
no-progress timeout.
- Around line 442-446: Update the startup select in the etcd readiness flow to
monitor etcd.Err() alongside ReadyNotify() and ctx.Done(). When an asynchronous
etcd failure is received, return it using the same error-wrapping pattern as
embed.StartEtcd, while preserving the existing ready and cancellation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 570e365b-6c08-4bc9-91a1-86a164096eae

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2787b and d8ded0c.

📒 Files selected for processing (1)
  • server/server.go

Comment thread server/server.go Outdated
Comment thread server/server.go
Extract the wait loop of waitEtcdReady into waitEtcdReadyProgress, which
depends only on a ready channel, an applied-index getter and the two
durations, so it can be driven deterministically without a real embedded
etcd. Cover the four behaviors: ready immediately, keep waiting while the
applied index advances (past what a fixed deadline would allow), give up
after no apply progress, and honor ctx cancellation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>
Comment thread server/server.go Outdated
case now := <-ticker.C:
applied := etcd.Server.AppliedIndex()
if applied > lastApplied {
log.Info("etcd is not ready yet but still making apply progress, keep waiting",

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 logs at Info level on every one-second poll while the applied index advances. The exact startup path this change targets may run for many minutes or hours, producing hundreds or thousands of repetitive production log entries and adding logging overhead during recovery. Please rate-limit the progress message or lower it to Debug.

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.

Done in ab2cee0 — the progress log is now rate-limited to at most once per 30s (etcdStartProgressLogInterval) instead of one line per 1s poll. Kept at Info so operators can still distinguish a slow recovery from a hang.

@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Jul 16, 2026
Comment thread server/server.go Outdated
// EtcdStartTimeout do we give up. This distinguishes a slow-but-healthy startup
// from a genuine hang, such as a removed member that can never rejoin the
// quorum. It also honors ctx cancellation for normal shutdown.
func (s *Server) waitEtcdReady(ctx context.Context, etcd *embed.Etcd) 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.

Blocking: the *Server receiver is still unused, and the repository revive configuration reports unused-receiver here, so the required static check cannot pass. The newly extracted wrapper still does not depend on any Server state; please make it a package-level function or remove the unused receiver in another idiomatic way.

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.

Done in ab2cee0 — made waitEtcdReady a package-level function so there is no unused *Server receiver. golangci-lint run ./server/ is now clean (0 issues).

Comment thread server/server.go Outdated
// from a genuine hang, such as a removed member that can never rejoin the
// quorum. It also honors ctx cancellation for normal shutdown.
func (s *Server) waitEtcdReady(ctx context.Context, etcd *embed.Etcd) error {
return waitEtcdReadyProgress(ctx, etcd.Server.ReadyNotify(), etcd.Server.AppliedIndex,

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.

Please also stop waiting when embedded etcd fails or stops before becoming ready. Listener and serving failures are reported through etcd.Err(), while internal EtcdServer.run failures can terminate the server and close etcd.Server.StopNotify() without producing an embed.Etcd.Err() value. Passing only ReadyNotify() into the core means a dead server is still misreported as ErrCancelStartEtcd after the full timeout. Please monitor both signals, preserve an underlying error from etcd.Err() when available, and fail fast on StopNotify().

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.

Done in ab2cee0waitEtcdReadyProgress now also selects on etcd.Server.StopNotify(). When the server stops before becoming ready it does a non-blocking read of etcd.Err() to preserve the underlying cause (wrapped in ErrStartEtcd), falling back to a bare ErrStartEtcd when none was published. Added a unit test for the stop-before-ready path.

Comment thread server/server.go
zap.Uint64("last-applied-index", lastApplied),
zap.Uint64("applied-index", applied))
lastApplied = applied
lastProgress = now

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.

now is the scheduled timestamp carried by the ticker event, but appliedIndex() is sampled only when this goroutine handles that event. If a tick remains buffered during a long scheduling or GC pause, this branch can observe fresh progress and record it with a stale time, causing a subsequent tick to declare a timeout immediately. Please use the actual sampling time from time.Now(), or preferably reset a dedicated no-progress timer whenever the applied index advances.

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.

Done in ab2cee0 — I now sample time.Now() when reading the applied index instead of reusing the ticker`s scheduled timestamp, so a tick buffered during a scheduling/GC pause cannot record progress with a stale time and trip a false no-progress timeout on the next tick.

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/etcd_start_test.go (1)

82-83: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use require.ErrorIs for sentinel error checks.

The coding guidelines mandate using errors.Is/As for sentinel error checks. Utilizing Testify's require.ErrorIs inherently delegates to errors.Is, making the test more idiomatic and providing much clearer failure messages compared to manual boolean evaluation.

  • server/etcd_start_test.go#L82-L83: replace re.Error(err) and re.True(errors.ErrorEqual(err, errs.ErrCancelStartEtcd)) with re.ErrorIs(err, errs.ErrCancelStartEtcd).
  • server/etcd_start_test.go#L99-L100: apply the identical replacement in the cancellation case.
🤖 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 `@server/etcd_start_test.go` around lines 82 - 83, Replace the manual error
assertions with re.ErrorIs(err, errs.ErrCancelStartEtcd) in both cancellation
cases: server/etcd_start_test.go lines 82-83 and 99-100. Remove the separate
re.Error and errors.ErrorEqual assertions while preserving the existing sentinel
error expectation.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@server/etcd_start_test.go`:
- Around line 90-102: Update the cancellation test around waitEtcdReadyProgress
to defer cancel() immediately after creating the cancellable context, ensuring
cleanup on early failure or panic. Remove the time-based start measurement and
re.Less assertion, while preserving the existing error and ErrCancelStartEtcd
assertions.

---

Nitpick comments:
In `@server/etcd_start_test.go`:
- Around line 82-83: Replace the manual error assertions with re.ErrorIs(err,
errs.ErrCancelStartEtcd) in both cancellation cases: server/etcd_start_test.go
lines 82-83 and 99-100. Remove the separate re.Error and errors.ErrorEqual
assertions while preserving the existing sentinel error expectation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d2d606c6-52c0-4e7e-bc76-f20d9c5a5eed

📥 Commits

Reviewing files that changed from the base of the PR and between d8ded0c and fc2e8be.

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

Comment thread server/etcd_start_test.go Outdated
Address review feedback on the etcd start no-progress timeout:

- Monitor etcd.Err() while waiting for readiness. If the embedded etcd
  hits a fatal error in the background before ReadyNotify() closes, the
  error is now returned (wrapped in ErrStartEtcd) immediately instead of
  stalling for the whole no-progress timeout and returning a generic
  error. waitEtcdReadyProgress takes the error channel as a parameter so
  it stays unit-testable.
- Start the EtcdStartTimeout GoDoc with the identifier name.
- Add a unit test for the async-error path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.23%. Comparing base (c2a47d8) to head (3b75894).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #11015   +/-   ##
=======================================
  Coverage   79.22%   79.23%           
=======================================
  Files         541      541           
  Lines       75965    76012   +47     
=======================================
+ Hits        60187    60231   +44     
- Misses      11531    11537    +6     
+ Partials     4247     4244    -3     
Flag Coverage Δ
unittests 79.23% <100.00%> (+<0.01%) ⬆️

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.

Follow-up on reviewer feedback for the etcd start no-progress timeout:

- Drop the unused *Server receiver on waitEtcdReady (revive
  unused-receiver, which was failing the static check) by making it a
  package-level function.
- Also monitor etcd.Server.StopNotify(): an internal EtcdServer.run
  failure can stop the server without publishing on etcd.Err(), which
  previously got misreported as a no-progress timeout. On stop, preserve
  the underlying etcd.Err() cause when available.
- Sample time.Now() when reading the applied index instead of reusing
  the ticker's scheduled timestamp, so a tick buffered during a
  scheduling/GC pause cannot trip a false no-progress timeout.
- Rate-limit the progress log to at most once per 30s (a slow recovery
  can run for many minutes), instead of logging on every poll.
- Test: drop the flaky timing upper-bound assertion in the
  cancellation case, add defer cancel(), and cover the server-stopped
  path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.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.

🧹 Nitpick comments (1)
server/etcd_start_test.go (1)

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

Use errors.Is for sentinel error checks.

As per coding guidelines, sentinel error checks should use errors.Is or errors.As. Using testify's require.ErrorIs inherently applies errors.Is and provides clearer failure messages than require.True combined with errors.ErrorEqual.

  • server/etcd_start_test.go#L113-L113: Replace re.True(errors.ErrorEqual(err, errs.ErrCancelStartEtcd)) with re.ErrorIs(err, errs.ErrCancelStartEtcd).
  • server/etcd_start_test.go#L130-L130: Replace re.True(errors.ErrorEqual(err, errs.ErrCancelStartEtcd)) with re.ErrorIs(err, errs.ErrCancelStartEtcd).
🤖 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 `@server/etcd_start_test.go` at line 113, Update both sentinel checks in
server/etcd_start_test.go at lines 113-113 and 130-130 to use testify’s
re.ErrorIs(err, errs.ErrCancelStartEtcd) instead of
re.True(errors.ErrorEqual(...)); no other changes are needed.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@server/etcd_start_test.go`:
- Line 113: Update both sentinel checks in server/etcd_start_test.go at lines
113-113 and 130-130 to use testify’s re.ErrorIs(err, errs.ErrCancelStartEtcd)
instead of re.True(errors.ErrorEqual(...)); no other changes are needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b44476c-552a-4f9e-9646-8e287719106a

📥 Commits

Reviewing files that changed from the base of the PR and between fc2e8be and ab2cee0.

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

@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 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-2 ab2cee0 link true /test pull-unit-test-next-gen-2

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/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server: make etcd start timeout a no-progress timeout instead of a fixed deadline

2 participants