| File: Dcp\PersistentPortAllocator.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Dcp; internal sealed class PersistentPortAllocator { private readonly object _lock = new(); private readonly DistributedApplicationModel _model; private readonly IConfiguration _configuration; private readonly DcpOptions _options; private readonly ProxylessEndpointPortAllocator _allocator; private readonly PersistentPortAllocationStore _store; private readonly ILogger<PersistentPortAllocator> _logger; private readonly Dictionary<PersistentPortAllocationKey, int> _reservedPorts = new(); private bool _initialized; public PersistentPortAllocator( DistributedApplicationModel model, IConfiguration configuration, IOptions<DcpOptions> options, ProxylessEndpointPortAllocator allocator, PersistentPortAllocationStore store, ILogger<PersistentPortAllocator> logger) { _model = model; _configuration = configuration; _options = options.Value; _allocator = allocator; _store = store; _logger = logger; } public bool PersistenceEnabled => _store.IsAvailable && (!_options.RandomizePorts || _options.PersistPortsInIsolatedMode); public int GetOrAllocatePersistentPort(PersistentPortAllocationKey key, ProtocolType protocol) { lock (_lock) { EnsureInitialized(); if (PersistenceEnabled && _reservedPorts.TryGetValue(key, out var reservedPort)) { return reservedPort; } var allocatedPort = _allocator.AllocatePort(protocol); if (PersistenceEnabled) { _reservedPorts[key] = allocatedPort; _store.SetPort(key, allocatedPort); } return allocatedPort; } } public int AllocateTransientPort(ProtocolType protocol) { lock (_lock) { EnsureInitialized(); return _allocator.AllocatePort(protocol); } } private int? GetLegacyPort(PersistentPortAllocationKey key) { var legacyConfigurationKey = GetLegacyConfigurationKey(key); var configuredPort = _configuration[legacyConfigurationKey]; if (configuredPort is null) { return null; } if (int.TryParse(configuredPort, NumberStyles.None, CultureInfo.InvariantCulture, out var port) && PortRange.IsValidPort(port)) { if (port >= _options.ProxylessEndpointPortRangeStart && port <= _options.ProxylessEndpointPortRangeEnd) { return port; } _logger.LogDebug( "Ignoring legacy persisted port {Port} for {PortKey} because it is outside the configured allocation range {RangeStart}-{RangeEnd}.", port, legacyConfigurationKey, _options.ProxylessEndpointPortRangeStart, _options.ProxylessEndpointPortRangeEnd); return null; } _logger.LogDebug("Ignoring invalid legacy persisted port value '{Port}' for {PortKey}.", configuredPort, legacyConfigurationKey); return null; } private static string GetLegacyConfigurationKey(PersistentPortAllocationKey key) { return $"Resources:{key.ResourceName}:{key.EndpointName}:port"; } private void EnsureInitialized() { if (_initialized) { return; } var activePortKeys = new Dictionary<PersistentPortAllocationKey, ActivePort>(); foreach (var resource in _model.Resources) { if (!resource.TryGetEndpoints(out var endpoints)) { continue; } foreach (var endpoint in endpoints) { if (DcpModelUtilities.TryGetEffectiveFixedPublicPort(resource, endpoint, _options.RandomizePorts, out var fixedPublicPort)) { _allocator.ExcludePort(fixedPublicPort); } if (resource is IComputeResource && !resource.IsContainer() && EndpointAnnotation.NormalizePort(endpoint.TargetPort) is int fixedTargetPort) { _allocator.ExcludePort(fixedTargetPort); } if (DcpModelUtilities.NeedsGeneratedPublicPort(resource, endpoint, _options.RandomizePorts)) { var publicPortKey = new PersistentPortAllocationKey(resource.Name, endpoint.Name); activePortKeys[publicPortKey] = new( endpoint.Protocol, resource.HasPersistentLifetime() && !endpoint.IsProxied, GetLegacyPort(publicPortKey)); } } } if (PersistenceEnabled) { foreach (var allocation in _store.GetAllocations()) { if (activePortKeys.TryGetValue(allocation.Key, out var activePort) && TryReservePort(allocation.Value, activePort, allowInUse: activePort.AllowInUse)) { _reservedPorts[allocation.Key] = allocation.Value; } } // Reserve legacy allocations before assigning any new ports. Current state takes // precedence when both stores contain a value for the same endpoint. foreach (var (key, activePort) in activePortKeys) { if (_reservedPorts.ContainsKey(key) || activePort.LegacyPort is not int legacyPort || !TryReservePort( legacyPort, activePort, allowInUse: activePort.AllowInUse && !_options.RandomizePorts)) { continue; } _reservedPorts[key] = legacyPort; _store.SetPort(key, legacyPort); } } _initialized = true; } private bool TryReservePort(int port, ActivePort activePort, bool allowInUse) { // A persistent proxyless workload may still own its saved AppHost-local port when the // AppHost restarts. Ownership cannot be verified before DCP adopts the workload, so // preserve occupied saved ports. Legacy user secrets are shared with isolated runs, so // those values must still be available before they can be migrated when ports are randomized. return _allocator.TryReservePort( port, activePort.Protocol, allowInUse); } private readonly record struct ActivePort( ProtocolType Protocol, bool AllowInUse, int? LegacyPort); }