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