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();
}
}