72 lines
2.8 KiB
C#
72 lines
2.8 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|