namespace Bitget_Server.Infrastructure.Bitget; /// /// Bitget REST API에 대한 클라이언트 사이드 Rate Limiter. /// Sliding window 방식으로 1초당 N회 호출 제한을 강제한다. /// 모든 외부 호출은 await AcquireAsync(...) 후에 수행되어야 한다. /// public class BitgetRateLimiter { private readonly int _maxRequestsPerSecond; private readonly Queue _requestTicks = new(); private readonly SemaphoreSlim _gate = new(1, 1); private readonly ILogger _logger; public BitgetRateLimiter(int maxRequestsPerSecond, ILogger logger) { // 0 또는 음수 → 무제한 모드. 양수 → 슬라이딩 윈도우 적용. _maxRequestsPerSecond = maxRequestsPerSecond <= 0 ? int.MaxValue : maxRequestsPerSecond; _logger = logger; if (_maxRequestsPerSecond == int.MaxValue) _logger.LogWarning("BitgetRateLimiter: RateLimitPerSecond <= 0 — 무제한 모드로 동작합니다."); } /// /// 호출 슬롯이 확보될 때까지 대기한 뒤 즉시 반환한다. /// 무제한 모드(_maxRequestsPerSecond == int.MaxValue) 에서는 즉시 반환. /// public async Task AcquireAsync(CancellationToken ct = default) { if (_maxRequestsPerSecond == int.MaxValue) return; await _gate.WaitAsync(ct).ConfigureAwait(false); try { int totalWaitedMs = 0; while (true) { var now = Environment.TickCount64; // 1초 이전 요청 기록 제거 while (_requestTicks.Count > 0 && now - _requestTicks.Peek() >= 1000) _requestTicks.Dequeue(); if (_requestTicks.Count < _maxRequestsPerSecond) { _requestTicks.Enqueue(now); if (totalWaitedMs >= 100) _logger.LogInformation("Rate limit throttled {WaitMs}ms (queue={Count}/{Max})", totalWaitedMs, _requestTicks.Count, _maxRequestsPerSecond); return; } // 가장 오래된 요청이 1초 경과할 때까지 대기 var waitMs = 1000 - (int)(now - _requestTicks.Peek()) + 1; if (waitMs < 1) waitMs = 1; totalWaitedMs += waitMs; _logger.LogDebug("Rate limit hit. Waiting {WaitMs}ms (queue={Count}/{Max})", waitMs, _requestTicks.Count, _maxRequestsPerSecond); _gate.Release(); try { await Task.Delay(waitMs, ct).ConfigureAwait(false); } finally { await _gate.WaitAsync(ct).ConfigureAwait(false); } } } finally { _gate.Release(); } } }