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';
}