Skip to content

mcs: use the configured lease for the service registry#11017

Open
bufferflies wants to merge 3 commits into
tikv:masterfrom
bufferflies:claude/busy-kirch-e4c3c6
Open

mcs: use the configured lease for the service registry#11017
bufferflies wants to merge 3 commits into
tikv:masterfrom
bufferflies:claude/busy-kirch-e4c3c6

Conversation

@bufferflies

@bufferflies bufferflies commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #11016

The service registry lease TTL is hardcoded to discovery.DefaultLeaseInSeconds (5s), ignoring each microservice's configurable leader lease. When the configured lease is larger than the registry lease, the shorter registry lease can expire first under network jitter or etcd pressure while the primary lease is still held. The PD server then evicts a node that legitimately still owns its primary lease, tearing down a healthy primary and causing a needless primary-lease failover.

What is changed and how does it work?

Reuse the configured leader lease as the registry lease TTL, floored at discovery.DefaultLeaseInSeconds so the registry entry never expires faster than the current default.

  • Add GetLeaderLease() int64 to the server interface in pkg/mcs/utils/util.go.
  • In Register, use lease := max(s.GetLeaderLease(), discovery.DefaultLeaseInSeconds).
  • TSO / Scheduling / Resource Manager return their configured lease; Router has no lease config and returns 0, falling back to the default.

Backward compatible: all lease-bearing configs default to 5, so max(5, 5) = 5 preserves current behavior; only an explicitly configured larger lease changes the registry TTL.

Check List

Tests

  • Manual test: go build ./pkg/mcs/... and go vet on the affected mcs packages pass.

Code changes

  • Has the configuration change (the existing lease config now also governs the service registry TTL; no new field added)

Side effects

  • None. Backward compatible; default behavior unchanged.

Related changes

  • None.

Release note

```release-note
None.
```

Summary by CodeRabbit

  • New Features

    • Added leader lease accessors across resource management, routing, scheduling, and timestamp services to expose the configured lease duration.
  • Improvements

    • Service registration now honors the configured leader lease when higher than the default, while enforcing a minimum lease period.
    • Refined service keep-alive retry intervals (with clamping) and standardized deregistration timeout behavior to avoid slow recovery with long TTLs.
    • Improved keyspace group node allocation by adding a primary-election check to prevent chicken-and-egg allocation issues.
  • Tests

    • Added a unit test covering long TTL behavior to ensure recovery-related timing remains unaffected.

The service registry lease TTL was hardcoded to
discovery.DefaultLeaseInSeconds (5s), ignoring the configurable leader
lease of each microservice. When the configured lease is larger than the
registry lease, the shorter registry lease can expire first under jitter
while the primary lease is still held, causing the PD server to evict a
healthy primary and trigger a needless failover.

Reuse the configured leader lease as the registry lease TTL, floored at
discovery.DefaultLeaseInSeconds so it never expires faster than the
current default. Services without a lease config (Router) return 0 and
fall back to the default.

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

ti-chi-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it.

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.

@ti-chi-bot ti-chi-bot Bot added 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 17, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 17, 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 qiuyesuifeng 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 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6209de9f-367c-44cc-ae25-f0747f1586a1

📥 Commits

Reviewing files that changed from the base of the PR and between 9be2870 and a4608a2.

📒 Files selected for processing (1)
  • pkg/keyspace/tso_keyspace_group.go

📝 Walkthrough

Walkthrough

MCS services expose leader lease values, and registration uses the configured lease as its registry TTL when larger than the discovery default. Discovery recovery timing is bounded, and keyspace allocation now checks primary ownership before assigning nodes.

Changes

MCS registry lease

