Я написал очень простой асинхронный ограничитель скорости, основанный на скользящем окне, главным образом путем адаптации решения синхронного скользящего окна с семафором и его методом 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
, где прошедшие отметки секундомера находятся чуть ниже нижнего уровня.Я подозреваю, что функция с нетривиальным временем выполнения исправит тест.Однако, это не говорит мне, почему это периодически терпит неудачу, это из-за неточности в секундомере?Есть ли лучший способ для меня, чтобы измерить эти временные интервалы?Есть ли ошибка в тестируемом коде, которую я пропускаю?Я обычно вижу, что это происходит ~ 5 раз за 10000.