using Elsa.Hosting.Management.Contracts; using Elsa.Hosting.Management.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Security.Cryptography; namespace Elsa.Hosting.Management.Services; /// /// Resolves the application instance name from , allowing a /// stable name to be configured so that per-instance transport entities are reused across restarts /// instead of accumulating. Falls back to a random name when no stable name is configured, which /// preserves the previous default behaviour. /// /// /// Resolution order: /// /// when set. /// The environment variable named by when configured and non-empty. /// A randomly generated name (legacy behaviour). /// /// public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNameProvider { internal const int AzureServiceBusSubscriptionNameMaxLength = 50; internal const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-tct"; private const int ShortenedNameHashLength = 16; internal static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; private readonly string _instanceName; /// /// Initializes a new instance of the class. /// public ConfiguredApplicationInstanceNameProvider( IOptions options, RandomIntIdentityGenerator randomIdentityGenerator, ILogger logger) { var value = options.Value; if (!string.IsNullOrWhiteSpace(value.InstanceName)) { _instanceName = ResolveConfiguredInstanceName(value.InstanceName, $"{nameof(ApplicationInstanceOptions)}.{nameof(ApplicationInstanceOptions.InstanceName)}", logger); return; } if (!string.IsNullOrWhiteSpace(value.InstanceNameEnvironmentVariable)) { var environmentVariable = value.InstanceNameEnvironmentVariable.Trim(); var fromEnvironment = Environment.GetEnvironmentVariable(environmentVariable); if (!string.IsNullOrWhiteSpace(fromEnvironment)) { _instanceName = ResolveConfiguredInstanceName(fromEnvironment, $"environment variable '{environmentVariable}'", logger); return; } logger.LogWarning( "The configured instance-name environment variable '{EnvironmentVariable}' is not set or empty. Falling back to a random instance name. " + "A random name causes per-instance transport entities (such as the Azure Service Bus change-token subscription) to be recreated on every restart, " + "which can accumulate until the transport's per-topic limit is reached.", environmentVariable); } _instanceName = randomIdentityGenerator.GenerateId(); } /// public string GetName() => _instanceName; private static string ResolveConfiguredInstanceName(string value, string source, ILogger logger) { var instanceName = value.Trim(); var isTooLong = instanceName.Length > ConfiguredInstanceNameMaxLength; var hasInvalidCharacters = !IsValidConfiguredInstanceName(instanceName); if (hasInvalidCharacters) { var errors = new List { $"The configured application instance name from {source} contains invalid characters. " + "Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a letter or number." }; if (isTooLong) { errors.Add( $"The configured application instance name from {source} is {instanceName.Length} characters long, but it must be {ConfiguredInstanceNameMaxLength} characters or fewer. " + $"The value is used to create per-instance transport entities such as '{instanceName}{TriggerChangeTokenSignalEndpointNameSuffix}', which must fit within Azure Service Bus's {AzureServiceBusSubscriptionNameMaxLength}-character subscription name limit."); } throw new InvalidOperationException(string.Join(" ", errors)); } if (!isTooLong) return instanceName; var shortenedName = ShortenConfiguredInstanceName(instanceName); logger.LogWarning( "The configured application instance name from {Source} is {Length} characters long, exceeding the {MaxLength}-character limit required for Azure Service Bus transport entities. " + "Using deterministic shortened instance name '{ShortenedName}' instead.", source, instanceName.Length, ConfiguredInstanceNameMaxLength, shortenedName); return shortenedName; } private static bool IsValidConfiguredInstanceName(string instanceName) { return instanceName.Length > 0 && IsAsciiLetterOrDigit(instanceName[0]) && IsAsciiLetterOrDigit(instanceName[^1]) && instanceName.All(c => IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_'); } private static bool IsAsciiLetterOrDigit(char value) => value is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9'; private static string ShortenConfiguredInstanceName(string instanceName) { var hash = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(instanceName))).ToLowerInvariant()[..ShortenedNameHashLength]; var prefixLength = ConfiguredInstanceNameMaxLength - hash.Length - 1; var prefix = instanceName[..prefixLength]; return $"{prefix}-{hash}"; } }