| | | 1 | | using CShells; |
| | | 2 | | using CShells.Configuration; |
| | | 3 | | using CShells.Management; |
| | | 4 | | using Elsa.Workflows.Api.Contracts; |
| | | 5 | | using Microsoft.Extensions.DependencyInjection; |
| | | 6 | | using Microsoft.Extensions.Logging; |
| | | 7 | | |
| | | 8 | | namespace Elsa.Workflows.Api.Services; |
| | | 9 | | |
| | 3 | 10 | | internal class ShellReloadOrchestrator(IServiceProvider serviceProvider, ILogger<ShellReloadOrchestrator> logger) : IShe |
| | | 11 | | { |
| | 1 | 12 | | private static readonly StringComparer ShellIdComparer = StringComparer.OrdinalIgnoreCase; |
| | 3 | 13 | | private readonly SemaphoreSlim _reloadLock = new(1, 1); |
| | | 14 | | private bool _disposed; |
| | | 15 | | |
| | | 16 | | public Task<ShellReloadResult> ReloadAllAsync(CancellationToken cancellationToken = default) => |
| | 5 | 17 | | ReloadInternalAsync(null, cancellationToken); |
| | | 18 | | |
| | | 19 | | public Task<ShellReloadResult> ReloadAsync(string shellId, CancellationToken cancellationToken = default) => |
| | 3 | 20 | | ReloadInternalAsync(shellId, cancellationToken); |
| | | 21 | | |
| | | 22 | | private async Task<ShellReloadResult> ReloadInternalAsync(string? requestedShellId, CancellationToken cancellationTo |
| | | 23 | | { |
| | 8 | 24 | | var requested = string.IsNullOrWhiteSpace(requestedShellId) ? null : requestedShellId; |
| | | 25 | | |
| | 8 | 26 | | if (!await _reloadLock.WaitAsync(0, cancellationToken)) |
| | 1 | 27 | | return CreateBusyResult(requested); |
| | | 28 | | |
| | | 29 | | try |
| | | 30 | | { |
| | 7 | 31 | | var shellManager = serviceProvider.GetService<IShellManager>(); |
| | 7 | 32 | | var shellSettingsProvider = serviceProvider.GetService<IShellSettingsProvider>(); |
| | 7 | 33 | | var shellSettingsCache = serviceProvider.GetService<IShellSettingsCache>(); |
| | | 34 | | |
| | 7 | 35 | | if (shellManager == null || shellSettingsProvider == null || shellSettingsCache == null) |
| | | 36 | | { |
| | 0 | 37 | | logger.LogWarning("Shell reload was requested, but the current host does not provide CShells management |
| | 0 | 38 | | 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 |
| | 6 | 48 | | } |
| | 1 | 49 | | catch (Exception exception) |
| | | 50 | | { |
| | 1 | 51 | | logger.LogWarning(exception, "Failed to load shell settings from the provider."); |
| | 1 | 52 | | return CreateFailureResult(requested, exception.Message); |
| | | 53 | | } |
| | | 54 | | |
| | 6 | 55 | | if (requested != null && !latestShells.ContainsKey(requested)) |
| | 1 | 56 | | return CreateNotFoundResult(requested); |
| | | 57 | | |
| | 5 | 58 | | var itemResults = new List<ShellReloadItemResult>(); |
| | | 59 | | |
| | 12 | 60 | | foreach (var currentShell in currentShells.Values |
| | 9 | 61 | | .Where(x => !latestShells.ContainsKey(x.Id.Name)) |
| | | 62 | | .OrderBy(x => x.Id.Name, ShellIdComparer)) |
| | | 63 | | { |
| | 1 | 64 | | var result = await RemoveShellAsync(shellManager, currentShell.Id, requested, cancellationToken); |
| | 1 | 65 | | itemResults.Add(result); |
| | | 66 | | } |
| | | 67 | | |
| | | 68 | | foreach (var latestShell in latestShells.Values.OrderBy(x => x.Id.Name, ShellIdComparer)) |
| | | 69 | | { |
| | 9 | 70 | | currentShells.TryGetValue(latestShell.Id.Name, out var previousShell); |
| | 9 | 71 | | var result = await UpsertShellAsync(shellManager, latestShell, previousShell, requested, cancellationTok |
| | 9 | 72 | | itemResults.Add(result); |
| | | 73 | | } |
| | | 74 | | |
| | 5 | 75 | | return CreateResult(requested, itemResults); |
| | | 76 | | } |
| | | 77 | | finally |
| | | 78 | | { |
| | 7 | 79 | | _reloadLock.Release(); |
| | | 80 | | } |
| | 8 | 81 | | } |
| | | 82 | | |
| | | 83 | | private async Task<ShellReloadItemResult> RemoveShellAsync(IShellManager shellManager, ShellId shellId, string? requ |
| | | 84 | | { |
| | | 85 | | try |
| | | 86 | | { |
| | 1 | 87 | | await shellManager.RemoveShellAsync(shellId, cancellationToken); |
| | | 88 | | |
| | 1 | 89 | | return new ShellReloadItemResult |
| | 1 | 90 | | { |
| | 1 | 91 | | ShellId = shellId.Name, |
| | 1 | 92 | | Outcome = ShellReloadItemOutcome.Removed, |
| | 1 | 93 | | Requested = IsRequested(shellId.Name, requestedShellId) |
| | 1 | 94 | | }; |
| | | 95 | | } |
| | 0 | 96 | | catch (Exception exception) |
| | | 97 | | { |
| | 0 | 98 | | logger.LogWarning(exception, "Failed to remove shell '{ShellId}' during reload.", shellId.Name); |
| | | 99 | | |
| | 0 | 100 | | return new ShellReloadItemResult |
| | 0 | 101 | | { |
| | 0 | 102 | | ShellId = shellId.Name, |
| | 0 | 103 | | Outcome = ShellReloadItemOutcome.InvalidConfiguration, |
| | 0 | 104 | | Requested = IsRequested(shellId.Name, requestedShellId), |
| | 0 | 105 | | Message = exception.Message |
| | 0 | 106 | | }; |
| | | 107 | | } |
| | 1 | 108 | | } |
| | | 109 | | |
| | | 110 | | private async Task<ShellReloadItemResult> UpsertShellAsync(IShellManager shellManager, ShellSettings latestShell, Sh |
| | | 111 | | { |
| | | 112 | | try |
| | | 113 | | { |
| | 9 | 114 | | if (previousShell == null) |
| | 1 | 115 | | await shellManager.AddShellAsync(Clone(latestShell), cancellationToken); |
| | | 116 | | else |
| | 8 | 117 | | await shellManager.UpdateShellAsync(Clone(latestShell), cancellationToken); |
| | | 118 | | |
| | 6 | 119 | | return new ShellReloadItemResult |
| | 6 | 120 | | { |
| | 6 | 121 | | ShellId = latestShell.Id.Name, |
| | 6 | 122 | | Outcome = ShellReloadItemOutcome.Reloaded, |
| | 6 | 123 | | Requested = IsRequested(latestShell.Id.Name, requestedShellId) |
| | 6 | 124 | | }; |
| | | 125 | | } |
| | | 126 | | catch (Exception exception) |
| | | 127 | | { |
| | 3 | 128 | | logger.LogWarning(exception, "Failed to reload shell '{ShellId}'.", latestShell.Id.Name); |
| | 3 | 129 | | var message = exception.Message; |
| | | 130 | | |
| | | 131 | | try |
| | | 132 | | { |
| | 3 | 133 | | await shellManager.RemoveShellAsync(latestShell.Id, cancellationToken); |
| | | 134 | | |
| | 3 | 135 | | if (previousShell != null) |
| | 3 | 136 | | await shellManager.AddShellAsync(Clone(previousShell), cancellationToken); |
| | 3 | 137 | | } |
| | 0 | 138 | | catch (Exception restoreException) |
| | | 139 | | { |
| | 0 | 140 | | logger.LogWarning(restoreException, "Failed to restore the previous configuration for shell '{ShellId}'. |
| | 0 | 141 | | message = $"{message} Previous configuration could not be restored: {restoreException.Message}"; |
| | 0 | 142 | | } |
| | | 143 | | |
| | 3 | 144 | | return new ShellReloadItemResult |
| | 3 | 145 | | { |
| | 3 | 146 | | ShellId = latestShell.Id.Name, |
| | 3 | 147 | | Outcome = ShellReloadItemOutcome.InvalidConfiguration, |
| | 3 | 148 | | Requested = IsRequested(latestShell.Id.Name, requestedShellId), |
| | 3 | 149 | | Message = message |
| | 3 | 150 | | }; |
| | | 151 | | } |
| | 9 | 152 | | } |
| | | 153 | | |
| | | 154 | | private static ShellReloadResult CreateBusyResult(string? requestedShellId) |
| | | 155 | | { |
| | 1 | 156 | | var shells = requestedShellId == null ? Array.Empty<ShellReloadItemResult>() : |
| | 1 | 157 | | [ |
| | 1 | 158 | | new ShellReloadItemResult |
| | 1 | 159 | | { |
| | 1 | 160 | | ShellId = requestedShellId, |
| | 1 | 161 | | Outcome = ShellReloadItemOutcome.Skipped, |
| | 1 | 162 | | Requested = true, |
| | 1 | 163 | | Message = "A shell reload is already in progress." |
| | 1 | 164 | | } |
| | 1 | 165 | | ]; |
| | | 166 | | |
| | 1 | 167 | | return new ShellReloadResult |
| | 1 | 168 | | { |
| | 1 | 169 | | Status = ShellReloadStatus.Busy, |
| | 1 | 170 | | RequestedShellId = requestedShellId, |
| | 1 | 171 | | ReloadedAt = DateTimeOffset.UtcNow, |
| | 1 | 172 | | Shells = shells |
| | 1 | 173 | | }; |
| | | 174 | | } |
| | | 175 | | |
| | | 176 | | private static ShellReloadResult CreateFailureResult(string? requestedShellId, string message) |
| | | 177 | | { |
| | 1 | 178 | | var shells = requestedShellId == null ? Array.Empty<ShellReloadItemResult>() : |
| | 1 | 179 | | [ |
| | 1 | 180 | | new ShellReloadItemResult |
| | 1 | 181 | | { |
| | 1 | 182 | | ShellId = requestedShellId, |
| | 1 | 183 | | Outcome = ShellReloadItemOutcome.Skipped, |
| | 1 | 184 | | Requested = true, |
| | 1 | 185 | | Message = message |
| | 1 | 186 | | } |
| | 1 | 187 | | ]; |
| | | 188 | | |
| | 1 | 189 | | return new ShellReloadResult |
| | 1 | 190 | | { |
| | 1 | 191 | | Status = ShellReloadStatus.Failed, |
| | 1 | 192 | | RequestedShellId = requestedShellId, |
| | 1 | 193 | | ReloadedAt = DateTimeOffset.UtcNow, |
| | 1 | 194 | | Shells = shells |
| | 1 | 195 | | }; |
| | | 196 | | } |
| | | 197 | | |
| | | 198 | | private static ShellReloadResult CreateNotFoundResult(string requestedShellId) => |
| | 1 | 199 | | new() |
| | 1 | 200 | | { |
| | 1 | 201 | | Status = ShellReloadStatus.NotFound, |
| | 1 | 202 | | RequestedShellId = requestedShellId, |
| | 1 | 203 | | ReloadedAt = DateTimeOffset.UtcNow, |
| | 1 | 204 | | Shells = |
| | 1 | 205 | | [ |
| | 1 | 206 | | new ShellReloadItemResult |
| | 1 | 207 | | { |
| | 1 | 208 | | ShellId = requestedShellId, |
| | 1 | 209 | | Outcome = ShellReloadItemOutcome.Unknown, |
| | 1 | 210 | | Requested = true, |
| | 1 | 211 | | Message = "The requested shell was not found in the current configuration source." |
| | 1 | 212 | | } |
| | 1 | 213 | | ] |
| | 1 | 214 | | }; |
| | | 215 | | |
| | | 216 | | private static ShellReloadResult CreateResult(string? requestedShellId, IReadOnlyCollection<ShellReloadItemResult> s |
| | | 217 | | { |
| | 5 | 218 | | var requestedResult = requestedShellId == null |
| | 5 | 219 | | ? null |
| | 7 | 220 | | : shellResults.FirstOrDefault(x => IsRequested(x.ShellId, requestedShellId)); |
| | 14 | 221 | | var hasFailures = shellResults.Any(x => x.Outcome is ShellReloadItemOutcome.InvalidConfiguration or ShellReloadI |
| | 5 | 222 | | var requestedFailed = requestedResult != null && requestedResult.Outcome != ShellReloadItemOutcome.Reloaded; |
| | | 223 | | |
| | 5 | 224 | | return new ShellReloadResult |
| | 5 | 225 | | { |
| | 5 | 226 | | Status = requestedFailed |
| | 5 | 227 | | ? ShellReloadStatus.RequestedShellFailed |
| | 5 | 228 | | : hasFailures ? ShellReloadStatus.Partial : ShellReloadStatus.Completed, |
| | 5 | 229 | | RequestedShellId = requestedShellId, |
| | 5 | 230 | | ReloadedAt = DateTimeOffset.UtcNow, |
| | 9 | 231 | | Shells = shellResults.OrderBy(x => x.ShellId, ShellIdComparer).ToArray() |
| | 5 | 232 | | }; |
| | | 233 | | } |
| | | 234 | | |
| | | 235 | | private static bool IsRequested(string shellId, string? requestedShellId) => |
| | 12 | 236 | | requestedShellId != null && ShellIdComparer.Equals(shellId, requestedShellId); |
| | | 237 | | |
| | | 238 | | private static ShellSettings Clone(ShellSettings source) |
| | | 239 | | { |
| | 33 | 240 | | var clone = new ShellSettings(source.Id, source.EnabledFeatures) |
| | 33 | 241 | | { |
| | 33 | 242 | | ConfigurationData = new Dictionary<string, object>(source.ConfigurationData, StringComparer.OrdinalIgnoreCas |
| | 33 | 243 | | }; |
| | | 244 | | |
| | 66 | 245 | | foreach (var configurator in source.FeatureConfigurators) |
| | 0 | 246 | | clone.FeatureConfigurators[configurator.Key] = configurator.Value; |
| | | 247 | | |
| | 33 | 248 | | return clone; |
| | | 249 | | } |
| | | 250 | | |
| | | 251 | | public void Dispose() |
| | | 252 | | { |
| | 3 | 253 | | if (_disposed) |
| | 0 | 254 | | return; |
| | | 255 | | |
| | 3 | 256 | | _reloadLock.Dispose(); |
| | 3 | 257 | | _disposed = true; |
| | 3 | 258 | | } |
| | | 259 | | } |