| | | 1 | | using Elsa.Hosting.Management.Contracts; |
| | | 2 | | using Elsa.Hosting.Management.Options; |
| | | 3 | | using Microsoft.Extensions.Logging; |
| | | 4 | | using Microsoft.Extensions.Options; |
| | | 5 | | |
| | | 6 | | namespace Elsa.Hosting.Management.Services; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Resolves the application instance name from <see cref="ApplicationInstanceOptions"/>, allowing a |
| | | 10 | | /// stable name to be configured so that per-instance transport entities are reused across restarts |
| | | 11 | | /// instead of accumulating. Falls back to a random name when no stable name is configured, which |
| | | 12 | | /// preserves the previous default behaviour. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// Resolution order: |
| | | 16 | | /// <list type="number"> |
| | | 17 | | /// <item><description><see cref="ApplicationInstanceOptions.InstanceName"/> when set.</description></item> |
| | | 18 | | /// <item><description>The environment variable named by <see cref="ApplicationInstanceOptions.InstanceNameEnvironmentVa |
| | | 19 | | /// <item><description>A randomly generated name (legacy behaviour).</description></item> |
| | | 20 | | /// </list> |
| | | 21 | | /// </remarks> |
| | | 22 | | public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNameProvider |
| | | 23 | | { |
| | | 24 | | internal const int AzureServiceBusSubscriptionNameMaxLength = 50; |
| | | 25 | | internal const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; |
| | 1 | 26 | | internal static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerCha |
| | | 27 | | |
| | | 28 | | private readonly string _instanceName; |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Initializes a new instance of the <see cref="ConfiguredApplicationInstanceNameProvider"/> class. |
| | | 32 | | /// </summary> |
| | 19 | 33 | | public ConfiguredApplicationInstanceNameProvider( |
| | 19 | 34 | | IOptions<ApplicationInstanceOptions> options, |
| | 19 | 35 | | RandomIntIdentityGenerator randomIdentityGenerator, |
| | 19 | 36 | | ILogger<ConfiguredApplicationInstanceNameProvider> logger) |
| | | 37 | | { |
| | 19 | 38 | | var value = options.Value; |
| | | 39 | | |
| | 19 | 40 | | if (!string.IsNullOrWhiteSpace(value.InstanceName)) |
| | | 41 | | { |
| | 11 | 42 | | _instanceName = ValidateConfiguredInstanceName(value.InstanceName, $"{nameof(ApplicationInstanceOptions)}.{n |
| | 6 | 43 | | return; |
| | | 44 | | } |
| | | 45 | | |
| | 8 | 46 | | if (!string.IsNullOrWhiteSpace(value.InstanceNameEnvironmentVariable)) |
| | | 47 | | { |
| | 6 | 48 | | var environmentVariable = value.InstanceNameEnvironmentVariable.Trim(); |
| | 6 | 49 | | var fromEnvironment = Environment.GetEnvironmentVariable(environmentVariable); |
| | | 50 | | |
| | 6 | 51 | | if (!string.IsNullOrWhiteSpace(fromEnvironment)) |
| | | 52 | | { |
| | 4 | 53 | | _instanceName = ValidateConfiguredInstanceName(fromEnvironment, $"environment variable '{environmentVari |
| | 3 | 54 | | return; |
| | | 55 | | } |
| | | 56 | | |
| | 2 | 57 | | logger.LogWarning( |
| | 2 | 58 | | "The configured instance-name environment variable '{EnvironmentVariable}' is not set or empty. Falling |
| | 2 | 59 | | "A random name causes per-instance transport entities (such as the Azure Service Bus change-token subscr |
| | 2 | 60 | | "which can accumulate until the transport's per-topic limit is reached.", |
| | 2 | 61 | | environmentVariable); |
| | | 62 | | } |
| | | 63 | | |
| | 4 | 64 | | _instanceName = randomIdentityGenerator.GenerateId(); |
| | 4 | 65 | | } |
| | | 66 | | |
| | | 67 | | /// <inheritdoc /> |
| | 13 | 68 | | public string GetName() => _instanceName; |
| | | 69 | | |
| | | 70 | | private static string ValidateConfiguredInstanceName(string value, string source) |
| | | 71 | | { |
| | 15 | 72 | | var instanceName = value.Trim(); |
| | | 73 | | |
| | 15 | 74 | | if (instanceName.Length <= ConfiguredInstanceNameMaxLength) |
| | | 75 | | { |
| | 13 | 76 | | if (IsValidConfiguredInstanceName(instanceName)) |
| | 9 | 77 | | return instanceName; |
| | | 78 | | |
| | 4 | 79 | | throw new InvalidOperationException( |
| | 4 | 80 | | $"The configured application instance name from {source} contains invalid characters. " + |
| | 4 | 81 | | "Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a letter |
| | | 82 | | } |
| | | 83 | | |
| | 2 | 84 | | throw new InvalidOperationException( |
| | 2 | 85 | | $"The configured application instance name from {source} is {instanceName.Length} characters long, but it mu |
| | 2 | 86 | | $"The value is used to create per-instance transport entities such as '{instanceName}{TriggerChangeTokenSign |
| | 2 | 87 | | "Configure a shorter stable name that is still unique for each concurrently running instance."); |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | private static bool IsValidConfiguredInstanceName(string instanceName) |
| | | 91 | | { |
| | 13 | 92 | | if (instanceName.Length == 0) |
| | 0 | 93 | | return false; |
| | | 94 | | |
| | 13 | 95 | | return IsAsciiLetterOrDigit(instanceName[0]) |
| | 13 | 96 | | && IsAsciiLetterOrDigit(instanceName[^1]) |
| | 79 | 97 | | && instanceName.All(c => IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_'); |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | private static bool IsAsciiLetterOrDigit(char value) => |
| | 91 | 101 | | value is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9'; |
| | | 102 | | } |