From 12ab626d860bdac5c5e412b2c06a21d5b7be43a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:19:39 +0000 Subject: [PATCH 1/4] Initial plan From 1ae4070800cf9f3a71e5fc19a7dcbfdaf77e38e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:41:05 +0000 Subject: [PATCH 2/4] Fix LeaderElector to use actual lease duration from Kubernetes object Changed LeaderElector.TryAcquireOrRenew to use the lease duration from the Kubernetes object (oldLeaderElectionRecord.LeaseDurationSeconds) instead of the configured lease duration (config.LeaseDuration). This fixes the issue where graceful step-downs (where leaders set lease duration to 1 second) were being ignored by C# clients, which continued to wait for the full configured lease duration before attempting to acquire leadership. This brings the C# implementation in line with the Go client-go library behavior. Fixes kubernetes-client/csharp#1819 Co-authored-by: tg123 <170430+tg123@users.noreply.github.com> --- src/KubernetesClient/LeaderElection/LeaderElector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KubernetesClient/LeaderElection/LeaderElector.cs b/src/KubernetesClient/LeaderElection/LeaderElector.cs index e7d86f9af..0ecda26b1 100644 --- a/src/KubernetesClient/LeaderElection/LeaderElector.cs +++ b/src/KubernetesClient/LeaderElection/LeaderElector.cs @@ -192,7 +192,7 @@ private async Task TryAcquireOrRenew(CancellationToken cancellationToken) } if (!string.IsNullOrEmpty(oldLeaderElectionRecord.HolderIdentity) - && observedTime + config.LeaseDuration > DateTimeOffset.Now + && observedTime + TimeSpan.FromSeconds(oldLeaderElectionRecord.LeaseDurationSeconds) > DateTimeOffset.Now && !IsLeader()) { // lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity From 826586fd9dc1065a44cccd1408ed5df85f0c11ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:23:20 +0000 Subject: [PATCH 3/4] Add unit test for actual lease duration fix Added LeaderElectionUsesActualLeaseDurationFromKubernetesObject test to verify that the leader election logic correctly uses the actual lease duration from the Kubernetes object (LeaseDurationSeconds) instead of the configured lease duration. This test specifically validates the graceful step-down scenario where a leader sets the lease duration to 1 second to signal it has stepped down, ensuring that other clients respect this short duration rather than waiting for the full configured duration. Co-authored-by: tg123 <170430+tg123@users.noreply.github.com> --- .../LeaderElection/LeaderElectionTests.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs b/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs index 7422dd047..52343ad39 100644 --- a/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs +++ b/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs @@ -409,6 +409,76 @@ public void LeaderElectionShouldReportLeaderItAcquiresOnStart() Assert.True(notifications.SequenceEqual(new[] { "foo1" })); } + [Fact] + public void LeaderElectionUsesActualLeaseDurationFromKubernetesObject() + { + // This test validates that the actual lease duration from the Kubernetes object + // is used instead of the configured lease duration when checking if a lease has expired. + // This is critical for graceful step-downs where a leader sets lease duration to 1 second. + + var l = new Mock(); + l.Setup(obj => obj.Identity).Returns("client1"); + + var firstCallTime = DateTime.UtcNow; + var callCount = 0; + + l.Setup(obj => obj.GetAsync(It.IsAny())) + .ReturnsAsync(() => + { + callCount++; + // Return a lease held by another client with a short 1-second duration (graceful step-down) + return new LeaderElectionRecord() + { + HolderIdentity = "client2", + AcquireTime = firstCallTime.AddSeconds(-5), + RenewTime = firstCallTime, + LeaderTransitions = 1, + LeaseDurationSeconds = 1, // Actual lease duration is 1 second (not the configured 10 seconds) + }; + }); + + var updateCalled = false; + l.Setup(obj => obj.UpdateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => + { + updateCalled = true; + return true; + }); + + var leaderElector = new LeaderElector(new LeaderElectionConfig(l.Object) + { + LeaseDuration = TimeSpan.FromSeconds(10), // Configured for 10 seconds + RetryPeriod = TimeSpan.FromMilliseconds(200), + RenewDeadline = TimeSpan.FromSeconds(9), + }); + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + + // Run the leader election + var task = Task.Run(async () => + { + try + { + await leaderElector.RunUntilLeadershipLostAsync(cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected when timeout occurs + } + }); + + // Wait for the task to complete or timeout + task.Wait(TimeSpan.FromSeconds(4)); + + // The key assertion: With the fix, the lease should be recognized as expired after ~1 second + // (the actual lease duration from the K8s object), not after 10 seconds (the configured duration). + // Therefore, UpdateAsync should have been called to attempt to acquire leadership. + Assert.True(updateCalled, + "UpdateAsync should have been called after the actual lease duration (1 second) expired, " + + "not after the configured lease duration (10 seconds). This test validates that the fix " + + "correctly uses oldLeaderElectionRecord.LeaseDurationSeconds instead of config.LeaseDuration."); + } + private class MockResourceLock : ILock { private static LeaderElectionRecord leaderRecord; From 1024833f5abe0dccd5a6f50dd7885a1fb0ffc34c Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Sun, 28 Dec 2025 01:24:03 -0800 Subject: [PATCH 4/4] Update tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../LeaderElection/LeaderElectionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs b/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs index 52343ad39..3d2adb2ec 100644 --- a/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs +++ b/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs @@ -452,7 +452,7 @@ public void LeaderElectionUsesActualLeaseDurationFromKubernetesObject() RenewDeadline = TimeSpan.FromSeconds(9), }); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); // Run the leader election var task = Task.Run(async () =>