diff --git a/src/Dapr.Testcontainers/Common/PortUtilities.cs b/src/Dapr.Testcontainers/Common/PortUtilities.cs index f479eea2f..a4cd898e8 100644 --- a/src/Dapr.Testcontainers/Common/PortUtilities.cs +++ b/src/Dapr.Testcontainers/Common/PortUtilities.cs @@ -11,16 +11,55 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; using System.Net; using System.Net.Sockets; namespace Dapr.Testcontainers.Common; +/// +/// Represents a temporary reservation for a TCP port. +/// +public sealed class PortReservation : IDisposable +{ + private Socket? _socket; + + internal PortReservation(Socket socket) + { + _socket = socket; + Port = ((IPEndPoint)socket.LocalEndPoint!).Port; + } + + /// + /// The reserved port number. + /// + public int Port { get; } + + /// + public void Dispose() + { + _socket?.Dispose(); + _socket = null; + } +} + /// /// Provides port-related utilities. /// public static class PortUtilities { + /// + /// Reserves an available TCP port until the returned reservation is disposed. + /// + /// A representing the reserved port. + public static PortReservation ReserveTcpPort() + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + return new PortReservation(socket); + } + /// /// Gets an available TCP port from the OS. This is a best-effort snapshot /// and does not reserve the port for later use. @@ -28,8 +67,7 @@ public static class PortUtilities /// The available port number. public static int GetAvailablePort() { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(new IPEndPoint(IPAddress.Any, 0)); - return ((IPEndPoint)socket.LocalEndPoint!).Port; + using var reservation = ReserveTcpPort(); + return reservation.Port; } } diff --git a/src/Dapr.Testcontainers/Common/Testing/DaprTestApplicationBuilder.cs b/src/Dapr.Testcontainers/Common/Testing/DaprTestApplicationBuilder.cs index 4cb574e1f..7d34c551c 100644 --- a/src/Dapr.Testcontainers/Common/Testing/DaprTestApplicationBuilder.cs +++ b/src/Dapr.Testcontainers/Common/Testing/DaprTestApplicationBuilder.cs @@ -123,14 +123,25 @@ public async Task BuildAndStartAsync() for (var attempt = 1; attempt <= maxAttempts; attempt++) { WebApplication? attemptApp = null; + PortReservation? httpReservation = null; + PortReservation? grpcReservation = null; try { - // Pre-assign prots for the app knows where Dapr will be (avoid collisions) - var httpPort = PortUtilities.GetAvailablePort(); - var grpcPort = PortUtilities.GetAvailablePort(); - while (grpcPort == httpPort) - grpcPort = PortUtilities.GetAvailablePort(); + // Pre-assign ports so the app knows where Dapr will be (avoid collisions) + httpReservation = PortUtilities.ReserveTcpPort(); + do + { + grpcReservation = PortUtilities.ReserveTcpPort(); + if (grpcReservation.Port == httpReservation.Port) + { + grpcReservation.Dispose(); + grpcReservation = null; + } + } while (grpcReservation is null); + + var httpPort = httpReservation.Port; + var grpcPort = grpcReservation.Port; harness.SetPorts(httpPort, grpcPort); @@ -142,6 +153,12 @@ public async Task BuildAndStartAsync() harness.SetAppPort(GetBoundPort(attemptApp)); } + // Release port reservations just before daprd starts to minimize collisions. + httpReservation.Dispose(); + grpcReservation.Dispose(); + httpReservation = null; + grpcReservation = null; + await harness.InitializeAsync(); return new DaprTestApplication(harness, attemptApp); @@ -162,7 +179,12 @@ public async Task BuildAndStartAsync() } } - // Try again with a frest set of ports + // Try again with a fresh set of ports + } + finally + { + httpReservation?.Dispose(); + grpcReservation?.Dispose(); } }