Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows parallel actions call #20

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions RateLimiter/CallCountByIntervalAwaitableConstraint.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provide an awaitable constraint based on number of times per duration
/// </summary>
public class CallCountByIntervalAwaitableConstraint : IAwaitableConstraint
{
/// <summary>
/// List of the last time stamps
/// </summary>
public IReadOnlyList<DateTime> TimeStamps => _TimeStamps.ToList();

/// <summary>
/// Stack of the last time stamps
/// </summary>
protected LimitedSizeStack<DateTime> _TimeStamps { get; }

private int _Count { get; }
private TimeSpan _TimeSpan { get; }
private SemaphoreSlim _Semaphore { get; } = new SemaphoreSlim(1, 1);
private ITime _Time { get; }

/// <summary>
/// Constructs a new AwaitableConstraint based on number of times per duration
/// </summary>
/// <param name="count"></param>
/// <param name="timeSpan"></param>
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<DateTime>(_Count);
_Time = time;
}

/// <summary>
/// returns a task that will complete once the constraint is fulfilled
/// </summary>
/// <param name="cancellationToken">
/// Cancel the wait
/// </param>
/// <returns>
/// A disposable that should be disposed upon task completion
/// </returns>
public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
{
try
{

await _Semaphore.WaitAsync(cancellationToken);
var count = 0;
var now = _Time.GetNow();
var target = now - _TimeSpan;
LinkedListNode<DateTime> 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;
}
}

/// <summary>
/// Clone CallCountByIntervalAwaitableConstraint
/// </summary>
/// <returns></returns>
public IAwaitableConstraint Clone()
{
return new CallCountByIntervalAwaitableConstraint(_Count, _TimeSpan, _Time);
}

private void OnEnded()
{
OnEnded(TimeSystem.StandardTime.GetNow());
}

/// <summary>
/// Called when action has been executed
/// </summary>
/// <param name="now"></param>
protected virtual void OnEnded(DateTime now)
{
}
}
}
10 changes: 10 additions & 0 deletions RateLimiter/TimeLimiter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ public static TimeLimiter GetFromMaxCountByInterval(int maxCount, TimeSpan timeS
return new TimeLimiter(new CountByIntervalAwaitableConstraint(maxCount, timeSpan));
}

/// <summary>
/// Returns a TimeLimiter based on a maximum number of parallel calls
/// </summary>
/// <param name="maxCount"></param>
/// <param name="timeSpan"></param>
/// <returns></returns>
public static TimeLimiter GetFromMaxCallCountByInterval(int maxCount, TimeSpan timeSpan)
{
return new TimeLimiter(new CallCountByIntervalAwaitableConstraint(maxCount, timeSpan));
}
/// <summary>
/// Create <see cref="TimeLimiter"/> that will save state using action passed through <paramref name="saveStateAction"/> parameter.
/// </summary>
Expand Down