namespace AcDream.App.Net; /// One named owner operation in the live-session reset transaction. internal sealed record LiveSessionResetStage(string Name, Action Reset); /// /// Executes every session-state reset stage even when an earlier owner fails. /// A failed attempt throws after the complete manifest has run; lifecycle code /// therefore cannot construct the next session until a later attempt converges. /// internal sealed class LiveSessionResetPlan { private readonly LiveSessionResetStage[] _stages; private int _executing; public LiveSessionResetPlan(IEnumerable stages) { ArgumentNullException.ThrowIfNull(stages); _stages = stages.ToArray(); var names = new HashSet(StringComparer.Ordinal); foreach (LiveSessionResetStage stage in _stages) { ArgumentNullException.ThrowIfNull(stage); if (string.IsNullOrWhiteSpace(stage.Name)) throw new ArgumentException("Reset stage names must be non-empty.", nameof(stages)); ArgumentNullException.ThrowIfNull(stage.Reset); if (!names.Add(stage.Name)) throw new ArgumentException( $"Duplicate live-session reset stage '{stage.Name}'.", nameof(stages)); } } public IReadOnlyList StageNames => Array.ConvertAll(_stages, static stage => stage.Name); public void Execute() { if (Interlocked.Exchange(ref _executing, 1) != 0) throw new InvalidOperationException( "Live-session reset cannot run concurrently or reentrantly."); List? failures = null; try { foreach (LiveSessionResetStage stage in _stages) { try { stage.Reset(); } catch (Exception error) { (failures ??= []).Add(new LiveSessionResetStageException( stage.Name, error)); } } } finally { Volatile.Write(ref _executing, 0); } if (failures is not null) throw new AggregateException( "Live-session state did not converge; a new session must not start.", failures); } } internal sealed class LiveSessionResetStageException( string stageName, Exception innerException) : Exception( $"Live-session reset stage '{stageName}' failed.", innerException) { public string StageName { get; } = stageName; }