< Summary

Information
Class: Elsa.Workflows.Api.Services.ShellReloadOrchestrator
Assembly: Elsa.Workflows.Api
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Services/ShellReloadOrchestrator.cs
Line coverage
88%
Covered lines: 135
Uncovered lines: 17
Coverable lines: 152
Total lines: 259
Line coverage: 88.8%
Branch coverage
84%
Covered branches: 39
Total branches: 46
Branch coverage: 84.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
ReloadAllAsync(...)100%11100%
ReloadAsync(...)100%11100%
ReloadInternalAsync()86.36%222292%
RemoveShellAsync()100%1147.05%
UpsertShellAsync()100%6685.18%
CreateBusyResult(...)50%22100%
CreateFailureResult(...)50%22100%
CreateNotFoundResult(...)100%11100%
CreateResult(...)100%1010100%
IsRequested(...)100%22100%
Clone(...)50%2285.71%
Dispose()50%2280%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Services/ShellReloadOrchestrator.cs

#LineLine coverage
 1using CShells;
 2using CShells.Configuration;
 3using CShells.Management;
 4using Elsa.Workflows.Api.Contracts;
 5using Microsoft.Extensions.DependencyInjection;
 6using Microsoft.Extensions.Logging;
 7
 8namespace Elsa.Workflows.Api.Services;
 9
 310internal class ShellReloadOrchestrator(IServiceProvider serviceProvider, ILogger<ShellReloadOrchestrator> logger) : IShe
 11{
 112    private static readonly StringComparer ShellIdComparer = StringComparer.OrdinalIgnoreCase;
 313    private readonly SemaphoreSlim _reloadLock = new(1, 1);
 14    private bool _disposed;
 15
 16    public Task<ShellReloadResult> ReloadAllAsync(CancellationToken cancellationToken = default) =>
 517        ReloadInternalAsync(null, cancellationToken);
 18
 19    public Task<ShellReloadResult> ReloadAsync(string shellId, CancellationToken cancellationToken = default) =>
 320        ReloadInternalAsync(shellId, cancellationToken);
 21
 22    private async Task<ShellReloadResult> ReloadInternalAsync(string? requestedShellId, CancellationToken cancellationTo
 23    {
 824        var requested = string.IsNullOrWhiteSpace(requestedShellId) ? null : requestedShellId;
 25
 826        if (!await _reloadLock.WaitAsync(0, cancellationToken))
 127            return CreateBusyResult(requested);
 28
 29        try
 30        {
 731            var shellManager = serviceProvider.GetService<IShellManager>();
 732            var shellSettingsProvider = serviceProvider.GetService<IShellSettingsProvider>();
 733            var shellSettingsCache = serviceProvider.GetService<IShellSettingsCache>();
 34
 735            if (shellManager == null || shellSettingsProvider == null || shellSettingsCache == null)
 36            {
 037                logger.LogWarning("Shell reload was requested, but the current host does not provide CShells management 
 038                return CreateFailureResult(requested, "Shell reload is not available in the current host.");
 39            }
 40
 41            IReadOnlyDictionary<string, ShellSettings> currentShells;
 42            IReadOnlyDictionary<string, ShellSettings> latestShells;
 43
 44            try
 45            {
 46                currentShells = shellSettingsCache.GetAll().Select(Clone).ToDictionary(x => x.Id.Name, ShellIdComparer);
 47                latestShells = (await shellSettingsProvider.GetShellSettingsAsync(cancellationToken)).Select(Clone).ToDi
 648            }
 149            catch (Exception exception)
 50            {
 151                logger.LogWarning(exception, "Failed to load shell settings from the provider.");
 152                return CreateFailureResult(requested, exception.Message);
 53            }
 54
 655            if (requested != null && !latestShells.ContainsKey(requested))
 156                return CreateNotFoundResult(requested);
 57
 558            var itemResults = new List<ShellReloadItemResult>();
 59
 1260            foreach (var currentShell in currentShells.Values
 961                                                   .Where(x => !latestShells.ContainsKey(x.Id.Name))
 62                                                   .OrderBy(x => x.Id.Name, ShellIdComparer))
 63            {
 164                var result = await RemoveShellAsync(shellManager, currentShell.Id, requested, cancellationToken);
 165                itemResults.Add(result);
 66            }
 67
 68            foreach (var latestShell in latestShells.Values.OrderBy(x => x.Id.Name, ShellIdComparer))
 69            {
 970                currentShells.TryGetValue(latestShell.Id.Name, out var previousShell);
 971                var result = await UpsertShellAsync(shellManager, latestShell, previousShell, requested, cancellationTok
 972                itemResults.Add(result);
 73            }
 74
 575            return CreateResult(requested, itemResults);
 76        }
 77        finally
 78        {
 779            _reloadLock.Release();
 80        }
 881    }
 82
 83    private async Task<ShellReloadItemResult> RemoveShellAsync(IShellManager shellManager, ShellId shellId, string? requ
 84    {
 85        try
 86        {
 187            await shellManager.RemoveShellAsync(shellId, cancellationToken);
 88
 189            return new ShellReloadItemResult
 190            {
 191                ShellId = shellId.Name,
 192                Outcome = ShellReloadItemOutcome.Removed,
 193                Requested = IsRequested(shellId.Name, requestedShellId)
 194            };
 95        }
 096        catch (Exception exception)
 97        {
 098            logger.LogWarning(exception, "Failed to remove shell '{ShellId}' during reload.", shellId.Name);
 99
 0100            return new ShellReloadItemResult
 0101            {
 0102                ShellId = shellId.Name,
 0103                Outcome = ShellReloadItemOutcome.InvalidConfiguration,
 0104                Requested = IsRequested(shellId.Name, requestedShellId),
 0105                Message = exception.Message
 0106            };
 107        }
 1108    }
 109
 110    private async Task<ShellReloadItemResult> UpsertShellAsync(IShellManager shellManager, ShellSettings latestShell, Sh
 111    {
 112        try
 113        {
 9114            if (previousShell == null)
 1115                await shellManager.AddShellAsync(Clone(latestShell), cancellationToken);
 116            else
 8117                await shellManager.UpdateShellAsync(Clone(latestShell), cancellationToken);
 118
 6119            return new ShellReloadItemResult
 6120            {
 6121                ShellId = latestShell.Id.Name,
 6122                Outcome = ShellReloadItemOutcome.Reloaded,
 6123                Requested = IsRequested(latestShell.Id.Name, requestedShellId)
 6124            };
 125        }
 126        catch (Exception exception)
 127        {
 3128            logger.LogWarning(exception, "Failed to reload shell '{ShellId}'.", latestShell.Id.Name);
 3129            var message = exception.Message;
 130
 131            try
 132            {
 3133                await shellManager.RemoveShellAsync(latestShell.Id, cancellationToken);
 134
 3135                if (previousShell != null)
 3136                    await shellManager.AddShellAsync(Clone(previousShell), cancellationToken);
 3137            }
 0138            catch (Exception restoreException)
 139            {
 0140                logger.LogWarning(restoreException, "Failed to restore the previous configuration for shell '{ShellId}'.
 0141                message = $"{message} Previous configuration could not be restored: {restoreException.Message}";
 0142            }
 143
 3144            return new ShellReloadItemResult
 3145            {
 3146                ShellId = latestShell.Id.Name,
 3147                Outcome = ShellReloadItemOutcome.InvalidConfiguration,
 3148                Requested = IsRequested(latestShell.Id.Name, requestedShellId),
 3149                Message = message
 3150            };
 151        }
 9152    }
 153
 154    private static ShellReloadResult CreateBusyResult(string? requestedShellId)
 155    {
 1156        var shells = requestedShellId == null ? Array.Empty<ShellReloadItemResult>() :
 1157        [
 1158            new ShellReloadItemResult
 1159            {
 1160                ShellId = requestedShellId,
 1161                Outcome = ShellReloadItemOutcome.Skipped,
 1162                Requested = true,
 1163                Message = "A shell reload is already in progress."
 1164            }
 1165        ];
 166
 1167        return new ShellReloadResult
 1168        {
 1169            Status = ShellReloadStatus.Busy,
 1170            RequestedShellId = requestedShellId,
 1171            ReloadedAt = DateTimeOffset.UtcNow,
 1172            Shells = shells
 1173        };
 174    }
 175
 176    private static ShellReloadResult CreateFailureResult(string? requestedShellId, string message)
 177    {
 1178        var shells = requestedShellId == null ? Array.Empty<ShellReloadItemResult>() :
 1179        [
 1180            new ShellReloadItemResult
 1181            {
 1182                ShellId = requestedShellId,
 1183                Outcome = ShellReloadItemOutcome.Skipped,
 1184                Requested = true,
 1185                Message = message
 1186            }
 1187        ];
 188
 1189        return new ShellReloadResult
 1190        {
 1191            Status = ShellReloadStatus.Failed,
 1192            RequestedShellId = requestedShellId,
 1193            ReloadedAt = DateTimeOffset.UtcNow,
 1194            Shells = shells
 1195        };
 196    }
 197
 198    private static ShellReloadResult CreateNotFoundResult(string requestedShellId) =>
 1199        new()
 1200        {
 1201            Status = ShellReloadStatus.NotFound,
 1202            RequestedShellId = requestedShellId,
 1203            ReloadedAt = DateTimeOffset.UtcNow,
 1204            Shells =
 1205            [
 1206                new ShellReloadItemResult
 1207                {
 1208                    ShellId = requestedShellId,
 1209                    Outcome = ShellReloadItemOutcome.Unknown,
 1210                    Requested = true,
 1211                    Message = "The requested shell was not found in the current configuration source."
 1212                }
 1213            ]
 1214        };
 215
 216    private static ShellReloadResult CreateResult(string? requestedShellId, IReadOnlyCollection<ShellReloadItemResult> s
 217    {
 5218        var requestedResult = requestedShellId == null
 5219            ? null
 7220            : shellResults.FirstOrDefault(x => IsRequested(x.ShellId, requestedShellId));
 14221        var hasFailures = shellResults.Any(x => x.Outcome is ShellReloadItemOutcome.InvalidConfiguration or ShellReloadI
 5222        var requestedFailed = requestedResult != null && requestedResult.Outcome != ShellReloadItemOutcome.Reloaded;
 223
 5224        return new ShellReloadResult
 5225        {
 5226            Status = requestedFailed
 5227                ? ShellReloadStatus.RequestedShellFailed
 5228                : hasFailures ? ShellReloadStatus.Partial : ShellReloadStatus.Completed,
 5229            RequestedShellId = requestedShellId,
 5230            ReloadedAt = DateTimeOffset.UtcNow,
 9231            Shells = shellResults.OrderBy(x => x.ShellId, ShellIdComparer).ToArray()
 5232        };
 233    }
 234
 235    private static bool IsRequested(string shellId, string? requestedShellId) =>
 12236        requestedShellId != null && ShellIdComparer.Equals(shellId, requestedShellId);
 237
 238    private static ShellSettings Clone(ShellSettings source)
 239    {
 33240        var clone = new ShellSettings(source.Id, source.EnabledFeatures)
 33241        {
 33242            ConfigurationData = new Dictionary<string, object>(source.ConfigurationData, StringComparer.OrdinalIgnoreCas
 33243        };
 244
 66245        foreach (var configurator in source.FeatureConfigurators)
 0246            clone.FeatureConfigurators[configurator.Key] = configurator.Value;
 247
 33248        return clone;
 249    }
 250
 251    public void Dispose()
 252    {
 3253        if (_disposed)
 0254            return;
 255
 3256        _reloadLock.Dispose();
 3257        _disposed = true;
 3258    }
 259}