Layer / File(s) Summary
Leader lease accessors
pkg/mcs/*/server/server.go, pkg/mcs/utils/util.go
The server interface adds GetLeaderLease(), and Resource Manager, Scheduling, and TSO return configured lease values while Router returns 0.
Registry lease selection
pkg/mcs/utils/util.go
Register passes max(s.GetLeaderLease(), discovery.DefaultLeaseInSeconds) to service registration.
Discovery recovery timing
pkg/mcs/discovery/register.go, pkg/mcs/discovery/register_test.go
Keepalive retry intervals are capped, deregistration uses the default request timeout, and long-TTL behavior is tested.

Keyspace primary allocation

Layer / File(s) Summary
Primary-aware node allocation
pkg/keyspace/tso_keyspace_group.go
Default groups skip primary lookup; other groups skip allocation when a primary exists, allocate when none exists, and log and skip on lookup errors.

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

Sequence Diagram(s)

sequenceDiagram
  participant MCS Server
  participant utils.Register
  participant discovery.ServiceRegister
  participant etcd
  MCS Server->>utils.Register: GetLeaderLease()
  utils.Register->>discovery.ServiceRegister: Create registry entry with max lease TTL
  discovery.ServiceRegister->>etcd: Renew keepalive at capped retry interval
  discovery.ServiceRegister->>etcd: Deregister with default request timeout
Loading

Suggested labels: ok-to-test, lgtm, approved

Suggested reviewers: lhy1024, rleungx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR adds an unrelated keyspace allocation change in pkg/keyspace/tso_keyspace_group.go beyond the registry-lease scope. Move the keyspace allocation edits into a separate PR and keep this change limited to service registry lease handling.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: using the configured lease for the MCS service registry.
Description check ✅ Passed The description covers the problem, issue number, implementation, checklist, side effects, and release note.
Linked Issues check ✅ Passed Implements the requested lease-based registry TTL and service fallbacks for #11016.
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.

Comment thread pkg/mcs/router/server/server.go Outdated
// GetLeaderLease returns the configured leader lease in seconds. The router
// service has no lease config, so it returns 0 and the registry falls back to
// discovery.DefaultLeaseInSeconds.
func (s *Server) GetLeaderLease() int64 {

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: this receiver is unused, and revive reports unused-receiver, so the statics job fails. Please make it unnamed: func (*Server) GetLeaderLease() int64.

Comment thread pkg/mcs/utils/util.go
// Reuse the configured leader lease as the registry lease TTL, but never
// go below discovery.DefaultLeaseInSeconds to keep the registry entry
// stable.
lease := max(s.GetLeaderLease(), discovery.DefaultLeaseInSeconds)

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.

Using the leader TTL here also changes ServiceRegister.renewKeepalive, which waits ttl/2 before its first re-registration attempt. With lease = 60, an expired registry entry remains absent for another 30 seconds, potentially prolonging the healthy-primary eviction this change is intended to prevent. Please retry immediately or use a capped, TTL-independent backoff.

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.

With registry TTL = leaderLease/2, the renew delay stays proportional but bounded. For lease=60: registry=30s, retry interval=15s. The floor of 5s prevents excessive retry at small lease values.

The shorter registry TTL (half of leader lease) also means that when a healthy primary is evicted, the registry entry reappears within 15s of re-registration — much faster than the original unbounded design.

Comment thread pkg/mcs/utils/util.go
serviceRegister := discovery.NewServiceRegister(s.Context(), s.GetEtcdClient(),
serviceName, s.GetAdvertiseListenAddr(), serializedEntry,
discovery.DefaultLeaseInSeconds)
serviceName, s.GetAdvertiseListenAddr(), serializedEntry, lease)

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 value also becomes the timeout in ServiceRegister.Deregister. Since lease is unbounded and deregistration is synchronous in every server Close method, an unreachable etcd endpoint can make lease = 3600 block shutdown for up to an hour. Please use a fixed or capped deregistration timeout instead of the registry TTL.

@lhy1024 lhy1024 left a comment

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.

Requesting changes for the newly introduced service-discovery and shutdown semantics. The existing inline reviews already identify the blocking Router lint failure and the unbounded deregistration timeout; the additional inline comment covers delayed removal of failed non-primary instances.

Comment thread pkg/mcs/utils/util.go
// Reuse the configured leader lease as the registry lease TTL, but never
// go below discovery.DefaultLeaseInSeconds to keep the registry entry
// stable.
lease := max(s.GetLeaderLease(), discovery.DefaultLeaseInSeconds)

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 introduces another behavior change that is not covered by the PR description: the registry contains all service instances, not only the primary. Therefore, increasing the registry TTL also delays removal of crashed followers. For example, with lease = 60, a dead endpoint may remain in service discovery, the TSO node balancer, and keyspace-group allocation state for roughly 60 seconds instead of 5 seconds. I verified the underlying lease behavior with embedded etcd: after a registry key with a 2-second TTL expired, an otherwise identical key with a 5-second TTL was still present. Please confirm that this delayed failure detection is acceptable and add coverage/documentation for it, or decouple leader-election tuning from service-discovery health semantics.

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.

Thanks for addressing the retry interval and deregistration timeout. Could you clarify whether the longer failure-detection window is an intentional tradeoff here? I verified it on the latest head with a small embedded-etcd test using the actual ServiceRegister: two instances were registered with 2s and 5s TTLs, then their keepalives were canceled without calling Deregister() to model an abrupt process exit. Once the 2s endpoint disappeared, Discover() still returned the 5s endpoint; the test passed consistently. Since all instances register themselves, would it make sense to either document this side effect and add equivalent coverage, or decouple service-discovery failure detection from the leader lease?

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.

Registry TTL is now set to leaderLease/2 (floor 5s), decoupling service-discovery health from the leader lease. This means:

  • With lease=60: registry TTL = 30s, primary election TTL = 60s. The stale registry entry expires 30s before the primary lease, so when the allocator's primary-existence guard (new in allocNodesToAllKeyspaceGroups) triggers re-allocation, the dead follower is already removed from existMembers.
  • With default lease=5: registry stays at 5s (unchanged behavior).
  • The floor of 5s prevents the registry from flapping at small lease values.

Added comments in pkg/mcs/utils/util.go documenting the rationale.

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.

Not intentional — fixed by capping registry TTL to leaderLease/2.

This eliminates the failure-detection gap you demonstrated: the registry entry for a crashed follower now always expires before the primary election lease. The keyspace-group allocator additionally guards allocation decisions behind a primary-existence check (GetKeyspaceGroupPrimaryByID), so a missing primary triggers re-allocation only after the registry has already cleaned stale followers from existMembers.

See updated pkg/mcs/utils/util.go:303-309 for the comment documenting the decoupling.

@ti-chi-bot

ti-chi-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

[LGTM Timeline notifier]

Timeline:

  • 2026-07-17 05:47:05.575221286 +0000 UTC m=+951811.611316342: ✖️🔁 reset by lhy1024.

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)
pkg/mcs/discovery/register.go (1)

116-123: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add defensive safeguard against non-positive ticker intervals.

Although upstream configurations currently enforce a default minimum lease TTL, adding a safeguard against sr.ttl <= 0 here ensures that time.NewTicker() will never panic with a "non-positive interval" if an invalid or uninitialized TTL ever slips through.

🛠️ Proposed safeguard
 func (sr *ServiceRegister) keepAliveRetryInterval() time.Duration {
+	if sr.ttl <= 0 {
+		return maxKeepAliveRetryInterval
+	}
 	interval := time.Duration(sr.ttl) * time.Second / 2
 	if interval > maxKeepAliveRetryInterval {
 		return maxKeepAliveRetryInterval
 	}
 	return interval
 }
🤖 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 `@pkg/mcs/discovery/register.go` around lines 116 - 123, Update
ServiceRegister.keepAliveRetryInterval to handle sr.ttl <= 0 by returning a
positive fallback interval before calculating the TTL-based value, ensuring
callers such as time.NewTicker never receive a non-positive duration while
preserving the existing maximum-interval cap.
🤖 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 `@pkg/mcs/discovery/register.go`:
- Around line 116-123: Update ServiceRegister.keepAliveRetryInterval to handle
sr.ttl <= 0 by returning a positive fallback interval before calculating the
TTL-based value, ensuring callers such as time.NewTicker never receive a
non-positive duration while preserving the existing maximum-interval cap.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8da1a933-0fe8-42a0-a60b-96897d6162a0

📥 Commits

Reviewing files that changed from the base of the PR and between da75f8d and 9be2870.

📒 Files selected for processing (3)
  • pkg/mcs/discovery/register.go
  • pkg/mcs/discovery/register_test.go
  • pkg/mcs/router/server/server.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/router/server/server.go

@codecov

codecov Bot commented Jul 17, 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 (a4608a2).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master   #11017    +/-   ##
========================================
  Coverage   79.22%   79.23%            
========================================
  Files         541      542     +1     
  Lines       75965    76125   +160     
========================================
+ Hits        60187    60317   +130     
- Misses      11531    11552    +21     
- Partials     4247     4256     +9     
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.

@bufferflies
bufferflies requested review from lhy1024 and rleungx July 17, 2026 07:09
Comment thread pkg/mcs/utils/util.go
// Reuse the configured leader lease as the registry lease TTL, but never
// go below discovery.DefaultLeaseInSeconds to keep the registry entry
// stable.
lease := max(s.GetLeaderLease(), discovery.DefaultLeaseInSeconds)

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.

Using the leader lease here also extends the registry TTL for followers. A crashed high-priority TSO follower may remain discoverable and cause the healthy primary to transfer leadership to an offline node. Please keep follower discovery liveness independent from the leader lease.

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.

All nodes must campaign for leadership by themselves, so it can't happen that a transfer of primary leadership occurs to the offline node. Client observer health checker to pick down unhealthy nodes not only by the register table.

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.

Thanks for clarifying. I agree that an offline node cannot successfully campaign and become primary by itself. However, the problematic transition happens before campaigning: primaryPriorityCheckLoop treats the stale registry entry as alive, TransferPrimary selects that offline address, writes it into the expected-primary key, and then calls p.Resign() on the healthy primary. The other online members then skip campaigning because the expected-primary value does not match them. The client-side observer health checker does not filter the server-side kgm.tsoNodes map used by this path. Also, the expected-primary marker lives for 3 * leaderLease, so with lease = 60 this can block a free election for up to roughly 180 seconds.

I reproduced this deterministically on the current head (9be287016) with embedded etcd. The test waits beyond the old fixed 5-second registry TTL, confirms that the offline follower is still registered because of its longer TTL, and then verifies that TransferPrimary resigns the healthy primary and points expected-primary at the offline follower:

func TestStaleRegistryCanBecomeExpectedPrimary(t *testing.T) {
    re := require.New(t)
    _, client, clean := etcdutil.NewTestEtcdCluster(t, 1, nil)
    defer clean()

    keypath.SetClusterID(uint64(time.Now().UnixNano()))
    defer keypath.ResetClusterID()

    ctx := context.Background()
    const (
        currentName = "current-primary"
        currentAddr = "http://127.0.0.1:10001"
        offlineName = "offline-follower"
        offlineAddr = "http://127.0.0.1:10002"
    )
    register := func(registerCtx context.Context, name, addr string, ttl int64) *discovery.ServiceRegister {
        entry := &discovery.ServiceRegistryEntry{Name: name, ServiceAddr: addr}
        value, err := entry.Serialize()
        re.NoError(err)
        sr := discovery.NewServiceRegister(registerCtx, client, constant.TSOServiceName, addr, value, ttl)
        re.NoError(sr.Register())
        return sr
    }

    current := register(ctx, currentName, currentAddr, constant.DefaultLease)
    defer func() { re.NoError(current.Deregister()) }()
    offlineCtx, offlineCancel := context.WithCancel(ctx)
    register(offlineCtx, offlineName, offlineAddr, 12)
    offlineCancel() // abrupt exit: stop keepalive without deregistration

    time.Sleep(time.Duration(discovery.DefaultLeaseInSeconds)*time.Second + 500*time.Millisecond)
    entries, err := discovery.GetMSMembers(constant.TSOServiceName, client)
    re.NoError(err)
    re.Len(entries, 2) // the offline follower outlives the old 5-second TTL

    msParam := &keypath.MsParam{ServiceName: constant.TSOServiceName, GroupID: 0}
    participant := member.NewParticipant(client, *msParam)
    participant.InitInfo(&tsopb.Participant{
        Name: currentName, Id: 1, ListenUrls: []string{currentAddr},
    }, "current primary")
    re.NoError(participant.Campaign(ctx, constant.DefaultLease))
    participant.PromoteSelf()

    re.NoError(TransferPrimary(client, participant, constant.TSOServiceName,
        currentName, "", 0, map[string]bool{currentAddr: true, offlineAddr: true}))
    re.False(participant.IsServing())

    resp, err := client.Get(ctx, keypath.ExpectedPrimaryPath(msParam))
    re.NoError(err)
    re.Len(resp.Kvs, 1)
    re.Equal(offlineAddr, string(resp.Kvs[0].Value))
}

This test passes consistently. Could we keep follower discovery liveness independent from the leader lease, or validate the selected transfer target with a server-side health check before resigning the current primary?

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.

Decoupled: registry TTL is now set to leaderLease/2 (floor 5s, see pkg/mcs/utils/util.go). Registry entries always expire before the primary election lease, so when the allocator detects a missing primary and triggers re-allocation via the new primary-existence guard in allocNodesToAllKeyspaceGroups, stale nodes are already cleaned from existMembers.

With default lease=5, registry stays at 5s (no change). With lease=60, registry becomes 30s — half the election window, still acceptable for follower detection.

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 leaderLease/2 decoupling directly addresses the scenario you reproduced. With lease=60:

  • registry TTL = 30s (half of primary election TTL)
  • The stale offline follower (http://127.0.0.1:10002) expires from the registry 30s after crash
  • primaryPriorityCheckLoop no longer sees it as alive
  • TransferPrimary won't select it as a target

With default lease=5, registry stays at 5s (floor) — no regression from current behavior.

The allocNodesToAllKeyspaceGroups goroutine now also guards re-allocation behind a primary-existence check for non-default groups, so allocation decisions are gated on the actual primary state rather than just the registry.

@bufferflies
bufferflies requested a review from rleungx July 24, 2026 02:09
Signed-off-by: bufferflies <1045931706@qq.com>
@ti-chi-bot

ti-chi-bot Bot commented Jul 24, 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 a4608a2 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. do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcs: use the configured lease for the service registry instead of a fixed value

3 participants