diff --git a/RateLimiter/CallCountByIntervalAwaitableConstraint.cs b/RateLimiter/CallCountByIntervalAwaitableConstraint.cs new file mode 100644 index 0000000..2bf2b0b --- /dev/null +++ b/RateLimiter/CallCountByIntervalAwaitableConstraint.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace RateLimiter +{ + /// + /// Provide an awaitable constraint based on number of times per duration + /// + public class CallCountByIntervalAwaitableConstraint : IAwaitableConstraint + { + /// + /// List of the last time stamps + /// + public IReadOnlyList TimeStamps => _TimeStamps.ToList(); + + /// + /// Stack of the last time stamps + /// + protected LimitedSizeStack _TimeStamps { get; } + + private int _Count { get; } + private TimeSpan _TimeSpan { get; } + private SemaphoreSlim _Semaphore { get; } = new SemaphoreSlim(1, 1); + private ITime _Time { get; } + + /// + /// Constructs a new AwaitableConstraint based on number of times per duration + /// + /// + /// + public CallCountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan) : this(count, timeSpan, TimeSystem.StandardTime) + { + } + + internal CallCountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan, ITime time) + { + if (count <= 0) + throw new ArgumentException("count should be strictly positive", nameof(count)); + + if (timeSpan.TotalMilliseconds <= 0) + throw new ArgumentException("timeSpan should be strictly positive", nameof(timeSpan)); + + _Count = count; + _TimeSpan = timeSpan; + _TimeStamps = new LimitedSizeStack(_Count); + _Time = time; + } + + /// + /// returns a task that will complete once the constraint is fulfilled + /// + /// + /// Cancel the wait + /// + /// + /// A disposable that should be disposed upon task completion + /// + public async Task WaitForReadiness(CancellationToken cancellationToken) + { + try + { + + await _Semaphore.WaitAsync(cancellationToken); + var count = 0; + var now = _Time.GetNow(); + var target = now - _TimeSpan; + LinkedListNode element = _TimeStamps.First, last = null; + while ((element != null) && (element.Value > target)) + { + last = element; + element = element.Next; + count++; + } + + if (count >= _Count) + { + var timeToWait = last.Value.Add(_TimeSpan) - now; + now = now.Add(timeToWait); + Debug.Assert(element == null); + Debug.Assert(last != null); + await _Time.GetDelay(timeToWait, cancellationToken); + } + _TimeStamps.Push(now); + _Semaphore.Release(); + + return new DisposeAction(OnEnded); + } + catch (Exception) + { + _Semaphore.Release(); + throw; + } + } + + /// + /// Clone CallCountByIntervalAwaitableConstraint + /// + /// + public IAwaitableConstraint Clone() + { + return new CallCountByIntervalAwaitableConstraint(_Count, _TimeSpan, _Time); + } + + private void OnEnded() + { + OnEnded(TimeSystem.StandardTime.GetNow()); + } + + /// + /// Called when action has been executed + /// + /// + protected virtual void OnEnded(DateTime now) + { + } + } +} diff --git a/RateLimiter/TimeLimiter.cs b/RateLimiter/TimeLimiter.cs index e36b8e9..13f176b 100644 --- a/RateLimiter/TimeLimiter.cs +++ b/RateLimiter/TimeLimiter.cs @@ -164,6 +164,16 @@ public static TimeLimiter GetFromMaxCountByInterval(int maxCount, TimeSpan timeS return new TimeLimiter(new CountByIntervalAwaitableConstraint(maxCount, timeSpan)); } + /// + /// Returns a TimeLimiter based on a maximum number of parallel calls + /// + /// + /// + /// + public static TimeLimiter GetFromMaxCallCountByInterval(int maxCount, TimeSpan timeSpan) + { + return new TimeLimiter(new CallCountByIntervalAwaitableConstraint(maxCount, timeSpan)); + } /// /// Create that will save state using action passed through parameter. ///