using System.Collections.Concurrent; namespace Azaion.Common.Extensions; public static class ThrottleExt { private class ThrottleState(Func action) { public Func Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action)); public bool IsCoolingDown = false; public bool CallScheduledDuringCooldown = false; public Task CooldownTask = Task.CompletedTask; public readonly object StateLock = new(); } private static readonly ConcurrentDictionary ThrottlerStates = new(); public static void Throttle(Func action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false) { ArgumentNullException.ThrowIfNull(action); if (actionId == Guid.Empty) throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId)); if (interval <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive."); var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action)); state.Action = action; lock (state.StateLock) { if (!state.IsCoolingDown) { state.IsCoolingDown = true; state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state); } else { if (scheduleCallAfterCooldown) state.CallScheduledDuringCooldown = true; } } } private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state) { try { await state.Action(); } catch (Exception ex) { Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}"); } finally { await Task.Delay(interval); lock (state.StateLock) { if (state.CallScheduledDuringCooldown) { state.CallScheduledDuringCooldown = false; state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state); } else { state.IsCoolingDown = false; } } } } }