namespace AcDream.App.Rendering.Wb; /// /// Per-resource progress for a teardown which must attempt every independent /// release even when an earlier release fails. Successful operations are never /// replayed. A backend which throws after changing ownership reports that fact /// with so the /// ledger can preserve the physical outcome while still surfacing the error. /// internal sealed class RetryableResourceReleaseLedger { private readonly Entry[] _entries; private int _remaining; private bool _advancing; public RetryableResourceReleaseLedger(IEnumerable<(string Name, Action Release)> entries) { ArgumentNullException.ThrowIfNull(entries); _entries = entries .Select(entry => new Entry(entry.Name, entry.Release)) .ToArray(); _remaining = _entries.Length; } public bool IsComplete => _remaining == 0; public int RemainingCount => _remaining; /// /// Attempts every unfinished operation once. Ordinary exceptions retain /// the operation for a later attempt; committed-outcome exceptions mark it /// complete. Either kind remains in the returned diagnostics. /// public ResourceReleaseAttempt Advance() { // Release callbacks may synchronously drain their owner. The active // top-level pass already owns every unfinished entry; a nested pass // must not give a failed later entry a second attempt in this frame. if (_advancing) return new ResourceReleaseAttempt(0, 0, []); _advancing = true; List? failures = null; int attempted = 0; int completed = 0; try { for (int i = 0; i < _entries.Length; i++) { Entry entry = _entries[i]; if (entry.Completed || entry.Running) continue; attempted++; entry.Running = true; try { entry.Release(); entry.Completed = true; _remaining--; completed++; } catch (Exception error) { bool committed = error is MeshReferenceMutationException { MutationCommitted: true, }; if (committed) { entry.Completed = true; _remaining--; completed++; } (failures ??= []).Add( new ResourceReleaseFailure(entry.Name, error, committed)); } finally { entry.Running = false; } } } finally { _advancing = false; } return new ResourceReleaseAttempt( attempted, completed, failures ?? []); } private sealed class Entry { public Entry(string name, Action release) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("A resource-release stage needs a name.", nameof(name)); ArgumentNullException.ThrowIfNull(release); Name = name; Release = release; } public string Name { get; } public Action Release { get; } public bool Completed { get; set; } public bool Running { get; set; } } } internal readonly record struct ResourceReleaseFailure( string Stage, Exception Error, bool MutationCommitted); internal readonly record struct ResourceReleaseAttempt( int AttemptedCount, int CompletedCount, IReadOnlyList Failures) { public bool HasFailures => Failures.Count != 0; public AggregateException ToException(string message) => new( message, Failures.Select(failure => new InvalidOperationException( $"Resource-release stage '{failure.Stage}' failed " + (failure.MutationCommitted ? "after committing its mutation." : "before committing its mutation."), failure.Error))); }