1use std::{collections::HashMap, sync::Mutex, time::Instant};
2
3use crate::{Path, enums::Category};
4
5#[derive(Debug, Clone, Copy)]
7pub struct BucketLimit {
8 pub requests_per_second: f64,
10 pub burst: u32,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct RateLimitKey {
28 pub path: Path,
29 pub category: Option<Category>,
30}
31
32impl RateLimitKey {
33 pub const fn new(path: Path) -> Self {
34 Self {
35 path,
36 category: None,
37 }
38 }
39
40 pub const fn with_category(path: Path, category: Category) -> Self {
41 Self {
42 path,
43 category: Some(category),
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
80pub struct RateLimiterConfig {
81 pub default: BucketLimit,
83 pub overrides: HashMap<RateLimitKey, BucketLimit>,
85}
86
87impl RateLimiterConfig {
88 pub fn uniform(requests_per_second: f64, burst: u32) -> Self {
93 Self {
94 default: BucketLimit {
95 requests_per_second,
96 burst,
97 },
98 overrides: HashMap::new(),
99 }
100 }
101
102 pub fn bybit_base_tier_defaults() -> Self {
111 let mut overrides = HashMap::new();
112
113 let mut set = |path: Path, category: Category, rps: f64, burst: u32| {
114 overrides.insert(
115 RateLimitKey::with_category(path, category),
116 BucketLimit {
117 requests_per_second: rps,
118 burst,
119 },
120 );
121 };
122
123 set(Path::TradeOrderCreate, Category::Linear, 20.0, 20);
125 set(Path::TradeOrderCreate, Category::Inverse, 10.0, 10);
126 set(Path::TradeOrderCreate, Category::Spot, 10.0, 10);
127 set(Path::TradeOrderCreate, Category::Option, 10.0, 10);
128
129 set(Path::TradeOrderAmend, Category::Linear, 10.0, 10);
131 set(Path::TradeOrderAmend, Category::Inverse, 10.0, 10);
132 set(Path::TradeOrderAmend, Category::Spot, 10.0, 10);
133 set(Path::TradeOrderAmend, Category::Option, 10.0, 10);
134
135 set(Path::TradeOrderCancel, Category::Linear, 20.0, 20);
137 set(Path::TradeOrderCancel, Category::Inverse, 10.0, 10);
138 set(Path::TradeOrderCancel, Category::Spot, 10.0, 10);
139 set(Path::TradeOrderCancel, Category::Option, 10.0, 10);
140
141 set(Path::TradeOrderCancelAll, Category::Linear, 20.0, 20);
143 set(Path::TradeOrderCancelAll, Category::Inverse, 1.0, 1);
144 set(Path::TradeOrderCancelAll, Category::Spot, 10.0, 10);
145 set(Path::TradeOrderCancelAll, Category::Option, 10.0, 10);
146
147 for path in [
151 Path::TradeOrderCreateBatch,
152 Path::TradeOrderAmendBatch,
153 Path::TradeOrderCancelBatch,
154 ] {
155 set(path, Category::Linear, 20.0, 20);
156 set(path, Category::Inverse, 10.0, 10);
157 set(path, Category::Spot, 10.0, 10);
158 set(path, Category::Option, 10.0, 10);
159 }
160
161 overrides.insert(
163 RateLimitKey::new(Path::TradeOrderRealtime),
164 BucketLimit {
165 requests_per_second: 50.0,
166 burst: 50,
167 },
168 );
169 overrides.insert(
170 RateLimitKey::new(Path::TradeOrderHistory),
171 BucketLimit {
172 requests_per_second: 50.0,
173 burst: 50,
174 },
175 );
176 overrides.insert(
177 RateLimitKey::new(Path::AccountWalletBalance),
178 BucketLimit {
179 requests_per_second: 50.0,
180 burst: 50,
181 },
182 );
183 overrides.insert(
184 RateLimitKey::new(Path::AccountFeeRate),
185 BucketLimit {
186 requests_per_second: 5.0,
187 burst: 5,
188 },
189 );
190
191 Self {
192 default: BucketLimit {
193 requests_per_second: 5.0,
194 burst: 5,
195 },
196 overrides,
197 }
198 }
199
200 pub fn with_override(mut self, key: RateLimitKey, limit: BucketLimit) -> Self {
201 self.overrides.insert(key, limit);
202 self
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub(crate) enum Rejection {
209 RetryAfter(u64),
211 Unsatisfiable { cost: u32, burst: u32 },
214}
215
216pub(crate) struct RateLimiter {
217 config: RateLimiterConfig,
218 buckets: Mutex<HashMap<RateLimitKey, Bucket>>,
219}
220
221impl std::fmt::Debug for RateLimiter {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 let g = self.buckets.lock().unwrap();
224 f.debug_struct("RateLimiter")
225 .field("default", &self.config.default)
226 .field("overrides", &self.config.overrides.len())
227 .field("active_buckets", &g.len())
228 .finish()
229 }
230}
231
232struct Bucket {
233 limit: BucketLimit,
234 tokens: f64,
235 last_refill: Instant,
236 server_remaining: Option<u64>,
237 server_reset_at_ms: Option<u64>,
238}
239
240impl Bucket {
241 fn new(limit: BucketLimit) -> Self {
242 Self {
243 tokens: limit.burst as f64,
244 limit,
245 last_refill: Instant::now(),
246 server_remaining: None,
247 server_reset_at_ms: None,
248 }
249 }
250}
251
252impl RateLimiter {
253 pub(crate) fn new(config: RateLimiterConfig) -> Self {
254 Self {
255 config,
256 buckets: Mutex::new(HashMap::new()),
257 }
258 }
259
260 fn limit_for(&self, key: &RateLimitKey) -> BucketLimit {
261 self.config
262 .overrides
263 .get(key)
264 .copied()
265 .unwrap_or(self.config.default)
266 }
267
268 pub(crate) fn check(&self, key: RateLimitKey, cost: u32) -> Result<(), Rejection> {
271 let limit = self.limit_for(&key);
272 if cost > limit.burst {
273 return Err(Rejection::Unsatisfiable {
274 cost,
275 burst: limit.burst,
276 });
277 }
278 let mut g = self.buckets.lock().unwrap();
279 let bucket = g.entry(key).or_insert_with(|| Bucket::new(limit));
280
281 if let (Some(0), Some(reset_at)) = (bucket.server_remaining, bucket.server_reset_at_ms) {
284 let now_ms = unix_ms();
285 if now_ms < reset_at {
286 return Err(Rejection::RetryAfter(reset_at - now_ms));
287 }
288 bucket.server_remaining = None;
290 bucket.server_reset_at_ms = None;
291 }
292
293 let rps = bucket.limit.requests_per_second;
296 let refills = rps.is_finite() && rps > 0.0;
297 let now = Instant::now();
298 if refills {
299 let elapsed = now.duration_since(bucket.last_refill).as_secs_f64();
300 bucket.tokens = (bucket.tokens + elapsed * rps).min(bucket.limit.burst as f64);
301 }
302 bucket.last_refill = now;
303
304 let needed = cost as f64;
305 if bucket.tokens < needed {
306 if !refills {
307 return Err(Rejection::Unsatisfiable {
308 cost,
309 burst: bucket.limit.burst,
310 });
311 }
312 let wait_ms = ((needed - bucket.tokens) / rps * 1000.0).ceil() as u64;
313 return Err(Rejection::RetryAfter(wait_ms));
314 }
315
316 bucket.tokens -= needed;
317 Ok(())
318 }
319
320 pub(crate) fn update(
322 &self,
323 key: RateLimitKey,
324 remaining: Option<u64>,
325 reset_at_ms: Option<u64>,
326 ) {
327 if remaining.is_none() && reset_at_ms.is_none() {
328 return;
329 }
330 let limit = self.limit_for(&key);
331 let mut g = self.buckets.lock().unwrap();
332 let bucket = g.entry(key).or_insert_with(|| Bucket::new(limit));
333 if let Some(r) = remaining {
334 bucket.server_remaining = Some(r);
335 }
336 if let Some(t) = reset_at_ms {
337 bucket.server_reset_at_ms = Some(t);
338 }
339 }
340}
341
342fn unix_ms() -> u64 {
343 std::time::SystemTime::now()
344 .duration_since(std::time::UNIX_EPOCH)
345 .unwrap_or_default()
346 .as_millis() as u64
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 const KEY: RateLimitKey = RateLimitKey::new(Path::MarketTickers);
354
355 fn limiter(requests_per_second: f64, burst: u32) -> RateLimiter {
356 RateLimiter::new(RateLimiterConfig::uniform(requests_per_second, burst))
357 }
358
359 #[test]
360 fn burst_is_available_then_requests_are_rejected_with_retry_after() {
361 let rl = limiter(10.0, 2);
362
363 assert_eq!(rl.check(KEY, 1), Ok(()));
364 assert_eq!(rl.check(KEY, 1), Ok(()));
365 assert!(matches!(
366 rl.check(KEY, 1),
367 Err(Rejection::RetryAfter(ms)) if ms > 0 && ms <= 100
368 ));
369 }
370
371 #[test]
372 fn tokens_refill_over_time() {
373 let rl = limiter(1000.0, 1);
374 assert_eq!(rl.check(KEY, 1), Ok(()));
375
376 std::thread::sleep(std::time::Duration::from_millis(5));
377
378 assert_eq!(rl.check(KEY, 1), Ok(()));
379 }
380
381 #[test]
382 fn cost_above_burst_is_unsatisfiable() {
383 let rl = limiter(10.0, 5);
384
385 assert_eq!(
386 rl.check(KEY, 10),
387 Err(Rejection::Unsatisfiable { cost: 10, burst: 5 })
388 );
389 }
390
391 #[test]
392 fn non_refilling_bucket_is_unsatisfiable_once_empty() {
393 let rl = limiter(0.0, 1);
394
395 assert_eq!(rl.check(KEY, 1), Ok(()));
396 assert_eq!(
397 rl.check(KEY, 1),
398 Err(Rejection::Unsatisfiable { cost: 1, burst: 1 })
399 );
400 }
401
402 #[test]
403 fn server_reported_exhaustion_blocks_until_reset() {
404 let rl = limiter(10.0, 10);
405 rl.update(KEY, Some(0), Some(unix_ms() + 60_000));
406
407 assert!(matches!(rl.check(KEY, 1), Err(Rejection::RetryAfter(_))));
408 }
409
410 #[test]
411 fn base_tier_defaults_cover_option_batches() {
412 let config = RateLimiterConfig::bybit_base_tier_defaults();
413 let key = RateLimitKey::with_category(Path::TradeOrderCreateBatch, Category::Option);
414
415 assert!(config.overrides[&key].burst >= 10);
416 }
417}