using System.Collections.Concurrent; namespace SharepointToolbox.Web.Services.Reports; /// /// Process-wide coordinator for scheduled-report execution. Covers two operator needs: /// • Cancel an in-flight run — every active run registers a linked /// keyed by schedule id, so the UI (or a /// global stop) can abort a report that is currently executing, whether it was /// started by the scheduler or by "Run now". /// • Pause future runs — a global flag the background scheduler honours, /// letting an admin suspend all cadence-triggered runs at once without toggling /// each schedule's Enabled flag. /// /// In-memory and singleton. The pause flag does NOT survive a process restart (a /// restart resumes the scheduler); per-schedule Enabled flags persist and are /// the durable way to keep a schedule off. /// public sealed class ScheduledRunCoordinator { private readonly ConcurrentDictionary _active = new(); private volatile bool _paused; /// True while the scheduler is globally paused (no schedules fire). public bool IsPaused => _paused; public void Pause() => _paused = true; public void Resume() => _paused = false; /// True while a run is registered for this schedule id. public bool IsRunning(string scheduleId) => _active.ContainsKey(scheduleId); /// Snapshot of schedule ids with a run in progress. public IReadOnlyCollection RunningIds => _active.Keys.ToArray(); /// /// Registers a run for and returns a token that trips /// when either the caller's token (e.g. app shutdown) or a /// / call fires. Returns null if a /// run is already registered for this schedule — callers must skip to avoid overlap. /// Always pair a non-null return with in a finally. /// public CancellationToken? TryBegin(string scheduleId, CancellationToken linked) { var cts = CancellationTokenSource.CreateLinkedTokenSource(linked); if (!_active.TryAdd(scheduleId, cts)) { cts.Dispose(); return null; } return cts.Token; } /// Deregisters and disposes the run for this schedule id. public void Complete(string scheduleId) { if (_active.TryRemove(scheduleId, out var cts)) cts.Dispose(); } /// Signals cancellation to the run for this schedule id. Returns false if none. public bool Cancel(string scheduleId) { if (_active.TryGetValue(scheduleId, out var cts)) { try { cts.Cancel(); return true; } catch (ObjectDisposedException) { return false; } // completed between lookup and cancel } return false; } /// Signals cancellation to every run in progress. Returns the count signalled. public int CancelAll() { int n = 0; foreach (var cts in _active.Values) { try { cts.Cancel(); n++; } catch (ObjectDisposedException) { /* completed concurrently */ } } return n; } }