This is an automated email from the ASF dual-hosted git repository.
hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 0b290b00c ci(csharp): auto-retry flaky integration tests (#3160)
0b290b00c is described below
commit 0b290b00c5b1469fb3f776682901dfbd816da285
Author: Krishna Vishal <[email protected]>
AuthorDate: Fri Apr 24 15:22:45 2026 +0530
ci(csharp): auto-retry flaky integration tests (#3160)
---
.github/actions/csharp-dotnet/pre-merge/action.yml | 2 +-
foreign/csharp/Directory.Packages.props | 1 +
.../Fixtures/IggyClusterFixture.cs | 70 +++++++++++++++-------
.../Iggy_SDK.Tests.Integration.csproj | 1 +
4 files changed, 52 insertions(+), 22 deletions(-)
diff --git a/.github/actions/csharp-dotnet/pre-merge/action.yml
b/.github/actions/csharp-dotnet/pre-merge/action.yml
index 2e7272503..ac50a072b 100644
--- a/.github/actions/csharp-dotnet/pre-merge/action.yml
+++ b/.github/actions/csharp-dotnet/pre-merge/action.yml
@@ -98,7 +98,7 @@ runs:
--coverage-output-format cobertura \
--coverage-output coverage.cobertura.xml \
--results-directory ./reports \
- -- --report-trx
+ -- --report-trx --retry-failed-tests 3
shell: bash
- name: Collect container logs
diff --git a/foreign/csharp/Directory.Packages.props
b/foreign/csharp/Directory.Packages.props
index 0d927b969..b2dee5c2a 100644
--- a/foreign/csharp/Directory.Packages.props
+++ b/foreign/csharp/Directory.Packages.props
@@ -10,6 +10,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub"
Version="10.0.102" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage"
Version="18.3.2" />
+ <PackageVersion Include="Microsoft.Testing.Extensions.Retry"
Version="2.2.1" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport"
Version="2.0.2" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Reqnroll.xunit.v3" Version="3.3.3" />
diff --git
a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyClusterFixture.cs
b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyClusterFixture.cs
index 5e6f8eabe..73c33edea 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyClusterFixture.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyClusterFixture.cs
@@ -29,10 +29,21 @@ public class IggyClusterFixture : IAsyncInitializer,
IAsyncDisposable
private const string LeaderAlias = "iggy-leader";
private const string FollowerAlias = "iggy-follower";
- // TcpListeners held open until just before the containers are started so
that the
- // OS keeps the chosen ports reserved across the whole fixture setup.
Parallel test
- // hosts (net8.0 + net10.0) would otherwise race on the gap between
picking the port
- // and docker binding it.
+ // Split the host-port pool by .NET major version so parallel `dotnet test`
+ // processes (net8.0 + net10.0) can never pick the same port and race
docker's
+ // allocator. The ranges sit below Linux's default ephemeral range
(32768+), so
+ // the kernel's auto-allocation won't steal from us either.
+ // net8.0 - 30800..30899
+ // net10.0 - 31000..31099
+ // Future TFMs slot in without overlap (e.g. net12.0 - 31200..31299).
+ private const ushort PortRangeSize = 100;
+ private static readonly ushort BasePort = (ushort)(30000 +
Environment.Version.Major * 100);
+ private static readonly ushort EndPort = (ushort)(BasePort +
PortRangeSize);
+
+ // Listeners only need to outlive the eight ReservePort() calls in the
+ // constructor so we don't pick the same port twice within one fixture.
+ // Partitioned ranges already guarantee sibling processes can't race us, so
+ // we can release them as soon as picking is done.
private readonly List<TcpListener> _portReservations = [];
private readonly IContainer _followerContainer;
private readonly ushort _followerHttpPort;
@@ -57,14 +68,21 @@ public class IggyClusterFixture : IAsyncInitializer,
IAsyncDisposable
public IggyClusterFixture()
{
- _leaderTcpPort = ReservePort();
- _leaderHttpPort = ReservePort();
- _leaderQuicPort = ReservePort();
- _leaderWsPort = ReservePort();
- _followerTcpPort = ReservePort();
- _followerHttpPort = ReservePort();
- _followerQuicPort = ReservePort();
- _followerWsPort = ReservePort();
+ try
+ {
+ _leaderTcpPort = ReservePort();
+ _leaderHttpPort = ReservePort();
+ _leaderQuicPort = ReservePort();
+ _leaderWsPort = ReservePort();
+ _followerTcpPort = ReservePort();
+ _followerHttpPort = ReservePort();
+ _followerQuicPort = ReservePort();
+ _followerWsPort = ReservePort();
+ }
+ finally
+ {
+ ReleaseReservedPorts();
+ }
_network = new NetworkBuilder()
.WithName($"iggy-cluster-{Guid.NewGuid():N}")
@@ -140,7 +158,6 @@ public class IggyClusterFixture : IAsyncInitializer,
IAsyncDisposable
public async ValueTask DisposeAsync()
{
- ReleaseReservedPorts();
await SaveContainerLogsAsync(_leaderContainer, "leader");
await SaveContainerLogsAsync(_followerContainer, "follower");
await _followerContainer.StopAsync();
@@ -151,10 +168,6 @@ public class IggyClusterFixture : IAsyncInitializer,
IAsyncDisposable
public async Task InitializeAsync()
{
await _network.CreateAsync();
- // Release the reservations at the last possible moment so the window
between
- // giving the port back to the OS and docker re-binding it is as small
as we can
- // make it.
- ReleaseReservedPorts();
await Task.WhenAll(_leaderContainer.StartAsync(),
_followerContainer.StartAsync());
}
@@ -170,10 +183,25 @@ public class IggyClusterFixture : IAsyncInitializer,
IAsyncDisposable
private ushort ReservePort()
{
- var listener = new TcpListener(IPAddress.Loopback, 0);
- listener.Start();
- _portReservations.Add(listener);
- return (ushort)((IPEndPoint)listener.LocalEndpoint).Port;
+ for (ushort candidate = BasePort; candidate < EndPort; candidate++)
+ {
+ try
+ {
+ var listener = new TcpListener(IPAddress.Loopback, candidate);
+ listener.Start();
+ _portReservations.Add(listener);
+ return candidate;
+ }
+ catch (SocketException)
+ {
+ // Port is held by a previous ReservePort() in this fixture
+ // (the common case) or by something else on the host; keep
+ // walking the range.
+ }
+ }
+
+ throw new InvalidOperationException(
+ $"No free ports available in [{BasePort}, {EndPort}) for .NET
{Environment.Version.Major}.x.");
}
private void ReleaseReservedPorts()
diff --git
a/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
b/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
index 9c8b3cf1e..1b229b82c 100644
---
a/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
+++
b/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
@@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage"
/>
+ <PackageReference Include="Microsoft.Testing.Extensions.Retry" />
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
<PackageReference Include="Shouldly" />
<PackageReference Include="Testcontainers" />