107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from time import monotonic_ns
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class BingxRateLimitSnapshot:
|
||
|
|
rest_remaining: int | None = None
|
||
|
|
rest_reset_ms: int | None = None
|
||
|
|
ws_listenkey_ops: int = 0
|
||
|
|
ws_listenkey_window_ns: int = 0
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class BingxCircuitBreakerSnapshot:
|
||
|
|
failure_count: int = 0
|
||
|
|
open_until_ns: int = 0
|
||
|
|
last_delay_ms: int = 0
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_open(self) -> bool:
|
||
|
|
return monotonic_ns() < self.open_until_ns
|
||
|
|
|
||
|
|
|
||
|
|
class BingxRateLimitTracker:
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self._rest_remaining: int | None = None
|
||
|
|
self._rest_reset_ms: int | None = None
|
||
|
|
self._ws_listenkey_ops = 0
|
||
|
|
self._ws_window_start_ns = monotonic_ns()
|
||
|
|
|
||
|
|
def update_rest_headers(self, headers: dict[str, str]) -> None:
|
||
|
|
remain = headers.get("x-ratelimit-requests-remain")
|
||
|
|
reset = headers.get("x-ratelimit-requests-expire")
|
||
|
|
self._rest_remaining = int(remain) if remain is not None and str(remain).isdigit() else self._rest_remaining
|
||
|
|
self._rest_reset_ms = int(reset) if reset is not None and str(reset).isdigit() else self._rest_reset_ms
|
||
|
|
|
||
|
|
def count_ws_listenkey_op(self) -> None:
|
||
|
|
now = monotonic_ns()
|
||
|
|
if now - self._ws_window_start_ns > 1_000_000_000:
|
||
|
|
self._ws_window_start_ns = now
|
||
|
|
self._ws_listenkey_ops = 0
|
||
|
|
self._ws_listenkey_ops += 1
|
||
|
|
|
||
|
|
def snapshot(self) -> BingxRateLimitSnapshot:
|
||
|
|
return BingxRateLimitSnapshot(
|
||
|
|
rest_remaining=self._rest_remaining,
|
||
|
|
rest_reset_ms=self._rest_reset_ms,
|
||
|
|
ws_listenkey_ops=self._ws_listenkey_ops,
|
||
|
|
ws_listenkey_window_ns=monotonic_ns() - self._ws_window_start_ns,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class BingxCircuitBreaker:
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
*,
|
||
|
|
failure_threshold: int = 3,
|
||
|
|
base_backoff_ms: int = 250,
|
||
|
|
max_backoff_ms: int = 2_000,
|
||
|
|
) -> None:
|
||
|
|
self._failure_threshold = max(1, int(failure_threshold))
|
||
|
|
self._base_backoff_ms = max(1, int(base_backoff_ms))
|
||
|
|
self._max_backoff_ms = max(self._base_backoff_ms, int(max_backoff_ms))
|
||
|
|
self._failure_count = 0
|
||
|
|
self._open_until_ns = 0
|
||
|
|
self._last_delay_ms = 0
|
||
|
|
|
||
|
|
async def wait_if_open(self) -> None:
|
||
|
|
remaining = self.open_remaining_secs()
|
||
|
|
if remaining > 0:
|
||
|
|
from asyncio import sleep
|
||
|
|
|
||
|
|
await sleep(remaining)
|
||
|
|
|
||
|
|
def open_remaining_secs(self) -> float:
|
||
|
|
remaining_ns = self._open_until_ns - monotonic_ns()
|
||
|
|
return max(0.0, remaining_ns / 1_000_000_000)
|
||
|
|
|
||
|
|
def snapshot(self) -> BingxCircuitBreakerSnapshot:
|
||
|
|
return BingxCircuitBreakerSnapshot(
|
||
|
|
failure_count=self._failure_count,
|
||
|
|
open_until_ns=self._open_until_ns,
|
||
|
|
last_delay_ms=self._last_delay_ms,
|
||
|
|
)
|
||
|
|
|
||
|
|
def record_success(self) -> None:
|
||
|
|
self._failure_count = 0
|
||
|
|
self._open_until_ns = 0
|
||
|
|
self._last_delay_ms = 0
|
||
|
|
|
||
|
|
def record_failure(self, *, rate_limited: bool = False, retry_after_ms: int | None = None) -> float:
|
||
|
|
now_ns = monotonic_ns()
|
||
|
|
if rate_limited and retry_after_ms is not None and retry_after_ms > 0:
|
||
|
|
self._failure_count = self._failure_threshold
|
||
|
|
self._last_delay_ms = retry_after_ms
|
||
|
|
self._open_until_ns = max(self._open_until_ns, now_ns + retry_after_ms * 1_000_000)
|
||
|
|
return retry_after_ms / 1000.0
|
||
|
|
|
||
|
|
self._failure_count += 1
|
||
|
|
delay_ms = min(self._base_backoff_ms * (2 ** (self._failure_count - 1)), self._max_backoff_ms)
|
||
|
|
self._last_delay_ms = delay_ms
|
||
|
|
if self._failure_count >= self._failure_threshold:
|
||
|
|
self._open_until_ns = max(self._open_until_ns, now_ns + delay_ms * 1_000_000)
|
||
|
|
return delay_ms / 1000.0
|