< Summary

Information
Class: Services.ApiServices.MlAutoApi.MlAutoRateLimiter
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MlAutoApi/MlAutoRateLimiter.cs
Line coverage
89%
Covered lines: 26
Uncovered lines: 3
Coverable lines: 29
Total lines: 62
Line coverage: 89.6%
Branch coverage
92%
Covered branches: 13
Total branches: 14
Branch coverage: 92.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_Enabled()100%11100%
WaitAsync()83.33%7670%
Penalize()100%11100%
TryAcquire(...)100%88100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/MlAutoApi/MlAutoRateLimiter.cs

#LineLine coverage
 1namespace Services.ApiServices.MlAutoApi;
 2
 3/// <summary>
 4/// Скользящее окно «не более N запросов в минуту» на один логин ML-Auto. Поставщик сам режет запросы сверх лимита
 5/// ответом HTTP 200 со STATUS != 200, поэтому без ограничителя фоновый опрос статусов упирается в лимит и ломает
 6/// заодно интерактивные вызовы. После ответа «лимит исчерпан» окно закрывается на <see cref="PenaltyDuration"/>.
 7/// </summary>
 8public sealed class MlAutoRateLimiter
 9{
 110    public static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
 111    public static readonly TimeSpan PenaltyDuration = TimeSpan.FromSeconds(60);
 12    /// <summary>Дольше этого вызов не ждёт очереди: интерактивный запрос лучше упасть, чем висеть минутами.</summary>
 113    public static readonly TimeSpan MaxWait = TimeSpan.FromSeconds(30);
 14
 15    private readonly int _limit;
 11416    private readonly Queue<DateTime> _hits = new();
 11417    private readonly object _gate = new();
 11418    private DateTime _blockedUntilUtc = DateTime.MinValue;
 19
 22820    public MlAutoRateLimiter(int requestsPerMinute) => _limit = requestsPerMinute;
 21
 22    /// <summary>Лимитер выключен (0 и меньше): пропускает всё.</summary>
 111123    public bool Enabled => _limit > 0;
 24
 25    public async Task WaitAsync(CancellationToken ct)
 26    {
 211127        if (!Enabled) return;
 11128        var waited = TimeSpan.Zero;
 029        while (true)
 30        {
 11131            var delay = TryAcquire(DateTime.UtcNow);
 22132            if (delay == TimeSpan.Zero) return;
 133            if (waited + delay > MaxWait)
 134                throw new MlAutoApiException("Превышен локальный лимит запросов к ML-Auto, повторите позже.") { IsRateLi
 035            await Task.Delay(delay, ct);
 036            waited += delay;
 37        }
 111038    }
 39
 40    /// <summary>Поставщик ответил «лимит исчерпан»: до конца паузы новые запросы не уходят.</summary>
 41    public void Penalize()
 42    {
 143        lock (_gate)
 144            _blockedUntilUtc = DateTime.UtcNow + PenaltyDuration;
 145    }
 46
 47    /// <summary>Занимает слот и возвращает ноль либо сколько ждать до освобождения.</summary>
 48    internal TimeSpan TryAcquire(DateTime nowUtc)
 49    {
 11550        lock (_gate)
 51        {
 11652            if (_blockedUntilUtc > nowUtc) return _blockedUntilUtc - nowUtc;
 11853            while (_hits.Count > 0 && _hits.Peek() <= nowUtc - Window) _hits.Dequeue();
 11454            if (_hits.Count < _limit)
 55            {
 11356                _hits.Enqueue(nowUtc);
 11357                return TimeSpan.Zero;
 58            }
 159            return _hits.Peek() + Window - nowUtc + TimeSpan.FromMilliseconds(10);
 60        }
 11561    }
 62}