Я бы попытался абстрагировать функциональность дросселирования, чтобы проверить ее самостоятельно.Я хотел бы создать класс Throttler
, который можно настроить с глобальными ограничениями и задержками параллелизма для каждого пользователя.В вашем случае конфигурация будет:
Глобальный предел параллелизма: 20
Глобальная задержка: 0 (разрешены одновременные запросы для разных пользователей)
Предел параллелизма на пользователя: 1
Задержка на пользователя: 4000
Вот реализация класса Throttler
.Предел параллелизма на пользователя для простоты опущен (для этого потребуется секунда SemaphoreSlim
на пользователя).
public class Throttler<TKey>
{
private readonly SemaphoreSlim _globalConcurrencySemaphore;
private readonly SemaphoreSlim _globalDelaySemaphore;
private readonly int _globalDelay;
private readonly int _perKeyDelay;
private readonly ConcurrentDictionary<TKey, SemaphoreSlim> _perKeyDelaySemaphores;
public Throttler(int globalConcurrencyLimit, int globalDelay, int perKeyDelay)
{
_globalConcurrencySemaphore = new SemaphoreSlim(globalConcurrencyLimit);
_globalDelaySemaphore = new SemaphoreSlim(1);
_globalDelay = globalDelay;
_perKeyDelay = perKeyDelay;
_perKeyDelaySemaphores = new ConcurrentDictionary<TKey, SemaphoreSlim>();
}
public async Task<TResult> Execute<TResult>(TKey key,
Func<Task<TResult>> taskFactory)
{
var perKeyDelaySemaphore = _perKeyDelaySemaphores.GetOrAdd(
key, _ => new SemaphoreSlim(1));
await perKeyDelaySemaphore.WaitAsync().ConfigureAwait(false);
ReleaseAsync(perKeyDelaySemaphore, _perKeyDelay);
await _globalDelaySemaphore.WaitAsync().ConfigureAwait(false);
ReleaseAsync(_globalDelaySemaphore, _globalDelay);
await _globalConcurrencySemaphore.WaitAsync().ConfigureAwait(false);
try
{
var task = taskFactory();
return await task.ConfigureAwait(false);
}
finally
{
_globalConcurrencySemaphore.Release();
}
}
private async void ReleaseAsync(SemaphoreSlim semaphore, int delay)
{
await Task.Delay(delay).ConfigureAwait(false);
semaphore.Release();
}
}
Задержка между получением одного семафора и следующим.Задержка HTTP-вызова не учитывается.
Пример использования:
var throttler = new Throttler<string>(20, 0, 4000);
var keys = new string[] { "Mike", "John", "Sarah", "Matt", "John", "Jacob",
"John", "Amy" };
var tasks = new List<Task>();
foreach (var key in keys)
{
tasks.Add(throttler.Execute(key, () => MockHttpCall(key)));
}
Task.WaitAll(tasks.ToArray());
async Task<int> MockHttpCall(string id)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} HTTP call for " + id);
await Task.Delay(300);
return 0;
}
Вывод:
11: 20: 41.635 HTTP-вызов для Майка
11: 20: 41.652 HTTP-вызов для Джона
11: 20: 41.652 HTTP-вызов для Сары
11: 20: 41.652 HTTP-вызов для Мэтта
11: 20: 41.653 HTTP-вызов для Джейкоба
11: 20: 41.654 HTTP-вызов для Эми
11: 20: 45.965 HTTP-вызов для Джона
11: 20: 50.272 HTTP-вызов для Джона