我已经写了一个非常基本的基于滑动窗口的异步速率限制器,主要是通过使用信号量及其AsyncWait方法修改同步滑动窗口解决方案。
public class TaskRateLimiter : ITaskRateLimiter
{
private SlidingWindow _window;
public TaskRateLimiter(int maxCount, TimeSpan time)
{
_window = new SlidingWindow(maxCount, time);
}
public async Task RateLimit(Func<Task> function)
{
await _window.WaitExecute();
await function();
}
public async Task<T> RateLimit<T>(Func<Task<T>> function)
{
await _window.WaitExecute();
return await function();
}
private class SlidingWindow
{
private int _count;
private TimeSpan _time;
private Queue<DateTime> _window = new Queue<DateTime>();
private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public SlidingWindow(int count, TimeSpan time)
{
_count = count;
_time = time;
}
private void RefreshWindow()
{
var now = DateTime.Now;
while (_window.Count > 0 && _window.Peek().Add(_time) < now)
{
_window.Dequeue();
}
}
private bool CanExecute()
{
RefreshWindow();
return _window.Count < _count;
}
public async Task WaitExecute()
{
await _semaphore.WaitAsync();
try
{
if (!CanExecute())
{
await Task.Delay(_window.Peek().Add(_time).Subtract(DateTime.Now));
}
_window.Enqueue(DateTime.Now);
}
finally
{
_semaphore.Release();
}
}
}
}
在开发环境中,该速率限制器的性能似乎不错,但是我担心没有工作单元测试就可以投入生产,但是我的单元测试会定期失败。
[TestClass]
public class TaskRateLimiterTests
{
private int _numEvents;
private TimeSpan _timeSpan;
private TaskRateLimiter _rateLimiter;
private Stopwatch _stopWatch;
private List<int> _testList;
private Func<Task> _funcToLimit;
private Func<Task<int>> _funcWithReturnToLimit;
[TestInitialize]
public void Setup()
{
_numEvents = 10;
_timeSpan = TimeSpan.FromMilliseconds(50);
_rateLimiter = new TaskRateLimiter(_numEvents, _timeSpan);
_stopWatch = new Stopwatch();
_testList = new List<int>();
_funcWithReturnToLimit = new Func<Task<int>>(() => { _testList.Add(5); return Task.FromResult(5); });
}
[TestMethod]
public async Task RateLimit_NotExceeded()
{
var tempList = new List<int>();
_stopWatch.Start();
for (int i = 0; i < _numEvents + 1; i++)
{
tempList.Add(await _rateLimiter.RateLimit(_funcWithReturnToLimit));
}
_stopWatch.Stop();
Assert.IsTrue(_stopWatch.Elapsed > _timeSpan)
Assert.IsTrue(tempList.All(x => x.Equals(5)));
}
}
我不喜欢测试与时间相关的任何内容的单元测试,但是对于速率限制器,我想不出任何其他方法来做到这一点。当测试失败时,我通常会看到以下内容:
实际:492630,下限:500000
秒表经过的刻度线刚好在下限的下方。 我怀疑执行时间短的函数可以修复测试。 但是,这并不能告诉我为什么它会定期失败,是因为秒表的不精确性吗? 我有更好的方法来测量这些时间间隔吗? 我忽略的被测代码中有错误吗? 我通常会在10,000次中发生5次。