-
-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Leader election failure to restart (#783)
This fixes the following behavioral issues noted when testing a leader-aware operator with transient network issues: - In `LeaderElectionBackgroundService`, if `elector.RunUntilLeadershipLostAsync()` throws, the exception is not observed in the library and no further attempts to become the leader occur. The library now logs any unexpected exceptions and tries to become the leader again. - A leader could not stop and then subsequently start being a leader once more due to cancellation token sources not being recreated. The library now disposes and recreates the cancellation token sources as required. - `LeaderAwareResourceWatcher<TEntity>.StoppedLeading` would erroneously pass a cancelled cancellation token to `ResourceWatcher<TEntity>`. The library now passes the `IHostApplicationLifetime.ApplicationStopped` token to the `ResourceWatcher<TEntity>` - we can assume that `ApplicationStopped` is a good indication that the stop should no longer be graceful.
- Loading branch information
Showing
6 changed files
with
174 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
test/KubeOps.Operator.Test/LeaderElector/LeaderElectionBackgroundService.Test.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
using FluentAssertions; | ||
|
||
using k8s.LeaderElection; | ||
|
||
using KubeOps.Operator.LeaderElection; | ||
|
||
using Microsoft.Extensions.Logging; | ||
|
||
using Moq; | ||
|
||
namespace KubeOps.Operator.Test.LeaderElector; | ||
|
||
public sealed class LeaderElectionBackgroundServiceTest | ||
{ | ||
[Fact] | ||
public async Task Elector_Throws_Should_Retry() | ||
{ | ||
// Arrange. | ||
var logger = Mock.Of<ILogger<LeaderElectionBackgroundService>>(); | ||
|
||
var electionLock = Mock.Of<ILock>(); | ||
|
||
var electionLockSubsequentCallEvent = new AutoResetEvent(false); | ||
bool hasElectionLockThrown = false; | ||
Mock.Get(electionLock) | ||
.Setup(electionLock => electionLock.GetAsync(It.IsAny<CancellationToken>())) | ||
.Returns<CancellationToken>( | ||
async cancellationToken => | ||
{ | ||
if (hasElectionLockThrown) | ||
{ | ||
// Signal to the test that a subsequent call has been made. | ||
electionLockSubsequentCallEvent.Set(); | ||
|
||
// Delay returning for a long time, allowing the test to stop the background service, in turn cancelling the cancellation token. | ||
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); | ||
throw new InvalidOperationException(); | ||
} | ||
|
||
hasElectionLockThrown = true; | ||
throw new Exception("Unit test exception"); | ||
}); | ||
|
||
var leaderElectionConfig = new LeaderElectionConfig(electionLock); | ||
var leaderElector = new k8s.LeaderElection.LeaderElector(leaderElectionConfig); | ||
|
||
var leaderElectionBackgroundService = new LeaderElectionBackgroundService(logger, leaderElector); | ||
|
||
// Act / Assert. | ||
await leaderElectionBackgroundService.StartAsync(CancellationToken.None); | ||
|
||
// Starting the background service should result in the lock attempt throwing, and then a subsequent attempt being made. | ||
// Wait for the subsequent event to be signalled, if we time out the test fails. | ||
electionLockSubsequentCallEvent.WaitOne(TimeSpan.FromMilliseconds(500)).Should().BeTrue(); | ||
|
||
await leaderElectionBackgroundService.StopAsync(CancellationToken.None); | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
test/KubeOps.Operator.Test/Watcher/ResourceWatcher{TEntity}.Test.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using System.Runtime.CompilerServices; | ||
|
||
using k8s; | ||
using k8s.Models; | ||
|
||
using KubeOps.Abstractions.Builder; | ||
using KubeOps.KubernetesClient; | ||
using KubeOps.Operator.Queue; | ||
using KubeOps.Operator.Watcher; | ||
|
||
using Microsoft.Extensions.Logging; | ||
|
||
using Moq; | ||
|
||
namespace KubeOps.Operator.Test.Watcher; | ||
|
||
public sealed class ResourceWatcherTest | ||
{ | ||
[Fact] | ||
public async Task Restarting_Watcher_Should_Trigger_New_Watch() | ||
{ | ||
// Arrange. | ||
var logger = Mock.Of<ILogger<ResourceWatcher<V1Pod>>>(); | ||
var serviceProvider = Mock.Of<IServiceProvider>(); | ||
var timedEntityQueue = new TimedEntityQueue<V1Pod>(); | ||
var operatorSettings = new OperatorSettings() { Namespace = "unit-test" }; | ||
var kubernetesClient = Mock.Of<IKubernetesClient>(); | ||
|
||
Mock.Get(kubernetesClient) | ||
.Setup(client => client.WatchAsync<V1Pod>("unit-test", null, null, true, It.IsAny<CancellationToken>())) | ||
.Returns<string?, string?, string?, bool?, CancellationToken>((_, _, _, _, cancellationToken) => WaitForCancellationAsync<(WatchEventType, V1Pod)>(cancellationToken)); | ||
|
||
var resourceWatcher = new ResourceWatcher<V1Pod>(logger, serviceProvider, timedEntityQueue, operatorSettings, kubernetesClient); | ||
|
||
// Act. | ||
// Start and stop the watcher. | ||
await resourceWatcher.StartAsync(CancellationToken.None); | ||
await resourceWatcher.StopAsync(CancellationToken.None); | ||
|
||
// Restart the watcher. | ||
await resourceWatcher.StartAsync(CancellationToken.None); | ||
|
||
// Assert. | ||
Mock.Get(kubernetesClient) | ||
.Verify(client => client.WatchAsync<V1Pod>("unit-test", null, null, true, It.IsAny<CancellationToken>()), Times.Exactly(2)); | ||
} | ||
|
||
private static async IAsyncEnumerable<T> WaitForCancellationAsync<T>([EnumeratorCancellation] CancellationToken cancellationToken) | ||
{ | ||
await Task.Delay(Timeout.Infinite, cancellationToken); | ||
yield return default!; | ||
} | ||
} |