Skip to content

Instantly share code, notes, and snippets.

@tintoy
Last active October 30, 2019 20:57
Show Gist options
  • Save tintoy/cb313dab12b4e70f0d78fcb0ad29e065 to your computer and use it in GitHub Desktop.
Save tintoy/cb313dab12b4e70f0d78fcb0ad29e065 to your computer and use it in GitHub Desktop.
Windowed rate-counter
/// <summary>
/// A simple windowed rate counter.
/// </summary>
/// <remarks>
/// Tracks an event count over a specific (moving) period of time. Locking omitted for clarity.
/// </remarks>
public class RateCounter
{
/// <summary>
/// Timestamps representing each event tracked by the counter.
/// </summary>
readonly Queue<DateTime> _timestamps = new Queue<DateTime>();
/// <summary>
/// The maximum rate permitted by the counter.
/// </summary>
readonly int _maxRate;
/// <summary>
/// The period of time over which the rate is evaluated.
/// </summary>
readonly TimeSpan _period;
/// <summary>
/// Create a new <see cref="RateCounter"/>.
/// </summary>
/// <param name="maxRate">
/// The maximum rate permitted by the counter.
/// </param>
/// <param name="period">
/// The period of time over which the rate is evaluated.
/// </param>
public RateCounter(int maxRate, TimeSpan period)
{
if (maxRate < 1)
throw new ArgumentOutOfRangeException(nameof(maxRate), maxRate, "Maximum rate cannot be less than 1.");
if (period <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(period), period, "Period must be a positive time span.");
_maxRate = maxRate;
_period = period;
}
/// <summary>
/// The period of time (window) over which the counter is evaluated.
/// </summary>
public TimeSpan Period => _period;
/// <summary>
/// The current rate measured by the counter.
/// </summary>
public int Rate => _timestamps.Count;
/// <summary>
/// The maximum rate permitted by the counter.
/// </summary>
public int MaxRate => _maxRate;
/// <summary>
/// Attempt to increment the rate by 1.
/// </summary>
/// <returns>
/// <c>false</c>, if the rate was not incremented because it is already at its maximum; otherwise, <c>true</c>.
/// </returns>
public bool Increment()
{
DateTime now = DateTime.Now;
TrimToWindow(now);
// We only increment if there is "room" for the new value.
// Since this rate counter is used to reject requests that are above tenant-level quotas,
// we don't want to register a request if we'd just wind up rejecting it.
if (Rate < MaxRate)
{
_timestamps.Enqueue(now);
return true;
}
return false;
}
/// <summary>
/// Trim the current set of timestamps, removing any that now lie outside the window represented by <see cref="Period"/>.
/// </summary>
/// <param name="now">
/// A <see cref="DateTime"/> representing the current timestamp.
/// </param>
void TrimToWindow(DateTime now)
{
while (_timestamps.TryPeek(out DateTime oldestTimestamp) && (now - oldestTimestamp) > Period)
_timestamps.Dequeue();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment