Skip to content
Merged
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
57 changes: 55 additions & 2 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ public async Task RunApplicationAsync(CancellationToken ct = default)
containers = _containerCreator.PrepareObjects().ToArray();
_containerCreator.PrepareContainerExecutables();
executables = _executableCreator.PrepareObjects(ct).ToArray();
AllocateExecutableTargetPorts(executables);

prepareResourcesActivity.SetDcpPreparedResourceCounts(containers.Length, executables.Length);
}
Expand Down Expand Up @@ -672,6 +673,13 @@ private void PrepareServices()
{
_proxylessEndpointPortAllocator.ExcludePort(persistedPort);
}

if (sp.ModelResource is IComputeResource &&
!sp.ModelResource.IsContainer() &&
EndpointAnnotation.NormalizePort(endpoint.TargetPort) is int fixedTargetPort)
{
_proxylessEndpointPortAllocator.ExcludePort(fixedTargetPort);
}
}
}

Expand Down Expand Up @@ -764,6 +772,49 @@ private static bool GetEffectiveIsProxied(IResource resource, EndpointAnnotation
return !resource.HasPersistentLifetime();
}

private void AllocateExecutableTargetPorts(IEnumerable<RenderedModelResource<Executable>> executables)
{
// Allocate per rendered executable so replicas receive distinct target ports.
foreach (var executable in executables)
{
if (!executable.DcpResource.TryGetAnnotationAsObjectList<ServiceProducerAnnotation>(
CustomResource.ServiceProducerAnnotation,
out var serviceProducerAnnotations))
{
continue;
}

var annotationsByServiceName = serviceProducerAnnotations.ToDictionary(a => a.ServiceName, StringComparer.Ordinal);
var annotationsChanged = false;

foreach (var serviceProducer in executable.ServicesProduced)
{
var endpoint = serviceProducer.EndpointAnnotation;
if (!endpoint.IsProxied ||
EndpointAnnotation.NormalizePort(endpoint.TargetPort) is not null)
{
continue;
}

var annotation = annotationsByServiceName[serviceProducer.Service.Metadata.Name];

// DCP's dynamic producer-port allocation probes an ephemeral port, releases it, and
// later passes it to the child process. Allocate from Aspire's non-ephemeral range
// instead so unrelated outbound connections cannot claim the port during that gap.
annotation.Port = _proxylessEndpointPortAllocator.AllocatePort(endpoint.Protocol);
annotationsChanged = true;
}

if (annotationsChanged)
{
// Annotation lists are deserialized copies, so persist the allocated ports back to the DCP resource.
executable.DcpResource.SetAnnotationAsObjectList(
CustomResource.ServiceProducerAnnotation,
serviceProducerAnnotations);
}
}
}

/// <summary>
/// Determines whether an endpoint definition has a fixed public port DCP should reserve or pre-exclude.
/// </summary>
Expand Down Expand Up @@ -817,7 +868,7 @@ private void EnsureProxylessEndpointPort(IResource resource, EndpointAnnotation
publicPort = _proxylessEndpointPortAllocator.AllocatePort(endpoint);
_logger.LogDebug("Allocated public port {Port} for proxyless endpoint '{EndpointName}' on resource '{ResourceName}'.", publicPort, endpoint.Name, resource.Name);

if (resource.HasPersistentLifetime())
if (resource.HasPersistentLifetime() && !_options.Value.RandomizePorts)
{
var secretKey = GetPersistedProxylessEndpointPortKey(resource, endpoint);
if (!_userSecretsManager.TrySetSecret(secretKey, publicPort.ToString(CultureInfo.InvariantCulture)))
Expand Down Expand Up @@ -845,7 +896,9 @@ private static bool NeedsPublicPort(IResource resource, EndpointAnnotation endpo

private int? TryGetPersistedProxylessEndpointPort(IResource resource, EndpointAnnotation endpoint)
{
if (!resource.HasPersistentLifetime() || !NeedsPublicPort(resource, endpoint))
if (_options.Value.RandomizePorts ||
!resource.HasPersistentLifetime() ||
!NeedsPublicPort(resource, endpoint))
{
return null;
}
Expand Down
6 changes: 4 additions & 2 deletions src/Aspire.Hosting/Dcp/DcpOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ internal sealed class DcpOptions
public bool RandomizePorts { get; set; }

/// <summary>
/// The first port in the range used to allocate unspecified public ports for proxyless endpoints.
/// The first port in the range used to allocate unspecified public ports for proxyless endpoints
/// and target ports for proxied executable endpoints.
/// </summary>
public int ProxylessEndpointPortRangeStart { get; set; } = 10000;

/// <summary>
/// The last port in the range used to allocate unspecified public ports for proxyless endpoints.
/// The last port in the range used to allocate unspecified public ports for proxyless endpoints
/// and target ports for proxied executable endpoints.
/// </summary>
/// <remarks>
/// The default leaves room for Aspire to persist stable allocated ports in the future while staying
Expand Down
12 changes: 11 additions & 1 deletion src/Aspire.Hosting/Dcp/ProxylessEndpointPortAllocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Aspire.Hosting.Dcp;

/// <summary>
/// Allocates and tracks public ports for proxyless endpoints that do not specify one.
/// Allocates and tracks non-ephemeral ports that Aspire selects before DCP starts a workload.
/// </summary>
/// <remarks>
/// Uses a stateful hybrid scan over the configured non-ephemeral port range. The allocator starts
Expand Down Expand Up @@ -100,6 +100,16 @@ public int AllocatePort(EndpointAnnotation endpoint)
}
}

public int AllocatePort(ProtocolType protocol)
{
lock (_lock)
{
ObjectDisposedException.ThrowIf(_disposed, this);

return AllocatePortCore(protocol);
}
}

public void ExcludePort(int port)
{
lock (_lock)
Expand Down
Loading
Loading