using Elsa.Hosting.Management.Contracts; using Elsa.Hosting.Management.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; 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-trigger-change-token-signal"; 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 = ValidateConfiguredInstanceName(value.InstanceName, $"{nameof(ApplicationInstanceOptions)}.{nameof(ApplicationInstanceOptions.InstanceName)}"); return; } if (!string.IsNullOrWhiteSpace(value.InstanceNameEnvironmentVariable)) { var environmentVariable = value.InstanceNameEnvironmentVariable.Trim(); var fromEnvironment = Environment.GetEnvironmentVariable(environmentVariable); if (!string.IsNullOrWhiteSpace(fromEnvironment)) { _instanceName = ValidateConfiguredInstanceName(fromEnvironment, $"environment variable '{environmentVariable}'"); 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 ValidateConfiguredInstanceName(string value, string source) { var instanceName = value.Trim(); if (instanceName.Length <= ConfiguredInstanceNameMaxLength) { if (IsValidConfiguredInstanceName(instanceName)) return instanceName; throw new InvalidOperationException( $"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."); } throw new InvalidOperationException( $"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. " + "Configure a shorter stable name that is still unique for each concurrently running instance."); } private static bool IsValidConfiguredInstanceName(string instanceName) { if (instanceName.Length == 0) return false; return 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'; }