elsa-core/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs
Sipke Schoorstra b08b10132c
address greptile review feedback (greploop iteration 1)
- Make validation constants internal and expose via InternalsVisibleTo so tests reference single source of truth
- Add empty-string guard to IsValidConfiguredInstanceName to prevent IndexOutOfRangeException
- Add ClusteringFeature_UsesConfiguredInstanceNameProvider test for Features.ClusteringFeature to match existing ShellFeatures coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-20 00:13:33 +02:00

103 lines
4.9 KiB
C#

using Elsa.Hosting.Management.Contracts;
using Elsa.Hosting.Management.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Elsa.Hosting.Management.Services;
/// <summary>
/// Resolves the application instance name from <see cref="ApplicationInstanceOptions"/>, 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.
/// </summary>
/// <remarks>
/// Resolution order:
/// <list type="number">
/// <item><description><see cref="ApplicationInstanceOptions.InstanceName"/> when set.</description></item>
/// <item><description>The environment variable named by <see cref="ApplicationInstanceOptions.InstanceNameEnvironmentVariable"/> when configured and non-empty.</description></item>
/// <item><description>A randomly generated name (legacy behaviour).</description></item>
/// </list>
/// </remarks>
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;
/// <summary>
/// Initializes a new instance of the <see cref="ConfiguredApplicationInstanceNameProvider"/> class.
/// </summary>
public ConfiguredApplicationInstanceNameProvider(
IOptions<ApplicationInstanceOptions> options,
RandomIntIdentityGenerator randomIdentityGenerator,
ILogger<ConfiguredApplicationInstanceNameProvider> 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();
}
/// <inheritdoc />
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';
}