Лучший способ отрегулировать исходящие запросы в C # по времени и некоторые дополнительные критерии? - PullRequest
0 голосов
/ 28 сентября 2019

Мне нужно вызвать внешний HTTP API, который разрешает только один запрос каждые 4 секунды по идентификатору пользователя.Пока я вызываю этот API, каждый раз отправляя новый идентификатор пользователя, я могу вызывать его с любой скоростью.

В этом коде я могу соответствовать скорости внешнего API, но я неделать это оптимальным способом, так как некоторые запросы блокируются предыдущими вызовами, даже если этот userId не должен ждать.(Проверьте комментарии в коде)

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            var caller = new ExternalAPICaller();

            caller.RunCalls();

            Console.ReadKey();
        }
    }

    public class ExternalAPICaller
    {
        private static SemaphoreSlim throttler = new SemaphoreSlim(20); // up to 20 concurrent calls

        private static List<string> userIds = new List<string>();

        private const int rateLimitByUser = 4000;

        public async Task CallAPIWithThrottling(string userId)
        {
            if (userIds.Contains(userId)) Thread.Sleep(rateLimitByUser);

            userIds.Add(userId);

            await throttler.WaitAsync();

            var task = MockHttpCall(userId);

            _ = task.ContinueWith(async s =>
            {
                await Task.Delay(rateLimitByUser);
                throttler.Release();
                userIds.Remove(userId);
            });
        }

        public Task MockHttpCall(string id)
        {
            Console.WriteLine("http call for " + id);
            Thread.Sleep(300);
            return Task.CompletedTask;
        }

        public async Task RunCalls()
        {
            await CallAPIWithThrottling("Mike");
            await CallAPIWithThrottling("John");
            await CallAPIWithThrottling("Sarah");
            await CallAPIWithThrottling("Matt");

            await CallAPIWithThrottling("John");
            await CallAPIWithThrottling("Jacob"); // this should be called right away, but the second John makes it wait

            await CallAPIWithThrottling("John");
            await CallAPIWithThrottling("Amy"); // this should be called right away, but the thrid John makes it wait
        }
    }
}

1 Ответ

1 голос
/ 28 сентября 2019

Я бы попытался абстрагировать функциональность дросселирования, чтобы проверить ее самостоятельно.Я хотел бы создать класс 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-вызов для Джона

...