using System.Collections.Concurrent; using Microsoft.Extensions.Logging; namespace IceImportBLL.IceImportRun; // Singleton (registered explicitly in IceImportModule.Register(), same as // AutomationSL.Hubs.AutomationRunNotifier's singleton registration) — a ConcurrentDictionary with // disciplined eviction via Complete(), never left to grow unbounded, per CLAUDE.md's // "Static Collections" anti-pattern guidance. public class IceImportRunCancellationRegistry : IIceImportRunCancellationRegistry { private readonly ConcurrentDictionary _tokens = new(); private readonly ILogger _logger; public IceImportRunCancellationRegistry(ILogger logger) => _logger = logger; public CancellationToken Register(long runId, CancellationToken callerToken) { var cts = CancellationTokenSource.CreateLinkedTokenSource(callerToken); // Replace-and-dispose any stale entry (shouldn't normally happen — one CTS per RunId for // its single background execution — but guards against a double-Register on retry paths). var previous = _tokens.AddOrUpdate(runId, cts, (_, old) => { old.Dispose(); return cts; }); return previous.Token; } public bool TryCancel(long runId) { if (!_tokens.TryGetValue(runId, out var cts)) return false; try { cts.Cancel(); return true; } catch (ObjectDisposedException) { // Run completed and was disposed between TryGetValue and Cancel — benign race. return false; } } public void Complete(long runId) { if (_tokens.TryRemove(runId, out var cts)) { try { cts.Dispose(); } catch (Exception ex) { _logger.LogWarning(ex, "IceImportRunCancellationRegistry: dispose failed for RunId {RunId}", runId); } } } }