Files
BitGet_API/Bitget_Server/Infrastructure/Bitget/BitgetRateLimiter.cs

72 lines
2.8 KiB
C#
Raw Normal View History

2026-04-20 20:27:42 +09:00
namespace Bitget_Server.Infrastructure.Bitget;
/// <summary>
/// Bitget REST API에 대한 클라이언트 사이드 Rate Limiter.
/// Sliding window 방식으로 1초당 N회 호출 제한을 강제한다.
/// 모든 외부 호출은 await AcquireAsync(...) 후에 수행되어야 한다.
/// </summary>
public class BitgetRateLimiter
{
private readonly int _maxRequestsPerSecond;
private readonly Queue<long> _requestTicks = new();
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly ILogger<BitgetRateLimiter> _logger;
public BitgetRateLimiter(int maxRequestsPerSecond, ILogger<BitgetRateLimiter> logger)
{
// 0 또는 음수 → 무제한 모드. 양수 → 슬라이딩 윈도우 적용.
_maxRequestsPerSecond = maxRequestsPerSecond <= 0 ? int.MaxValue : maxRequestsPerSecond;
_logger = logger;
if (_maxRequestsPerSecond == int.MaxValue)
_logger.LogWarning("BitgetRateLimiter: RateLimitPerSecond <= 0 — 무제한 모드로 동작합니다.");
}
/// <summary>
/// 호출 슬롯이 확보될 때까지 대기한 뒤 즉시 반환한다.
/// 무제한 모드(_maxRequestsPerSecond == int.MaxValue) 에서는 즉시 반환.
/// </summary>
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();
}
}
}