Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/Dapr.Testcontainers/Common/PortUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,63 @@
// limitations under the License.
// ------------------------------------------------------------------------

using System;
using System.Net;
using System.Net.Sockets;

namespace Dapr.Testcontainers.Common;

/// <summary>
/// Represents a temporary reservation for a TCP port.
/// </summary>
public sealed class PortReservation : IDisposable
{
private Socket? _socket;

internal PortReservation(Socket socket)
{
_socket = socket;
Port = ((IPEndPoint)socket.LocalEndPoint!).Port;
}

/// <summary>
/// The reserved port number.
/// </summary>
public int Port { get; }

/// <inheritdoc />
public void Dispose()
{
_socket?.Dispose();
_socket = null;
}
}

/// <summary>
/// Provides port-related utilities.
/// </summary>
public static class PortUtilities
{
/// <summary>
/// Reserves an available TCP port until the returned reservation is disposed.
/// </summary>
/// <returns>A <see cref="PortReservation"/> representing the reserved port.</returns>
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);
}

/// <summary>
/// Gets an available TCP port from the OS. This is a best-effort snapshot
/// and does not reserve the port for later use.
/// </summary>
/// <returns>The available port number.</returns>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,25 @@ public async Task<DaprTestApplication> 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);

Expand All @@ -142,6 +153,12 @@ public async Task<DaprTestApplication> 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);
Expand All @@ -162,7 +179,12 @@ public async Task<DaprTestApplication> BuildAndStartAsync()
}
}

// Try again with a frest set of ports
// Try again with a fresh set of ports
}
finally
{
httpReservation?.Dispose();
grpcReservation?.Dispose();
}
}

Expand Down
Loading