Skip to content

Commit a74c7a1

Browse files
committed
Fix flaky test with proper synchronization using internal method
1 parent 2145c70 commit a74c7a1

3 files changed

Lines changed: 24 additions & 8 deletions

File tree

src/MultiLock/LeaderElectionService.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -680,8 +680,7 @@ private async Task BroadcastEventsAsync()
680680
{
681681
// Read until the channel is completed (not until cancelled)
682682
// This ensures all pending events are broadcast during graceful shutdown
683-
// Note: ReadAllAsync() completes gracefully when the channel writer is completed,
684-
// so no try-catch is needed for normal operation
683+
// Note: ReadAllAsync() completes gracefully when the channel writer is completed
685684
await foreach (LeadershipChangedEventArgs eventArgs in broadcastChannel.Reader.ReadAllAsync().ConfigureAwait(false))
686685
{
687686
// Broadcast to all subscriber channels
@@ -705,8 +704,17 @@ private async Task BroadcastEventsAsync()
705704
/// An async enumerable that yields leadership change events as they occur.
706705
/// The enumeration will continue until the service is disposed or the cancellation token is triggered.
707706
/// </returns>
708-
public async IAsyncEnumerable<LeadershipChangedEventArgs> GetLeadershipChangesAsync(
709-
[EnumeratorCancellation] CancellationToken cancellationToken = default)
707+
public IAsyncEnumerable<LeadershipChangedEventArgs> GetLeadershipChangesAsync(
708+
CancellationToken cancellationToken = default)
709+
=> GetLeadershipChangesAsyncCore(cancellationToken, subscriberRegistered: null);
710+
711+
/// <summary>
712+
/// Internal overload that signals when the subscriber is registered.
713+
/// Used by tests to avoid race conditions.
714+
/// </summary>
715+
internal async IAsyncEnumerable<LeadershipChangedEventArgs> GetLeadershipChangesAsyncCore(
716+
[EnumeratorCancellation] CancellationToken cancellationToken,
717+
TaskCompletionSource? subscriberRegistered)
710718
{
711719
ThrowIfDisposed();
712720

@@ -723,6 +731,9 @@ public async IAsyncEnumerable<LeadershipChangedEventArgs> GetLeadershipChangesAs
723731
subscriberChannels.Add(subscriberChannel);
724732
}
725733

734+
// Signal that the subscriber is now registered
735+
subscriberRegistered?.TrySetResult();
736+
726737
try
727738
{
728739
// Read from the subscriber channel

src/MultiLock/MultiLock.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
<PackageReference Include="Microsoft.Extensions.Options" />
1414
</ItemGroup>
1515

16+
<ItemGroup>
17+
<InternalsVisibleTo Include="MultiLock.Tests" />
18+
</ItemGroup>
19+
1620
</Project>

tests/MultiLock.Tests/AsyncEnumerableApiTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,16 +324,17 @@ public async Task GetLeadershipChangesAsync_CancellationToken_ShouldStopEnumerat
324324
{
325325
// Arrange
326326
ServiceProvider services = TestHelpers.CreateLeaderElectionService("test-participant");
327-
ILeaderElectionService service = services.GetRequiredService<ILeaderElectionService>();
327+
var service = (LeaderElectionService)services.GetRequiredService<ILeaderElectionService>();
328328
var events = new List<LeadershipChangedEventArgs>();
329329
object eventsLock = new();
330330
var cts = new CancellationTokenSource();
331+
var subscriberRegistered = new TaskCompletionSource();
331332

332333
// Act - Start listening BEFORE starting service
333334
CancellationToken cancellationToken = cts.Token;
334335
var eventTask = Task.Run(async () =>
335336
{
336-
await foreach (LeadershipChangedEventArgs e in service.GetLeadershipChangesAsync(cancellationToken))
337+
await foreach (LeadershipChangedEventArgs e in service.GetLeadershipChangesAsyncCore(cancellationToken, subscriberRegistered))
337338
{
338339
lock (eventsLock)
339340
{
@@ -342,8 +343,8 @@ public async Task GetLeadershipChangesAsync_CancellationToken_ShouldStopEnumerat
342343
}
343344
}, cancellationToken);
344345

345-
// Give the event listener task a chance to start
346-
await Task.Delay(TimeSpan.FromMilliseconds(50), cts.Token);
346+
// Wait for the subscriber to be registered (no race condition)
347+
await subscriberRegistered.Task;
347348

348349
await service.StartAsync(cts.Token);
349350
await service.WaitForLeadershipAsync(cts.Token);

0 commit comments

Comments
 (0)